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