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