source: fedd/federation/authorizer.py @ 12aa3fe

Last change on this file since 12aa3fe was 67fa1cf, checked in by Ted Faber <faber@…>, 11 years ago

MOve over to ABAC 0.1.4

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