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