Changeset 3765 for TI12-security
- Timestamp:
- 04/04/08 16:16:32 (12 years ago)
- Location:
- TI12-security/trunk/python/ndg.security.common/ndg/security/common/authz
- Files:
-
- 2 added
- 2 edited
Legend:
- Unmodified
- Added
- Removed
-
TI12-security/trunk/python/ndg.security.common/ndg/security/common/authz/pdp.py
r3759 r3765 1 2 3 #_____________________________________________________________________________ 4 class PDPInterfaceError(GatekeeperError): 5 """Exception handling for NDG Gatekeeper Resource interface class 6 class.""" 7 pass 8 9 10 #_____________________________________________________________________________ 11 class PDPInterface: 12 """An abstract base class to define the interface to the Policy Decision 13 Point for the Gatekeeper (PEP). 14 15 Each NDG resource should implement a derived class which implements 16 the way a resource roles is served from the given resource.""" 17 18 # User defined class may wish to specify a URI or path for a configuration 19 # file 20 def __init__(self, resrcID=None, filePath=None): 21 """Abstract base class - derive from this class to define 22 resource role interface to Gatekeeper""" 23 raise NotImplementedError(\ 24 self.__init__.__doc__.replace('\n ','')) 25 26 27 def getPermissions(self, role): 28 """Derived method should return the permissions for the given resource 29 role. Format is a tuple e.g. 30 31 ('r', 'w', 'x'): read, write and execute permission granted for this 32 role 33 (): access denied 34 ('r',): read only access 35 ('r', 'x'): read and execute permission granted 36 37 This method is needed for the interface to the Gatekeeper class""" 38 raise NotImplementedError( 39 self.__getPermissions.__doc__.replace('\n ','')) 40 41 42 def readAccess(self, role): 43 """Derived method should return the role for read access to the 44 resource - should return boolean for access granted/denied""" 45 raise NotImplementedError( 46 self.readAccess.__doc__.replace('\n ','')) 47 48 49 def writeAccess(self, role): 50 """Derived method should return the role for write access to the 51 resource - should return boolean for access granted/denied""" 52 raise NotImplementedError( 53 self.writeAccess.__doc__.replace('\n ','')) 54 55 56 def executeAccess(self, role): 57 """Derived method should return the role for execute access to the 58 resource - should return boolean for access granted/denied""" 59 raise NotImplementedError( 60 self.executeAccess.__doc__.replace('\n ','')) 61 62 63 class tmp: 64 65 66 #_________________________________________________________________________ 67 def __call__(self, input): 68 """Get the permissions for the input file, list of roles or 69 Attribute Certificate containing roles. A Dictionary of permissions 70 are returned indexed by role name. Permissions are expressed as a 71 tuple containing the relevant permissions flags e.g. ('r', 'w', 'x') 72 for read/write/execute permission or e.g. ('x') for exceute only 73 permission""" 74 75 roleList = self.__formatInput(input) 76 77 return dict([(role, self.__pdpObj.getPermissions(role)) \ 78 for role in roleList]) 79 80 81 getPermissions = __call__ 82 83 84 def __formatInput(self, input): 85 """Convert generic input into a list of roles - use with access 86 routines""" 87 88 if isinstance(input, list): 89 # Input is list of roles 90 return input 91 92 elif isinstance(input, basestring): 93 # Input is a role 94 return [input] 95 96 elif isinstance(input, AttCert): 97 # Input is an Attribute Certificate 98 # Check signature of AttCert 99 try: 100 input.isValid(raiseExcep=True, 101 certFilePathList=self.__prop['caCertFilePath']) 102 except Exception, e: 103 raise PEPError, "Access denied for input: %s" % str(e) 104 105 return input.roles 106 else: 107 raise PEPError("Input must be a role, role list or " + \ 108 "Attribute Certificate type") 109 110 #_________________________________________________________________________ 111 def readAccess(self, input): 112 """Determine read access permitted against the given 113 input role/role list or Attribute Certificate roles 114 115 Return a dictionary of booleans for access granted/denied keyed 116 by role name""" 117 118 roleList = self.__formatInput(input) 119 120 return dict([(role, self.__pdpObj.readAccess(role)) \ 121 for role in roleList]) 122 123 124 #_________________________________________________________________________ 125 def writeAccess(self, input): 126 """Determine write access permitted against the given 127 input role/role list or Attribute Certificate roles 128 129 Return a dictionary of booleans for access granted/denied keyed 130 by role name""" 131 132 roleList = self.__formatInput(input) 133 134 return dict([(role, self.__pdpObj.writeAccess(role)) \ 135 for role in roleList]) 136 137 138 #_________________________________________________________________________ 139 def executeAccess(self, input): 140 """Determine execute access permitted against the given 141 input role/role list or Attribute Certificate roles 142 143 Return a dictionary of booleans for access granted/denied keyed 144 by role name""" 145 146 roleList = self.__formatInput(input) 147 148 return dict([(role, self.__pdpObj.executeAccess(role)) \ 149 for role in roleList]) 150 151 1 """NDG Policy Decision Point for NDG Browse - access constraints for a 2 resource are determined from MOLES access constraints in the data 3 4 NERC Data Grid Project 5 """ 6 __author__ = "P J Kershaw" 7 __date__ = "04/04/08" 8 __copyright__ = "(C) 200* STFC & NERC" 9 __contact__ = "P.J.Kershaw@rl.ac.uk" 10 __license__ = \ 11 """This software may be distributed under the terms of the Q Public 12 License, version 1.0 or later.""" 13 __contact__ = "P.J.Kershaw@rl.ac.uk" 14 __revision__ = "$Id:gatekeeper.py 3079 2007-11-30 09:39:46Z pjkersha $" 15 16 import logging 17 log = logging.getLogger(__name__) 18 152 19 import sys # tracefile config param may be set to e.g. sys.stderr 153 20 import urllib2 154 21 import socket 155 22 23 from ndg.security.common.authz.pdp import PDPInterface, PDPError, \ 24 PDPUserAccessDenied, PDPUserNotLoggedIn, PDPMissingResourceConstraints 25 156 26 from ndg.security.common.SessionMgr import SessionMgrClient, SessionNotFound,\ 157 27 SessionCertTimeError, SessionExpired, InvalidSession, \ 158 AttributeRequestDenied 159 160 def HandleSecurity(*args): 161 return PullModelHandler(*args)() 162 163 class URLCannotBeOpened(Exception): 28 AttributeRequestDenied 29 30 class InvalidAttributeCertificate(PDPError): 31 "The certificate containing authorisation roles is invalid" 32 def __init__(self, msg=InvalidAttributeCertificate.__doc__): 33 PDPError.__init__(msg) 34 35 class SessionExpiredMsg(PDPError): 36 'Session has expired. Please re-login' 37 def __init__(self, msg=SessionExpiredMsg.__doc__): 38 PDPError.__init__(msg) 39 40 class InvalidSessionMsg(PDPError): 41 'Session is invalid. Please try re-login' 42 def __init__(self, msg=InvalidSessionMsg.__doc__): 43 PDPError.__init__(msg) 44 45 class InitSessionCtxError(PDPError): 46 'A problem occured initialising a session connection' 47 def __init__(self, msg=InvalidSessionMsg.__doc__): 48 PDPError.__init__(msg) 49 50 class AttributeCertificateRequestError(PDPError): 51 'A problem occured requesting a certificate containing authorisation roles' 52 def __init__(self, msg=InvalidSessionMsg.__doc__): 53 PDPError.__init__(msg) 54 55 class URLCannotBeOpened(PDPError): 164 56 """Raise from canURLBeOpened PullModelHandler class method 165 57 if URL is invalid - this method is used to check the AA 166 58 service""" 167 59 168 class PullModelHandler(object): 169 """Make access control decision based on CSML constraint and user security 170 token""" 171 172 AccessAllowedMsg = "Access Allowed" 173 InvalidAttributeCertificate = \ 174 "The certificate containing your authorisation roles is invalid" 175 NotLoggedInMsg = 'Not Logged in' 176 SessionExpiredMsg = 'Session has expired. Please re-login' 177 InvalidSessionMsg = 'Session is invalid. Please try re-login' 178 InvalidSecurityCondition = 'Invalid Security Condition' 179 180 def __init__(self, uri, securityElement, securityTokens): 60 61 class MolesPDP(PDPInterface): 62 """Make access control decision based on a MOLES access constraint 63 (applies to CSML too) and user security token""" 64 65 molesXPathQueryPfx = \ 66 '{http://ndg.nerc.ac.uk/moles}simpleCondition/{http://ndg.nerc.ac.uk/moles}' 67 roleXPathQuery = molesXPathQueryPfx + 'attrauthRole' 68 roleXPathQuery = molesXPathQueryPfx + 'dgAttributeAuthority' 69 70 defParam = {'aaURI': '', 71 'sslCACertFilePathList': [], 72 'tracefile': '', 73 'acCACertFilePathList': [], 74 'acIssuer': ''} 75 76 77 def __init__(self, cfgFilePath=None, **cfgKw): 181 78 """Initialise settings for WS-Security and SSL for SOAP 182 79 call to Session Manager … … 194 91 </moles:simpleCondition> 195 92 NB: xmlns:moles="http://ndg.nerc.ac.uk/moles 196 197 @type: pylons.session 198 @param securityTokens: dict-like session object containing security 199 tokens""" 200 201 self.uri = uri 202 self.securityElement = securityElement 203 self.securityTokens = securityTokens 204 205 206 def __call__(self, **kw): 207 """Convenience wrapper for checkAccess""" 208 return self.checkAccess(**kw) 209 210 211 def checkAccess(self, 212 uri=None, 213 securityElement=None, 214 securityTokens=None): 93 """ 94 95 self.cfgFilePath = cfgFilePath 96 self.resrcURI = None 97 self.securityElement = None 98 self.userHandle = None 99 100 # Set from config file 101 if cfgFilePath: 102 self._readConfig() 103 104 # Separate keywords into PDP and WS-Security specific items 105 paramNames = cfgKw.keys() 106 for paramName in paramNames: 107 if paramName in NDGBrowsePDP.defParam: 108 # Keywords are deleted as they are set 109 setattr(self, paramName, cfgKw.pop('paramName')) 110 111 # Remaining keys must be for WS-Security config 112 self.wssCfg = cfgKw 113 114 115 def _readConfig(self, section='DEFAULT'): 116 '''Read PDP configuration file''' 117 cfg = SafeConfigParser() 118 cfg.read(self.cfgFilePath) 119 120 # Copy directly into attribute of this object 121 for paramName, paramVal in PEP.defParam.items(): 122 if isinstance(paramVal, list): 123 paramListVal = expVars(cfg(section, paramName)).split() 124 setattr(self, paramName, paramListVal) 125 else: 126 setattr(self, paramName, expVars(cfg(section, paramName))) 127 128 129 def accessPermitted(self, resrcHandle, userHandle, accessType=None): 215 130 """Make an access control decision based on whether the user is 216 131 authenticated and has the required roles 132 133 @type resrcHandle: dict 134 @param resrcHandle: dict 'uri' = resource URI, 'securityElement' = 135 ElementTree type MOLES security Element 136 137 @type userHandle: dict 138 @param userHandle: dict with keys 'sid' = user session ID, 139 'h' = Session Manager URI 140 141 @type accessType: - 142 @param accessType: not implemented - logs a warning if set 143 144 @rtype: bool 145 @return: True if access permitted; False if denied or else raise 146 an Exception 217 147 218 148 @type uri: string … … 230 160 231 161 @type: pylons.session 232 @param securityTokens: dict-like session object containing security162 @param userHandle: dict-like session object containing security 233 163 tokens. Resets equivalent object attribute.""" 234 164 235 # tokens and element may be set from __init__ or as args to this 236 # method. If the latter copy them into self 237 if uri: 238 self.uri = uri 239 240 if securityTokens: 241 self.securityTokens = securityTokens 242 243 if securityElement: 244 self.securityElement=securityElement 245 246 # Check self.securityTokens - if not set then the user mustn't be 247 # logged in. This situation is possible if a user has been denied 248 # access to data and then tried to logout - after log out they are 249 # redirected back to the page where they tried accessing data but this 250 # time they will have no security credential set 251 if not self.securityTokens: 252 # Try to recover and do something sensible 253 # 254 # TODO: this adds insult to injury if the person has just been 255 # denied access to data. Instead do a redirect back to the 256 # discovery page? 257 # P J Kershaw 10/08/07 258 log.info("Exiting from PEP: user is not logged in") 259 return False, self.__class__.NotLoggedInMsg 260 261 xpathr='{http://ndg.nerc.ac.uk/moles}simpleCondition/{http://ndg.nerc.ac.uk/moles}attrauthRole' 262 xpathaa='{http://ndg.nerc.ac.uk/moles}simpleCondition/{http://ndg.nerc.ac.uk/moles}dgAttributeAuthority' 263 roleE,aaE=self.securityElement.find(xpathr),self.securityElement.find(xpathaa) 264 if roleE is None: 265 log.error("PEP: role not found in dataset element: %s" % \ 266 self.securityElement) 267 return False, self.__class__.InvalidSecurityCondition 268 269 self.reqRole=roleE.text 270 271 # Check Attribute Authority address 272 try: 273 PullModelHandler.urlCanBeOpened(aaE.text) 274 except (URLCannotBeOpened, AttributeError): 275 # Catch situation where either Attribute Authority address in the 276 # data invalid or none was set. In this situation verify 277 # against the Attribute Authority set in the config 278 log.info('PEP: Attribute Authority address is invalid ' + \ 279 'in data "%s" - defaulting to config file setting' % \ 280 self.securityElement) 281 self.reqAAURI = g.securityCfg.aaURI 282 283 # Create Session Manager client 284 self.smClnt = SessionMgrClient(uri=self.securityTokens['h'], 285 sslCACertFilePathList=g.securityCfg.sslCACertFilePathList, 286 sslPeerCertCN=g.securityCfg.sslPeerCertCN, 287 signingCertFilePath=g.securityCfg.wssCertFilePath, 288 signingPriKeyFilePath=g.securityCfg.wssPriKeyFilePath, 289 signingPriKeyPwd=g.securityCfg.wssPriKeyPwd, 290 caCertFilePathList=g.securityCfg.wssCACertFilePathList, 291 tracefile=g.securityCfg.tracefile) 292 293 return self.__pullSessionAttCert() 294 295 296 def __pullSessionAttCert(self): 165 # Resource handle contains URI and ElementTree resource security 166 # element 167 try: 168 self.resrcURI = resrcHandle['uri'] 169 self.securityElement = resrcHandle['securityElement'] 170 except KeyError, e: 171 log.error("Resource handle missing key %s" % e) 172 raise PDPMissingResourceConstraints() 173 174 # User handle contains 'h' = Session Manager URI and 'sid' user 175 # Session ID 176 try: 177 self.smURI = userHandle['h'] 178 self.userSessID = userHandle['sid'] 179 except KeyError, e: 180 log.error("User handle missing key %s" % e) 181 raise PDPUserNotLoggedIn() 182 183 184 roleElem = self.securityElement.find(NDGBrowsePDP.roleXPathQuery) 185 if roleElem is None or not roleElem.text: 186 log.error("PDP: role not set in MOLES security " + \ 187 "constraints") 188 raise PDPMissingResourceConstraints() 189 190 self.reqRole = roleElem.text 191 192 aaElem = self.securityElement.find(NDGBrowsePDP.aaXPathQuery) 193 194 # Sanity check on Attribute Authority URI 195 if aaElem and aaElem.text: 196 aaURI = aaElem.text 197 198 # Check Attribute Authority address 199 try: 200 NDGBrowsePDP.urlCanBeOpened(aaURI) 201 except URLCannotBeOpened, e: 202 # Catch situation where either Attribute Authority address in the 203 # data invalid or none was set. In this situation verify 204 # against the Attribute Authority set in the config 205 206 log.warning('PDP: MOLES security constraint ' + \ 207 'Attribute Authority address is invalid - ' + \ 208 'defaulting to config file setting: %s; ' % \ 209 self.aaURI + \ 210 'error message is: %s' % e) 211 aaURI = self.aaURI 212 else: 213 log.warning("PDP: Attribute Authority element not " + \ 214 "set in MOLES security constraints - defaulting " + \ 215 "to config file setting: %s" % self.aaURI) 216 aaURI = self.aaURI 217 218 # Retrieve Attirbute Certificate from user's session held by 219 # Session Manager 220 attCert = self._pullUserSessionAttCert(aaURI) 221 222 # Check its validity 223 self._checkAttCert(attCert) 224 225 log.info('PDP - access granted for user "%s" ' % \ 226 attCert.userId + \ 227 'to "%s" secured with role "%s" ' % \ 228 (self.resrcURI, self.reqRole) + \ 229 'using attribute certificate:\n\n%s' % attCert) 230 231 232 def _pullUserSessionAttCert(self, aaURI): 297 233 """Check to see if the Session Manager can deliver an Attribute 298 234 Certificate with the required role to gain access to the resource 299 in question""" 300 235 in question 236 237 @type aaURI: string 238 @param aaURI: address of Attribute Authority that the Session Manager 239 will call in order to request an AC on behalf of the user""" 240 241 try: 242 # Create Session Manager client 243 self.smClnt = SessionMgrClient(uri=self.smURI, 244 cfgfilePath=self.cfgFilePath, 245 sslCACertFilePathList=self.sslCACertFilePathList, 246 tracefile=self.tracefile, 247 **self.wssCfg) 248 except Exception, e: 249 log.error("PDP: creating Session Manager client: %s"%e) 250 raise InitSessionCtxError() 251 252 301 253 try: 302 254 # Make request for attribute certificate 303 attCert = self.smClnt.getAttCert(attAuthorityURI=self.reqAAURI, 304 sessID=self.securityTokens['sid'], 305 reqRole=self.reqRole) 255 attCert = self.smClnt.getAttCert(attAuthorityURI=aaURI, 256 sessID=self.userSessID, 257 reqRole=self.reqRole) 258 return attCert 259 306 260 except AttributeRequestDenied, e: 307 261 log.info(\ 308 "PEP - request for attribute certificate denied: %s"%e)309 r eturn False, str(e)262 "PDP -request for attribute certificate denied: %s" % e) 263 raise PDPUserAccessDenied() 310 264 311 265 except SessionNotFound, e: 312 log.info("P EP -no session found: %s" % e)313 r eturn False, self.__class__.NotLoggedInMsg266 log.info("PDP -no session found: %s" % e) 267 raise PDPUserNotLoggedIn() 314 268 315 269 except SessionExpired, e: 316 log.info("P EP -session expired: %s" % e)317 r eturn False, self.__class__.SessionExpiredMsg270 log.info("PDP -session expired: %s" % e) 271 raise InvalidSessionMsg() 318 272 319 273 except SessionCertTimeError, e: 320 log.info("P EP -session cert. time error: %s" % e)321 r eturn False, self.__class__.InvalidSessionMsg274 log.info("PDP -session cert. time error: %s" % e) 275 raise InvalidSessionMsg() 322 276 323 277 except InvalidSession, e: 324 log.info("P EP -invalid user session: %s" % e)325 r eturn False, self.__class__.InvalidSessionMsg278 log.info("PDP -invalid user session: %s" % e) 279 raise InvalidSessionMsg() 326 280 327 281 except Exception, e: 328 raise GateKeeperError, "PEP request for attribute certificate: "+\ 329 str(e) 330 331 # Check attribute certificate is valid 332 attCert.certFilePathList = g.securityCfg.acCACertFilePathList 333 attCert.isValid(raiseExcep=True) 334 282 log.error("PDP request for attribute certificate: %s" % e) 283 raise AttributeCertificateRequestError() 284 285 286 def _checkAttCert(self, attCert): 287 '''Check attribute certificate is valid 288 289 @type attCert: ndg.security.common.AttCert.AttCert 290 @param attCert: attribute certificate to be check for validity''' 291 attCert.certFilePathList = self.acCACertFilePathList 292 try: 293 attCert.isValid(raiseExcep=True) 294 except Exception, e: 295 log.error("Attribute Certificate: %s" % e) 296 raise InvalidAttributeCertificate() 297 335 298 # Check it's issuer is as expected 336 299 if attCert.issuer != g.securityCfg.acIssuer: 337 log.info('P EP -access denied: Attribute Certificate ' + \300 log.info('PDP -access denied: Attribute Certificate ' + \ 338 301 'issuer DN, "%s" ' % attCert.issuer + \ 339 302 'must match this data provider\'s Attribute Authority ' + \ 340 303 'DN: "%s"' % g.securityCfg.acIssuer) 341 return False, self.__class__.InvalidAttributeCertificate 342 343 log.info('PEP - access granted for user "%s" '%attCert.userId+\ 344 'to "%s" secured with role "%s" ' % (self.uri,self.reqRole)+\ 345 'using attribute certificate:\n\n%s' % attCert) 346 347 return True, self.__class__.AccessAllowedMsg 304 raise InvalidAttributeCertificate() 305 348 306 349 307 @classmethod … … 361 319 urllib2.urlopen(url) 362 320 except (urllib2.HTTPError, urllib2.URLError, 363 socket.error, socket.sslerror ):321 socket.error, socket.sslerror, AttributeError): 364 322 if raiseExcep: 365 raise URLCannotBeOpened 323 raise URLCannotBeOpened() 366 324 367 325 found = True … … 371 329 372 330 return found 373 374 375 class SecurityConfigError(Exception): 376 """Handle errors from parsing security config items""" 377 378 class SecurityConfig(object): 379 """Get Security related parameters from the Pylons NDG config file""" 380 381 def parse(self, cfg, section='NDG_SECURITY'): 382 '''Get PKI settings for Attribute Authority and Session Manager from 383 the configuration file 384 385 @type cfg: ConfigParser object 386 @param cfg: reference to configuration file.''' 387 388 tracefileExpr = cfg.get(section, 'tracefile') 389 if tracefileExpr: 390 self.tracefile = eval(tracefileExpr) 391 392 self.smURI = cfg.get(section, 'sessionMgrURI') 393 self.aaURI = cfg.get(section, 'attAuthorityURI') 394 395 try: 396 self.wssCACertFilePathList = \ 397 cfg.get(section, 'wssCACertFilePathList').split() 398 399 except AttributeError: 400 raise SecurityConfigError, \ 401 'No "wssCACertFilePathList" security setting' 402 403 # Attribute Certificate Issuer 404 self.acIssuer = cfg.get(section, 'acIssuer') 405 406 # verification of X.509 cert back to CA 407 try: 408 self.acCACertFilePathList = cfg.get(section, 409 'acCACertFilePathList').split() 410 except AttributeError: 411 raise SecurityConfigError, \ 412 'No "acCACertFilePathList" security setting' 413 414 415 def __repr__(self): 416 return '\n'.join(["%s=%s" % (k,v) for k,v in self.__dict__.items() \ 417 if k[:2] != "__"]) 331 332 333 def makeDecision(resrcHandle, userHandle, accessType=None, **kw): 334 '''One call Wrapper interface to PDP''' 335 return NDGBrowsePDP(**kw)(resrcHandle, userHandle) 336 418 337 -
TI12-security/trunk/python/ndg.security.common/ndg/security/common/authz/pep.py
r3759 r3765 64 64 'Keyword "%s" is not a valid config parameter"' % \ 65 65 paramName) 66 setattr(self, paramName, expVars( self._cfg(section, paramName)))66 setattr(self, paramName, expVars(prop['paramName'])) 67 67 68 68 paramFound = [param for paramName in PEP.defParam \ … … 163 163 164 164 accessPermitted = __call__ 165 166 167 class PDPInterface(object):168 """PEP (Gatekeeper) interface to a Policy Decision Point169 170 PDPs must adhere to this interface by subclassing from it"""171 172 def __init__(self, cfgFilePath):173 "__init__ should accept a config file path as its arg"174 raise NotImplementedError("%s\n%s" % (PDPInterface.__doc__,175 PDPInterface.__init__.__doc__))176 177 178 def accessPermitted(selfresrcHandle, userHandle, accessType, *arg, **kw):179 """Make an Access control decision with this behaviour:180 181 @type resrcHandle: any - determined by derived class PDP182 @param resrcHandle: a handle to the resource to make access decision183 for. This could be for example a resource ID string, or a dict or184 other object to hold resource information required by the PDP185 186 @type userHandle: any - determined by derived class PDP187 @param userHandle: a handle to the user requesting access.188 e.g. a user ID, an attribute certificate or a handle to a service189 which can be interrogated to get the required information190 191 @type accessType: any - determined by derived class PDP192 @param accessType: the type of access being requested e.g. read,193 read/write, put etc.194 195 @rtype: bool196 @return: True if access permitted; False if denied or else raise197 an Exception198 199 Nb.200 201 *arg and **kw are included to enable further customisation,202 resrcHandle, userHandle and accessType are merely indicators.203 204 The alias to this method 'accessPermitted'"""205 raise NotImplementedError("%s\n%s" % (PDPInterface.__doc__,206 PDPInterface.accessPermitted.__doc__))207 return False
Note: See TracChangeset
for help on using the changeset viewer.