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