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