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