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