source: fedd/federation/authorizer.py @ 52131d3

compt_changesinfo-ops
Last change on this file since 52131d3 was 52131d3, checked in by Ted Faber <faber@…>, 13 years ago

Really make sure the files are not the same.

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