Мне нужна помощь для интеграции duo_ python 2FA в Django -mama-cas - PullRequest
0 голосов
/ 26 марта 2020

Я настроил django -mama-cas в качестве основного центрального сервера аутентификации для веб-сайтов департаментов. Веб-сайты размещаются в веб-платформе drupal / php и используют phpCAS для клиента CAS. Мне нужно каким-то образом интегрировать duo_ python в сервер django -mama-cas.

Я не уверен, с чего начать, и какие-либо советы по поводу файлов или классов для редактирования что я ищу

Насколько я понимаю, эти [файлы, классы и т. Д.] Приведены ниже:

settings.py

import os
import ldap
from django_auth_ldap.config import LDAPSearch, GroupOfNamesType, PosixGroupType

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# SECURITY WARNING: keep the secret key used in production secret!
with open('/n/fs/stage/signon/cas_container/casServer/secret_key.txt') as f:
    SECRET_KEY = f.read().strip()

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False

ALLOWED_HOSTS = ['signon.example.com']

# Application definition
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'mama_cas',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'casServer.urls'

# Django Templates Settings: https://docs.djangoproject.com/en/2.2/topics/templates/
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]
# WSGI Settings: https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
WSGI_APPLICATION = 'casServer.wsgi.application'

# Database Settings: https://docs.djangoproject.com/en/2.2/ref/databases/
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'OPTIONS': {
            'read_default_file': '/n/fs/stage/signon/cas_container/casServer/my.cnf',
            'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
        },
    }
}
# Django Authentication Settings: https://docs.djangoproject.com/en/2.2/topics/auth/
# Django-Auth-LDAP Settings: https://django-auth-ldap.readthedocs.io/en/latest/index.html
AUTHENTICATION_BACKENDS = [
     "django_auth_ldap.backend.LDAPBackend",
     "django.contrib.auth.backends.ModelBackend",
]

AUTH_LDAP_SERVER_URI = "ldaps://ldap.example.com:636"
AUTH_LDAP_USER_SEARCH = LDAPSearch("ou=People,dc=cs,dc=example,dc=com", ldap.SCOPE_SUBTREE, "(uid=%(user)s)")
AUTH_LDAP_GROUP_SEARCH = LDAPSearch("ou=Group,dc=cs,dc=example,dc=com", ldap.SCOPE_SUBTREE, '(&(objectClass=posixGroup))')
AUTH_LDAP_GROUP_TYPE = PosixGroupType(name_attr='cn')
AUTH_LDAP_FIND_GROUP_PERMS = True
AUTH_LDAP_CACHE_TIMEOUT = 600
AUTH_LDAP_USER_ATTR_MAP = {
    "first_name": "givenName",
    "last_name": "sn",
    "email": "mail",
    }

# Django Password Validation
AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]

# Django-Mama-Cas Settings: https://django-mama-cas.readthedocs.io/en/latest/settings.html
MAMA_CAS_SERVICE_BACKENDS = ['mama_cas.services.backends.SettingsBackend']

MAMA_CAS_SERVICES = [
    {
        'SERVICE': '^https://[^\.]+\.example\.com/',
        'CALLBACKS': [
            'mama_cas.callbacks.user_name_attributes',
        ],
        'LOGOUT_ALLOW': True,
        'LOGOUT_URL': 'https://signon.example.com/logout',
        #'PROXY_ALLOW': True,
        #'PROXY_PATTERN': '^https://proxy\.example\.com',
    }
]

MAMA_CAS_TICKET_EXPIRE = ['90']

# https://docs.djangoproject.com/en/2.2/topics/i18n/
LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'America/New_York'

USE_I18N = True

USE_L10N = True

USE_TZ = True

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')

основной сайт urls.py

from django.contrib import admin
from django.urls import include, path
from django.conf.urls import url

urlpatterns = [
    url(r'', include('mama_cas.urls')),
    path('admin/', admin.site.urls),

mama_cas / urls.py

"""
(2) CAS server URIs as described in the CAS protocol.
"""

from django.conf.urls import include,  url
from django.urls import include, path

from mama_cas.views import LoginView
from mama_cas.views import LogoutView
from mama_cas.views import ValidateView
from mama_cas.views import ServiceValidateView
from mama_cas.views import ProxyValidateView
from mama_cas.views import ProxyView
from mama_cas.views import WarnView
from mama_cas.views import SamlValidateView

urlpatterns = [
    url(r'^login?$', LoginView.as_view(), name='cas_login'),
    url(r'^logout/?$', LogoutView.as_view(), name='cas_logout'),
    url(r'^validate/?$', ValidateView.as_view(), name='cas_validate'),
    url(r'^serviceValidate/?$', ServiceValidateView.as_view(), name='cas_service_validate'),
    url(r'^proxyValidate/?$', ProxyValidateView.as_view(), name='cas_proxy_validate'),
    url(r'^proxy/?$', ProxyView.as_view(), name='cas_proxy'),
    url(r'^p3/serviceValidate/?$', ServiceValidateView.as_view(), name='cas_p3_service_validate'),
    url(r'^p3/proxyValidate/?$', ProxyValidateView.as_view(), name='cas_p3_proxy_validate'),
    url(r'^warn/?$', WarnView.as_view(), name='cas_warn'),
    url(r'^samlValidate/?$', SamlValidateView.as_view(), name='cas_saml_validate'),
]

mama_cas / views.py

import logging

from django.conf import settings
from django.contrib import messages
from django.contrib.auth import login
from django.http import HttpResponse
from django.utils.translation import ugettext as _
from django.views.generic import FormView
from django.views.generic import TemplateView
from django.views.generic import View

from mama_cas.compat import defused_etree
from mama_cas.compat import is_authenticated
from mama_cas.exceptions import ValidationError
from mama_cas.forms import LoginForm
from mama_cas.mixins import CasResponseMixin
from mama_cas.mixins import CsrfProtectMixin
from mama_cas.mixins import LoginRequiredMixin
from mama_cas.cas import logout_user
from mama_cas.cas import validate_service_ticket
from mama_cas.cas import validate_proxy_ticket
from mama_cas.cas import validate_proxy_granting_ticket
from mama_cas.mixins import NeverCacheMixin
from mama_cas.models import ProxyTicket
from mama_cas.models import ServiceTicket
from mama_cas.response import ValidationResponse
from mama_cas.response import ProxyResponse
from mama_cas.response import SamlValidationResponse
from mama_cas.services import service_allowed
from mama_cas.utils import add_query_params
from mama_cas.utils import clean_service_url
from mama_cas.utils import redirect
from mama_cas.utils import to_bool


logger = logging.getLogger(__name__)


login_view_template_name = getattr(settings,
                                   'MAMA_CAS_LOGIN_TEMPLATE',
                                   'mama_cas/login.html')

warn_view_template_name = getattr(settings,
                                  'MAMA_CAS_WARN_TEMPLATE',
                                  'mama_cas/warn.html')


class LoginView(CsrfProtectMixin, NeverCacheMixin, FormView):
    """
    (2.1 and 2.2) Credential requestor and acceptor.

    This view operates as a credential requestor when a GET request
    is received, and a credential acceptor for POST requests.
    """
    template_name = login_view_template_name
    form_class = LoginForm

    def get_form_kwargs(self):
        """
        Django >= 1.11 supports a request sent to the authenticator
        so we grab that here and pass it along to the form so it can be
        handed off to the authenticators.
        """
        kwargs = super(LoginView, self).get_form_kwargs()
        kwargs['request'] = self.request
        return kwargs

    def get(self, request, *args, **kwargs):
        """
        (2.1) As a credential requestor, /login accepts three optional
        parameters:

        1. ``service``: the identifier of the application the client is
           accessing. We assume this identifier to be a URL.
        2. ``renew``: requires a client to present credentials
           regardless of any existing single sign-on session.
        3. ``gateway``: causes the client to not be prompted for
           credentials. If a single sign-on session exists the user
           will be logged in and forwarded to the specified service.
           Otherwise, the user remains logged out and is forwarded to
           the specified service.
        """
        service = request.GET.get('service')
        renew = to_bool(request.GET.get('renew'))
        gateway = to_bool(request.GET.get('gateway'))

        if renew:
            logger.debug("Renew request received by credential requestor")
        elif gateway and service:
            logger.debug("Gateway request received by credential requestor")
            if is_authenticated(request.user):
                st = ServiceTicket.objects.create_ticket(service=service, user=request.user)
                if self.warn_user():
                    return redirect('cas_warn', params={'service': service, 'ticket': st.ticket})
                return redirect(service, params={'ticket': st.ticket})
            else:
                return redirect(service)
        elif is_authenticated(request.user):
            if service:
                logger.debug("Service ticket request received by credential requestor")
                st = ServiceTicket.objects.create_ticket(service=service, user=request.user)
                if self.warn_user():
                    return redirect('cas_warn', params={'service': service, 'ticket': st.ticket})
                return redirect(service, params={'ticket': st.ticket})
            else:
                msg = _("You are logged in as %s") % request.user
                messages.success(request, msg)
        return super(LoginView, self).get(request, *args, **kwargs)

    def warn_user(self):
        """
        Returns ``True`` if the ``warn`` parameter is set in the
        current session. Otherwise, returns ``False``.
        """
        return self.request.session.get('warn', False)

    def form_valid(self, form):
        """
        (2.2) As a credential acceptor, /login requires two parameters:

        1. ``username``: the username provided by the client
        2. ``password``: the password provided by the client

        If authentication is successful, the single sign-on session is
        created. If a service is provided, a ``ServiceTicket`` is
        created and the client is redirected to the service URL with
        the ``ServiceTicket`` included. If no service is provided, the
        login page is redisplayed with a message indicating a
        successful login.

        If authentication fails, the login form is redisplayed with an
        error message describing the reason for failure.

        The credential acceptor accepts one optional parameter:

        1. ``warn``: causes the user to be prompted when successive
           authentication attempts occur within the single sign-on
           session.
        """
        login(self.request, form.user)
        logger.info("Single sign-on session started for %s" % form.user)

        if form.cleaned_data.get('warn'):
            self.request.session['warn'] = True

        service = self.request.GET.get('service')
        if service:
            st = ServiceTicket.objects.create_ticket(service=service, user=self.request.user, primary=True)
            return redirect(service, params={'ticket': st.ticket})
        return redirect('cas_login')


class WarnView(NeverCacheMixin, LoginRequiredMixin, TemplateView):
    """
    (2.2.1) Disables transparent authentication by informing the user
    that service authentication is taking place. The user can choose
    to continue or cancel the authentication attempt.
    """
    template_name = warn_view_template_name

    def get(self, request, *args, **kwargs):
        service = request.GET.get('service')
        ticket = request.GET.get('ticket')

        if not service_allowed(service):
            return redirect('cas_login')

        msg = _("Do you want to access %(service)s as %(user)s?") % {
                'service': clean_service_url(service),
                'user': request.user}
        messages.info(request, msg)
        kwargs['service'] = add_query_params(service, {'ticket': ticket})
        return super(WarnView, self).get(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        return kwargs


class LogoutView(NeverCacheMixin, View):
    """
    (2.3) End a client's single sign-on session.

    Accessing this view ends an existing single sign-on session,
    requiring a new single sign-on session to be established for
    future authentication attempts.

    (2.3.1) If ``service`` is specified and
    ``MAMA_CAS_FOLLOW_LOGOUT_URL`` is ``True``, the client will be
    redirected to the specified service URL. [CAS 3.0]
    """
    def get(self, request, *args, **kwargs):
        service = request.GET.get('service')
        if not service:
            service = request.GET.get('url')
        follow_url = getattr(settings, 'MAMA_CAS_FOLLOW_LOGOUT_URL', True)
        logout_user(request)
        if service and follow_url:
            return redirect(service)
        return redirect('cas_login')


class ValidateView(NeverCacheMixin, View):
    """
    (2.4) Check the validity of a service ticket. [CAS 1.0]

    When both ``service`` and ``ticket`` are provided, this view
    responds with a plain-text response indicating a ``ServiceTicket``
    validation success or failure. Whether or not the validation
    succeeds, the ``ServiceTicket`` is consumed, rendering it invalid
    for future authentication attempts.

    If ``renew`` is specified, validation will only succeed if the
    ``ServiceTicket`` was issued from the presentation of the user's
    primary credentials, not from an existing single sign-on session.
    """
    def get(self, request, *args, **kwargs):
        service = request.GET.get('service')
        ticket = request.GET.get('ticket')
        renew = to_bool(request.GET.get('renew'))

        try:
            st, attributes, pgt = validate_service_ticket(service, ticket, renew=renew)
            content = "yes\n%s\n" % st.user.get_username()
        except ValidationError:
            content = "no\n\n"
        return HttpResponse(content=content, content_type='text/plain')


class ServiceValidateView(NeverCacheMixin, CasResponseMixin, View):
    """
    (2.5) Check the validity of a service ticket. [CAS 2.0]

    When both ``service`` and ``ticket`` are provided, this view
    responds with an XML-fragment response indicating a
    ``ServiceTicket`` validation success or failure. Whether or not
    validation succeeds, the ticket is consumed, rendering it invalid
    for future authentication attempts.

    If ``renew`` is specified, validation will only succeed if the
    ``ServiceTicket`` was issued from the presentation of the user's
    primary credentials, not from an existing single sign-on session.

    If ``pgtUrl`` is specified, the response will include a
    ``ProxyGrantingTicket`` if the proxy callback URL has a valid SSL
    certificate and responds with a successful HTTP status code.
    """
    response_class = ValidationResponse

    def get_context_data(self, **kwargs):
        service = self.request.GET.get('service')
        ticket = self.request.GET.get('ticket')
        pgturl = self.request.GET.get('pgtUrl')
        renew = to_bool(self.request.GET.get('renew'))

        try:
            st, attributes, pgt = validate_service_ticket(service, ticket, pgturl=pgturl, renew=renew)
            return {'ticket': st, 'pgt': pgt, 'attributes': attributes, 'error': None}
        except ValidationError as e:
            logger.warning("%s %s" % (e.code, e))
            return {'ticket': None, 'error': e}


class ProxyValidateView(NeverCacheMixin, CasResponseMixin, View):
    """
    (2.6) Perform the same validation tasks as ServiceValidateView and
    additionally validate proxy tickets. [CAS 2.0]

    When both ``service`` and ``ticket`` are provided, this view
    responds with an XML-fragment response indicating a ``ProxyTicket``
    or ``ServiceTicket`` validation success or failure. Whether or not
    validation succeeds, the ticket is consumed, rendering it invalid
    for future authentication attempts.

    If ``renew`` is specified, validation will only succeed if the
    ``ServiceTicket`` was issued from the presentation of the user's
    primary credentials, not from an existing single sign-on session.

    If ``pgtUrl`` is specified, the response will include a
    ``ProxyGrantingTicket`` if the proxy callback URL has a valid SSL
    certificate and responds with a successful HTTP status code.
    """
    response_class = ValidationResponse

    def get_context_data(self, **kwargs):
        service = self.request.GET.get('service')
        ticket = self.request.GET.get('ticket')
        pgturl = self.request.GET.get('pgtUrl')
        renew = to_bool(self.request.GET.get('renew'))

        try:
            if not ticket or ticket.startswith(ProxyTicket.TICKET_PREFIX):
                # If no ticket parameter is present, attempt to validate it
                # anyway so the appropriate error is raised
                pt, attributes, pgt, proxies = validate_proxy_ticket(service, ticket, pgturl=pgturl)
                return {'ticket': pt, 'pgt': pgt, 'attributes': attributes, 'proxies': proxies, 'error': None}
            else:
                st, attributes, pgt = validate_service_ticket(service, ticket, pgturl=pgturl, renew=renew)
                return {'ticket': st, 'pgt': pgt, 'attributes': attributes, 'proxies': None, 'error': None}
        except ValidationError as e:
            logger.warning("%s %s" % (e.code, e))
            return {'ticket': None, 'error': e}


class ProxyView(NeverCacheMixin, CasResponseMixin, View):
    """
    (2.7) Provide proxy tickets to services that have acquired proxy-
    granting tickets. [CAS 2.0]

    When both ``pgt`` and ``targetService`` are specified, this view
    responds with an XML-fragment response indicating a
    ``ProxyGrantingTicket`` validation success or failure. If
    validation succeeds, a ``ProxyTicket`` will be created and included
    in the response.
    """
    response_class = ProxyResponse

    def get_context_data(self, **kwargs):
        pgt = self.request.GET.get('pgt')
        target_service = self.request.GET.get('targetService')

        try:
            pt = validate_proxy_granting_ticket(pgt, target_service)
            return {'ticket': pt, 'error': None}
        except ValidationError as e:
            logger.warning("%s %s" % (e.code, e))
            return {'ticket': None, 'error': e}


class SamlValidateView(NeverCacheMixin, View):
    """
    (4.2) Check the validity of a service ticket provided by a
    SAML 1.1 request document provided by a HTTP POST. [CAS 3.0]
    """
    response_class = SamlValidationResponse
    content_type = 'text/xml'

    def post(self, request, *args, **kwargs):
        context = self.get_context_data(**kwargs)
        return self.render_to_response(context)

    def render_to_response(self, context):
        return self.response_class(context, content_type=self.content_type)

    def get_context_data(self, **kwargs):
        target = self.request.GET.get('TARGET')

        assert defused_etree, '/samlValidate endpoint requires defusedxml to be installed'

        try:
            root = defused_etree.parse(self.request, forbid_dtd=True).getroot()
            ticket = root.find('.//{urn:oasis:names:tc:SAML:1.0:protocol}AssertionArtifact').text
        except (defused_etree.ParseError, ValueError, AttributeError):
            ticket = None

        try:
            st, attributes, pgt = validate_service_ticket(target, ticket, require_https=True)
            return {'ticket': st, 'pgt': pgt, 'attributes': attributes, 'error': None}
        except ValidationError as e:
            logger.warning("%s %s" % (e.code, e))
            return {'ticket': None, 'error': e}

duo_ python

duo_ python: django

...