コード例 #1
0
ファイル: casclient.py プロジェクト: terapyon/anz.casclient
class AnzCASClient( BasePlugin, Cacheable ):
    ''' Anz CAS client, Implement as a PAS plugin. '''

    implements( IAnzCASClient )

    meta_type = 'Anz CAS Client'

    # Session variable use to save assertion
    CAS_ASSERTION = '__cas_assertion'
    CAS_REDIRECT_URL = '__anz_casclient_redirect_url'
    CAS_REDIRECT_MSG = '__anz_casclient_redirect_msg'

    # The start of the CAS server URL
    casServerUrlPrefix = ''

    # An identify of current service.
    # CAS will redirects to here after login.
    # Set this explicitly but not determine it automatically from request makes
    # us get more security assurance.
    # https://wiki.jasig.org/display/CASC/CASFilter
    serviceUrl = ''

    # Whether to store the Assertion in session or not.
    # If sessions are not used, proxy granting ticket will be required for
    # each request. Default set to True.
    useSession = True

    # If set to True, CAS will ask user for credentials again to authenticate,
    # this may be used for high-security applications. Default set to False.
    renew = False

    # If set to True, CAS will not ask the user for credentials. If the
    # user has a pre-existing single sign-on session with CAS, or if a single
    # sign-on session can be established through non-interactive means
    # (i.e. trust authentication), CAS MAY redirect the client to the URL
    # specified by the "service" parameter, appending a valid service ticket.
    # (CAS also MAY interpose an advisory page informing the client that a CAS
    # authentication has taken place.) If the client does not have a single
    # sign-on session with CAS, and a non-interactive authentication cannot be
    # established, CAS MUST redirect the client to the URL specified by the
    # "service" parameter with no "ticket" parameter appended to the URL. If
    # the "service" parameter is not specified and "gateway" is set, the
    # behavior of CAS is undefined. It is RECOMMENDED that in this case, CAS
    # request credentials as if neither parameter was specified.
    # This parameter is not compatible with the "renew" parameter. Behavior
    # is undefined if both are set to True.
    # See details here: http://www.jasig.org/cas/client-integration/gateway
    gateway = False

    # Use which CAS protocol to validate ticket
    # one of ['CAS 1.0','CAS 2.0']
    ticketValidationSpecification = 'CAS 1.0'
    ticketValidationSpecification_values = ['CAS 1.0','CAS 2.0']

    # The start of the proxy callback url.
    # You should set it point to an instance of this class with protocol 'https'.
    # The result url will be '{proxyCallbackUrlPrefix}/proxyCallback'.
    # If set, means this service will be used as a proxier to access
    # back-end service on behalf of a particular user.
    proxyCallbackUrlPrefix = ''

    # If you provide either the acceptAnyProxy or the allowedProxyChains
    # parameters, a Cas20ProxyTicketValidator will be constructed. Otherwise
    # a Cas20ServiceTicketValidator will be constructed that does not accept
    # proxy tickets.

    # Whether any proxy is OK
    acceptAnyProxy = False

    # Allowed proxy chains.
    # Each acceptable proxy chain should include a space-separated list of URLs.
    # These URLs are proxier's proxyCallbackUrl.
    allowedProxyChains = []

    # Allowed Redirect From Cookie
    allowedRedirectFromCookie = False

    specificRedirectLinks = []

    security = ClassSecurityInfo()

    _properties = (
        {
            'id': 'serviceUrl',
            'lable': 'Service URL',
            'type': 'string',
            'mode': 'w'
            },
        {
            'id': 'casServerUrlPrefix',
            'lable': 'CAS Server URL Prefix',
            'type': 'string',
            'mode': 'w'
            },
        {
            'id': 'useSession',
            'lable': 'Use Session',
            'type': 'boolean',
            'mode': 'w'
            },
        {
            'id': 'renew',
            'lable': 'Renew',
            'type': 'boolean',
            'mode': 'w'
            },
        {
            'id': 'gateway',
            'lable': 'Gateway',
            'type': 'boolean',
            'mode': 'w'
            },
        {
            'id': 'ticketValidationSpecification',
            'lable': 'Ticket Validation Specification',
            'select_variable': 'ticketValidationSpecification_values',
            'type': 'selection',
            'mode': 'w'
            },
        {
            'id': 'proxyCallbackUrlPrefix',
            'lable': 'Proxy Callback URL Prefix',
            'type': 'string',
            'mode': 'w'
            },
        {
            'id': 'acceptAnyProxy',
            'lable': 'Accept Any Proxy',
            'type': 'boolean',
            'mode': 'w'
            },
        {
            'id': 'allowedProxyChains',
            'label': 'Allowed Proxy Chains',
            'type': 'lines',
            'mode': 'w'
            },
        {
            'id': 'allowedRedirectFromCookie',
            'lable': 'Allowed Redirect From Cookie',
            'type': 'boolean',
            'mode': 'w'
            },
        {
            'id': 'specificRedirectLinks',
            'lable': 'Specific Redirect link if allowedRedirectFromCookie == True, condition|link_url|message',
            'type': 'lines',
            'mode': 'w'
            },
        )

    def __init__( self, id, title ):
        self._id = self.id = id
        self.title = title
        self._pgtStorage = ProxyGrantingTicketStorage()
        self._sessionStorage = SessionMappingStorage()

    security.declarePrivate( 'extractCredentials' )
    def extractCredentials( self, request ):
        ''' Extract credentials from session or 'request'. '''
        creds = {}

        # Do logout if logout request found
        logoutRequest = request.form.get( 'logoutRequest', '' )
        if logoutRequest:
            self.logoutCallback()
            return creds

        sdm = getattr( self, 'session_data_manager', None )
        assert sdm is not None, 'No session data manager found!'

        session = sdm.getSessionData( create=0 )
        assertion = self.getAssertion( session )
        if not assertion:
            # Not already authenticated. Is there a ticket in the URL?
            ticket = request.form.get( 'ticket', None )
            if not ticket:
                return None # No CAS authentification

            service = self.getService()
            assertion = self.validateServiceTicket( service, ticket )

            # Save current user's session to be used by 'Single Sign Out'
            if not session:
                session = sdm.getSessionData( create=1 )

            # Get session token as id, it is more reliable
            sessionId = session.getContainerKey()
            self._sessionStorage.addSession( ticket, sessionId )

            # Create a session in the default Plone session factory for username
            # depending on the PLONE version used
            username = assertion.getPrincipal().getId()
            if PLONE4:
                # It's needed to cast username which is an unicode type to an
                # str as plone.session does a direct concatenation of unicode
                # username and other string types that leads to an UnicodeDecode
                # error otherwise. It's needed to address plone.session to do
                # not so. Meanwhile, casting the username assumes that there are
                # non ascii chars in it.
                self.session._setupSession(str(username), request.response)
            else:
                # is PLONE3
                cookie = self.session.source.createIdentifier(username)
                creds['cookie'] = cookie
                creds['source'] = 'plone.session'
                self.session.setupSession(username, request.response)

            # Save assertion into session
            if self.useSession:
                # During ticket validation process, the server with this client
                # installed will be callback several times by CAS, this will
                # makes the session data object we get before stale, so here
                # we get it again.
                session = sdm.getSessionData()
                session.set( self.CAS_ASSERTION, assertion )

        creds['login'] = assertion.getPrincipal().getId()
        if self.allowedRedirectFromCookie:
            _redirect_url = request.cookies.get(self.CAS_REDIRECT_URL)
            if _redirect_url is not None:
                request.response.expireCookie(self.CAS_REDIRECT_URL)
                _msg = request.cookies.get(self.CAS_REDIRECT_MSG)
                if _msg:
                    messages = IStatusMessage(request)
                    if not isinstance(_msg, unicode):
                        _msg = _msg.decode('utf-8')
                    messages.add(_msg, type=u"info")
                    request.response.expireCookie(self.CAS_REDIRECT_MSG)
                return request.response.redirect(_redirect_url)
        return creds

    security.declarePrivate( 'authenticateCredentials' )
    def authenticateCredentials( self, credentials ):
        if credentials['extractor'] != self.getId():
            return None

        login = credentials['login']
        return ( login, login )

    security.declarePrivate( 'challenge' )
    def challenge( self, request, response, **kw ):
        ''' Challenge the user for credentials. '''
        session = request.SESSION
        try:
            login = session[self.CAS_ASSERTION].getPrincipal().getId()
        except (LookupError, TypeError):
            login = None
        # Remove current credentials.
        session[self.CAS_ASSERTION] = None
        if self.allowedRedirectFromCookie:
            if login is None:
                response.setCookie(self.CAS_REDIRECT_URL, request.URL0, path='/')
            else:
                org_url = request.URL0
                _url = ""
                _msg = ""
                specific_links = []
                for data in self.specificRedirectLinks:
                    if not data.strip():
                        continue
                    try:
                        specific_links.append([d.strip() for d in data.strip().split("|", 2)])
                    except:
                        pass
                for link in specific_links:
                    if link[0] in org_url:
                        if len(link) > 1:
                            _url = request.BASE0 + "/" + link[1]
                        if len(link) > 2:
                            _msg = link[2]
                        break
                if not _url:
                    if hasattr(request, 'URL2'):
                        _url = request.URL2
                    elif hasattr(request, 'URL1'):
                        _url = request.URL1
                    else:
                        _url = org_url
                response.setCookie(self.CAS_REDIRECT_URL, _url, path='/')
                if _msg:
                    response.setCookie(self.CAS_REDIRECT_MSG, _msg, path='/')

        # Redirect to CAS login URL.
        if self.casServerUrlPrefix:
            url = self.getLoginURL() + '?service=' + self.getService()
            if self.renew:
                url += '&renew=true'
            if self.gateway:
                url += '&gateway=true'

            response.redirect( url, lock=1 )
            return 1

        # Fall through to the standard unauthorized() call.
        return 0

    security.declarePrivate( 'resetCredentials' )
    def resetCredentials( self, request, response ):
        ''' Clears credentials and redirects to CAS logout page. '''
        session = request.SESSION
        session.clear()

        if self.casServerUrlPrefix:
            return response.redirect( self.getLogoutURL(), lock=1 )

    security.declarePublic( 'proxyCallback' )
    def proxyCallback( self, pgtId=None, pgtIou=None ):
        ''' See interfaces.IAnzCASClient. '''
        ret = 'success'
        if pgtId and pgtIou:
            self._pgtStorage.add( pgtIou, pgtId )
            ret = '<?xml version=\"1.0\"?>'
            ret += '<casClient:proxySuccess xmlns:casClient="http://www.yale.edu/tp/casClient" />'

        return ret

    security.declarePublic( 'logoutCallback' )
    def logoutCallback( self ):
        ''' See interfaces.IAnzCASClient. '''
        msg = 'No session id found.'
        dom = minidom.parseString( self.REQUEST.form.get('logoutRequest','') )
        SAMLP_NS = 'urn:oasis:names:tc:SAML:2.0:protocol'
        elements = dom.getElementsByTagNameNS( SAMLP_NS,
                                               'SessionIndex' )
        if elements:
            mappingId = elements[0].firstChild.data
            sessionId = self._sessionStorage.getSessionId( mappingId )
            self._sessionStorage.removeByMappingId( mappingId )

            sdm = getattr( self, 'session_data_manager', None )
            assert sdm is not None, 'No session data manager found!'

            session = sdm.getSessionDataByKey( sessionId )
            if session:
                session.clear()

                # We must commit here to make sure the session will be cleared.
                transaction.commit()

            msg = 'Logout seccess.'

        return msg

    security.declarePublic( 'validateProxyTicket' )
    def validateProxyTicket( self, ticket ):
        ''' See interfaces.IAnzCASClient. '''
        validator = Cas20ProxyTicketValidator(
            self.casServerUrlPrefix,
            self._pgtStorage,
            acceptAnyProxy=self.acceptAnyProxy,
            allowedProxyChains=self.allowedProxyChains,
            renew=self.renew )

        try:
            assertion = validator.validate( ticket, self.getService() )
        except BaseException, e:
            LOG.warning( e )
            return False, None
        except Exception, e:
            LOG.warning( e )
            return False, None
コード例 #2
0
class AnzCASClient(BasePlugin, Cacheable):
    ''' Anz CAS client, Implement as a PAS plugin. '''

    implements(IAnzCASClient)

    meta_type = 'Anz CAS Client'

    # Session variable use to save assertion
    CAS_ASSERTION = '__cas_assertion'

    # The start of the CAS server URL
    casServerUrlPrefix = ''

    # An identify of current service.
    # CAS will redirects to here after login.
    # Set this explicitly but not determine it automatically from request makes
    # us get more security assurance.
    # https://wiki.jasig.org/display/CASC/CASFilter
    serviceUrl = ''

    # Whether to store the Assertion in session or not.
    # If sessions are not used, proxy granting ticket will be required for
    # each request. Default set to True.
    useSession = True

    # If set to True, CAS will ask user for credentials again to authenticate,
    # this may be used for high-security applications. Default set to False.
    renew = False

    # If set to True, CAS will not ask the user for credentials. If the
    # user has a pre-existing single sign-on session with CAS, or if a single
    # sign-on session can be established through non-interactive means
    # (i.e. trust authentication), CAS MAY redirect the client to the URL
    # specified by the "service" parameter, appending a valid service ticket.
    # (CAS also MAY interpose an advisory page informing the client that a CAS
    # authentication has taken place.) If the client does not have a single
    # sign-on session with CAS, and a non-interactive authentication cannot be
    # established, CAS MUST redirect the client to the URL specified by the
    # "service" parameter with no "ticket" parameter appended to the URL. If
    # the "service" parameter is not specified and "gateway" is set, the
    # behavior of CAS is undefined. It is RECOMMENDED that in this case, CAS
    # request credentials as if neither parameter was specified.
    # This parameter is not compatible with the "renew" parameter. Behavior
    # is undefined if both are set to True.
    # See details here: http://www.jasig.org/cas/client-integration/gateway
    gateway = False

    # Use which CAS protocol to validate ticket
    # one of ['CAS 1.0','CAS 2.0']
    ticketValidationSpecification = 'CAS 1.0'
    ticketValidationSpecification_values = ['CAS 1.0', 'CAS 2.0']

    # The start of the proxy callback url.
    # You should set it point to an instance of this class with protocol 'https'.
    # The result url will be '{proxyCallbackUrlPrefix}/proxyCallback'.
    # If set, means this service will be used as a proxier to access
    # back-end service on behalf of a particular user.
    proxyCallbackUrlPrefix = ''

    # If you provide either the acceptAnyProxy or the allowedProxyChains
    # parameters, a Cas20ProxyTicketValidator will be constructed. Otherwise
    # a Cas20ServiceTicketValidator will be constructed that does not accept
    # proxy tickets.

    # Whether any proxy is OK
    acceptAnyProxy = False

    # Allowed proxy chains.
    # Each acceptable proxy chain should include a space-separated list of URLs.
    # These URLs are proxier's proxyCallbackUrl.
    allowedProxyChains = []

    security = ClassSecurityInfo()

    _properties = (
        {
            'id': 'serviceUrl',
            'lable': 'Service URL',
            'type': 'string',
            'mode': 'w'
        },
        {
            'id': 'casServerUrlPrefix',
            'lable': 'CAS Server URL Prefix',
            'type': 'string',
            'mode': 'w'
        },
        {
            'id': 'useSession',
            'lable': 'Use Session',
            'type': 'boolean',
            'mode': 'w'
        },
        {
            'id': 'renew',
            'lable': 'Renew',
            'type': 'boolean',
            'mode': 'w'
        },
        {
            'id': 'gateway',
            'lable': 'Gateway',
            'type': 'boolean',
            'mode': 'w'
        },
        {
            'id': 'ticketValidationSpecification',
            'lable': 'Ticket Validation Specification',
            'select_variable': 'ticketValidationSpecification_values',
            'type': 'selection',
            'mode': 'w'
        },
        {
            'id': 'proxyCallbackUrlPrefix',
            'lable': 'Proxy Callback URL Prefix',
            'type': 'string',
            'mode': 'w'
        },
        {
            'id': 'acceptAnyProxy',
            'lable': 'Accept Any Proxy',
            'type': 'boolean',
            'mode': 'w'
        },
        {
            'id': 'allowedProxyChains',
            'label': 'Allowed Proxy Chains',
            'type': 'lines',
            'mode': 'w'
        },
    )

    def __init__(self, id, title):
        self._id = self.id = id
        self.title = title
        self._pgtStorage = ProxyGrantingTicketStorage()
        self._sessionStorage = SessionMappingStorage()

    security.declarePrivate('extractCredentials')

    def extractCredentials(self, request):
        ''' Extract credentials from session or 'request'. '''
        creds = {}

        # Do logout if logout request found
        logoutRequest = request.form.get('logoutRequest', '')
        if logoutRequest:
            self.logoutCallback()
            return creds

        sdm = getattr(self, 'session_data_manager', None)
        assert sdm is not None, 'No session data manager found!'

        session = sdm.getSessionData(create=0)
        assertion = self.getAssertion(session)
        if not assertion:
            # Not already authenticated. Is there a ticket in the URL?
            ticket = request.form.get('ticket', None)
            if not ticket:
                return None  # No CAS authentification

            service = self.getService()
            assertion = self.validateServiceTicket(service, ticket)

            # Save current user's session to be used by 'Single Sign Out'
            if not session:
                session = sdm.getSessionData(create=1)

            # Get session token as id, it is more reliable
            sessionId = session.getContainerKey()
            self._sessionStorage.addSession(ticket, sessionId)

            # Create a session in the default Plone session factory for username
            # depending on the PLONE version used
            username = assertion.getPrincipal().getId()
            if PLONE4:
                # It's needed to cast username which is an unicode type to an
                # str as plone.session does a direct concatenation of unicode
                # username and other string types that leads to an UnicodeDecode
                # error otherwise. It's needed to address plone.session to do
                # not so. Meanwhile, casting the username assumes that there are
                # non ascii chars in it.
                self.session._setupSession(str(username), request.response)
            else:
                # is PLONE3
                cookie = self.session.source.createIdentifier(username)
                creds['cookie'] = cookie
                creds['source'] = 'plone.session'
                self.session.setupSession(username, request.response)

            # Save assertion into session
            if self.useSession:
                # During ticket validation process, the server with this client
                # installed will be callback several times by CAS, this will
                # makes the session data object we get before stale, so here
                # we get it again.
                session = sdm.getSessionData()
                session.set(self.CAS_ASSERTION, assertion)

        creds['login'] = assertion.getPrincipal().getId()
        return creds

    security.declarePrivate('authenticateCredentials')

    def authenticateCredentials(self, credentials):
        if credentials['extractor'] != self.getId():
            return None

        login = credentials['login']
        return (login, login)

    security.declarePrivate('challenge')

    def challenge(self, request, response, **kw):
        ''' Challenge the user for credentials. '''
        # Remove current credentials.
        session = request.SESSION
        session[self.CAS_ASSERTION] = None

        # Redirect to CAS login URL.
        if self.casServerUrlPrefix:
            url = self.getLoginURL() + '?service=' + self.getService()
            if self.renew:
                url += '&renew=true'
            if self.gateway:
                url += '&gateway=true'

            response.redirect(url, lock=1)
            return 1

        # Fall through to the standard unauthorized() call.
        return 0

    security.declarePrivate('resetCredentials')

    def resetCredentials(self, request, response):
        ''' Clears credentials and redirects to CAS logout page. '''
        session = request.SESSION
        session.clear()

        if self.casServerUrlPrefix:
            return response.redirect(self.getLogoutURL(), lock=1)

    security.declarePublic('proxyCallback')

    def proxyCallback(self, pgtId=None, pgtIou=None):
        ''' See interfaces.IAnzCASClient. '''
        ret = 'success'
        if pgtId and pgtIou:
            self._pgtStorage.add(pgtIou, pgtId)
            ret = '<?xml version=\"1.0\"?>'
            ret += '<casClient:proxySuccess xmlns:casClient="http://www.yale.edu/tp/casClient" />'

        return ret

    security.declarePublic('logoutCallback')

    def logoutCallback(self):
        ''' See interfaces.IAnzCASClient. '''
        msg = 'No session id found.'
        dom = minidom.parseString(self.REQUEST.form.get('logoutRequest', ''))
        SAMLP_NS = 'urn:oasis:names:tc:SAML:2.0:protocol'
        elements = dom.getElementsByTagNameNS(SAMLP_NS, 'SessionIndex')
        if elements:
            mappingId = elements[0].firstChild.data
            sessionId = self._sessionStorage.getSessionId(mappingId)
            self._sessionStorage.removeByMappingId(mappingId)

            sdm = getattr(self, 'session_data_manager', None)
            assert sdm is not None, 'No session data manager found!'

            session = sdm.getSessionDataByKey(sessionId)
            if session:
                session.clear()

                # We must commit here to make sure the session will be cleared.
                transaction.commit()

            msg = 'Logout seccess.'

        return msg

    security.declarePublic('validateProxyTicket')

    def validateProxyTicket(self, ticket):
        ''' See interfaces.IAnzCASClient. '''
        validator = Cas20ProxyTicketValidator(
            self.casServerUrlPrefix,
            self._pgtStorage,
            acceptAnyProxy=self.acceptAnyProxy,
            allowedProxyChains=self.allowedProxyChains,
            renew=self.renew)

        try:
            assertion = validator.validate(ticket, self.getService())
        except BaseException, e:
            LOG.warning(e)
            return False, None
        except Exception, e:
            LOG.warning(e)
            return False, None
コード例 #3
0
ファイル: casclient.py プロジェクト: terapyon/anz.casclient
 def __init__( self, id, title ):
     self._id = self.id = id
     self.title = title
     self._pgtStorage = ProxyGrantingTicketStorage()
     self._sessionStorage = SessionMappingStorage()
コード例 #4
0
 def __init__(self, id, title):
     self._id = self.id = id
     self.title = title
     self._pgtStorage = ProxyGrantingTicketStorage()
     self._sessionStorage = SessionMappingStorage()