Commit 79fd3913 authored by zkuang's avatar zkuang

[ FEATURE CAS ] add dockerfile and other build stuff

parent 85e9b0d3
FROM python:3.8
ADD . /opt/sso
WORKDIR /opt/sso
RUN pip install -i https://mirrors.aliyun.com/pypi/simple/ pip -U && \
pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ && \
pip install -r requirements.txt
RUN mv ./models.py /usr/local/lib/python3.8/site-packages/mama_cas/models.py
CMD [ "gunicorn", "-b", "0.0.0.0:80", "account.wsgi"]
\ No newline at end of file
......@@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/3.0/ref/settings/
"""
import os
import json
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
......@@ -20,12 +21,12 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '1gboig1)_)ljuz_h0&8m%s84ybxz4*z!w0pu$0qi#mobo#poae'
SECRET_KEY = os.environ.get('SECRET_KEY') # '1gboig1)_)ljuz_h0&8m%s84ybxz4*z!w0pu$0qi#mobo#poae'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
ALLOWED_HOSTS = ['sso.gzego.com']
ALLOWED_HOSTS = json.loads(os.environ.get('ALLOWED_HOSTS')) # ['sso.gzego.com']
# Application definition
......@@ -77,11 +78,11 @@ WSGI_APPLICATION = 'account.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'sso',
'USER': 'cas',
'PASSWORD': '123456',
'HOST': 'mysql', # Or an IP Address that your DB is hosted on
'PORT': '3306',
'NAME': os.environ.get('DB_NAME'), # 'sso',
'USER': os.environ.get('DB_USERNAME'), # 'cas',
'PASSWORD': os.environ.get('DB_PASSWORD'), # '123456',
'HOST': os.environ.get('DB_HOST'), # 'mysql', # Or an IP Address that your DB is hosted on
'PORT': os.environ.get('DB_PORT'), # '3306',
}
}
......@@ -130,13 +131,32 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'static')
MAMA_CAS_ENABLE_SINGLE_SIGN_OUT = True
MAMA_CAS_SERVICES = [
{
'SERVICE': 'http://carbinet.gzego.com:8080/api/cas',
# the service env variable is a json string like this:
# redirect_url logout_url redirect_url logout_url
# ["http://carbinet.gzego.com:8080/api/cas", "http://carbinet.gzego.com:8080/api/cas/logout", ".................", "................."]
#
def make_service_config(redirect_url, logout_url):
return {
'SERVICE': redirect_url,
'CALLBACKS': [
'mama_cas.callbacks.user_model_attributes',
],
'LOGOUT_ALLOW': True,
'LOGOUT_URL': 'http://carbinet.gzego.com:8080/api/cas/logout',
},
]
\ No newline at end of file
'LOGOUT_URL': logout_url,
}
service_urls = json.loads(os.environ.get('CAS_SERVICES'))
MAMA_CAS_SERVICES = [make_service_config(redirect, logout) for redirect, logout in zip(service_urls[0::2], service_urls[1::2])]
# MAMA_CAS_SERVICES = [
# {
# 'SERVICE': 'http://carbinet.gzego.com:8080/api/cas',
# 'CALLBACKS': [
# 'mama_cas.callbacks.user_model_attributes',
# ],
# 'LOGOUT_ALLOW': True,
# 'LOGOUT_URL': 'http://carbinet.gzego.com:8080/api/cas/logout',
# },
# ]
\ No newline at end of file
from __future__ import unicode_literals
from datetime import timedelta
import logging
import os
import re
import time
from django.conf import settings
from django.db import models
from django.db.models import Q
from django.utils.crypto import get_random_string
# from django.utils.encoding import python_2_unicode_compatible
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
import requests
from mama_cas.compat import Session
from mama_cas.exceptions import InvalidProxyCallback
from mama_cas.exceptions import InvalidRequest
from mama_cas.exceptions import InvalidService
from mama_cas.exceptions import InvalidTicket
from mama_cas.exceptions import UnauthorizedServiceProxy
from mama_cas.exceptions import ValidationError
from mama_cas.request import SingleSignOutRequest
from mama_cas.services import get_logout_url
from mama_cas.services import logout_allowed
from mama_cas.services import service_allowed
from mama_cas.services import proxy_allowed
from mama_cas.services import proxy_callback_allowed
from mama_cas.utils import add_query_params
from mama_cas.utils import clean_service_url
from mama_cas.utils import is_scheme_https
from mama_cas.utils import match_service
logger = logging.getLogger(__name__)
class TicketManager(models.Manager):
def create_ticket(self, ticket=None, **kwargs):
"""
Create a new ``Ticket``. Additional arguments are passed to the
``create()`` function. Return the newly created ``Ticket``.
"""
if not ticket:
ticket = self.create_ticket_str()
if 'service' in kwargs:
kwargs['service'] = clean_service_url(kwargs['service'])
if 'expires' not in kwargs:
expires = now() + timedelta(seconds=self.model.TICKET_EXPIRE)
kwargs['expires'] = expires
t = self.create(ticket=ticket, **kwargs)
logger.debug("Created %s %s" % (t.name, t.ticket))
return t
def create_ticket_str(self, prefix=None):
"""
Generate a sufficiently opaque ticket string to ensure the ticket is
not guessable. If a prefix is provided, prepend it to the string.
"""
if not prefix:
prefix = self.model.TICKET_PREFIX
return "%s-%d-%s" % (prefix, int(time.time()),
get_random_string(length=self.model.TICKET_RAND_LEN))
def validate_ticket(self, ticket, service, renew=False, require_https=False):
"""
Given a ticket string and service identifier, validate the
corresponding ``Ticket``. If validation succeeds, return the
``Ticket``. If validation fails, raise an appropriate error.
If ``renew`` is ``True``, ``ServiceTicket`` validation will
only succeed if the ticket was issued from the presentation
of the user's primary credentials.
If ``require_https`` is ``True``, ``ServiceTicket`` validation
will only succeed if the service URL scheme is HTTPS.
"""
if not ticket:
raise InvalidRequest("No ticket string provided")
if not self.model.TICKET_RE.match(ticket):
raise InvalidTicket("Ticket string %s is invalid" % ticket)
try:
t = self.get(ticket=ticket)
except self.model.DoesNotExist:
raise InvalidTicket("Ticket %s does not exist" % ticket)
if t.is_consumed():
raise InvalidTicket("%s %s has already been used" %
(t.name, ticket))
if t.is_expired():
raise InvalidTicket("%s %s has expired" % (t.name, ticket))
if not service:
raise InvalidRequest("No service identifier provided")
if require_https and not is_scheme_https(service):
raise InvalidService("Service %s is not HTTPS" % service)
if not service_allowed(service):
raise InvalidService("Service %s is not a valid %s URL" %
(service, t.name))
try:
if not match_service(t.service, service):
raise InvalidService("%s %s for service %s is invalid for "
"service %s" % (t.name, ticket, t.service, service))
except AttributeError:
pass
try:
if renew and not t.is_primary():
raise InvalidTicket("%s %s was not issued via primary "
"credentials" % (t.name, ticket))
except AttributeError:
pass
logger.debug("Validated %s %s" % (t.name, ticket))
return t
def delete_invalid_tickets(self):
"""
Delete consumed or expired ``Ticket``s that are not referenced
by other ``Ticket``s. Invalid tickets are no longer valid for
authentication and can be safely deleted.
A custom management command is provided that executes this method
on all applicable models by running ``manage.py cleanupcas``.
"""
for ticket in self.filter(Q(consumed__isnull=False) |
Q(expires__lte=now())).order_by('-expires'):
try:
ticket.delete()
except models.ProtectedError:
pass
def consume_tickets(self, user):
"""
Consume all valid ``Ticket``s for a specified user. This is run
when the user logs out to ensure all issued tickets are no longer
valid for future authentication attempts.
"""
for ticket in self.filter(user=user, consumed__isnull=True,
expires__gt=now()):
ticket.consume()
# @python_2_unicode_compatible
class Ticket(models.Model):
"""
``Ticket`` is an abstract base class implementing common methods
and fields for CAS tickets.
"""
TICKET_EXPIRE = getattr(settings, 'MAMA_CAS_TICKET_EXPIRE', 90)
TICKET_RAND_LEN = getattr(settings, 'MAMA_CAS_TICKET_RAND_LEN', 32)
TICKET_RE = re.compile("^[A-Z]{2,3}-[0-9]{10,}-[a-zA-Z0-9]{%d}$" % TICKET_RAND_LEN)
ticket = models.CharField(_('ticket'), max_length=255, unique=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'),
on_delete=models.CASCADE)
expires = models.DateTimeField(_('expires'))
consumed = models.DateTimeField(_('consumed'), null=True)
objects = TicketManager()
class Meta:
abstract = True
def __str__(self):
return self.ticket
@property
def name(self):
return self._meta.verbose_name
def consume(self):
"""
Consume a ``Ticket`` by populating the ``consumed`` field with
the current datetime. A consumed ``Ticket`` is invalid for future
authentication attempts.
"""
self.consumed = now()
self.save()
def is_consumed(self):
"""
Check a ``Ticket``s consumed state, consuming it in the process.
"""
if self.consumed is None:
self.consume()
return False
return True
def is_expired(self):
"""
Check a ``Ticket``s expired state. Return ``True`` if the ticket is
expired, and ``False`` otherwise.
"""
return self.expires <= now()
class ServiceTicketManager(TicketManager):
def request_sign_out(self, user):
"""
Send a single logout request to each service accessed by a
specified user. This is called at logout when single logout
is enabled.
If requests-futures is installed, asynchronous requests will
be sent. Otherwise, synchronous requests will be sent.
"""
session = Session()
for ticket in self.filter(user=user, consumed__gte=user.last_login):
ticket.request_sign_out(session=session)
class ServiceTicket(Ticket):
"""
(3.1) A ``ServiceTicket`` is used by the client as a credential to
obtain access to a service. It is obtained upon a client's presentation
of credentials and a service identifier to /login.
"""
TICKET_PREFIX = 'ST'
service = models.CharField(_('service'), max_length=255)
primary = models.BooleanField(_('primary'), default=False)
objects = ServiceTicketManager()
class Meta:
verbose_name = _('service ticket')
verbose_name_plural = _('service tickets')
def is_primary(self):
"""
Check the credential origin for a ``ServiceTicket``. If the ticket was
issued from the presentation of the user's primary credentials,
return ``True``, otherwise return ``False``.
"""
if self.primary:
return True
return False
def request_sign_out(self, session=requests):
"""
Send a POST request to the ``ServiceTicket``s logout URL to
request sign-out.
"""
if logout_allowed(self.service):
request = SingleSignOutRequest(context={'ticket': self})
url = get_logout_url(self.service) or self.service
session.post(url, data={'logoutRequest': request.render_content()})
logger.info("Single sign-out request sent to %s" % url)
class ProxyTicket(Ticket):
"""
(3.2) A ``ProxyTicket`` is used by a service as a credential to obtain
access to a back-end service on behalf of a client. It is obtained upon
a service's presentation of a ``ProxyGrantingTicket`` and a service
identifier.
"""
TICKET_PREFIX = 'PT'
service = models.CharField(_('service'), max_length=255)
granted_by_pgt = models.ForeignKey('ProxyGrantingTicket',
verbose_name=_('granted by proxy-granting ticket'),
on_delete=models.CASCADE)
class Meta:
verbose_name = _('proxy ticket')
verbose_name_plural = _('proxy tickets')
class ProxyGrantingTicketManager(TicketManager):
def create_ticket(self, service, pgturl, **kwargs):
"""
When a ``pgtUrl`` parameter is provided to ``/serviceValidate`` or
``/proxyValidate``, attempt to create a new ``ProxyGrantingTicket``.
If validation succeeds, create and return the ``ProxyGrantingTicket``.
If validation fails, return ``None``.
"""
pgtid = self.create_ticket_str()
pgtiou = self.create_ticket_str(prefix=self.model.IOU_PREFIX)
try:
self.validate_callback(service, pgturl, pgtid, pgtiou)
except ValidationError as e:
logger.warning("%s %s" % (e.code, e))
return None
else:
# pgtUrl validation succeeded, so create a new PGT with the
# previously generated ticket strings
return super(ProxyGrantingTicketManager, self).create_ticket(ticket=pgtid, iou=pgtiou, **kwargs)
def validate_callback(self, service, pgturl, pgtid, pgtiou):
"""Verify the provided proxy callback URL."""
if not proxy_allowed(service):
raise UnauthorizedServiceProxy("%s is not authorized to use proxy authentication" % service)
if not is_scheme_https(pgturl):
raise InvalidProxyCallback("Proxy callback %s is not HTTPS" % pgturl)
if not proxy_callback_allowed(service, pgturl):
raise InvalidProxyCallback("%s is not an authorized proxy callback URL" % pgturl)
# Verify that the SSL certificate is valid
verify = os.environ.get('REQUESTS_CA_BUNDLE', True)
try:
requests.get(pgturl, verify=verify, timeout=5)
except requests.exceptions.SSLError:
raise InvalidProxyCallback("SSL certificate validation failed for proxy callback %s" % pgturl)
except requests.exceptions.RequestException as e:
raise InvalidProxyCallback(e)
# Callback certificate appears valid, so send the ticket strings
pgturl = add_query_params(pgturl, {'pgtId': pgtid, 'pgtIou': pgtiou})
try:
response = requests.get(pgturl, verify=verify, timeout=5)
except requests.exceptions.RequestException as e:
raise InvalidProxyCallback(e)
try:
response.raise_for_status()
except requests.exceptions.HTTPError as e:
raise InvalidProxyCallback("Proxy callback %s returned %s" % (pgturl, e))
class ProxyGrantingTicket(Ticket):
"""
(3.3) A ``ProxyGrantingTicket`` is used by a service to obtain proxy
tickets for obtaining access to a back-end service on behalf of a
client. It is obtained upon validation of a ``ServiceTicket`` or a
``ProxyTicket``.
"""
TICKET_PREFIX = 'PGT'
IOU_PREFIX = 'PGTIOU'
TICKET_EXPIRE = getattr(settings, 'SESSION_COOKIE_AGE')
iou = models.CharField(_('iou'), max_length=255, unique=True)
granted_by_st = models.ForeignKey(ServiceTicket, null=True, blank=True,
on_delete=models.PROTECT,
verbose_name=_('granted by service ticket'))
granted_by_pt = models.ForeignKey(ProxyTicket, null=True, blank=True,
on_delete=models.PROTECT,
verbose_name=_('granted by proxy ticket'))
objects = ProxyGrantingTicketManager()
class Meta:
verbose_name = _('proxy-granting ticket')
verbose_name_plural = _('proxy-granting tickets')
def is_consumed(self):
"""Check a ``ProxyGrantingTicket``s consumed state."""
return self.consumed is not None
......@@ -4,8 +4,10 @@ chardet==3.0.4
Django==3.0.3
django-cas-ng==4.1.0
django-mama-cas==2.4.0
gunicorn==20.0.4
idna==2.9
lxml==4.5.0
mysqlclient==1.4.6
python-cas==1.5.0
pytz==2019.3
requests==2.23.0
......
#!/usr/bin/env bash
# Use this script to test if a given TCP host/port are available
WAITFORIT_cmdname=${0##*/}
echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
usage()
{
cat << USAGE >&2
Usage:
$WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
-h HOST | --host=HOST Host or IP under test
-p PORT | --port=PORT TCP port under test
Alternatively, you specify the host and port as host:port
-s | --strict Only execute subcommand if the test succeeds
-q | --quiet Don't output any status messages
-t TIMEOUT | --timeout=TIMEOUT
Timeout in seconds, zero for no timeout
-- COMMAND ARGS Execute command with args after the test finishes
USAGE
exit 1
}
wait_for()
{
if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
else
echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
fi
WAITFORIT_start_ts=$(date +%s)
while :
do
if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
nc -z $WAITFORIT_HOST $WAITFORIT_PORT
WAITFORIT_result=$?
else
(echo > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
WAITFORIT_result=$?
fi
if [[ $WAITFORIT_result -eq 0 ]]; then
WAITFORIT_end_ts=$(date +%s)
echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
break
fi
sleep 1
done
return $WAITFORIT_result
}
wait_for_wrapper()
{
# In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
if [[ $WAITFORIT_QUIET -eq 1 ]]; then
timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
else
timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
fi
WAITFORIT_PID=$!
trap "kill -INT -$WAITFORIT_PID" INT
wait $WAITFORIT_PID
WAITFORIT_RESULT=$?
if [[ $WAITFORIT_RESULT -ne 0 ]]; then
echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
fi
return $WAITFORIT_RESULT
}
# process arguments
while [[ $# -gt 0 ]]
do
case "$1" in
*:* )
WAITFORIT_hostport=(${1//:/ })
WAITFORIT_HOST=${WAITFORIT_hostport[0]}
WAITFORIT_PORT=${WAITFORIT_hostport[1]}
shift 1
;;
--child)
WAITFORIT_CHILD=1
shift 1
;;
-q | --quiet)
WAITFORIT_QUIET=1
shift 1
;;
-s | --strict)
WAITFORIT_STRICT=1
shift 1
;;
-h)
WAITFORIT_HOST="$2"
if [[ $WAITFORIT_HOST == "" ]]; then break; fi
shift 2
;;
--host=*)
WAITFORIT_HOST="${1#*=}"
shift 1
;;
-p)
WAITFORIT_PORT="$2"
if [[ $WAITFORIT_PORT == "" ]]; then break; fi
shift 2
;;
--port=*)
WAITFORIT_PORT="${1#*=}"
shift 1
;;
-t)
WAITFORIT_TIMEOUT="$2"
if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
shift 2
;;
--timeout=*)
WAITFORIT_TIMEOUT="${1#*=}"
shift 1
;;
--)
shift
WAITFORIT_CLI=("$@")
break
;;
--help)
usage
;;
*)
echoerr "Unknown argument: $1"
usage
;;
esac
done
if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
echoerr "Error: you need to provide a host and port to test."
usage
fi
WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}
# check to see if timeout is from busybox?
WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)
if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
WAITFORIT_ISBUSY=1
WAITFORIT_BUSYTIMEFLAG="-t"
else
WAITFORIT_ISBUSY=0
WAITFORIT_BUSYTIMEFLAG=""
fi
if [[ $WAITFORIT_CHILD -gt 0 ]]; then
wait_for
WAITFORIT_RESULT=$?
exit $WAITFORIT_RESULT
else
if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
wait_for_wrapper
WAITFORIT_RESULT=$?
else
wait_for
WAITFORIT_RESULT=$?
fi
fi
if [[ $WAITFORIT_CLI != "" ]]; then
if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
exit $WAITFORIT_RESULT
fi
exec "${WAITFORIT_CLI[@]}"
else
exit $WAITFORIT_RESULT
fi
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment