source: fedd/federation/authorizer.py @ 2627eb3

axis_examplecompt_changesinfo-ops
Last change on this file since 2627eb3 was e65150a, checked in by Ted Faber <faber@…>, 13 years ago

Make common ops use fewer options. Refactor.

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