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