source: fedd/federation/authorizer.py @ 2f45140

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

Correct updating

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