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