Changeset 7287
- Timestamp:
- 06/08/10 09:49:47 (11 years ago)
- Location:
- TI12-security/trunk/NDGSecurity/python
- Files:
-
- 7 edited
Legend:
- Unmodified
- Added
- Removed
-
TI12-security/trunk/NDGSecurity/python/ndg_security_common/ndg/security/common/saml_utils/esg/xml/etree.py
r7076 r7287 11 11 import logging 12 12 log = logging.getLogger(__name__) 13 import re14 13 15 14 from xml.etree import ElementTree 16 15 17 16 from ndg.saml.xml import XMLTypeParseError, UnknownAttrProfile 18 from ndg.saml.xml.etree import (AttributeValueElementTreeBase, ResponseElementTree, 19 QName) 17 from ndg.saml.xml.etree import (AttributeValueElementTreeBase, 18 ResponseElementTree, 19 QName) 20 20 21 21 from ndg.security.common.saml_utils.esg import XSGroupRoleAttributeValue -
TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/wsgi/authz/__init__.py
r7155 r7287 15 15 from time import time 16 16 from urlparse import urlunsplit 17 from httplib import UNAUTHORIZED, FORBIDDEN 17 import httplib 18 18 19 19 from paste.cascade import Cascade … … 22 22 23 23 from ndg.security.common.utils.classfactory import importClass 24 from ndg.security.common.X509 import X509Cert25 from ndg.saml.saml2.binding.soap.client.attributequery import \26 AttributeQuerySslSOAPBinding27 28 24 from ndg.security.common.credentialwallet import SAMLCredentialWallet 29 from ndg.security.server.wsgi import (NDGSecurityMiddlewareBase, 30 NDGSecurityMiddlewareConfigError) 31 32 from ndg.security.server.wsgi import (NDGSecurityMiddlewareBase, 33 NDGSecurityMiddlewareConfigError) 34 from ndg.security.server.wsgi.session import (SessionMiddlewareBase, 35 SessionHandlerMiddleware) 25 from ndg.security.server.wsgi import NDGSecurityMiddlewareBase 26 from ndg.security.server.wsgi.authz.pep import SamlPepFilter 36 27 from ndg.security.server.wsgi.authz.result_handler import \ 37 28 PEPResultHandlerMiddlewareBase 38 29 from ndg.security.server.wsgi.authz.result_handler.basic import \ 39 30 PEPResultHandlerMiddleware 40 41 from ndg.security.common.authz.pip import (PIPBase, PIPAttributeQuery,42 PIPAttributeResponse)43 44 from ndg.security.common.authz import Subject45 from ndg.security.common.authz.msi import (Policy, PDP, Request, Response,46 Resource)47 31 48 32 49 class PEPFilterError(Exception): 50 """Base class for PEPFilter exception types""" 33 class Http403ForbiddenStatusHandler(object): 34 """Handler to catch HTTP 403 Forbidden responses. It integrates with 35 AuthKit's MultiHandler. This enables the given middleware to be substituted 36 into the WSGI stack should a 403 status be detected set from upstream 37 middleware. 38 39 @cvar TRIGGER_HTTP_STATUS_CODE: status code to catch - HTTP 403 Forbidden 40 @type TRIGGER_HTTP_STATUS_CODE: basestring 41 """ 42 TRIGGER_HTTP_STATUS_CODE = str(httplib.FORBIDDEN) 51 43 52 53 class PEPFilterConfigError(PEPFilterError):54 """Configuration related error for PEPFilter"""55 56 57 class PEPFilter(SessionMiddlewareBase):58 """PEP (Policy Enforcement Point) WSGI Middleware. The PEP enforces59 access control decisions made by the PDP (Policy Decision Point). In60 this case, it follows the WSG middleware filter pattern and is configured61 in a pipeline upstream of the application(s) which it protects. if an62 access denied decision is made, the PEP enforces this by returning a63 403 Forbidden HTTP response without the application middleware executing64 65 SessionMiddlewareBase base class defines user session key and66 isAuthenticated property67 """68 TRIGGER_HTTP_STATUS_CODE = str(FORBIDDEN)69 MIDDLEWARE_ID = 'PEPFilter'70 POLICY_PARAM_PREFIX = 'policy.'71 72 SESSION_KEYNAME = 'sessionKey'73 POLICY_FILEPATH_PARAMNAME = 'filePath'74 75 def __init__(self, app, global_conf, prefix='', **local_conf):76 """Initialise the PIP (Policy Information Point) and PDP (Policy77 Decision Point). The PDP makes access control decisions based on78 a given policy. The PIP manages the retrieval of user credentials on79 behalf of the PDP80 81 @type app: callable following WSGI interface82 @param app: next middleware application in the chain83 @type global_conf: dict84 @param global_conf: PasteDeploy global configuration dictionary85 @type prefix: basestring86 @param prefix: prefix for configuration items87 @type local_conf: dict88 @param local_conf: PasteDeploy application specific configuration89 dictionary90 91 """92 # Initialise the PDP reading in the policy93 policyCfg = PEPFilter._filterKeywords(local_conf,94 PEPFilter.POLICY_PARAM_PREFIX)95 self.policyFilePath = policyCfg[PEPFilter.POLICY_FILEPATH_PARAMNAME]96 policy = Policy.Parse(policyCfg[PEPFilter.POLICY_FILEPATH_PARAMNAME])97 98 # Initialise the Policy Information Point to None. This object is99 # created and set later. See AuthorizationMiddlewareBase.100 self.pdp = PDP(policy, None)101 102 self.sessionKey = local_conf.get(PEPFilter.SESSION_KEYNAME,103 PEPFilter.propertyDefaults[104 PEPFilter.SESSION_KEYNAME])105 106 super(PEPFilter, self).__init__(app,107 global_conf,108 prefix=prefix,109 **local_conf)110 111 @NDGSecurityMiddlewareBase.initCall112 def __call__(self, environ, start_response):113 """114 @type environ: dict115 @param environ: WSGI environment variables dictionary116 @type start_response: function117 @param start_response: standard WSGI start response function118 @rtype: iterable119 @return: response120 """121 session = environ.get(self.sessionKey)122 if session is None:123 raise PEPFilterConfigError('No beaker session key "%s" found in '124 'environ' % self.sessionKey)125 126 queryString = environ.get('QUERY_STRING', '')127 resourceURI = urlunsplit(('', '', self.pathInfo, queryString, ''))128 129 # Check for a secured resource130 matchingTargets = self._getMatchingTargets(resourceURI)131 targetMatch = len(matchingTargets) > 0132 if not targetMatch:133 log.debug("PEPFilter.__call__: granting access - no matching URI "134 "path target was found in the policy for URI path [%s]",135 resourceURI)136 return self._app(environ, start_response)137 138 log.debug("PEPFilter.__call__: found matching target(s):\n\n %s\n"139 "\nfrom policy file [%s] for URI Path=[%s]\n",140 '\n'.join(["RegEx=%s" % t for t in matchingTargets]),141 self.policyFilePath,142 resourceURI)143 144 if not self.isAuthenticated:145 log.info("PEPFilter.__call__: user is not authenticated - setting "146 "HTTP 401 response ...")147 148 # Set a 401 response for an authentication handler to capture149 return self._setErrorResponse(code=UNAUTHORIZED)150 151 log.debug("PEPFilter.__call__: creating request to call PDP to check "152 "user authorisation ...")153 154 # Make a request object to pass to the PDP.155 request = Request()156 request.subject[Subject.USERID_NS] = session['username']157 request.resource[Resource.URI_NS] = resourceURI158 159 160 # Call the PDP161 response = self.pdp.evaluate(request)162 163 # Record the result in the user's session to enable later164 # interrogation by the AuthZResultHandlerMiddleware165 PEPFilter.setSession(session, request, response)166 167 if response.status == Response.DECISION_PERMIT:168 log.info("PEPFilter.__call__: PDP granted access for URI path "169 "[%s] using policy [%s]",170 resourceURI,171 self.policyFilePath)172 173 return self._app(environ, start_response)174 else:175 log.info("PEPFilter.__call__: PDP returned a status of [%s] "176 "denying access for URI path [%s] using policy [%s]",177 response.decisionValue2String[response.status],178 resourceURI,179 self.policyFilePath)180 181 # Trigger AuthZResultHandlerMiddleware by setting a response182 # with HTTP status code equal to the TRIGGER_HTTP_STATUS_CODE class183 # attribute value184 triggerStatusCode = int(PEPFilter.TRIGGER_HTTP_STATUS_CODE)185 return self._setErrorResponse(code=triggerStatusCode)186 187 44 @classmethod 188 def setSession(cls, session, request, response, save=True): 189 """Set PEP context information in the Beaker session using standard key 190 names 191 192 @param session: beaker session 193 @type session: beaker.session.SessionObject 194 @param request: authorisation request 195 @type request: ndg.security.common.authz.msi.Request 196 @param response: authorisation response 197 @type response: ndg.security.common.authz.msi.Response 198 @param save: determines whether session is saved or not 199 @type save: bool 200 """ 201 session[cls.PEPCTX_SESSION_KEYNAME] = { 202 cls.PEPCTX_REQUEST_SESSION_KEYNAME: request, 203 cls.PEPCTX_RESPONSE_SESSION_KEYNAME: response, 204 cls.PEPCTX_TIMESTAMP_SESSION_KEYNAME: time() 205 } 206 207 if save: 208 session.save() 209 210 def _getMatchingTargets(self, resourceURI): 211 """This method may only be called following __call__ as __call__ 212 updates the pathInfo property 213 214 @type resourceURI: basestring 215 @param resourceURI: the URI of the requested resource 216 @rtype: list 217 @return: return list of policy target objects matching the current 218 path 219 """ 220 matchingTargets = [target for target in self.pdp.policy.targets 221 if target.regEx.match(resourceURI) is not None] 222 return matchingTargets 223 224 def multiHandlerInterceptFactory(self): 225 """Return a checker function for use with AuthKit's MultiHandler. 226 MultiHandler can be used to catch HTTP 403 Forbidden responses set by 227 an application and call middleware (AuthZResultMiddleware) to handle 228 the access denied message. 229 """ 230 231 def multiHandlerIntercept(environ, status, headers): 232 """AuthKit MultiHandler checker function to intercept 233 unauthorised response status codes from applications to be 234 protected. This function's definition is embedded into a 235 factory method so that this function has visibility to the 236 PEPFilter object's attributes if required. 237 238 @type environ: dict 239 @param environ: WSGI environment dictionary 240 @type status: basestring 241 @param status: HTTP response code set by application middleware 242 that this intercept function is to protect 243 @type headers: list 244 @param headers: HTTP response header content""" 245 246 if status.startswith(PEPFilter.TRIGGER_HTTP_STATUS_CODE): 247 log.debug("PEPFilter: found [%s] status for URI path [%s]: " 248 "invoking access denied response", 249 PEPFilter.TRIGGER_HTTP_STATUS_CODE, 250 environ['PATH_INFO']) 251 return True 252 else: 253 # No match - it's publicly accessible 254 log.debug("PEPFilter: the return status [%s] for this URI " 255 "path [%s] didn't match the trigger status [%s]", 256 status, 257 environ['PATH_INFO'], 258 PEPFilter.TRIGGER_HTTP_STATUS_CODE) 259 return False 260 261 return multiHandlerIntercept 262 263 @staticmethod 264 def _filterKeywords(conf, prefix): 265 filteredConf = {} 266 prefixLen = len(prefix) 267 for k, v in conf.items(): 268 if k.startswith(prefix): 269 filteredConf[k[prefixLen:]] = conf.pop(k) 270 271 return filteredConf 272 273 def _getPDP(self): 274 if self._pdp is None: 275 raise TypeError("PDP object has not been initialised") 276 return self._pdp 277 278 def _setPDP(self, pdp): 279 if not isinstance(pdp, (PDP, None.__class__)): 280 raise TypeError("Expecting %s or None type for pdp; got %r" % 281 (PDP.__class__.__name__, pdp)) 282 self._pdp = pdp 283 284 pdp = property(fget=_getPDP, 285 fset=_setPDP, 286 doc="Policy Decision Point object makes access control " 287 "decisions on behalf of the PEP") 288 289 290 class SamlPIPMiddlewareError(Exception): 291 """Base class for SAML based Policy Information Point WSGI middleware 292 exception types 293 """ 294 295 296 class SamlPIPMiddlewareConfigError(SamlPIPMiddlewareError): 297 """Configuration related error for SAML Policy Information Point WSGI 298 middleware 299 """ 300 301 302 class SamlPIPMiddleware(PIPBase, NDGSecurityMiddlewareBase): 303 '''Extend Policy Information Point to enable caching of SAML credentials in 304 a SAMLCredentialWallet object held in beaker.session 305 ''' 306 ENVIRON_KEYNAME = 'ndg.security.server.wsgi.authz.SamlPIPMiddleware' 307 308 propertyDefaults = { 309 'sessionKey': 'beaker.session.ndg.security', 310 } 311 propertyDefaults.update(NDGSecurityMiddlewareBase.propertyDefaults) 312 313 CREDENTIAL_WALLET_SESSION_KEYNAME = \ 314 SessionHandlerMiddleware.CREDENTIAL_WALLET_SESSION_KEYNAME 315 USERNAME_SESSION_KEYNAME = \ 316 SessionHandlerMiddleware.USERNAME_SESSION_KEYNAME 317 318 ATTRIBUTE_QUERY_ATTRNAME = 'attributeQuery' 319 LEN_ATTRIBUTE_QUERY_ATTRNAME = len(ATTRIBUTE_QUERY_ATTRNAME) 320 321 def __init__(self, app, global_conf, prefix='', **local_conf): 322 ''' 323 @type app: callable following WSGI interface 324 @param app: next middleware application in the chain 325 @type global_conf: dict 326 @param global_conf: PasteDeploy global configuration dictionary 327 @type prefix: basestring 328 @param prefix: prefix for configuration items 329 @type local_conf: dict 330 @param local_conf: PasteDeploy application specific configuration 331 dictionary 332 ''' 333 self.session = None 334 self.__attributeQueryBinding = AttributeQuerySslSOAPBinding() 335 336 nameOffset = len(prefix) 337 for k in local_conf.keys(): 338 if k.startswith(prefix): 339 val = local_conf.pop(k) 340 name = k[nameOffset:] 341 setattr(self, name, val) 342 343 if not self.__attributeQueryBinding.issuerName: 344 issuerX509Cert = X509Cert.Read( 345 self.__attributeQueryBinding.sslCtxProxy.sslCertFilePath) 346 self.__attributeQueryBinding.issuerName = str(issuerX509Cert.dn) 347 348 NDGSecurityMiddlewareBase.__init__(self, app, {}) 349 350 def __setattr__(self, name, value): 351 """Enable setting of AttributeQuerySslSOAPBinding attributes from 352 names starting with attributeQuery.* / attributeQuery_*. Addition for 353 setting these values from ini file 354 """ 355 356 # Coerce into setting AttributeQuerySslSOAPBinding attributes - 357 # names must start with 'attributeQuery\W' e.g. 358 # attributeQuery.clockSkew or attributeQuery_issuerDN 359 if name.startswith(SamlPIPMiddleware.ATTRIBUTE_QUERY_ATTRNAME): 360 setattr(self.__attributeQueryBinding, 361 name[SamlPIPMiddleware.LEN_ATTRIBUTE_QUERY_ATTRNAME+1:], 362 value) 363 else: 364 super(SamlPIPMiddleware, self).__setattr__(name, value) 365 366 @property 367 def attributeQueryBinding(self): 368 """SAML SOAP Attribute Query client binding object""" 369 return self.__attributeQueryBinding 370 371 def __call__(self, environ, start_response): 372 """Take a copy of the session object so that it is in scope for 373 attributeQuery call and add this instance to the environ 374 so that the PEPFilter can retrieve it and pass on to the PDP 45 def intercept(cls, environ, status, headers): 46 """Checker function for AuthKit Multihandler 375 47 376 48 @type environ: dict 377 @param environ: WSGI environment variables dictionary 378 @type start_response: function 379 @param start_response: standard WSGI start response function 380 @rtype: iterable 381 @return: response 382 """ 383 self.session = environ.get(self.sessionKey) 384 if self.session is None: 385 raise SamlPIPMiddlewareConfigError('No beaker session key "%s" ' 386 'found in environ' % 387 self.sessionKey) 388 environ[SamlPIPMiddleware.ENVIRON_KEYNAME] = self 49 @param environ: WSGI environment dictionary 50 @type status: basestring 51 @param status: HTTP response code set by application middleware 52 that this intercept function is to protect 53 @type headers: list 54 @param headers: HTTP response header content""" 389 55 390 return self._app(environ, start_response) 56 if status.startswith(cls.TRIGGER_HTTP_STATUS_CODE): 57 log.debug("Found [%s] status for URI path [%s]: invoking access " 58 "denied response", 59 cls.TRIGGER_HTTP_STATUS_CODE, 60 environ['PATH_INFO']) 61 return True 62 else: 63 # No match - it's publicly accessible 64 log.debug("The return status [%s] for this URI path [%s] didn't " 65 "match the trigger status [%s]", 66 status, 67 environ['PATH_INFO'], 68 cls.TRIGGER_HTTP_STATUS_CODE) 69 return False 70 391 71 392 def attributeQuery(self, attributeQuery): 393 """Query the Attribute Authority specified in the request to retrieve 394 the attributes if any corresponding to the subject 395 396 @type attributeResponse: PIPAttributeQuery 397 @param attributeResponse: 398 @rtype: PIPAttributeResponse 399 @return: response containing the attributes retrieved from the 400 Attribute Authority""" 401 if not isinstance(attributeQuery, PIPAttributeQuery): 402 raise TypeError('Expecting %r type for input "attributeQuery"; ' 403 'got %r' % (PIPAttributeQuery, 404 type(attributeQuery))) 405 406 attributeAuthorityURI = attributeQuery[ 407 PIPAttributeQuery.ATTRIBUTEAUTHORITY_NS] 408 409 log.debug("SamlPIPMiddleware: received attribute query: %r", 410 attributeQuery) 411 412 # Check for a wallet in the current session - if not present, create 413 # one. See ndg.security.server.wsgi.authn.SessionHandlerMiddleware 414 # for session keys. The 'credentialWallet' key is deleted along with 415 # any other security keys when the user logs out 416 credentialWalletKeyName = \ 417 SamlPIPMiddleware.CREDENTIAL_WALLET_SESSION_KEYNAME 418 usernameKeyName = SamlPIPMiddleware.USERNAME_SESSION_KEYNAME 419 420 if not credentialWalletKeyName in self.session: 421 log.debug("SamlPIPMiddleware.attributeQuery: adding a " 422 "Credential Wallet to user session [%s] ...", 423 self.session[usernameKeyName]) 424 425 credentialWallet = SAMLCredentialWallet() 426 credentialWallet.userId = self.session[usernameKeyName] 427 428 self.session[credentialWalletKeyName] = credentialWallet 429 self.session.save() 430 else: 431 # Take reference to wallet for efficiency 432 credentialWallet = self.session[credentialWalletKeyName] 433 434 # Check for existing credentials cached in wallet 435 credentialItem = credentialWallet.credentialsKeyedByURI.get( 436 attributeAuthorityURI) 437 if credentialItem is None: 438 # No assertion is cached - make a fresh SAML Attribute Query 439 self.attributeQueryBinding.subjectID = credentialWallet.userId 440 response = self.attributeQueryBinding.send( 441 uri=attributeAuthorityURI) 442 for assertion in response.assertions: 443 credentialWallet.addCredential(assertion, 444 attributeAuthorityURI=attributeAuthorityURI, 445 verifyCredential=False) 446 447 log.debug("SamlPIPMiddleware.attributeQuery: updating Credential " 448 "Wallet with retrieved SAML Attribute Assertion " 449 "for user session [%s]", self.session[usernameKeyName]) 450 else: 451 log.debug("SamlPIPMiddleware.attributeQuery: retrieved existing " 452 "SAML Attribute Assertion cached in Credential Wallet " 453 "for user session [%s]", self.session[usernameKeyName]) 454 455 attributeResponse = PIPAttributeResponse() 456 attributeResponse[Subject.ROLES_NS] = [] 457 458 # Unpack assertion attribute values and add to the response object 459 for credentialItem in credentialWallet.credentials.values(): 460 for statement in credentialItem.credential.attributeStatements: 461 for attribute in statement.attributes: 462 attributeResponse[Subject.ROLES_NS] += [ 463 attributeValue.value 464 for attributeValue in attribute.attributeValues 465 if attributeValue.value not in attributeResponse[ 466 Subject.ROLES_NS] 467 ] 468 469 log.debug("SamlPIPMiddleware.attributeQuery response: %r", 470 attributeResponse) 471 472 return attributeResponse 473 474 475 class AuthorizationMiddlewareError(Exception): 476 """Base class for AuthorizationMiddlewareBase exceptions""" 477 478 479 class AuthorizationMiddlewareConfigError(Exception): 480 """AuthorizationMiddlewareBase configuration related exceptions""" 72 class AuthorisationFilterConfigError(Exception): 73 """AuthorisationFilterBase configuration related exceptions""" 481 74 482 75 483 class AuthorizationMiddlewareBase(NDGSecurityMiddlewareBase): 484 '''Virtual class - A base Handler to call Policy Enforcement Point 485 middleware to intercept requests and enforce access control decisions. 76 class AuthorisationFilter(object): 77 '''NDG Security Authorisation filter wraps the Policy Enforcement Point 78 (PEP) filter to intercept requests and enforce access control decisions and 79 result handler middleware which enables a customised response given an 80 authorisation denied decision from the PEP filter. 81 ''' 82 PEP_PARAM_PREFIX = 'pep.' 83 RESULT_HANDLER_PARAMNAME = "resultHandler" 84 RESULT_HANDLER_PARAM_PREFIX = RESULT_HANDLER_PARAMNAME + '.' 85 RESULT_HANDLER_STATIC_CONTENT_DIR_PARAMNAME = 'staticContentDir' 486 86 487 Extend THIS class adding the new type to any WSGI middleware chain ahead of 488 the application(s) which it is to protect. To make an implementation for 489 this virtual class, set PIP_MIDDLEWARE_CLASS in the derived type to a 490 valid Policy Information Point Class. Use in conjunction with 491 ndg.security.server.wsgi.authn.AuthenticationMiddleware 492 ''' 493 PEP_PARAM_PREFIX = 'pep.filter.' 494 PIP_PARAM_PREFIX = 'pip.' 495 PEP_RESULT_HANDLER_PARAMNAME = "pepResultHandler" 496 PEP_RESULT_HANDLER_PARAM_PREFIX = PEP_RESULT_HANDLER_PARAMNAME + '.' 497 PEP_RESULT_HANDLER_STATIC_CONTENT_DIR_PARAMNAME = 'staticContentDir' 498 499 class PIP_MIDDLEWARE_CLASS(object): 500 """Policy Information Point WSGI middleware abstract base, 501 implementations should retrieve user credentials to enable the PDP to 502 make access control decisions 503 """ 504 def __init__(self, app, global_conf, prefix='', **local_conf): 505 raise NotImplementedError(' '.join( 506 AuthorizationMiddlewareBase.PIP_MIDDLEWARE_CLASS.__doc__.split()) 507 ) 508 509 def __init__(self, app, global_conf, prefix='', **app_conf): 87 @classmethod 88 def filter_app_factory(cls, app, global_conf, prefix='', **app_conf): 510 89 """Set-up Policy Enforcement Point to enforce access control decisions 511 90 based on the URI path requested and/or the HTTP response code set by … … 523 102 dictionary 524 103 """ 525 cls = AuthorizationMiddlewareBase 104 # Allow for static content for use with PEP result handler middleware 105 resultHandlerParamPrefix = prefix + cls.RESULT_HANDLER_PARAM_PREFIX 106 resultHandlerStaticContentDirParamName = \ 107 resultHandlerParamPrefix + \ 108 cls.RESULT_HANDLER_STATIC_CONTENT_DIR_PARAMNAME 526 109 527 # Allow for static content for use with PEP result handler middleware 528 pepResultHandlerParamPrefix = prefix + \ 529 cls.PEP_RESULT_HANDLER_PARAM_PREFIX 530 pepResultHandlerStaticContentDirParamName = \ 531 pepResultHandlerParamPrefix + \ 532 cls.PEP_RESULT_HANDLER_STATIC_CONTENT_DIR_PARAMNAME 110 resultHandlerStaticContentDir = app_conf.get( 111 resultHandlerStaticContentDirParamName) 112 if resultHandlerStaticContentDir is not None: 113 staticApp = StaticURLParser(resultHandlerStaticContentDir) 114 app = Cascade([app, staticApp], catch=(httplib.NOT_FOUND,)) 115 116 pepPrefix = prefix + cls.PEP_PARAM_PREFIX 117 pepFilter = SamlPepFilter.filter_app_factory(app, 118 global_conf, 119 prefix=pepPrefix, 120 **app_conf) 533 121 534 pepResultHandlerStaticContentDir = app_conf.get( 535 pepResultHandlerStaticContentDirParamName) 536 if pepResultHandlerStaticContentDir is not None: 537 staticApp = StaticURLParser(pepResultHandlerStaticContentDir) 538 app = Cascade([app, staticApp], catch=(404,)) 122 # Now add the multi-handler to enable result handler to be invoked on 123 # 403 forbidden status from upstream middleware 124 app = MultiHandler(pepFilter) 539 125 540 authzPrefix = prefix + cls.PEP_PARAM_PREFIX 541 pepFilter = PEPFilter(app, 542 global_conf, 543 prefix=authzPrefix, 544 **app_conf) 545 pepInterceptFunc = pepFilter.multiHandlerInterceptFactory() 546 547 # Slot in the Policy Information Point in the WSGI stack at this point 548 # so that it can take a copy of the beaker session object from environ 549 # ahead of the PDP's request to it for an Attribute Certificate 550 pipPrefix = cls.PIP_PARAM_PREFIX 551 pipFilter = self.__class__.PIP_MIDDLEWARE_CLASS(pepFilter, 552 global_conf, 553 prefix=pipPrefix, 554 **app_conf) 555 pepFilter.pdp.pip = pipFilter 556 557 app = MultiHandler(pipFilter) 558 559 pepResultHandlerClassName = app_conf.pop( 560 prefix+cls.PEP_RESULT_HANDLER_PARAMNAME, 561 None) 562 if pepResultHandlerClassName is None: 563 pepResultHandler = PEPResultHandlerMiddleware 126 resultHandlerClassName = app_conf.pop( 127 prefix+cls.RESULT_HANDLER_PARAMNAME, 128 None) 129 if resultHandlerClassName is None: 130 resultHandler = PEPResultHandlerMiddleware 564 131 else: 565 pepResultHandler = importClass(pepResultHandlerClassName,132 resultHandler = importClass(resultHandlerClassName, 566 133 objectType=PEPResultHandlerMiddlewareBase) 567 134 568 app.add_method( PEPFilter.MIDDLEWARE_ID,569 pepResultHandler.filter_app_factory,135 app.add_method(resultHandler.__class__.__name__, 136 resultHandler.filter_app_factory, 570 137 global_conf, 571 prefix= pepResultHandlerParamPrefix,138 prefix=resultHandlerParamPrefix, 572 139 **app_conf) 573 140 574 app.add_checker(PEPFilter.MIDDLEWARE_ID, pepInterceptFunc) 575 576 super(AuthorizationMiddlewareBase, self).__init__(app, {}) 577 578 579 class SAMLAuthorizationMiddleware(AuthorizationMiddlewareBase): 580 """Implementation of AuthorizationMiddlewareBase using the SAML Policy 581 Information Point interface. This retrieves attributes over the SOAP/SAML 582 Attribute Authority interface 583 (ndg.security.server.wsgi.saml.attributeinterface.SOAPAttributeInterfaceMiddleware) and caches 584 SAML Assertions in a 585 ndg.security.common.credentialWallet.SAMLCredentialWallet 586 """ 587 PIP_MIDDLEWARE_CLASS = SamlPIPMiddleware 141 app.add_checker(resultHandler.__class__.__name__, 142 Http403ForbiddenStatusHandler.intercept) 143 144 return app -
TI12-security/trunk/NDGSecurity/python/ndg_security_server/ndg/security/server/wsgi/authz/pep.py
r7257 r7287 23 23 24 24 25 class SamlPep MiddlewareConfigError(Exception):25 class SamlPepFilterConfigError(Exception): 26 26 """Error with SAML PEP configuration settings""" 27 27 28 28 29 class SamlPep Middleware(SessionMiddlewareBase):29 class SamlPepFilter(SessionMiddlewareBase): 30 30 '''Policy Enforcement Point for ESG with SAML based Interface 31 31 … … 124 124 @param kw: configuration settings 125 125 dictionary 126 @raise SamlPep MiddlewareConfigError: missing option setting(s)126 @raise SamlPepFilterConfigError: missing option setting(s) 127 127 ''' 128 128 # Parse authorisation decision query options … … 131 131 132 132 # Parse other options 133 for name in SamlPep Middleware.PARAM_NAMES:133 for name in SamlPepFilter.PARAM_NAMES: 134 134 paramName = prefix + name 135 135 value = kw.get(paramName) 136 136 if value is None: 137 raise SamlPep MiddlewareConfigError('Missing option %r' %137 raise SamlPepFilterConfigError('Missing option %r' % 138 138 paramName) 139 139 setattr(self, name, value) … … 172 172 # place upstream of this middleware in the WSGI stack 173 173 if self.sessionKey not in environ: 174 raise SamlPepMiddlewareConfigError('No beaker session key "%s" ' 175 'found in environ' % 176 self.sessionKey) 174 raise SamlPepFilterConfigError('No beaker session key "%s" found ' 175 'in environ' % self.sessionKey) 177 176 self.session = environ[self.sessionKey] 178 177 -
TI12-security/trunk/NDGSecurity/python/ndg_security_test/ndg/security/test/config/authorisationservice/policy.xml
r7257 r7287 6 6 RuleCombiningAlgId="urn:oasis:names:tc:xacml:1.0:rule-combining-algorithm:permit-overrides"> 7 7 <Description> 8 NDG XACML example for unit tests: allow access for resource URIs 9 matching given regular expressions. The subject must have at least one 10 of a set of named attributes allocated 8 Example for NDG Security unit tests: allow access for resource URIs 9 defined in the rules. All other URIs are blocked from access 10 11 See ndg.security.test.unit.wsgi.authz.test_authz to see the various 12 rules tested out 11 13 </Description> 12 14 … … 78 80 <Rule RuleId="urn:ndg:security:secured-uri-rule" Effect="Permit"> 79 81 <!-- 80 Rule target(s) define which requests apply to the particular rule 82 Secure a URI path and all sub-paths using a regular expression to 83 define a URI pattern 81 84 --> 82 85 <Target> … … 99 102 100 103 The user must have at least one of the roles set - in this 101 case ' urn:siteA:security:authz:1.0:attr:staff'104 case 'staff' 102 105 --> 103 106 <Condition> … … 118 121 <Resources> 119 122 <Resource> 120 <ResourceMatch MatchId="urn:oasis:names:tc:xacml: 2.0:function:anyURI-regexp-match">123 <ResourceMatch MatchId="urn:oasis:names:tc:xacml:1.0:function:anyURI-equal"> 121 124 <ResourceAttributeDesignator 122 AttributeId="urn: siteA:security:authz:1.0:attr:resourceURI"125 AttributeId="urn:oasis:names:tc:xacml:1.0:resource:resource-id" 123 126 DataType="http://www.w3.org/2001/XMLSchema#anyURI"/> 124 <AttributeValue DataType="http://www.w3.org/2001/XMLSchema#anyURI">^http://localhost/test_accessGrantedToSecuredURI</AttributeValue> 127 <AttributeValue DataType="http://www.w3.org/2001/XMLSchema#anyURI">http://localhost/test_accessGrantedToSecuredURI</AttributeValue> 128 </ResourceMatch> 129 </Resource> 130 </Resources> 131 </Target> 132 <Condition> 133 <Apply FunctionId="urn:oasis:names:tc:xacml:1.0:function:string-at-least-one-member-of"> 134 <SubjectAttributeDesignator 135 AttributeId="urn:ndg:security:authz:1.0:attr" 136 DataType="http://www.w3.org/2001/XMLSchema#string"/> 137 <Apply FunctionId="urn:oasis:names:tc:xacml:1.0:function:string-bag"> 138 <AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">staff</AttributeValue> 139 <AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">administrator</AttributeValue> 140 </Apply> 141 </Apply> 142 </Condition> 143 </Rule> 144 <Rule RuleId="Access Granted to secured URI Rule modified for special admin query argument" Effect="Permit"> 145 <!-- 146 This rule is a modified version of the above to allow for a real use 147 case where adding a special query argument grants extra privileges 148 associated with an administrator 149 --> 150 <Target> 151 <Resources> 152 <Resource> 153 <ResourceMatch MatchId="urn:oasis:names:tc:xacml:1.0:function:anyURI-equal"> 154 <ResourceAttributeDesignator 155 AttributeId="urn:oasis:names:tc:xacml:1.0:resource:resource-id" 156 DataType="http://www.w3.org/2001/XMLSchema#anyURI"/> 157 <AttributeValue DataType="http://www.w3.org/2001/XMLSchema#anyURI">http://localhost/test_accessGrantedToSecuredURI?admin=1</AttributeValue> 125 158 </ResourceMatch> 126 159 </Resource> … … 128 161 <Subjects> 129 162 <Subject> 130 <SubjectMatch >163 <SubjectMatch MatchId="urn:oasis:names:tc:xacml:1.0:function:string-equal"> 131 164 <SubjectAttributeDesignator 132 165 AttributeId="urn:ndg:security:authz:1.0:attr" 133 166 DataType="http://www.w3.org/2001/XMLSchema#string"/> 134 <AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string"> keepout</AttributeValue>167 <AttributeValue DataType="http://www.w3.org/2001/XMLSchema#string">administrator</AttributeValue> 135 168 </SubjectMatch> 136 169 </Subject> -
TI12-security/trunk/NDGSecurity/python/ndg_security_test/ndg/security/test/unit/wsgi/authz/pep-result-handler-test.ini
r7077 r7287 19 19 20 20 [filter:AuthZFilter] 21 paste.filter_app_factory=ndg.security.server.wsgi.authz: SAMLAuthorizationMiddleware.filter_app_factory21 paste.filter_app_factory=ndg.security.server.wsgi.authz:AuthorisationFilter.filter_app_factory 22 22 prefix = authz. 23 policy.filePath = %(here)s/saml-policy.xml24 23 25 authz.pepResultHandler = ndg.security.server.wsgi.authz.result_handler.redirect.HTTPRedirectPEPResultHandlerMiddleware 26 authz.pepResultHandler.redirectURI = /nowhere 24 # This result handler responds with a redirect request to the client if access 25 # denied to the original requested URI 26 authz.resultHandler = ndg.security.server.wsgi.authz.result_handler.redirect.HTTPRedirectPEPResultHandlerMiddleware 27 authz.resultHandler.redirectURI = /test_accessGrantedToSecuredURI 27 28 28 # Settings for Policy Information Point used by the Policy Decision Point to29 # retrieve subject attributes from the Attribute Authority associated with the 30 # resource to be accessed 29 # Settings for the Policy Enforcement Point 30 authz.pep.sessionKey = beaker.session.ndg.security 31 authz.pep.authzServiceURI = https://localhost:9443/authorisation-service 31 32 32 33 # If omitted, DN of SSL Cert is used 33 pip.attributeQuery.issuerName = 34 pip.attributeQuery.clockSkewTolerance = 0. 35 pip.attributeQuery.queryAttributes.0 = urn:siteA:security:authz:1.0:attr, , http://www.w3.org/2001/XMLSchema#string 36 pip.attributeQuery.sslCACertDir=%(testConfigDir)s/ca 37 pip.attributeQuery.sslCertFilePath=%(testConfigDir)s/pki/test.crt 38 pip.attributeQuery.sslPriKeyFilePath=%(testConfigDir)s/pki/test.key 34 authz.pep.authzDecisionQuery.issuerName = /O=NDG/OU=BADC/CN=test 35 authz.pep.authzDecisionQuery.issuerFormat = urn:oasis:names:tc:SAML:1.1:nameid-format:x509SubjectName 36 authz.pep.authzDecisionQuery.subjectIdFormat = urn:esg:openid 37 authz.pep.authzDecisionQuery.clockSkewTolerance = 0. 38 authz.pep.authzDecisionQuery.sslCACertDir=%(testConfigDir)s/ca 39 authz.pep.authzDecisionQuery.sslCertFilePath=%(testConfigDir)s/pki/test.crt 40 authz.pep.authzDecisionQuery.sslPriKeyFilePath=%(testConfigDir)s/pki/test.key -
TI12-security/trunk/NDGSecurity/python/ndg_security_test/ndg/security/test/unit/wsgi/authz/saml-test.ini
r7168 r7287 13 13 14 14 [pipeline:main] 15 pipeline = AuthZFilter TestApp15 pipeline = PolicyEnforcementPointFilter TestApp 16 16 17 17 [app:TestApp] 18 18 paste.app_factory = ndg.security.test.unit.wsgi.authz.test_authz:TestAuthZMiddleware 19 19 20 [filter: AuthZFilter]21 paste.filter_app_factory=ndg.security.server.wsgi.authz.pep:SamlPep Middleware.filter_app_factory20 [filter:PolicyEnforcementPointFilter] 21 paste.filter_app_factory=ndg.security.server.wsgi.authz.pep:SamlPepFilter.filter_app_factory 22 22 prefix = pep. 23 23 pep.sessionKey = beaker.session.ndg.security 24 24 pep.authzServiceURI = https://localhost:9443/authorisation-service 25 26 pep.pepResultHandler = ndg.security.test.unit.wsgi.authz.test_authz.RedirectFollowingAccessDenied27 25 28 26 # Settings for Policy Information Point used by the Policy Decision Point to -
TI12-security/trunk/NDGSecurity/python/ndg_security_test/ndg/security/test/unit/wsgi/authz/test_authz.py
r7257 r7287 29 29 from ndg.security.server.wsgi.authz.result_handler.redirect import \ 30 30 HTTPRedirectPEPResultHandlerMiddleware 31 from ndg.security.server.wsgi.authz.pep import SamlPep MiddlewareConfigError31 from ndg.security.server.wsgi.authz.pep import SamlPepFilterConfigError 32 32 33 33 … … 149 149 class TestAuthZMiddleware(object): 150 150 '''Test Application for the Authentication handler to protect''' 151 response= "Test Authorization application"151 RESPONSE = "Test Authorization application" 152 152 153 153 def __init__(self, app_conf, **local_conf): … … 177 177 start_response(status, 178 178 [('Content-length', 179 str(len(TestAuthZMiddleware. response))),179 str(len(TestAuthZMiddleware.RESPONSE))), 180 180 ('Content-type', 'text/plain')]) 181 181 182 return [TestAuthZMiddleware. response]182 return [TestAuthZMiddleware.RESPONSE + ' returned: ' + status] 183 183 184 184 … … 188 188 def save(self): 189 189 pass 190 191 192 class SamlWSGIAuthZTestCase(BaseTestCase): 190 191 192 class BaseAuthzFilterTestCase(BaseTestCase): 193 """Base class for NDG Security WSGI authorisation filters 194 """ 193 195 INI_FILE = 'saml-test.ini' 194 196 THIS_DIR = path.dirname(path.abspath(__file__)) 197 INI_FILEPATH = None # Set in __init__ to enable derived classes to alter 195 198 SESSION_KEYNAME = 'beaker.session.ndg.security' 196 199 197 def __init__(self, *args, **kwargs): 200 def __init__(self, *args, **kwargs): 201 """Test the authorisation filter using Paste fixture and set up 202 Authorisation and Attribute Services needed for making authorisation 203 decisions 204 """ 198 205 BaseTestCase.__init__(self, *args, **kwargs) 199 206 200 wsgiapp = loadapp('config:'+ SamlWSGIAuthZTestCase.INI_FILE,201 relative_to= SamlWSGIAuthZTestCase.THIS_DIR)207 wsgiapp = loadapp('config:'+self.__class__.INI_FILE, 208 relative_to=self.__class__.THIS_DIR) 202 209 self.app = paste.fixture.TestApp(wsgiapp) 203 210 211 self.__class__.INI_FILEPATH = os.path.join(self.__class__.THIS_DIR, 212 self.__class__.INI_FILE) 213 204 214 self.startSiteAAttributeAuthority(withSSL=True, 205 port=SamlWSGIAuthZTestCase.SITEA_SSL_ATTRIBUTEAUTHORITY_PORTNUM) 206 207 self.startAuthorisationService() 215 port=self.__class__.SITEA_SSL_ATTRIBUTEAUTHORITY_PORTNUM) 216 217 self.startAuthorisationService() 218 219 220 class SamlPepFilterTestCase(BaseAuthzFilterTestCase): 221 """Test SAML based Policy Enforcement Filter. This has a SAML authorisation 222 decision query interface to call to a remote authorisation service""" 208 223 209 224 def test01CatchNoBeakerSessionFound(self): … … 211 226 # PEPFilterConfigError is raised if no beaker.session is set in 212 227 # environ 213 self.assertRaises(SamlPep MiddlewareConfigError, self.app.get,228 self.assertRaises(SamlPepFilterConfigError, self.app.get, 214 229 '/test_200') 215 230 … … 223 238 response = self.app.get('/test_200', 224 239 extra_environ=extra_environ) 240 print response 225 241 226 242 def test03Catch401WithLoggedIn(self): … … 238 254 extra_environ=extra_environ, 239 255 status=401) 256 print response 240 257 241 258 def test04Catch403WithLoggedIn(self): … … 246 263 extra_environ = { 247 264 self.__class__.SESSION_KEYNAME: 248 BeakerSessionStub(username=Saml WSGIAuthZTestCase.OPENID_URI),265 BeakerSessionStub(username=SamlPepFilterTestCase.OPENID_URI), 249 266 'REMOTE_USER': self.__class__.OPENID_URI 250 267 } … … 252 269 extra_environ=extra_environ, 253 270 status=403) 271 print response 254 272 255 273 def test05Catch401WithNotLoggedInAndSecuredURI(self): 256 # AuthZ middleware grants access because the URI requested ishas no274 # AuthZ middleware grants access because the URI requested has no 257 275 # subject restriction set in the policy rule 258 276 … … 263 281 extra_environ=extra_environ, 264 282 status=401) 283 print response 265 284 266 285 def test06AccessDeniedForSecuredURI(self): … … 270 289 extra_environ = { 271 290 self.__class__.SESSION_KEYNAME: 272 BeakerSessionStub(username=Saml WSGIAuthZTestCase.OPENID_URI),291 BeakerSessionStub(username=SamlPepFilterTestCase.OPENID_URI), 273 292 'REMOTE_USER': self.__class__.OPENID_URI 274 293 } … … 278 297 status=403) 279 298 print response 280 self.assert_("Insufficient privileges to access the "281 "resource" in response)282 299 283 300 def test07AccessGrantedForSecuredURI(self): … … 287 304 extra_environ = { 288 305 self.__class__.SESSION_KEYNAME: 289 BeakerSessionStub(username=Saml WSGIAuthZTestCase.OPENID_URI),306 BeakerSessionStub(username=SamlPepFilterTestCase.OPENID_URI), 290 307 'REMOTE_USER': self.__class__.OPENID_URI 291 308 } … … 294 311 extra_environ=extra_environ, 295 312 status=200) 296 self.assert_(TestAuthZMiddleware.response in response) 297 print response 298 299 def test08AccessDeniedForAdminQueryArg(self): 313 self.assert_(TestAuthZMiddleware.RESPONSE in response) 314 print response 315 316 317 class PEPResultHandlerTestCase(BaseAuthzFilterTestCase): 318 """Test Authorisation Filter - this contains the PEP filter and a result 319 handler which enables customisation of behaviour on 403 Forbidden responses 320 """ 321 INI_FILE = 'pep-result-handler-test.ini' 322 AUTHZ_FILTER_SECTION = 'filter:AuthZFilter' 323 AUTHZ_RESULT_HANDLER_REDIRECT_URI_OPTNAME = 'authz.resultHandler.redirectURI' 324 325 def __init__(self, *arg, **kw): 326 BaseAuthzFilterTestCase.__init__(self, *arg, **kw) 327 328 cfgParser = SafeConfigParser() 329 cfgParser.read(self.__class__.INI_FILEPATH) 330 331 self.redirectURI = cfgParser.get(self.__class__.AUTHZ_FILTER_SECTION, 332 self.__class__.AUTHZ_RESULT_HANDLER_REDIRECT_URI_OPTNAME) 333 334 def test01RedirectPEPResultHandlerMiddleware(self): 335 # User is logged in but doesn't have the required credentials for 336 extra_environ = { 337 self.__class__.SESSION_KEYNAME: 338 BeakerSessionStub(username=self.__class__.OPENID_URI), 339 'REMOTE_USER': self.__class__.OPENID_URI 340 } 341 342 # Expecting result handler to be invoked overriding the 403 response 343 response = self.app.get('/test_accessDeniedToSecuredURI', 344 extra_environ=extra_environ, 345 status=302) 346 print("Result handler has intercepted the 403 Forbidden response " 347 "from the PEP and set this redirect response instead: %s" % 348 response) 349 self.assert_(response.header_dict.get('location') == self.redirectURI) 350 351 def test02RedirectFollowingAccessDeniedForAdminQueryArg(self): 300 352 301 353 # User is logged in but doesn't have the required credentials for … … 303 355 extra_environ = { 304 356 self.__class__.SESSION_KEYNAME: 305 BeakerSessionStub(username=Saml WSGIAuthZTestCase.OPENID_URI),357 BeakerSessionStub(username=SamlPepFilterTestCase.OPENID_URI), 306 358 'REMOTE_USER': self.__class__.OPENID_URI 307 359 } … … 312 364 # into play the PEP result handler defined in this module, 313 365 # RedirectFollowingAccessDenied. This class reinvokes the request 314 # but without the admin query argument. Access is then granted for 315 # the redirected request 366 # but without the admin query argument (see the ini file for what this 367 # location is. Access is then granted because the user has access 368 # rights for the new location. 316 369 response = self.app.get('/test_accessGrantedToSecuredURI', 317 370 params={'admin': 1}, 318 371 extra_environ=extra_environ, 319 372 status=302) 320 try: 321 redirectResponse = response.follow(extra_environ=extra_environ) 322 except paste.fixture.AppError, e: 323 self.failIf(TestAuthZMiddleware.response not in response) 324 print response 325 326 327 class PEPResultHandlerTestCase(BaseTestCase): 328 INI_FILE = 'pep-result-handler-test.ini' 329 THIS_DIR = os.path.dirname(os.path.abspath(__file__)) 330 INI_FILEPATH = path.join(THIS_DIR, INI_FILE) 331 SESSION_KEYNAME = 'beaker.session.ndg.security' 332 333 def __init__(self, *arg, **kw): 334 BaseTestCase.__init__(self, *arg, **kw) 335 336 here_dir = os.path.dirname(os.path.abspath(__file__)) 337 wsgiapp = loadapp('config:'+self.__class__.INI_FILE, 338 relative_to=self.__class__.THIS_DIR) 339 self.app = paste.fixture.TestApp(wsgiapp) 340 341 cfg = SafeConfigParser(dict(here=self.__class__.THIS_DIR)) 342 cfg.read(self.__class__.INI_FILEPATH) 343 self.redirectURI = cfg.get('filter:AuthZFilter', 344 'authz.pepResultHandler.redirectURI') 345 346 self.startSiteAAttributeAuthority(withSSL=True, 347 port=SamlWSGIAuthZTestCase.SITEA_SSL_ATTRIBUTEAUTHORITY_PORTNUM) 348 349 350 def testRedirectPEPResultHandlerMiddleware(self): 351 # User is logged in but doesn't have the required credentials for 352 # access 353 raise NotImplementedError('TODO: fix recursion error') 354 # extra_environ = { 355 # self.__class__.SESSION_KEYNAME: 356 # BeakerSessionStub(username=self.__class__.OPENID_URI) 357 # } 358 # 359 # # Expecting redirect response to specified redirect URI 360 # response = self.app.get('/test_accessDeniedToSecuredURI', 361 # extra_environ=extra_environ, 362 # status=302) 363 # print(response) 364 # self.assert_(response.header_dict.get('location') == self.redirectURI) 373 374 print("Redirect Handler has interrupted the 403 Denied response and " 375 "added this redirect response instead: %s" % response) 376 377 # Follow the redirect - the policy should allow access to the new 378 # location 379 redirectResponse = response.follow(extra_environ=extra_environ, 380 status=200) 381 print("Following the redirect to location %r gives this response: %s" % 382 (response.header_dict.get('location'), redirectResponse)) 383 365 384 366 385 if __name__ == "__main__":
Note: See TracChangeset
for help on using the changeset viewer.