a little more robust against odd conditions
[sfa.git] / sfa / trust / auth.py
1 #
2 # SfaAPI authentication 
3 #
4 import sys
5 from types import StringTypes
6
7 from sfa.util.faults import InsufficientRights, MissingCallerGID, MissingTrustedRoots, PermissionError, \
8     BadRequestHash, ConnectionKeyGIDMismatch, SfaPermissionDenied, CredentialNotVerifiable, Forbidden, \
9     BadArgs
10 from sfa.util.sfalogging import logger
11 from sfa.util.config import Config
12 from sfa.util.xrn import Xrn, get_authority
13
14 from sfa.trust.gid import GID
15 from sfa.trust.rights import Rights
16 from sfa.trust.certificate import Keypair, Certificate
17 from sfa.trust.credential import Credential
18 from sfa.trust.trustedroots import TrustedRoots
19 from sfa.trust.hierarchy import Hierarchy
20 from sfa.trust.sfaticket import SfaTicket
21 from sfa.trust.speaksfor_util import determine_speaks_for
22
23
24 class Auth:
25     """
26     Credential based authentication
27     """
28
29     def __init__(self, peer_cert = None, config = None ):
30         self.peer_cert = peer_cert
31         self.hierarchy = Hierarchy()
32         if not config:
33             self.config = Config()
34         self.load_trusted_certs()
35
36     def load_trusted_certs(self):
37         self.trusted_cert_list = TrustedRoots(self.config.get_trustedroots_dir()).get_list()
38         self.trusted_cert_file_list = TrustedRoots(self.config.get_trustedroots_dir()).get_file_list()
39
40     # this convenience methods extracts speaking_for_xrn from the passed options using 'geni_speaking_for'
41     def checkCredentialsSpeaksFor (self, *args, **kwds):
42         if 'options' not in kwds:
43             logger.error ("checkCredentialsSpeaksFor was not passed options=options")
44             return
45         # remove the options arg
46         options=kwds['options']; del kwds['options']
47         # compute the speaking_for_xrn arg and pass it to checkCredentials
48         if options is None: speaking_for_xrn=None
49         else:               speaking_for_xrn=options.get('geni_speaking_for',None)
50         kwds['speaking_for_xrn']=speaking_for_xrn
51         return self.checkCredentials (*args, **kwds)
52
53     # do not use mutable as default argument 
54     # http://docs.python-guide.org/en/latest/writing/gotchas/#mutable-default-arguments
55     def checkCredentials(self, creds, operation, xrns=None, 
56                          check_sliver_callback=None, 
57                          speaking_for_xrn=None):
58         if xrns is None: xrns=[]
59         def log_invalid_cred(cred):
60             if not isinstance (cred, StringTypes):
61                 logger.info("cannot validate credential %s - expecting a string"%cred)
62                 error="checkCredentials: expected a string, received %s"%(type(cred))
63             else:
64                 cred_obj=Credential(string=cred)
65                 logger.info("failed to validate credential - dump=%s"%cred_obj.dump_string(dump_parents=True))
66                 error = sys.exc_info()[:2]
67             return error
68
69         # if xrns are specified they cannot be None or empty string
70         if xrns:
71             for xrn in xrns:
72                 if not xrn:
73                     raise BadArgs("Invalid urn or hrn")
74
75         
76         if not isinstance(xrns, list):
77             xrns = [xrns]
78
79         slice_xrns = Xrn.filter_type(xrns, 'slice')
80         sliver_xrns = Xrn.filter_type(xrns, 'sliver')
81
82         # we are not able to validate slivers in the traditional way so 
83         # we make sure not to include sliver urns/hrns in the core validation loop
84         hrns = [Xrn(xrn).hrn for xrn in xrns if xrn not in sliver_xrns] 
85         valid = []
86         if not isinstance(creds, list):
87             creds = [creds]
88         logger.debug("Auth.checkCredentials with %d creds on hrns=%s"%(len(creds),hrns))
89         # won't work if either creds or hrns is empty - let's make it more explicit
90         if not creds: raise Forbidden("no credential provided")
91         if not hrns: hrns = [None]
92         error=[None,None]
93
94         speaks_for_gid = determine_speaks_for(logger, creds, self.peer_cert,
95                                               speaking_for_xrn, self.trusted_cert_list)
96
97         if self.peer_cert and \
98            not self.peer_cert.is_pubkey(speaks_for_gid.get_pubkey()):
99             valid = creds
100         else:
101             for cred in creds:
102                 for hrn in hrns:
103                     try:
104                         self.check(cred, operation, hrn)
105                         valid.append(cred)
106                     except:
107                         error = log_invalid_cred(cred)
108         
109         # make sure all sliver xrns are validated against the valid credentials
110         if sliver_xrns:
111             if not check_sliver_callback:
112                 msg = "sliver verification callback method not found." 
113                 msg += " Unable to validate sliver xrns: %s" % sliver_xrns
114                 raise Forbidden(msg)
115             check_sliver_callback(valid, sliver_xrns)
116                 
117         if not len(valid):
118             raise Forbidden("Invalid credential %s -- %s"%(error[0],error[1]))
119         
120         return valid
121         
122         
123     def check(self, credential, operation, hrn = None):
124         """
125         Check the credential against the peer cert (callerGID included 
126         in the credential matches the caller that is connected to the 
127         HTTPS connection, check if the credential was signed by a 
128         trusted cert and check if the credential is allowed to perform 
129         the specified operation.    
130         """
131         cred = Credential(cred=credential)    
132         self.client_cred = cred
133         logger.debug("Auth.check: handling hrn=%s and credential=%s"%\
134                          (hrn,cred.get_summary_tostring()))
135
136         if cred.type not in ['geni_sfa']:
137             raise CredentialNotVerifiable(cred.type, "%s not supported" % cred.type)
138         self.client_gid = self.client_cred.get_gid_caller()
139         self.object_gid = self.client_cred.get_gid_object()
140         
141         # make sure the client_gid is not blank
142         if not self.client_gid:
143             raise MissingCallerGID(self.client_cred.get_subject())
144        
145         # validate the client cert if it exists
146         if self.peer_cert:
147             self.verifyPeerCert(self.peer_cert, self.client_gid)                   
148
149         # make sure the client is allowed to perform the operation
150         if operation:
151             if not self.client_cred.can_perform(operation):
152                 raise InsufficientRights(operation)
153
154         if self.trusted_cert_list:
155             self.client_cred.verify(self.trusted_cert_file_list, self.config.SFA_CREDENTIAL_SCHEMA)
156         else:
157            raise MissingTrustedRoots(self.config.get_trustedroots_dir())
158        
159         # Make sure the credential's target matches the specified hrn. 
160         # This check does not apply to trusted peers 
161         trusted_peers = [gid.get_hrn() for gid in self.trusted_cert_list]
162         if hrn and self.client_gid.get_hrn() not in trusted_peers:
163             target_hrn = self.object_gid.get_hrn()
164             if not hrn == target_hrn:
165                 raise PermissionError("Target hrn: %s doesn't match specified hrn: %s " % \
166                                        (target_hrn, hrn) )       
167         return True
168
169     def check_ticket(self, ticket):
170         """
171         Check if the tickt was signed by a trusted cert
172         """
173         if self.trusted_cert_list:
174             client_ticket = SfaTicket(string=ticket)
175             client_ticket.verify_chain(self.trusted_cert_list)
176         else:
177            raise MissingTrustedRoots(self.config.get_trustedroots_dir())
178
179         return True 
180
181     def verifyPeerCert(self, cert, gid):
182         # make sure the client_gid matches client's certificate
183         if not cert.is_pubkey(gid.get_pubkey()):
184             raise ConnectionKeyGIDMismatch(gid.get_subject()+":"+cert.get_subject())            
185
186     def verifyGidRequestHash(self, gid, hash, arglist):
187         key = gid.get_pubkey()
188         if not key.verify_string(str(arglist), hash):
189             raise BadRequestHash(hash)
190
191     def verifyCredRequestHash(self, cred, hash, arglist):
192         gid = cred.get_gid_caller()
193         self.verifyGidRequestHash(gid, hash, arglist)
194
195     def validateGid(self, gid):
196         if self.trusted_cert_list:
197             gid.verify_chain(self.trusted_cert_list)
198
199     def validateCred(self, cred):
200         if self.trusted_cert_list:
201             cred.verify(self.trusted_cert_file_list)
202
203     def authenticateGid(self, gidStr, argList, requestHash=None):
204         gid = GID(string = gidStr)
205         self.validateGid(gid)
206         # request_hash is optional
207         if requestHash:
208             self.verifyGidRequestHash(gid, requestHash, argList)
209         return gid
210
211     def authenticateCred(self, credStr, argList, requestHash=None):
212         cred = Credential(string = credStr)
213         self.validateCred(cred)
214         # request hash is optional
215         if requestHash:
216             self.verifyCredRequestHash(cred, requestHash, argList)
217         return cred
218
219     def authenticateCert(self, certStr, requestHash):
220         cert = Certificate(string=certStr)
221         # xxx should be validateCred ??
222         self.validateCred(cert)   
223
224     def gidNoop(self, gidStr, value, requestHash):
225         self.authenticateGid(gidStr, [gidStr, value], requestHash)
226         return value
227
228     def credNoop(self, credStr, value, requestHash):
229         self.authenticateCred(credStr, [credStr, value], requestHash)
230         return value
231
232     def verify_cred_is_me(self, credential):
233         is_me = False 
234         cred = Credential(string=credential)
235         caller_gid = cred.get_gid_caller()
236         caller_hrn = caller_gid.get_hrn()
237         if caller_hrn != self.config.SFA_INTERFACE_HRN:
238             raise SfaPermissionDenied(self.config.SFA_INTEFACE_HRN)
239
240         return   
241         
242     def get_auth_info(self, auth_hrn):
243         """
244         Given an authority name, return the information for that authority.
245         This is basically a stub that calls the hierarchy module.
246         
247         @param auth_hrn human readable name of authority  
248         """
249
250         return self.hierarchy.get_auth_info(auth_hrn)
251
252
253     def veriry_auth_belongs_to_me(self, name):
254         """
255         Verify that an authority belongs to our hierarchy. 
256         This is basically left up to the implementation of the hierarchy
257         module. If the specified name does not belong, ane exception is 
258         thrown indicating the caller should contact someone else.
259
260         @param auth_name human readable name of authority
261         """
262
263         # get auth info will throw an exception if the authority doesnt exist
264         self.get_auth_info(name)
265
266
267     def verify_object_belongs_to_me(self, name):
268         """
269         Verify that an object belongs to our hierarchy. By extension,
270         this implies that the authority that owns the object belongs
271         to our hierarchy. If it does not an exception is thrown.
272     
273         @param name human readable name of object        
274         """
275         auth_name = self.get_authority(name)
276         if not auth_name:
277             auth_name = name 
278         if name == self.config.SFA_INTERFACE_HRN:
279             return
280         self.verify_auth_belongs_to_me(auth_name) 
281              
282     def verify_auth_belongs_to_me(self, name):
283         # get auth info will throw an exception if the authority doesnt exist
284         self.get_auth_info(name) 
285
286
287     def verify_object_permission(self, name):
288         """
289         Verify that the object gid that was specified in the credential
290         allows permission to the object 'name'. This is done by a simple
291         prefix test. For example, an object_gid for plc.arizona would 
292         match the objects plc.arizona.slice1 and plc.arizona.
293     
294         @param name human readable name to test  
295         """
296         object_hrn = self.object_gid.get_hrn()
297         if object_hrn == name:
298             return
299         if name.startswith(object_hrn + "."):
300             return
301         #if name.startswith(get_authority(name)):
302             #return
303     
304         raise PermissionError(name)
305
306     def determine_user_rights(self, caller_hrn, reg_record):
307         """
308         Given a user credential and a record, determine what set of rights the
309         user should have to that record.
310         
311         This is intended to replace determine_user_rights() and
312         verify_cancreate_credential()
313         """
314
315         rl = Rights()
316         type = reg_record.type
317
318         logger.debug("entering determine_user_rights with record %s and caller_hrn %s"%(reg_record, caller_hrn))
319
320         if type == 'slice':
321             # researchers in the slice are in the DB as-is
322             researcher_hrns = [ user.hrn for user in reg_record.reg_researchers ]
323             # locating PIs attached to that slice
324             slice_pis=reg_record.get_pis()
325             pi_hrns = [ user.hrn for user in slice_pis ]
326             if (caller_hrn in researcher_hrns + pi_hrns):
327                 rl.add('refresh')
328                 rl.add('embed')
329                 rl.add('bind')
330                 rl.add('control')
331                 rl.add('info')
332
333         elif type == 'authority':
334             pi_hrns = [ user.hrn for user in reg_record.reg_pis ]
335             if (caller_hrn == self.config.SFA_INTERFACE_HRN):
336                 rl.add('authority')
337                 rl.add('sa')
338                 rl.add('ma')
339             if (caller_hrn in pi_hrns):
340                 rl.add('authority')
341                 rl.add('sa')
342             # NOTE: for the PL implementation, this 'operators' list 
343             # amounted to users with 'tech' role in that site 
344             # it seems like this is not needed any longer, so for now I just drop that
345             # operator_hrns = reg_record.get('operator',[])
346             # if (caller_hrn in operator_hrns):
347             #    rl.add('authority')
348             #    rl.add('ma')
349
350         elif type == 'user':
351             rl.add('refresh')
352             rl.add('resolve')
353             rl.add('info')
354
355         elif type == 'node':
356             rl.add('operator')
357
358         return rl
359
360     def get_authority(self, hrn):
361         return get_authority(hrn)
362
363     def filter_creds_by_caller(self, creds, caller_hrn_list):
364         """
365         Returns a list of creds who's gid caller matches the 
366         specified caller hrn
367         """
368         if not isinstance(creds, list):
369             creds = [creds]
370         creds = []
371         if not isinstance(caller_hrn_list, list):
372             caller_hrn_list = [caller_hrn_list]
373         for cred in creds:
374             try:
375                 tmp_cred = Credential(string=cred)
376                 if tmp_cred.get_gid_caller().get_hrn() in [caller_hrn_list]:
377                     creds.append(cred)
378             except: pass
379         return creds
380