source: fedd/federation/authorizer.py @ d064566

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

Updates, initial checkin

  • Property mode set to 100644
File size: 17.9 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        self.update_dir = os.path.abspath(update)
220        if self.save_dir:
221            self.save_dir = os.path.abspath(self.save_dir)
222        # If the me parameter is a combination certificate, split it into the
223        # abac_authorizer save directory (if any) for use with creddy.
224        if self.me is not None and abac_pem_type(self.me) == 'both':
225            if self.save_dir:
226                keyfile="%s/key.pem" % self.save_dir
227                certfile = "%s/cert.pem" % self.save_dir
228
229                # Clear a spot for the new key and cert files.
230                for fn in (keyfile, certfile):
231                    if os.access(fn, os.F_OK):
232                        os.unlink(fn)
233
234                self.key, self.me = abac_split_cert(self.me, keyfile, certfile)
235                self.local_files = True
236                self.init_libcreddy_id()
237            else:
238                raise abac_authorizer.bad_cert_error("Combination " + \
239                        "certificate and nowhere to split it");
240        else:
241            self.key = key
242        self.context = ABAC.Context()
243        if me:
244            self.fedid = fedid(file=self.me)
245            rv = self.context.load_id_file(self.me)
246            if rv != 0:
247                raise abac_authorizer.bad_name(
248                        'Cannot load identity from %s' % me)
249        else:
250            self.fedid = None
251
252        if isinstance(certs, basestring):
253            certs = [ certs ] 
254
255        for dir in certs or []:
256            self.context.load_directory(dir)
257
258        if load:
259            self.load(load)
260
261    def init_libcreddy_id(self):
262        self.creddy_id = None
263        try:
264            self.creddy_id = Creddy.ID(self.me)
265        except:
266            raise abac_authorizer.bad_cert_error('Cannot load cert %s' \
267                    % self.me)
268
269        try:
270            self.creddy_id.load_privkey(self.key)
271        except:
272            self.creddy_id = None
273            raise abac_authorized_bad_cert_error('Cannot load key %s' \
274                    % self.key)
275
276
277
278    # Modify the pickling operations so that the context and lock are not
279    # pickled
280
281    def __getstate__(self):
282        d = self.__dict__.copy()
283        del d['lock']
284        del d['context']
285        del d['creddy_id']
286        return d
287
288    def __setstate__(self, d):
289        # Import everything from the pickle dict (except what we excluded in
290        # __getstate__)
291        self.__dict__.update(d)
292        # Initialize the unpicklables
293        self.context = ABAC.Context()
294        self.lock = Lock()
295
296    @staticmethod
297    def clean_attr(attr):
298        '''
299        Convert anything not in A-Z_ into an _ and make sure that the string
300        begins with a non-_.  Convert to s string to keep swig happy.
301        '''
302        a = abac_authorizer.clean_attr_re.sub('_', attr)
303        return str(abac_authorizer.clean_prefix_attr_re.sub('', a))
304
305
306    def import_credentials(self, file_list=None, data_list=None):
307        '''
308        Import a list of credentials.  Be careful about dependencies and only
309        use one logic loop.  if files are given, append them to the list of
310        chunks and then try to read each chunk as an ID and then the failed
311        ones as attributes.
312        '''
313        if file_list:
314            if data_list is None: data_list = []
315            for fn in file_list:
316                # Try to parse file as a Creddy ID, so we can import PEM files
317                try:
318                    cid = Creddy.ID(fn)
319                    data_list.append(cid.cert_chunk())
320                    continue
321                except:
322                    # Not an ID
323                    pass
324                try:
325                    f = open(fn, 'r')
326                    data_list.append(f.read())
327                    f.close()
328                except EnvironmentError, e:
329                    # Ignore unreadable files
330                    pass
331        # Now one way or another data_list is ready
332        if data_list:
333            rv = []
334            attr_list = [ ]
335            for d in data_list:
336                if self.context.load_id_chunk(d) != ABAC.ABAC_CERT_SUCCESS:
337                    attr_list.append(d)
338                else:
339                    rv.append(True)
340
341            for d in attr_list:
342                v = self.context.load_attribute_chunk(d) 
343                rv.append(v == ABAC.ABAC_CERT_SUCCESS)
344            return all(rv)
345        else:
346            return False
347
348    def import_credential(self, file=None, data=None):
349        if data:
350            if self.context.load_id_chunk(data) != ABAC.ABAC_CERT_SUCCESS:
351                return self.context.load_attribute_chunk(data) == \
352                        ABAC.ABAC_CERT_SUCCESS
353            else:
354                return True
355        elif file:
356            if self.context.load_id_file(file) != ABAC.ABAC_CERT_SUCCESS:
357                return self.context.load_attribute_file(file) == \
358                        ABAC.ABAC_CERT_SUCCESS
359            else:
360                return True
361        else:
362            return False
363
364    def set_attribute(self, name=None, attr=None, cert=None):
365        if name and attr:
366            if isinstance(name, tuple):
367                raise abac_authorizer.bad_name(
368                        "ABAC doesn't understand three-names")
369            # Convert non-string attributes to strings
370            if not isinstance(attr, basestring):
371                attr = "%s" % attr
372
373            if self.creddy_id:
374                # Create a credential and insert it into context
375                # This will simplify when we have libcreddy
376                try:
377                    attrcert = Creddy.Attribute(self.creddy_id, 
378                            self.clean_attr(attr), 3600 * 24 * 365 * 10)
379                    attrcert.principal("%s" % name)
380                    attrcert.bake()
381                except:
382                    raise abac_authorizer.bad_cert_error(
383                            "Cannot create attribute cert")
384                self.lock.acquire()
385                # load it to context and remove the file
386                rv = self.context.load_attribute_chunk(attrcert.cert_chunk())
387                self.lock.release()
388                return rv
389            else:
390                raise abac_authorizer.attribute_error(
391                        "Identity and key not specified on creation")
392        elif cert:
393            # Insert this credential into the context
394            self.lock.acquire()
395            rv = self.context.load_attribute_chunk(cert)
396            self.lock.release()
397            return rv
398        else:
399            raise abac_authorizer.attribute_error(
400                    "Neither name/attr nor cert is set")
401
402    def unset_attribute(self, name, attr):
403        if isinstance(name, tuple):
404            raise abac_authorizer.bad_name(
405                    "ABAC doesn't understand three-names")
406        # Convert non-string attributes to strings
407        if not isinstance(attr, basestring):
408            attr = "%s" % attr
409        cattr = self.clean_attr(attr)
410        self.lock.acquire()
411        ctxt = ABAC.Context()
412        ids = set()
413        for c in self.context.credentials():
414            h = c.head()
415            t = c.tail()
416            if h.is_role() and t.is_principal():
417                if t.principal() == '%s' % name and \
418                        h.principal() == '%s' % self.fedid and \
419                        h.role_name() == cattr:
420                    continue
421
422            id = c.issuer_cert()
423            if id not in ids:
424                ctxt.load_id_chunk(id)
425                ids.add(id)
426            ctxt.load_attribute_chunk(c.attribute_cert())
427        self.context = ctxt
428        self.lock.release()
429
430    @staticmethod
431    def starts_with_fedid(attr):
432        """
433        Return true if the first 40 characters of the string are hex digits
434        followed by a dot.  False otherwise.  Used in check_attribute.
435        """
436        if attr.find('.') == 40:
437            return all([ x in hexdigits for x in attr[0:40]])
438        else:
439            return False
440
441
442    def check_attribute(self, name, attr, with_proof=False):
443        if isinstance(name, tuple):
444            raise abac_authorizer.bad_name(
445                    "ABAC doesn't understand three-names")
446        else:
447            # Convert non-string attributes to strings
448            if not isinstance(attr, basestring):
449                attr = "%s" % attr
450            # Attributes that start with a fedid only have the part of the
451            # attribute after the dot cleaned.  Others are completely cleaned
452            # and have the owner fedid attached.
453            if self.starts_with_fedid(attr):
454                r, a = attr.split('.',1)
455                a = "%s.%s" % ( r, self.clean_attr(a))
456            else: 
457                a = "%s.%s" % (self.fedid, self.clean_attr(attr))
458
459            a = str(a)
460            n = str("%s" % name)
461
462            self.lock.acquire()
463            # Sigh. Unicode vs swig and swig seems to lose.  Make sure
464            # everything we pass into ABAC is a str not a unicode.
465            rv, p = self.context.query(a, n)
466            # XXX delete soon
467            if not rv and attr in self.globals: 
468                rv = True
469                p = None
470            self.lock.release()
471            if with_proof: return rv, proof(self.fedid, name, a, p)
472            else: return rv
473
474    def set_global_attribute(self, attr):
475        """
476        Set a global attribute.  All names, even those otherwise unknown to the
477        authorizer have this attribute.
478        """
479        self.lock.acquire()
480        self.globals.add(self.clean_attr(attr))
481        self.lock.release()
482
483    def unset_global_attribute(self, attr):
484        """
485        Remove a global attribute
486        """
487
488        self.lock.acquire()
489        self.globals.discard(self.clean_attr(attr))
490        self.lock.release()
491
492    def clone(self):
493        self.lock.acquire()
494        rv = abac_authorizer(me=self.me, key=self.key)
495        rv.globals = self.globals.copy()
496        rv.context = ABAC.Context(self.context)
497        self.lock.release()
498        return rv
499
500    def copy_file(self, src, dest, mode=0600):
501        '''
502        Copy src to dest with file mode mode.  May raise exceptions on file ops
503        '''
504        same_file = False
505        try:
506            ss = os.stat(src)
507            ds = os.stat(dest)
508
509            if  ds.st_ino == ss.st_ino: same_file = True
510        except EnvironmentError:
511            pass
512
513        if not same_file:
514            d = open(dest, 'w')
515            s = open(src, 'r')
516            d.write(s.read())
517            s.close()
518            d.close()
519        os.chmod(dest, mode)
520
521
522
523    def save(self, dir=None):
524        self.lock.acquire()
525        if dir:
526            self.save_dir = os.path.abspath(dir)
527        else:
528            dir = self.save_dir
529        if dir is None:
530            self.lock.release()
531            raise abac_authorizer.no_file_error("No load directory specified")
532        try:
533            if not os.access(dir, os.F_OK):
534                os.mkdir(dir)
535
536            # if self.key and self.me were split, copy the split files into the
537            # new directory.
538            if self.local_files:
539                self.copy_file(self.key, '%s/key.pem' % dir)
540                self.copy_file(self.me, '%s/cert.pem' % dir)
541                # Point the key and me members to the new locations for
542                # pickling
543                hold_key = self.key
544                hold_me = self.me
545                self.key = '%s/key.pem' % dir
546                self.me = '%s/cert.pem' % dir
547
548            f = open("%s/state" % dir, "w")
549            pickle.dump(self, f)
550            f.close()
551
552            if self.local_files:
553                self.key = hold_key
554                self.me = hold_me
555
556            if not os.access("%s/certs" %dir, os.F_OK):
557                os.mkdir("%s/certs" % dir)
558
559            # Clear the certs subdir
560            for fn in [ f for f in os.listdir("%s/certs" % dir) \
561                    if abac_authorizer.cred_file_re.match(f)]:
562                os.unlink('%s/certs/%s' % (dir, fn))
563
564            # Save the context
565            ii = 0
566            ai = 0
567            seenid = set()
568            seenattr = set()
569            for c in self.context.credentials():
570                id = c.issuer_cert()
571                attr = c.attribute_cert()
572                # NB: file naming conventions matter here.  The trailing_ID and
573                # _attr are required by ABAC.COntext.load_directory()
574                if id and id not in seenid:
575                    f = open("%s/certs/ID_%05d_ID.der" % (dir, ii), "w")
576                    f.write(id)
577                    f.close()
578                    ii += 1
579                    seenid.add(id)
580                if attr and attr not in seenattr:
581                    f = open("%s/certs/attr_%05d_attr.der" % (dir, ai), "w")
582                    f.write(attr)
583                    f.close()
584                    ai += 1
585                    seenattr.add(attr)
586        except EnvironmentError, e:
587            self.lock.release()
588            raise e
589        except pickle.PickleError, e:
590            self.lock.release()
591            raise e
592        self.lock.release()
593
594    def load(self, dir=None):
595        self.lock.acquire()
596        if dir:
597            self.save_dir = dir
598        else:
599            dir = self.save_dir
600        if dir is None:
601            self.lock.release()
602            raise abac_authorizer.no_file_error("No load directory specified")
603        try:
604            if os.access("%s/state" % dir, os.R_OK):
605                f = open("%s/state" % dir, "r")
606                st = pickle.load(f)
607                f.close()
608                # Copy the useful attributes from the pickled state
609                for a in ('globals', 'key', 'me', 'cert', 'fedid'):
610                    setattr(self, a, getattr(st, a, None))
611
612            # Initialize the new context with the new identity
613            self.context = ABAC.Context()
614            if self.me:
615                self.context.load_id_file(self.me)
616                if self.key:
617                    self.init_libcreddy_id()
618            self.context.load_directory("%s/certs" % dir)
619            self.save_dir = dir
620        except EnvironmentError, e:
621            self.lock.release()
622            raise e
623        except pickle.PickleError, e:
624            self.lock.release()
625            raise e
626        self.lock.release()
627
628    @staticmethod
629    def encode_credential(c):
630        return '%s <- %s' % (c.head().string(), c.tail().string())
631
632    def get_creds_for_principal(self, fid):
633        look_for = set(["%s" % fid])
634        found_attrs = set()
635        next_look = set()
636        found = set([])
637
638        self.lock.acquire()
639        while look_for:
640            for c in self.context.credentials():
641                tail = c.tail()
642                # XXX: This needs to be more aggressive for linked stuff
643                if tail.string() in look_for and c not in found:
644                    found.add(c)
645                    next_look.add(c.head().string())
646
647            look_for = next_look
648            next_look = set()
649        self.lock.release()
650       
651        return found
652
653    def update(self):
654        rv = False
655        if update_dir is None: return True
656        lf = None
657        files = []
658        try:
659            lf = open('%s/lock' % self.update_dir, 'r')
660        except EnvironmentError, e:
661            return rv
662        try:
663            fcntl.flock(lf, fcntl.LOCK_EX)
664            files = os.listdir(update_dir)
665            rv = self.import_credentials(file_list=files)
666            try: 
667                for fn in files:
668                    os.remove(fn)
669            except EnvironmentError, e:
670                # failure to clear the update directory is no biggie
671                pass
672            rv = True
673        finally:
674            fcntl.flock(fl, fcntl.LOCK_UN)
675        return rv
676
677           
678       
679
680    def __str__(self):
681
682        self.lock.acquire()
683        rv = "%s" % self.fedid
684        add = join([abac_authorizer.encode_credential(c)
685            for c in self.context.credentials()], '\n');
686        if add: rv += "\n%s" % add
687        self.lock.release()
688        return rv
689
Note: See TracBrowser for help on using the repository browser.