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
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
def __init__( self, id, title ): self._id = self.id = id self.title = title self._pgtStorage = ProxyGrantingTicketStorage() self._sessionStorage = SessionMappingStorage()
def __init__(self, id, title): self._id = self.id = id self.title = title self._pgtStorage = ProxyGrantingTicketStorage() self._sessionStorage = SessionMappingStorage()