source: fedd/federation/authorizer.py @ e982b92

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

swig doesn't understand unicode strings. Make sure that attributes are
pure strings.

  • Property mode set to 100644
File size: 16.4 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 fedid 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        if data_list:
305            return any([self.import_credential(data=d) for d in data_list])
306        elif file_list:
307            return any([self.import_credential(file=f) for f in file_list])
308        else:
309            return False
310
311    def import_credential(self, file=None, data=None):
312        if data:
313            if self.context.load_id_chunk(data) != ABAC.ABAC_CERT_SUCCESS:
314                return self.context.load_attribute_chunk(data) == \
315                        ABAC.ABAC_CERT_SUCCESS
316            else:
317                return True
318        elif file:
319            if self.context.load_id_file(file) != ABAC.ABAC_CERT_SUCCESS:
320                return self.context.load_attribute_file(file) == \
321                        ABAC.ABAC_CERT_SUCCESS
322            else:
323                return True
324        else:
325            return False
326
327    def set_attribute(self, name=None, attr=None, cert=None):
328        if name and attr:
329            if isinstance(name, tuple):
330                raise abac_authorizer.bad_name(
331                        "ABAC doesn't understand three-names")
332            # Convert non-string attributes to strings
333            if not isinstance(attr, basestring):
334                attr = "%s" % attr
335
336            if self.creddy_id:
337                # Create a credential and insert it into context
338                # This will simplify when we have libcreddy
339                try:
340                    attrcert = Creddy.Attribute(self.creddy_id, 
341                            self.clean_attr(attr), 3600 * 24 * 365 * 10)
342                    attrcert.principal("%s" % name)
343                    attrcert.bake()
344                except:
345                    raise abac_authorizer.bad_cert_error(
346                            "Cannot create attribute cert")
347                self.lock.acquire()
348                # load it to context and remove the file
349                rv = self.context.load_attribute_chunk(attrcert.cert_chunk())
350                self.lock.release()
351                return rv
352            else:
353                raise abac_authorizer.attribute_error(
354                        "Identity and key not specified on creation")
355        elif cert:
356            # Insert this credential into the context
357            self.lock.acquire()
358            rv = self.context.load_attribute_chunk(cert)
359            self.lock.release()
360            return rv
361        else:
362            raise abac_authorizer.attribute_error(
363                    "Neither name/attr nor cert is set")
364
365    def unset_attribute(self, name, 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        cattr = self.clean_attr(attr)
373        self.lock.acquire()
374        ctxt = ABAC.Context()
375        ids = set()
376        for c in self.context.credentials():
377            h = c.head()
378            t = c.tail()
379            if h.is_role() and t.is_principal():
380                if t.principal() == '%s' % name and \
381                        h.principal() == '%s' % self.fedid and \
382                        h.role_name() == cattr:
383                    continue
384
385            id = c.issuer_cert()
386            if id not in ids:
387                ctxt.load_id_chunk(id)
388                ids.add(id)
389            ctxt.load_attribute_chunk(c.attribute_cert())
390        self.context = ctxt
391        self.lock.release()
392
393    @staticmethod
394    def starts_with_fedid(attr):
395        """
396        Return true if the first 40 characters of the string are hex digits
397        followed by a dot.  False otherwise.  Used in check_attribute.
398        """
399        if attr.find('.') == 40:
400            return all([ x in hexdigits for x in attr[0:40]])
401        else:
402            return False
403
404
405    def check_attribute(self, name, attr, with_proof=False):
406        if isinstance(name, tuple):
407            raise abac_authorizer.bad_name(
408                    "ABAC doesn't understand three-names")
409        else:
410            # Convert non-string attributes to strings
411            if not isinstance(attr, basestring):
412                attr = "%s" % attr
413            # Attributes that start with a fedid only have the part of the
414            # attribute after the dot cleaned.  Others are completely cleaned
415            # and have the owner fedid attached.
416            if self.starts_with_fedid(attr):
417                r, a = attr.split('.',1)
418                a = "%s.%s" % ( r, self.clean_attr(a))
419            else: 
420                a = "%s.%s" % (self.fedid, self.clean_attr(attr))
421
422            a = str(a)
423            n = str("%s" % name)
424
425            self.lock.acquire()
426            # Sigh. Unicode vs swig and swig seems to lose.  Make sure
427            # everything we pass into ABAC is a str not a unicode.
428            rv, p = self.context.query(a, n)
429            # XXX delete soon
430            if not rv and attr in self.globals: 
431                rv = True
432                p = None
433            self.lock.release()
434            if with_proof: return rv, proof(self.fedid, name, a, p)
435            else: return rv
436
437    def set_global_attribute(self, attr):
438        """
439        Set a global attribute.  All names, even those otherwise unknown to the
440        authorizer have this attribute.
441        """
442        self.lock.acquire()
443        self.globals.add(self.clean_attr(attr))
444        self.lock.release()
445
446    def unset_global_attribute(self, attr):
447        """
448        Remove a global attribute
449        """
450
451        self.lock.acquire()
452        self.globals.discard(self.clean_attr(attr))
453        self.lock.release()
454
455    def clone(self):
456        self.lock.acquire()
457        rv = abac_authorizer(me=self.me, key=self.key)
458        rv.globals = self.globals.copy()
459        rv.context = ABAC.Context(self.context)
460        self.lock.release()
461        return rv
462
463    def copy_file(self, src, dest, mode=0600):
464        '''
465        Copy src to dest with file mode mode.  May raise exceptions on file ops
466        '''
467        same_file = False
468        try:
469            ss = os.stat(src)
470            ds = os.stat(dest)
471
472            if  ds.st_ino == ss.st_ino: same_file = True
473        except EnvironmentError:
474            pass
475
476        if not same_file:
477            d = open(dest, 'w')
478            s = open(src, 'r')
479            d.write(s.read())
480            s.close()
481            d.close()
482        os.chmod(dest, mode)
483
484
485
486    def save(self, dir=None):
487        self.lock.acquire()
488        if dir:
489            self.save_dir = os.path.abspath(dir)
490        else:
491            dir = self.save_dir
492        if dir is None:
493            self.lock.release()
494            raise abac_authorizer.no_file_error("No load directory specified")
495        try:
496            if not os.access(dir, os.F_OK):
497                os.mkdir(dir)
498
499            # if self.key and self.me were split, copy the split files into the
500            # new directory.
501            if self.local_files:
502                self.copy_file(self.key, '%s/key.pem' % dir)
503                self.copy_file(self.me, '%s/cert.pem' % dir)
504                # Point the key and me members to the new locations for
505                # pickling
506                hold_key = self.key
507                hold_me = self.me
508                self.key = '%s/key.pem' % dir
509                self.me = '%s/cert.pem' % dir
510
511            f = open("%s/state" % dir, "w")
512            pickle.dump(self, f)
513            f.close()
514
515            if self.local_files:
516                self.key = hold_key
517                self.me = hold_me
518
519            if not os.access("%s/certs" %dir, os.F_OK):
520                os.mkdir("%s/certs" % dir)
521
522            # Clear the certs subdir
523            for fn in [ f for f in os.listdir("%s/certs" % dir) \
524                    if abac_authorizer.cred_file_re.match(f)]:
525                os.unlink('%s/certs/%s' % (dir, fn))
526
527            # Save the context
528            ii = 0
529            ai = 0
530            seenid = set()
531            seenattr = set()
532            for c in self.context.credentials():
533                id = c.issuer_cert()
534                attr = c.attribute_cert()
535                # NB: file naming conventions matter here.  The trailing_ID and
536                # _attr are required by ABAC.COntext.load_directory()
537                if id and id not in seenid:
538                    f = open("%s/certs/ID_%03d_ID.der" % (dir, ii), "w")
539                    f.write(id)
540                    f.close()
541                    ii += 1
542                    seenid.add(id)
543                if attr and attr not in seenattr:
544                    f = open("%s/certs/attr_%03d_attr.der" % (dir, ai), "w")
545                    f.write(attr)
546                    f.close()
547                    ai += 1
548                    seenattr.add(attr)
549        except EnvironmentError, e:
550            self.lock.release()
551            raise e
552        except pickle.PickleError, e:
553            self.lock.release()
554            raise e
555        self.lock.release()
556
557    def load(self, dir=None):
558        self.lock.acquire()
559        if dir:
560            self.save_dir = dir
561        else:
562            dir = self.save_dir
563        if dir is None:
564            self.lock.release()
565            raise abac_authorizer.no_file_error("No load directory specified")
566        try:
567            if os.access("%s/state" % dir, os.R_OK):
568                f = open("%s/state" % dir, "r")
569                st = pickle.load(f)
570                f.close()
571                # Copy the useful attributes from the pickled state
572                for a in ('globals', 'key', 'me', 'cert', 'fedid'):
573                    setattr(self, a, getattr(st, a, None))
574
575            # Initialize the new context with the new identity
576            self.context = ABAC.Context()
577            if self.me:
578                self.context.load_id_file(self.me)
579                if self.key:
580                    self.init_libcreddy_id()
581            self.context.load_directory("%s/certs" % dir)
582            self.save_dir = dir
583        except EnvironmentError, e:
584            self.lock.release()
585            raise e
586        except pickle.PickleError, e:
587            self.lock.release()
588            raise e
589        self.lock.release()
590
591    @staticmethod
592    def encode_credential(c):
593        return '%s <- %s' % (c.head().string(), c.tail().string())
594
595    def get_creds_for_principal(self, fid):
596        look_for = set(["%s" % fid])
597        found_attrs = set()
598        next_look = set()
599        found = set([])
600
601        self.lock.acquire()
602        while look_for:
603            for c in self.context.credentials():
604                tail = c.tail()
605                # XXX: This needs to be more aggressive for linked stuff
606                if tail.string() in look_for and c not in found:
607                    found.add(c)
608                    next_look.add(c.head().string())
609
610            look_for = next_look
611            next_look = set()
612        self.lock.release()
613       
614        return found
615
616    def __str__(self):
617
618        self.lock.acquire()
619        rv = "%s" % self.fedid
620        add = join([abac_authorizer.encode_credential(c)
621            for c in self.context.credentials()], '\n');
622        if add: rv += "\n%s" % add
623        self.lock.release()
624        return rv
625
Note: See TracBrowser for help on using the repository browser.