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