source: fedd/federation/authorizer.py @ e83f2f2

axis_examplecompt_changesinfo-ops
Last change on this file since e83f2f2 was e83f2f2, checked in by Ted Faber <faber@…>, 9 years ago

Move proofs around. Lots of changes, including fault handling.

  • Property mode set to 100644
File size: 15.1 KB
Line 
1#/usr/local/bin/python
2
3from tempfile import mkstemp
4from subprocess import call
5from threading import Lock
6
7from string import join, hexdigits
8
9from fedid import fedid
10from remote_service import service_caller
11from service_error import service_error
12from util import abac_pem_type, abac_split_cert
13from proof import proof
14
15
16import ABAC
17import pickle
18
19import sys
20import os, os.path
21import re
22
23class authorizer_base:
24    """
25    Classes based on this one keep track of authorization attributes for the
26    various modules running.  This base class holds some utility functions that
27    they all potentially use.
28    """
29
30    # general error exception for badly formed names. 
31    class bad_name(RuntimeError): pass
32    # difficulty creating an attribute
33    class attribute_error(RuntimeError): pass
34
35    @staticmethod
36    def auth_name(name):
37        """
38        Helper to convert a non-unicode local name to a unicode string.  Mixed
39        representations can needlessly confuse the authorizer.
40        """
41        if isinstance(name, basestring):
42            if not isinstance(name, unicode): return unicode(name)
43            else: return name
44        else: return name
45
46    @staticmethod
47    def valid_name(name):
48        """
49        Ensure that the given name is valid.  A valid name can either be a
50        triple of strings and fedids representing one of our generalized Emulab
51        names or a single fedid.  Compound names can include wildcards (None)
52        and must anchor to a fedid at their highest level (unless they're all
53        None)
54
55        This either returns True or throws an exception.  More an assertion
56        than a function.
57        """
58        if isinstance(name, tuple) and len(name) == 3:
59            for n in name:
60                if n: 
61                    if not (isinstance(n, basestring) or isinstance(n, fedid)):
62                        raise authorizer_base.bad_name(
63                                "names must be either a triple or a fedid")
64            for n in name: 
65                if n:
66                    if isinstance(n, fedid):
67                        return True
68                    else:
69                        raise authorizer_base.bad_name(
70                                "Compound names must be " + \
71                                "rooted in fedids: %s" % str(name))
72
73            return True
74        elif isinstance(name, fedid):
75            return True
76        else:
77            raise authorizer_base.bad_name(
78                    "Names must be a triple or a fedid (%s)" % name)
79
80
81class authorizer(authorizer_base):
82    """
83    This class keeps track of authorization attributes for the various modules
84    running.  When it gets smarter it will be the basis for a real
85    attribute-based authentication system.
86    """
87    def __init__(self, def_attr="testbed"):
88        self.attrs = { }
89        self.globals=set()
90
91    def set_attribute(self, name, attr):
92        """
93        Attach attr to name.  Multiple attrs can be attached.
94        """
95        self.valid_name(name)
96        if isinstance(name, tuple):
97            aname = tuple([ self.auth_name(n) for n in name])
98        else:
99            aname = self.auth_name(name)
100
101        if not self.attrs.has_key(aname):
102            self.attrs[aname] = set()
103        self.attrs[aname].add(attr)
104
105    def unset_attribute(self, name, attr):
106        """
107        Remove an attribute from name
108        """
109        self.valid_name(name)
110        if isinstance(name, tuple):
111            aname = tuple([ self.auth_name(n) for n in name])
112        else:
113            aname = self.auth_name(name)
114
115        attrs = self.attrs.get(aname, None)
116        if attrs: attrs.discard(attr)
117
118    def check_attribute(self, name, attr, with_proof=False):
119        """
120        Return True if name has attr (or if attr is global).  Tuple names match
121        any tuple name that matches all names present and has None entries in
122        other fileds.  For tuple names True implies that there is a matching
123        tuple name with the attribute.
124        """
125        def tup(tup, i, p):
126            mask = 1 << i
127            if p & mask : return authorizer.auth_name(tup[i])
128            else: return None
129
130        self.valid_name(name)
131        if attr in self.globals:
132            if with_proof: return True, proof("me", name, attr)
133            else: return True
134
135        if isinstance(name, tuple):
136            for p in range(0,8):
137                lookup = ( tup(name, 0, p), tup(name,1, p), tup(name,2,p))
138                if self.attrs.has_key(lookup):
139                    if attr in self.attrs[lookup]:
140                        if with_proof: return True, proof("me", name, attr)
141                        else: return True
142                # Drop through
143                if with_proof: return False, proof("me", name, attr)
144                else: return False
145        else:
146            if with_proof:
147                return attr in self.attrs.get(self.auth_name(name), set()), \
148                        proof("me", name, attr)
149            else:
150                return attr in self.attrs.get(self.auth_name(name), set())
151
152    def set_global_attribute(self, attr):
153        """
154        Set a global attribute.  All names, even those otherwise unknown to the
155        authorizer have this attribute.
156        """
157        self.globals.add(attr)
158
159    def unset_global_attribute(self, attr):
160        """
161        Remove a global attribute
162        """
163
164        self.globals.discard(attr)
165
166    def import_credentials(self, file_list=None, data_list=None):
167        return False
168
169    def __str__(self):
170        rv = ""
171        rv += "attrs %s\n" % self.attrs
172        rv += "globals %s" % self.globals
173        return rv
174
175    def clone(self):
176        rv = authorizer()
177        rv.attrs = self.attrs.copy()
178        rv.globals = self.globals.copy()
179        return rv
180
181    def save(self, fn=None):
182        if fn:
183            f = open(fn, "w")
184            pickle.dump(self, f)
185            f.close()
186
187    def load(self, fn=None):
188        if fn:
189            f = open(fn, "r")
190            a = pickle.load(f)
191            f.close()
192            self.attrs = a.attrs
193            self.globals = a.globals
194       
195
196class abac_authorizer(authorizer_base):
197    """
198    Use the ABAC authorization system to make attribute decisions.
199    """
200
201    clean_attr_re = re.compile('[^A-Za-z0-9_]+')
202    cred_file_re = re.compile('.*\.der$')
203    bad_name = authorizer_base.bad_name
204    attribute_error = authorizer_base.attribute_error
205    class no_file_error(RuntimeError): pass
206    class bad_cert_error(RuntimeError): pass
207
208    def __init__(self, certs=None, me=None, key=None, load=None, save=None):
209        self.creddy = '/usr/local/bin/creddy'
210        self.globals = set()
211        self.lock = Lock()
212        self.me = me
213        self.save_dir = load or save
214        if self.save_dir:
215            self.save_dir = os.path.abspath(self.save_dir)
216        # If the me parameter is a combination certificate, split it into the
217        # abac_authorizer save directory (if any) for use with creddy.
218        if self.me is not None and abac_pem_type(self.me) == 'both':
219            if self.save_dir:
220                keyfile="%s/key.pem" % self.save_dir
221                certfile = "%s/cert.pem" % self.save_dir
222
223                # Clear a spot for the new key and cert files.
224                for fn in (keyfile, certfile):
225                    if os.access(fn, os.F_OK):
226                        os.unlink(fn)
227
228                self.key, self.me = abac_split_cert(self.me, keyfile, certfile)
229            else:
230                raise abac_authorizer.bad_cert_error("Combination " + \
231                        "certificate and nowhere to split it");
232        else:
233            self.key = key
234        self.context = ABAC.Context()
235        if me:
236            self.fedid = fedid(file=self.me)
237            rv = self.context.load_id_file(self.me)
238            if rv != 0:
239                raise abac_authorizer.bad_name(
240                        'Cannot load identity from %s' % me)
241        else:
242            self.fedid = None
243
244        if isinstance(certs, basestring):
245            certs = [ certs ] 
246
247        for dir in certs or []:
248            self.context.load_directory(dir)
249
250        if load:
251            self.load(load)
252
253    # Modify the pickling operations so that the context and lock are not
254    # pickled
255
256    def __getstate__(self):
257        d = self.__dict__.copy()
258        del d['lock']
259        del d['context']
260        return d
261
262    def __setstate__(self, d):
263        # Import everything from the pickle dict (except what we excluded in
264        # __getstate__)
265        self.__dict__.update(d)
266        # Initialize the unpicklables
267        self.context = ABAC.Context()
268        self.lock = Lock()
269
270    @staticmethod
271    def clean_attr(attr):
272        return abac_authorizer.clean_attr_re.sub('_', attr)
273
274
275    def import_credentials(self, file_list=None, data_list=None):
276        if data_list:
277            return any([self.import_credential(data=d) for d in data_list])
278        elif file_list:
279            return any([self.import_credential(file=f) for f in file_list])
280        else:
281            return False
282
283    def import_credential(self, file=None, data=None):
284        if data:
285            if self.context.load_id_chunk(data) != ABAC.ABAC_CERT_SUCCESS:
286                return self.context.load_attribute_chunk(data) == \
287                        ABAC.ABAC_CERT_SUCCESS
288            else:
289                return True
290        elif file:
291            if self.context.load_id_file(file) != ABAC.ABAC_CERT_SUCCESS:
292                return self.context.load_attribute_file(file) == \
293                        ABAC.ABAC_CERT_SUCCESS
294            else:
295                return True
296        else:
297            return False
298
299    def set_attribute(self, name=None, attr=None, cert=None):
300        if name and attr:
301            if isinstance(name, tuple):
302                raise abac_authorizer.bad_name(
303                        "ABAC doesn't understand three-names")
304            # Convert non-string attributes to strings
305            if not isinstance(attr, basestring):
306                attr = "%s" % attr
307
308            if self.me and self.key:
309                # Create a credential and insert it into context
310                # This will simplify when we have libcreddy
311                try:
312                    # create temp file
313                    f, fn = mkstemp()
314                    os.close(f)
315                except EnvironmentError, e:
316                    raise abac_authorizer.attribute_error(
317                            "Cannot create temp file: %s" %e)
318
319                # Create the attribute certificate with creddy
320                cmd = [self.creddy, '--attribute', '--issuer=%s' % self.me, 
321                    '--key=%s' % self.key, '--role=%s' % self.clean_attr(attr), 
322                    '--subject-id=%s' % name, '--out=%s' % fn]
323                rv = call(cmd)
324                if rv == 0:
325                    self.lock.acquire()
326                    # load it to context and remove the file
327                    rv = self.context.load_attribute_file(fn)
328                    self.lock.release()
329                    os.unlink(fn)
330                else:
331                    os.unlink(fn)
332                    raise abac_authorizer.attribute_error(
333                            "creddy returned %s" % rv)
334            else:
335                raise abac_authorizer.attribute_error(
336                        "Identity and key not specified on creation")
337        elif cert:
338            # Insert this credential into the context
339            self.lock.acquire()
340            self.context.load_attribute_chunk(cert)
341            self.lock.release()
342        else:
343            raise abac_authorizer.attribute_error(
344                    "Neither name/attr nor cert is set")
345
346    def unset_attribute(self, name, attr):
347        if isinstance(name, tuple):
348            raise abac_authorizer.bad_name(
349                    "ABAC doesn't understand three-names")
350        # Convert non-string attributes to strings
351        if not isinstance(attr, basestring):
352            attr = "%s" % attr
353        cattr = self.clean_attr(attr)
354        self.lock.acquire()
355        ctxt = ABAC.Context()
356        ids = set()
357        for c in self.context.credentials():
358            h = c.head()
359            t = c.tail()
360            if h.is_role() and t.is_principal():
361                if t.principal() == '%s' % name and \
362                        h.principal() == '%s' % self.fedid and \
363                        h.role_name() == cattr:
364                    continue
365
366            id = c.issuer_cert()
367            if id not in ids:
368                ctxt.load_id_chunk(id)
369                ids.add(id)
370            ctxt.load_attribute_chunk(c.attribute_cert())
371        self.context = ctxt
372        self.lock.release()
373
374    @staticmethod
375    def starts_with_fedid(attr):
376        """
377        Return true if the first 40 characters of the string are hex digits
378        followed by a dot.  False otherwise.  Used in check_attribute.
379        """
380        if attr.find('.') == 40:
381            return all([ x in hexdigits for x in attr[0:40]])
382        else:
383            return False
384
385
386    def check_attribute(self, name, attr, with_proof=False):
387        if isinstance(name, tuple):
388            raise abac_authorizer.bad_name(
389                    "ABAC doesn't understand three-names")
390        else:
391            # Convert non-string attributes to strings
392            if not isinstance(attr, basestring):
393                attr = "%s" % attr
394            # Attributes that start with a fedid only have the part of the
395            # attribute after the dot cleaned.  Others are completely cleaned
396            # and have the owner fedid attached.
397            if self.starts_with_fedid(attr):
398                r, a = attr.split('.',1)
399                a = "%s.%s" % ( r, self.clean_attr(a))
400            else: 
401                a = "%s.%s" % (self.fedid, self.clean_attr(attr))
402
403            a = str(a)
404            n = str("%s" % name)
405
406            self.lock.acquire()
407            # Sigh. Unicode vs swig and swig seems to lose.  Make sure
408            # everything we pass into ABAC is a str not a unicode.
409            rv, p = self.context.query(a, n)
410            # XXX delete soon
411            if not rv and attr in self.globals: 
412                rv = True
413                p = None
414            self.lock.release()
415            if with_proof: return rv, proof(self.fedid, name, a, p)
416            else: return rv
417
418    def set_global_attribute(self, attr):
419        """
420        Set a global attribute.  All names, even those otherwise unknown to the
421        authorizer have this attribute.
422        """
423        self.lock.acquire()
424        self.globals.add(self.clean_attr(attr))
425        self.lock.release()
426
427    def unset_global_attribute(self, attr):
428        """
429        Remove a global attribute
430        """
431
432        self.lock.acquire()
433        self.globals.discard(self.clean_attr(attr))
434        self.lock.release()
435
436    def clone(self):
437        self.lock.acquire()
438        rv = abac_authorizer(me=self.me, key=self.key)
439        rv.globals = self.globals.copy()
440        rv.context = ABAC.Context(self.context)
441        self.lock.release()
442        return rv
443
444    def save(self, dir=None):
445        self.lock.acquire()
446        if dir:
447            self.save_dir = os.path.abspath(dir)
448        else:
449            dir = self.save_dir
450        if dir is None:
451            self.lock.release()
452            raise abac_authorizer.no_file_error("No load directory specified")
453        try:
454            if not os.access(dir, os.F_OK):
455                os.mkdir(dir)
456
457            f = open("%s/state" % dir, "w")
458            pickle.dump(self, f)
459            f.close()
460
461            if not os.access("%s/certs" %dir, os.F_OK):
462                os.mkdir("%s/certs" % dir)
463
464            # Clear the certs subdir
465            for fn in [ f for f in os.listdir("%s/certs" % dir) \
466                    if abac_authorizer.cred_file_re.match(f)]:
467                os.unlink('%s/certs/%s' % (dir, fn))
468
469            # Save the context
470            ii = 0
471            ai = 0
472            seenid = set()
473            seenattr = set()
474            for c in self.context.credentials():
475                id = c.issuer_cert()
476                attr = c.attribute_cert()
477                # NB: file naming conventions matter here.  The trailing_ID and
478                # _attr are required by ABAC.COntext.load_directory()
479                if id and id not in seenid:
480                    f = open("%s/certs/ID_%03d_ID.der" % (dir, ii), "w")
481                    f.write(id)
482                    f.close()
483                    ii += 1
484                    seenid.add(id)
485                if attr and attr not in seenattr:
486                    f = open("%s/certs/attr_%03d_attr.der" % (dir, ai), "w")
487                    f.write(attr)
488                    f.close()
489                    ai += 1
490                    seenattr.add(attr)
491        except EnvironmentError, e:
492            self.lock.release()
493            raise e
494        except pickle.PickleError, e:
495            self.lock.release()
496            raise e
497        self.lock.release()
498
499    def load(self, dir=None):
500        self.lock.acquire()
501        if dir:
502            self.save_dir = dir
503        else:
504            dir = self.save_dir
505        if dir is None:
506            self.lock.release()
507            raise abac_authorizer.no_file_error("No load directory specified")
508        try:
509            if os.access("%s/state" % dir, os.R_OK):
510                f = open("%s/state" % dir, "r")
511                st = pickle.load(f)
512                f.close()
513                # Copy the useful attributes from the pickled state
514                for a in ('globals', 'key', 'me', 'cert', 'fedid'):
515                    setattr(self, a, getattr(st, a, None))
516
517            # Initialize the new context with the new identity
518            self.context = ABAC.Context()
519            if self.me:
520                self.context.load_id_file(self.me)
521            self.context.load_directory("%s/certs" % dir)
522            self.save_dir = dir
523        except EnvironmentError, e:
524            self.lock.release()
525            raise e
526        except pickle.PickleError, e:
527            self.lock.release()
528            raise e
529        self.lock.release()
530
531    @staticmethod
532    def encode_credential(c):
533        return '%s <- %s' % (c.head().string(), c.tail().string())
534
535    def get_creds_for_principal(self, fid):
536        look_for = set(["%s" % fid])
537        found_attrs = set()
538        next_look = set()
539        found = set([])
540
541        self.lock.acquire()
542        while look_for:
543            for c in self.context.credentials():
544                tail = c.tail()
545                # XXX: This needs to be more aggressive for linked stuff
546                if tail.string() in look_for and c not in found:
547                    found.add(c)
548                    next_look.add(c.head().string())
549
550            look_for = next_look
551            next_look = set()
552        self.lock.release()
553       
554        return found
555
556    def __str__(self):
557
558        self.lock.acquire()
559        rv = "%s" % self.fedid
560        add = join([abac_authorizer.encode_credential(c)
561            for c in self.context.credentials()], '\n');
562        if add: rv += "\n%s" % add
563        self.lock.release()
564        return rv
565
Note: See TracBrowser for help on using the repository browser.