2 # SfaAPI authentication
6 from sfa.util.faults import InsufficientRights, MissingCallerGID, \
7 MissingTrustedRoots, PermissionError, BadRequestHash, \
8 ConnectionKeyGIDMismatch, SfaPermissionDenied, CredentialNotVerifiable, \
10 from sfa.util.sfalogging import logger
11 from sfa.util.py23 import StringType
12 from sfa.util.config import Config
13 from sfa.util.xrn import Xrn, get_authority
15 from sfa.trust.gid import GID
16 from sfa.trust.rights import Rights
17 from sfa.trust.certificate import Keypair, Certificate
18 from sfa.trust.credential import Credential
19 from sfa.trust.trustedroots import TrustedRoots
20 from sfa.trust.hierarchy import Hierarchy
21 from sfa.trust.sfaticket import SfaTicket
22 from sfa.trust.speaksfor_util import determine_speaks_for
27 Credential based authentication
30 def __init__(self, peer_cert = None, config = None ):
31 self.peer_cert = peer_cert
32 self.hierarchy = Hierarchy()
34 self.config = Config()
35 self.load_trusted_certs()
37 def load_trusted_certs(self):
38 self.trusted_cert_list = \
39 TrustedRoots(self.config.get_trustedroots_dir()).get_list()
40 self.trusted_cert_file_list = \
41 TrustedRoots(self.config.get_trustedroots_dir()).get_file_list()
43 # this convenience methods extracts speaking_for_xrn
44 # from the passed options using 'geni_speaking_for'
45 def checkCredentialsSpeaksFor (self, *args, **kwds):
46 if 'options' not in kwds:
47 logger.error ("checkCredentialsSpeaksFor was not passed options=options")
49 # remove the options arg
50 options = kwds['options']; del kwds['options']
51 # compute the speaking_for_xrn arg and pass it to checkCredentials
52 if options is None: speaking_for_xrn = None
53 else: speaking_for_xrn = options.get('geni_speaking_for', None)
54 kwds['speaking_for_xrn'] = speaking_for_xrn
55 return self.checkCredentials(*args, **kwds)
57 # do not use mutable as default argument
58 # http://docs.python-guide.org/en/latest/writing/gotchas/#mutable-default-arguments
59 def checkCredentials(self, creds, operation, xrns=None,
60 check_sliver_callback=None,
61 speaking_for_xrn=None):
62 if xrns is None: xrns = []
64 def log_invalid_cred(cred):
65 if not isinstance (cred, StringType):
66 logger.info("cannot validate credential %s - expecting a string"%cred)
67 error = ('TypeMismatch',
68 "checkCredentials: expected a string, received {} -- {}"
69 .format(type(cred), cred))
71 cred_obj = Credential(string=cred)
72 logger.info("failed to validate credential - dump=%s"%\
73 cred_obj.dump_string(dump_parents=True))
74 error = sys.exc_info()[:2]
77 # if xrns are specified they cannot be None or empty string
81 raise BadArgs("Invalid urn or hrn")
84 if not isinstance(xrns, list):
87 slice_xrns = Xrn.filter_type(xrns, 'slice')
88 sliver_xrns = Xrn.filter_type(xrns, 'sliver')
90 # we are not able to validate slivers in the traditional way so
91 # we make sure not to include sliver urns/hrns in the core validation loop
92 hrns = [Xrn(xrn).hrn for xrn in xrns if xrn not in sliver_xrns]
94 if not isinstance(creds, list):
96 logger.debug("Auth.checkCredentials with %d creds on hrns=%s"%(len(creds),hrns))
97 # won't work if either creds or hrns is empty - let's make it more explicit
98 if not creds: raise Forbidden("no credential provided")
99 if not hrns: hrns = [None]
101 speaks_for_gid = determine_speaks_for(logger, creds, self.peer_cert,
102 speaking_for_xrn, self.trusted_cert_list)
104 if self.peer_cert and \
105 not self.peer_cert.is_pubkey(speaks_for_gid.get_pubkey()):
111 self.check(cred, operation, hrn)
114 error = log_invalid_cred(cred)
116 # make sure all sliver xrns are validated against the valid credentials
118 if not check_sliver_callback:
119 msg = "sliver verification callback method not found."
120 msg += " Unable to validate sliver xrns: %s" % sliver_xrns
122 check_sliver_callback(valid, sliver_xrns)
125 raise Forbidden("Invalid credential %s -- %s"%(error[0],error[1]))
129 def check(self, credential, operation, hrn = None):
131 Check the credential against the peer cert (callerGID) included
132 in the credential matches the caller that is connected to the
133 HTTPS connection, check if the credential was signed by a
134 trusted cert and check if the credential is allowed to perform
135 the specified operation.
137 cred = Credential(cred=credential)
138 self.client_cred = cred
139 logger.debug("Auth.check: handling hrn=%s and credential=%s"%\
140 (hrn,cred.pretty_cred()))
142 if cred.type not in ['geni_sfa']:
143 raise CredentialNotVerifiable(cred.type, "%s not supported" % cred.type)
144 self.client_gid = self.client_cred.get_gid_caller()
145 self.object_gid = self.client_cred.get_gid_object()
147 # make sure the client_gid is not blank
148 if not self.client_gid:
149 raise MissingCallerGID(self.client_cred.pretty_subject())
151 # validate the client cert if it exists
153 self.verifyPeerCert(self.peer_cert, self.client_gid)
155 # make sure the client is allowed to perform the operation
157 if not self.client_cred.can_perform(operation):
158 raise InsufficientRights(operation)
160 if self.trusted_cert_list:
161 self.client_cred.verify(self.trusted_cert_file_list,
162 self.config.SFA_CREDENTIAL_SCHEMA)
164 raise MissingTrustedRoots(self.config.get_trustedroots_dir())
166 # Make sure the credential's target matches the specified hrn.
167 # This check does not apply to trusted peers
168 trusted_peers = [gid.get_hrn() for gid in self.trusted_cert_list]
169 if hrn and self.client_gid.get_hrn() not in trusted_peers:
170 target_hrn = self.object_gid.get_hrn()
171 if not hrn == target_hrn:
172 raise PermissionError("Target hrn: %s doesn't match specified hrn: %s " % \
176 def check_ticket(self, ticket):
178 Check if the ticket was signed by a trusted cert
180 if self.trusted_cert_list:
181 client_ticket = SfaTicket(string=ticket)
182 client_ticket.verify_chain(self.trusted_cert_list)
184 raise MissingTrustedRoots(self.config.get_trustedroots_dir())
188 def verifyPeerCert(self, cert, gid):
189 # make sure the client_gid matches client's certificate
190 if not cert.is_pubkey(gid.get_pubkey()):
191 raise ConnectionKeyGIDMismatch(gid.get_subject()+":"+cert.get_subject())
193 def verifyGidRequestHash(self, gid, hash, arglist):
194 key = gid.get_pubkey()
195 if not key.verify_string(str(arglist), hash):
196 raise BadRequestHash(hash)
198 def verifyCredRequestHash(self, cred, hash, arglist):
199 gid = cred.get_gid_caller()
200 self.verifyGidRequestHash(gid, hash, arglist)
202 def validateGid(self, gid):
203 if self.trusted_cert_list:
204 gid.verify_chain(self.trusted_cert_list)
206 def validateCred(self, cred):
207 if self.trusted_cert_list:
208 cred.verify(self.trusted_cert_file_list)
210 def authenticateGid(self, gidStr, argList, requestHash=None):
211 gid = GID(string = gidStr)
212 self.validateGid(gid)
213 # request_hash is optional
215 self.verifyGidRequestHash(gid, requestHash, argList)
218 def authenticateCred(self, credStr, argList, requestHash=None):
219 cred = Credential(string = credStr)
220 self.validateCred(cred)
221 # request hash is optional
223 self.verifyCredRequestHash(cred, requestHash, argList)
226 def authenticateCert(self, certStr, requestHash):
227 cert = Certificate(string=certStr)
228 # xxx should be validateCred ??
229 self.validateCred(cert)
231 def gidNoop(self, gidStr, value, requestHash):
232 self.authenticateGid(gidStr, [gidStr, value], requestHash)
235 def credNoop(self, credStr, value, requestHash):
236 self.authenticateCred(credStr, [credStr, value], requestHash)
239 def verify_cred_is_me(self, credential):
241 cred = Credential(string=credential)
242 caller_gid = cred.get_gid_caller()
243 caller_hrn = caller_gid.get_hrn()
244 if caller_hrn != self.config.SFA_INTERFACE_HRN:
245 raise SfaPermissionDenied(self.config.SFA_INTEFACE_HRN)
249 def get_auth_info(self, auth_hrn):
251 Given an authority name, return the information for that authority.
252 This is basically a stub that calls the hierarchy module.
254 @param auth_hrn human readable name of authority
257 return self.hierarchy.get_auth_info(auth_hrn)
260 def veriry_auth_belongs_to_me(self, name):
262 Verify that an authority belongs to our hierarchy.
263 This is basically left up to the implementation of the hierarchy
264 module. If the specified name does not belong, ane exception is
265 thrown indicating the caller should contact someone else.
267 @param auth_name human readable name of authority
270 # get auth info will throw an exception if the authority doesnt exist
271 self.get_auth_info(name)
274 def verify_object_belongs_to_me(self, name):
276 Verify that an object belongs to our hierarchy. By extension,
277 this implies that the authority that owns the object belongs
278 to our hierarchy. If it does not an exception is thrown.
280 @param name human readable name of object
282 auth_name = self.get_authority(name)
285 if name == self.config.SFA_INTERFACE_HRN:
287 self.verify_auth_belongs_to_me(auth_name)
289 def verify_auth_belongs_to_me(self, name):
290 # get auth info will throw an exception if the authority doesnt exist
291 self.get_auth_info(name)
294 def verify_object_permission(self, name):
296 Verify that the object gid that was specified in the credential
297 allows permission to the object 'name'. This is done by a simple
298 prefix test. For example, an object_gid for plc.arizona would
299 match the objects plc.arizona.slice1 and plc.arizona.
301 @param name human readable name to test
303 object_hrn = self.object_gid.get_hrn()
304 if object_hrn == name:
306 if name.startswith(object_hrn + "."):
308 #if name.startswith(get_authority(name)):
311 raise PermissionError(name)
313 def determine_user_rights(self, caller_hrn, reg_record):
315 Given a user credential and a record, determine what set of rights the
316 user should have to that record.
318 This is intended to replace determine_user_rights() and
319 verify_cancreate_credential()
323 type = reg_record.type
325 logger.debug("entering determine_user_rights with record %s and caller_hrn %s"%\
326 (reg_record, caller_hrn))
329 # researchers in the slice are in the DB as-is
330 researcher_hrns = [ user.hrn for user in reg_record.reg_researchers ]
331 # locating PIs attached to that slice
332 slice_pis = reg_record.get_pis()
333 pi_hrns = [ user.hrn for user in slice_pis ]
334 if (caller_hrn in researcher_hrns + pi_hrns):
341 elif type == 'authority':
342 pi_hrns = [ user.hrn for user in reg_record.reg_pis ]
343 if (caller_hrn == self.config.SFA_INTERFACE_HRN):
347 if (caller_hrn in pi_hrns):
350 # NOTE: for the PL implementation, this 'operators' list
351 # amounted to users with 'tech' role in that site
352 # it seems like this is not needed any longer, so for now I just drop that
353 # operator_hrns = reg_record.get('operator',[])
354 # if (caller_hrn in operator_hrns):
355 # rl.add('authority')
368 def get_authority(self, hrn):
369 return get_authority(hrn)
371 def filter_creds_by_caller(self, creds, caller_hrn_list):
373 Returns a list of creds who's gid caller matches the
376 if not isinstance(creds, list):
379 if not isinstance(caller_hrn_list, list):
380 caller_hrn_list = [caller_hrn_list]
383 tmp_cred = Credential(string=cred)
384 if tmp_cred.get_gid_caller().get_hrn() in [caller_hrn_list]: