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