do not depend on types.StringTypes anymore
[sfa.git] / sfa / trust / auth.py
1 #
2 # SfaAPI authentication 
3 #
4 import sys
5
6 from sfa.util.faults import InsufficientRights, MissingCallerGID, \
7     MissingTrustedRoots, PermissionError, BadRequestHash, \
8     ConnectionKeyGIDMismatch, SfaPermissionDenied, CredentialNotVerifiable, \
9     Forbidden, BadArgs
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
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         error = (None, None)
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))
70             else:
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]
75             return error
76
77         # if xrns are specified they cannot be None or empty string
78         if xrns:
79             for xrn in xrns:
80                 if not xrn:
81                     raise BadArgs("Invalid urn or hrn")
82
83         
84         if not isinstance(xrns, list):
85             xrns = [xrns]
86
87         slice_xrns  = Xrn.filter_type(xrns, 'slice')
88         sliver_xrns = Xrn.filter_type(xrns, 'sliver')
89
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] 
93         valid = []
94         if not isinstance(creds, list):
95             creds = [creds]
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]
100
101         speaks_for_gid = determine_speaks_for(logger, creds, self.peer_cert,
102                                               speaking_for_xrn, self.trusted_cert_list)
103
104         if self.peer_cert and \
105            not self.peer_cert.is_pubkey(speaks_for_gid.get_pubkey()):
106             valid = creds
107         else:
108             for cred in creds:
109                 for hrn in hrns:
110                     try:
111                         self.check(cred, operation, hrn)
112                         valid.append(cred)
113                     except:
114                         error = log_invalid_cred(cred)
115         
116         # make sure all sliver xrns are validated against the valid credentials
117         if sliver_xrns:
118             if not check_sliver_callback:
119                 msg = "sliver verification callback method not found." 
120                 msg += " Unable to validate sliver xrns: %s" % sliver_xrns
121                 raise Forbidden(msg)
122             check_sliver_callback(valid, sliver_xrns)
123                 
124         if not len(valid):
125             raise Forbidden("Invalid credential %s -- %s"%(error[0],error[1]))
126         
127         return valid
128         
129     def check(self, credential, operation, hrn = None):
130         """
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.    
136         """
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()))
141
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()
146         
147         # make sure the client_gid is not blank
148         if not self.client_gid:
149             raise MissingCallerGID(self.client_cred.pretty_subject())
150        
151         # validate the client cert if it exists
152         if self.peer_cert:
153             self.verifyPeerCert(self.peer_cert, self.client_gid)                   
154
155         # make sure the client is allowed to perform the operation
156         if operation:
157             if not self.client_cred.can_perform(operation):
158                 raise InsufficientRights(operation)
159
160         if self.trusted_cert_list:
161             self.client_cred.verify(self.trusted_cert_file_list,
162                                     self.config.SFA_CREDENTIAL_SCHEMA)
163         else:
164            raise MissingTrustedRoots(self.config.get_trustedroots_dir())
165        
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 " % \
173                                        (target_hrn, hrn) )       
174         return True
175
176     def check_ticket(self, ticket):
177         """
178         Check if the ticket was signed by a trusted cert
179         """
180         if self.trusted_cert_list:
181             client_ticket = SfaTicket(string=ticket)
182             client_ticket.verify_chain(self.trusted_cert_list)
183         else:
184            raise MissingTrustedRoots(self.config.get_trustedroots_dir())
185
186         return True 
187
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())            
192
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)
197
198     def verifyCredRequestHash(self, cred, hash, arglist):
199         gid = cred.get_gid_caller()
200         self.verifyGidRequestHash(gid, hash, arglist)
201
202     def validateGid(self, gid):
203         if self.trusted_cert_list:
204             gid.verify_chain(self.trusted_cert_list)
205
206     def validateCred(self, cred):
207         if self.trusted_cert_list:
208             cred.verify(self.trusted_cert_file_list)
209
210     def authenticateGid(self, gidStr, argList, requestHash=None):
211         gid = GID(string = gidStr)
212         self.validateGid(gid)
213         # request_hash is optional
214         if requestHash:
215             self.verifyGidRequestHash(gid, requestHash, argList)
216         return gid
217
218     def authenticateCred(self, credStr, argList, requestHash=None):
219         cred = Credential(string = credStr)
220         self.validateCred(cred)
221         # request hash is optional
222         if requestHash:
223             self.verifyCredRequestHash(cred, requestHash, argList)
224         return cred
225
226     def authenticateCert(self, certStr, requestHash):
227         cert = Certificate(string=certStr)
228         # xxx should be validateCred ??
229         self.validateCred(cert)   
230
231     def gidNoop(self, gidStr, value, requestHash):
232         self.authenticateGid(gidStr, [gidStr, value], requestHash)
233         return value
234
235     def credNoop(self, credStr, value, requestHash):
236         self.authenticateCred(credStr, [credStr, value], requestHash)
237         return value
238
239     def verify_cred_is_me(self, credential):
240         is_me = False 
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)
246
247         return   
248         
249     def get_auth_info(self, auth_hrn):
250         """
251         Given an authority name, return the information for that authority.
252         This is basically a stub that calls the hierarchy module.
253         
254         @param auth_hrn human readable name of authority  
255         """
256
257         return self.hierarchy.get_auth_info(auth_hrn)
258
259
260     def veriry_auth_belongs_to_me(self, name):
261         """
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.
266
267         @param auth_name human readable name of authority
268         """
269
270         # get auth info will throw an exception if the authority doesnt exist
271         self.get_auth_info(name)
272
273
274     def verify_object_belongs_to_me(self, name):
275         """
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.
279     
280         @param name human readable name of object        
281         """
282         auth_name = self.get_authority(name)
283         if not auth_name:
284             auth_name = name 
285         if name == self.config.SFA_INTERFACE_HRN:
286             return
287         self.verify_auth_belongs_to_me(auth_name) 
288              
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) 
292
293
294     def verify_object_permission(self, name):
295         """
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.
300     
301         @param name human readable name to test  
302         """
303         object_hrn = self.object_gid.get_hrn()
304         if object_hrn == name:
305             return
306         if name.startswith(object_hrn + "."):
307             return
308         #if name.startswith(get_authority(name)):
309             #return
310     
311         raise PermissionError(name)
312
313     def determine_user_rights(self, caller_hrn, reg_record):
314         """
315         Given a user credential and a record, determine what set of rights the
316         user should have to that record.
317         
318         This is intended to replace determine_user_rights() and
319         verify_cancreate_credential()
320         """
321
322         rl = Rights()
323         type = reg_record.type
324
325         logger.debug("entering determine_user_rights with record %s and caller_hrn %s"%\
326                      (reg_record, caller_hrn))
327
328         if type == 'slice':
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):
335                 rl.add('refresh')
336                 rl.add('embed')
337                 rl.add('bind')
338                 rl.add('control')
339                 rl.add('info')
340
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):
344                 rl.add('authority')
345                 rl.add('sa')
346                 rl.add('ma')
347             if (caller_hrn in pi_hrns):
348                 rl.add('authority')
349                 rl.add('sa')
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')
356             #    rl.add('ma')
357
358         elif type == 'user':
359             rl.add('refresh')
360             rl.add('resolve')
361             rl.add('info')
362
363         elif type == 'node':
364             rl.add('operator')
365
366         return rl
367
368     def get_authority(self, hrn):
369         return get_authority(hrn)
370
371     def filter_creds_by_caller(self, creds, caller_hrn_list):
372         """
373         Returns a list of creds who's gid caller matches the 
374         specified caller hrn
375         """
376         if not isinstance(creds, list):
377             creds = [creds]
378         creds = []
379         if not isinstance(caller_hrn_list, list):
380             caller_hrn_list = [caller_hrn_list]
381         for cred in creds:
382             try:
383                 tmp_cred = Credential(string=cred)
384                 if tmp_cred.get_gid_caller().get_hrn() in [caller_hrn_list]:
385                     creds.append(cred)
386             except: pass
387         return creds
388