source: fedd/federation/authorizer.py @ cf7bc45

compt_changes
Last change on this file since cf7bc45 was cf7bc45, checked in by Ted Faber <faber@…>, 12 years ago

Tool to add credentials to an existing abac authorizer

  • Property mode set to 100644
File size: 17.3 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 deter 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 Creddy
18import pickle
19
20import sys
21import os, os.path
22import re
23
24class authorizer_base:
25    """
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.
29    """
30
31    # general error exception for badly formed names. 
32    class bad_name(RuntimeError): pass
33    # difficulty creating an attribute
34    class attribute_error(RuntimeError): pass
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
47    @staticmethod
48    def valid_name(name):
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)):
63                        raise authorizer_base.bad_name(
64                                "names must be either a triple or a fedid")
65            for n in name: 
66                if n:
67                    if isinstance(n, fedid):
68                        return True
69                    else:
70                        raise authorizer_base.bad_name(
71                                "Compound names must be " + \
72                                "rooted in fedids: %s" % str(name))
73
74            return True
75        elif isinstance(name, fedid):
76            return True
77        else:
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()
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
119    def check_attribute(self, name, attr, with_proof=False):
120        """
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.
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)
132        if attr in self.globals:
133            if with_proof: return True, proof("me", name, attr)
134            else: return True
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]:
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
146        else:
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())
152
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)
166
167    def import_credentials(self, file_list=None, data_list=None):
168        return False
169
170    def __str__(self):
171        rv = ""
172        rv += "attrs %s\n" % self.attrs
173        rv += "globals %s" % self.globals
174        return rv
175
176    def clone(self):
177        rv = authorizer()
178        rv.attrs = self.attrs.copy()
179        rv.globals = self.globals.copy()
180        return rv
181
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
195       
196
197class abac_authorizer(authorizer_base):
198    """
199    Use the ABAC authorization system to make attribute decisions.
200    """
201
202    clean_attr_re = re.compile('[^A-Za-z0-9_]+')
203    clean_prefix_attr_re = re.compile('^_+')
204    cred_file_re = re.compile('.*\.der$')
205    bad_name = authorizer_base.bad_name
206    attribute_error = authorizer_base.attribute_error
207    class no_file_error(RuntimeError): pass
208    class bad_cert_error(RuntimeError): pass
209
210    def __init__(self, certs=None, me=None, key=None, load=None, save=None):
211        self.creddy_id = None
212        self.globals = set()
213        self.lock = Lock()
214        self.me = me
215        self.save_dir = load or save
216        self.local_files = False
217        if self.save_dir:
218            self.save_dir = os.path.abspath(self.save_dir)
219        # If the me parameter is a combination certificate, split it into the
220        # abac_authorizer save directory (if any) for use with creddy.
221        if self.me is not None and abac_pem_type(self.me) == 'both':
222            if self.save_dir:
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)
232                self.local_files = True
233                self.init_libcreddy_id()
234            else:
235                raise abac_authorizer.bad_cert_error("Combination " + \
236                        "certificate and nowhere to split it");
237        else:
238            self.key = key
239        self.context = ABAC.Context()
240        if me:
241            self.fedid = fedid(file=self.me)
242            rv = self.context.load_id_file(self.me)
243            if rv != 0:
244                raise abac_authorizer.bad_name(
245                        'Cannot load identity from %s' % me)
246        else:
247            self.fedid = None
248
249        if isinstance(certs, basestring):
250            certs = [ certs ] 
251
252        for dir in certs or []:
253            self.context.load_directory(dir)
254
255        if load:
256            self.load(load)
257
258    def init_libcreddy_id(self):
259        self.creddy_id = None
260        try:
261            self.creddy_id = Creddy.ID(self.me)
262        except:
263            raise abac_authorizer.bad_cert_error('Cannot load cert %s' \
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
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']
282        del d['creddy_id']
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
293    @staticmethod
294    def clean_attr(attr):
295        '''
296        Convert anything not in A-Z_ into an _ and make sure that the string
297        begins with a non-_.  Convert to s string to keep swig happy.
298        '''
299        a = abac_authorizer.clean_attr_re.sub('_', attr)
300        return str(abac_authorizer.clean_prefix_attr_re.sub('', a))
301
302
303    def import_credentials(self, file_list=None, data_list=None):
304        '''
305        Import a list of credentials.  Be careful about dependencies and only
306        use one logic loop.  if files are given, append them to the list of
307        chunks and then try to read each chunk as an ID and then the failed
308        ones as attributes.
309        '''
310        if file_list:
311            if data_list is None: data_list = []
312            for fn in file_list:
313                # Try to parse file as a Creddy ID, so we can import PEM files
314                try:
315                    cid = Creddy.ID(fn)
316                    data_list.append(cid.cert_chunk())
317                    continue
318                except:
319                    # Not an ID
320                    pass
321                try:
322                    f = open(fn, 'r')
323                    data_list.append(f.read())
324                    f.close()
325                except EnvironmentError, e:
326                    # Ignore unreadable files
327                    pass
328        # Now one way or another data_list is ready
329        if data_list:
330            rv = False
331            attr_list = [ ]
332            for d in data_list:
333                if self.context.load_id_chunk(d) != ABAC.ABAC_CERT_SUCCESS:
334                    attr_list.append(d)
335                else:
336                    rv = True
337
338            for d in attr_list:
339                v = self.context.load_attribute_chunk(d) 
340                if v == ABAC.ABAC_CERT_SUCCESS:
341                    rv = True
342            return rv
343        else:
344            return False
345
346    def import_credential(self, file=None, data=None):
347        if data:
348            if self.context.load_id_chunk(data) != ABAC.ABAC_CERT_SUCCESS:
349                return self.context.load_attribute_chunk(data) == \
350                        ABAC.ABAC_CERT_SUCCESS
351            else:
352                return True
353        elif file:
354            if self.context.load_id_file(file) != ABAC.ABAC_CERT_SUCCESS:
355                return self.context.load_attribute_file(file) == \
356                        ABAC.ABAC_CERT_SUCCESS
357            else:
358                return True
359        else:
360            return False
361
362    def set_attribute(self, name=None, attr=None, cert=None):
363        if name and attr:
364            if isinstance(name, tuple):
365                raise abac_authorizer.bad_name(
366                        "ABAC doesn't understand three-names")
367            # Convert non-string attributes to strings
368            if not isinstance(attr, basestring):
369                attr = "%s" % attr
370
371            if self.creddy_id:
372                # Create a credential and insert it into context
373                # This will simplify when we have libcreddy
374                try:
375                    attrcert = Creddy.Attribute(self.creddy_id, 
376                            self.clean_attr(attr), 3600 * 24 * 365 * 10)
377                    attrcert.principal("%s" % name)
378                    attrcert.bake()
379                except:
380                    raise abac_authorizer.bad_cert_error(
381                            "Cannot create attribute cert")
382                self.lock.acquire()
383                # load it to context and remove the file
384                rv = self.context.load_attribute_chunk(attrcert.cert_chunk())
385                self.lock.release()
386                return rv
387            else:
388                raise abac_authorizer.attribute_error(
389                        "Identity and key not specified on creation")
390        elif cert:
391            # Insert this credential into the context
392            self.lock.acquire()
393            rv = self.context.load_attribute_chunk(cert)
394            self.lock.release()
395            return rv
396        else:
397            raise abac_authorizer.attribute_error(
398                    "Neither name/attr nor cert is set")
399
400    def unset_attribute(self, name, attr):
401        if isinstance(name, tuple):
402            raise abac_authorizer.bad_name(
403                    "ABAC doesn't understand three-names")
404        # Convert non-string attributes to strings
405        if not isinstance(attr, basestring):
406            attr = "%s" % attr
407        cattr = self.clean_attr(attr)
408        self.lock.acquire()
409        ctxt = ABAC.Context()
410        ids = set()
411        for c in self.context.credentials():
412            h = c.head()
413            t = c.tail()
414            if h.is_role() and t.is_principal():
415                if t.principal() == '%s' % name and \
416                        h.principal() == '%s' % self.fedid and \
417                        h.role_name() == cattr:
418                    continue
419
420            id = c.issuer_cert()
421            if id not in ids:
422                ctxt.load_id_chunk(id)
423                ids.add(id)
424            ctxt.load_attribute_chunk(c.attribute_cert())
425        self.context = ctxt
426        self.lock.release()
427
428    @staticmethod
429    def starts_with_fedid(attr):
430        """
431        Return true if the first 40 characters of the string are hex digits
432        followed by a dot.  False otherwise.  Used in check_attribute.
433        """
434        if attr.find('.') == 40:
435            return all([ x in hexdigits for x in attr[0:40]])
436        else:
437            return False
438
439
440    def check_attribute(self, name, attr, with_proof=False):
441        if isinstance(name, tuple):
442            raise abac_authorizer.bad_name(
443                    "ABAC doesn't understand three-names")
444        else:
445            # Convert non-string attributes to strings
446            if not isinstance(attr, basestring):
447                attr = "%s" % attr
448            # Attributes that start with a fedid only have the part of the
449            # attribute after the dot cleaned.  Others are completely cleaned
450            # and have the owner fedid attached.
451            if self.starts_with_fedid(attr):
452                r, a = attr.split('.',1)
453                a = "%s.%s" % ( r, self.clean_attr(a))
454            else: 
455                a = "%s.%s" % (self.fedid, self.clean_attr(attr))
456
457            a = str(a)
458            n = str("%s" % name)
459
460            self.lock.acquire()
461            # Sigh. Unicode vs swig and swig seems to lose.  Make sure
462            # everything we pass into ABAC is a str not a unicode.
463            rv, p = self.context.query(a, n)
464            # XXX delete soon
465            if not rv and attr in self.globals: 
466                rv = True
467                p = None
468            self.lock.release()
469            if with_proof: return rv, proof(self.fedid, name, a, p)
470            else: return rv
471
472    def set_global_attribute(self, attr):
473        """
474        Set a global attribute.  All names, even those otherwise unknown to the
475        authorizer have this attribute.
476        """
477        self.lock.acquire()
478        self.globals.add(self.clean_attr(attr))
479        self.lock.release()
480
481    def unset_global_attribute(self, attr):
482        """
483        Remove a global attribute
484        """
485
486        self.lock.acquire()
487        self.globals.discard(self.clean_attr(attr))
488        self.lock.release()
489
490    def clone(self):
491        self.lock.acquire()
492        rv = abac_authorizer(me=self.me, key=self.key)
493        rv.globals = self.globals.copy()
494        rv.context = ABAC.Context(self.context)
495        self.lock.release()
496        return rv
497
498    def copy_file(self, src, dest, mode=0600):
499        '''
500        Copy src to dest with file mode mode.  May raise exceptions on file ops
501        '''
502        same_file = False
503        try:
504            ss = os.stat(src)
505            ds = os.stat(dest)
506
507            if  ds.st_ino == ss.st_ino: same_file = True
508        except EnvironmentError:
509            pass
510
511        if not same_file:
512            d = open(dest, 'w')
513            s = open(src, 'r')
514            d.write(s.read())
515            s.close()
516            d.close()
517        os.chmod(dest, mode)
518
519
520
521    def save(self, dir=None):
522        self.lock.acquire()
523        if dir:
524            self.save_dir = os.path.abspath(dir)
525        else:
526            dir = self.save_dir
527        if dir is None:
528            self.lock.release()
529            raise abac_authorizer.no_file_error("No load directory specified")
530        try:
531            if not os.access(dir, os.F_OK):
532                os.mkdir(dir)
533
534            # if self.key and self.me were split, copy the split files into the
535            # new directory.
536            if self.local_files:
537                self.copy_file(self.key, '%s/key.pem' % dir)
538                self.copy_file(self.me, '%s/cert.pem' % dir)
539                # Point the key and me members to the new locations for
540                # pickling
541                hold_key = self.key
542                hold_me = self.me
543                self.key = '%s/key.pem' % dir
544                self.me = '%s/cert.pem' % dir
545
546            f = open("%s/state" % dir, "w")
547            pickle.dump(self, f)
548            f.close()
549
550            if self.local_files:
551                self.key = hold_key
552                self.me = hold_me
553
554            if not os.access("%s/certs" %dir, os.F_OK):
555                os.mkdir("%s/certs" % dir)
556
557            # Clear the certs subdir
558            for fn in [ f for f in os.listdir("%s/certs" % dir) \
559                    if abac_authorizer.cred_file_re.match(f)]:
560                os.unlink('%s/certs/%s' % (dir, fn))
561
562            # Save the context
563            ii = 0
564            ai = 0
565            seenid = set()
566            seenattr = set()
567            for c in self.context.credentials():
568                id = c.issuer_cert()
569                attr = c.attribute_cert()
570                # NB: file naming conventions matter here.  The trailing_ID and
571                # _attr are required by ABAC.COntext.load_directory()
572                if id and id not in seenid:
573                    f = open("%s/certs/ID_%05d_ID.der" % (dir, ii), "w")
574                    f.write(id)
575                    f.close()
576                    ii += 1
577                    seenid.add(id)
578                if attr and attr not in seenattr:
579                    f = open("%s/certs/attr_%05d_attr.der" % (dir, ai), "w")
580                    f.write(attr)
581                    f.close()
582                    ai += 1
583                    seenattr.add(attr)
584        except EnvironmentError, e:
585            self.lock.release()
586            raise e
587        except pickle.PickleError, e:
588            self.lock.release()
589            raise e
590        self.lock.release()
591
592    def load(self, dir=None):
593        self.lock.acquire()
594        if dir:
595            self.save_dir = dir
596        else:
597            dir = self.save_dir
598        if dir is None:
599            self.lock.release()
600            raise abac_authorizer.no_file_error("No load directory specified")
601        try:
602            if os.access("%s/state" % dir, os.R_OK):
603                f = open("%s/state" % dir, "r")
604                st = pickle.load(f)
605                f.close()
606                # Copy the useful attributes from the pickled state
607                for a in ('globals', 'key', 'me', 'cert', 'fedid'):
608                    setattr(self, a, getattr(st, a, None))
609
610            # Initialize the new context with the new identity
611            self.context = ABAC.Context()
612            if self.me:
613                self.context.load_id_file(self.me)
614                if self.key:
615                    self.init_libcreddy_id()
616            self.context.load_directory("%s/certs" % dir)
617            self.save_dir = dir
618        except EnvironmentError, e:
619            self.lock.release()
620            raise e
621        except pickle.PickleError, e:
622            self.lock.release()
623            raise e
624        self.lock.release()
625
626    @staticmethod
627    def encode_credential(c):
628        return '%s <- %s' % (c.head().string(), c.tail().string())
629
630    def get_creds_for_principal(self, fid):
631        look_for = set(["%s" % fid])
632        found_attrs = set()
633        next_look = set()
634        found = set([])
635
636        self.lock.acquire()
637        while look_for:
638            for c in self.context.credentials():
639                tail = c.tail()
640                # XXX: This needs to be more aggressive for linked stuff
641                if tail.string() in look_for and c not in found:
642                    found.add(c)
643                    next_look.add(c.head().string())
644
645            look_for = next_look
646            next_look = set()
647        self.lock.release()
648       
649        return found
650
651    def __str__(self):
652
653        self.lock.acquire()
654        rv = "%s" % self.fedid
655        add = join([abac_authorizer.encode_credential(c)
656            for c in self.context.credentials()], '\n');
657        if add: rv += "\n%s" % add
658        self.lock.release()
659        return rv
660
Note: See TracBrowser for help on using the repository browser.