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