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