source: fedd/federation/authorizer.py @ 2627eb3

axis_examplecompt_changesinfo-ops
Last change on this file since 2627eb3 was e65150a, checked in by Ted Faber <faber@…>, 13 years ago

Make common ops use fewer options. Refactor.

  • Property mode set to 100644
File size: 14.6 KB
Line 
1#/usr/local/bin/python
2
3from string import join
4from tempfile import mkstemp
5from subprocess import call
6from threading import Lock
7
8from string import join, hexdigits
9
10from fedid import fedid
11from remote_service import service_caller
12from service_error import service_error
13from util import abac_pem_type, abac_split_cert
14
15
16import ABAC
17import pickle
18
19import sys
20import os, os.path
21import re
22
23class authorizer_base:
24    """
25    Classes based on this one keep track of authorization attributes for the
26    various modules running.  This base class holds some utility functions that
27    they all potentially use.
28    """
29
30    # general error exception for badly formed names. 
31    class bad_name(RuntimeError): pass
32    # difficulty creating an attribute
33    class attribute_error(RuntimeError): pass
34
35    @staticmethod
36    def auth_name(name):
37        """
38        Helper to convert a non-unicode local name to a unicode string.  Mixed
39        representations can needlessly confuse the authorizer.
40        """
41        if isinstance(name, basestring):
42            if not isinstance(name, unicode): return unicode(name)
43            else: return name
44        else: return name
45
46    @staticmethod
47    def valid_name(name):
48        """
49        Ensure that the given name is valid.  A valid name can either be a
50        triple of strings and fedids representing one of our generalized Emulab
51        names or a single fedid.  Compound names can include wildcards (None)
52        and must anchor to a fedid at their highest level (unless they're all
53        None)
54
55        This either returns True or throws an exception.  More an assertion
56        than a function.
57        """
58        if isinstance(name, tuple) and len(name) == 3:
59            for n in name:
60                if n: 
61                    if not (isinstance(n, basestring) or isinstance(n, fedid)):
62                        raise authorizer_base.bad_name(
63                                "names must be either a triple or a fedid")
64            for n in name: 
65                if n:
66                    if isinstance(n, fedid):
67                        return True
68                    else:
69                        raise authorizer_base.bad_name(
70                                "Compound names must be " + \
71                                "rooted in fedids: %s" % str(name))
72
73            return True
74        elif isinstance(name, fedid):
75            return True
76        else:
77            raise authorizer_base.bad_name(
78                    "Names must be a triple or a fedid (%s)" % name)
79
80
81class authorizer(authorizer_base):
82    """
83    This class keeps track of authorization attributes for the various modules
84    running.  When it gets smarter it will be the basis for a real
85    attribute-based authentication system.
86    """
87    def __init__(self, def_attr="testbed"):
88        self.attrs = { }
89        self.globals=set()
90
91    def set_attribute(self, name, attr):
92        """
93        Attach attr to name.  Multiple attrs can be attached.
94        """
95        self.valid_name(name)
96        if isinstance(name, tuple):
97            aname = tuple([ self.auth_name(n) for n in name])
98        else:
99            aname = self.auth_name(name)
100
101        if not self.attrs.has_key(aname):
102            self.attrs[aname] = set()
103        self.attrs[aname].add(attr)
104
105    def unset_attribute(self, name, attr):
106        """
107        Remove an attribute from name
108        """
109        self.valid_name(name)
110        if isinstance(name, tuple):
111            aname = tuple([ self.auth_name(n) for n in name])
112        else:
113            aname = self.auth_name(name)
114
115        attrs = self.attrs.get(aname, None)
116        if attrs: attrs.discard(attr)
117
118    def check_attribute(self, name, attr):
119        """
120        Return True if name has attr (or if attr is global).  Tuple names match
121        any tuple name that matches all names present and has None entries in
122        other fileds.  For tuple names True implies that there is a matching
123        tuple name with the attribute.
124        """
125        def tup(tup, i, p):
126            mask = 1 << i
127            if p & mask : return authorizer.auth_name(tup[i])
128            else: return None
129
130        self.valid_name(name)
131        if attr in self.globals:
132            return True
133
134        if isinstance(name, tuple):
135            for p in range(0,8):
136                lookup = ( tup(name, 0, p), tup(name,1, p), tup(name,2,p))
137                if self.attrs.has_key(lookup):
138                    if attr in self.attrs[lookup]:
139                        return True
140        else:
141            return  attr in self.attrs.get(self.auth_name(name), set())
142
143    def set_global_attribute(self, attr):
144        """
145        Set a global attribute.  All names, even those otherwise unknown to the
146        authorizer have this attribute.
147        """
148        self.globals.add(attr)
149
150    def unset_global_attribute(self, attr):
151        """
152        Remove a global attribute
153        """
154
155        self.globals.discard(attr)
156
157    def import_credentials(self, file_list=None, data_list=None):
158        return False
159
160    def __str__(self):
161        rv = ""
162        rv += "attrs %s\n" % self.attrs
163        rv += "globals %s" % self.globals
164        return rv
165
166    def clone(self):
167        rv = authorizer()
168        rv.attrs = self.attrs.copy()
169        rv.globals = self.globals.copy()
170        return rv
171
172    def save(self, fn=None):
173        if fn:
174            f = open(fn, "w")
175            pickle.dump(self, f)
176            f.close()
177
178    def load(self, fn=None):
179        if fn:
180            f = open(fn, "r")
181            a = pickle.load(f)
182            f.close()
183            self.attrs = a.attrs
184            self.globals = a.globals
185       
186
187class abac_authorizer(authorizer_base):
188    """
189    Use the ABAC authorization system to make attribute decisions.
190    """
191
192    clean_attr_re = re.compile('[^A-Za-z0-9_]+')
193    cred_file_re = re.compile('.*\.der$')
194    bad_name = authorizer_base.bad_name
195    attribute_error = authorizer_base.attribute_error
196    class no_file_error(RuntimeError): pass
197    class bad_cert_error(RuntimeError): pass
198
199    def __init__(self, certs=None, me=None, key=None, load=None, save=None):
200        self.creddy = '/usr/local/bin/creddy'
201        self.globals = set()
202        self.lock = Lock()
203        self.me = me
204        self.save_dir = load or save
205        if self.save_dir:
206            self.save_dir = os.path.abspath(self.save_dir)
207        # If the me parameter is a combination certificate, split it into the
208        # abac_authorizer save directory (if any) for use with creddy.
209        if self.me is not None and abac_pem_type(self.me) == 'both':
210            if self.save_dir:
211                keyfile="%s/key.pem" % self.save_dir
212                certfile = "%s/cert.pem" % self.save_dir
213
214                # Clear a spot for the new key and cert files.
215                for fn in (keyfile, certfile):
216                    if os.access(fn, os.F_OK):
217                        os.unlink(fn)
218
219                self.key, self.me = abac_split_cert(self.me, keyfile, certfile)
220            else:
221                raise abac_authorizer.bad_cert_error("Combination " + \
222                        "certificate and nowhere to split it");
223        else:
224            self.key = key
225        self.context = ABAC.Context()
226        if me:
227            self.fedid = fedid(file=self.me)
228            rv = self.context.load_id_file(self.me)
229            if rv != 0:
230                raise abac_authorizer.bad_name(
231                        'Cannot load identity from %s' % me)
232        else:
233            self.fedid = None
234
235        if isinstance(certs, basestring):
236            certs = [ certs ] 
237
238        for dir in certs or []:
239            self.context.load_directory(dir)
240
241        if load:
242            self.load(load)
243
244    # Modify the pickling operations so that the context and lock are not
245    # pickled
246
247    def __getstate__(self):
248        d = self.__dict__.copy()
249        del d['lock']
250        del d['context']
251        return d
252
253    def __setstate__(self, d):
254        # Import everything from the pickle dict (except what we excluded in
255        # __getstate__)
256        self.__dict__.update(d)
257        # Initialize the unpicklables
258        self.context = ABAC.Context()
259        self.lock = Lock()
260
261    @staticmethod
262    def clean_attr(attr):
263        return abac_authorizer.clean_attr_re.sub('_', attr)
264
265
266    def import_credentials(self, file_list=None, data_list=None):
267        if data_list:
268            return any([self.import_credential(data=d) for d in data_list])
269        elif file_list:
270            return any([self.import_credential(file=f) for f in file_list])
271        else:
272            return False
273
274    def import_credential(self, file=None, data=None):
275        if data:
276            if self.context.load_id_chunk(data) != ABAC.ABAC_CERT_SUCCESS:
277                return self.context.load_attribute_chunk(data) == \
278                        ABAC.ABAC_CERT_SUCCESS
279            else:
280                return True
281        elif file:
282            if self.context.load_id_file(file) != ABAC.ABAC_CERT_SUCCESS:
283                return self.context.load_attribute_file(file) == \
284                        ABAC.ABAC_CERT_SUCCESS
285            else:
286                return True
287        else:
288            return False
289
290    def set_attribute(self, name=None, attr=None, cert=None):
291        if name and attr:
292            if isinstance(name, tuple):
293                raise abac_authorizer.bad_name(
294                        "ABAC doesn't understand three-names")
295            # Convert non-string attributes to strings
296            if not isinstance(attr, basestring):
297                attr = "%s" % attr
298
299            if self.me and self.key:
300                # Create a credential and insert it into context
301                # This will simplify when we have libcreddy
302                try:
303                    # create temp file
304                    f, fn = mkstemp()
305                    os.close(f)
306                except EnvironmentError, e:
307                    raise abac_authorizer.attribute_error(
308                            "Cannot create temp file: %s" %e)
309
310                # Create the attribute certificate with creddy
311                cmd = [self.creddy, '--attribute', '--issuer=%s' % self.me, 
312                    '--key=%s' % self.key, '--role=%s' % self.clean_attr(attr), 
313                    '--subject-id=%s' % name, '--out=%s' % fn]
314                rv = call(cmd)
315                if rv == 0:
316                    self.lock.acquire()
317                    # load it to context and remove the file
318                    rv = self.context.load_attribute_file(fn)
319                    self.lock.release()
320                    os.unlink(fn)
321                else:
322                    os.unlink(fn)
323                    raise abac_authorizer.attribute_error(
324                            "creddy returned %s" % rv)
325            else:
326                raise abac_authorizer.attribute_error(
327                        "Identity and key not specified on creation")
328        elif cert:
329            # Insert this credential into the context
330            self.lock.acquire()
331            self.context.load_attribute_chunk(cert)
332            self.lock.release()
333        else:
334            raise abac_authorizer.attribute_error(
335                    "Neither name/attr nor cert is set")
336
337    def unset_attribute(self, name, attr):
338        if isinstance(name, tuple):
339            raise abac_authorizer.bad_name(
340                    "ABAC doesn't understand three-names")
341        # Convert non-string attributes to strings
342        if not isinstance(attr, basestring):
343            attr = "%s" % attr
344        cattr = self.clean_attr(attr)
345        self.lock.acquire()
346        ctxt = ABAC.Context()
347        ids = set()
348        for c in self.context.credentials():
349            h = c.head()
350            t = c.tail()
351            if h.is_role() and t.is_principal():
352                if t.principal() == '%s' % name and \
353                        h.principal() == '%s' % self.fedid and \
354                        h.role_name() == cattr:
355                    continue
356
357            id = c.issuer_cert()
358            if id not in ids:
359                ctxt.load_id_chunk(id)
360                ids.add(id)
361            ctxt.load_attribute_chunk(c.attribute_cert())
362        self.context = ctxt
363        self.lock.release()
364
365    @staticmethod
366    def starts_with_fedid(attr):
367        """
368        Return true if the first 40 characters of the string are hex digits
369        followed by a dot.  False otherwise.  Used in check_attribute.
370        """
371        if attr.find('.') == 40:
372            return all([ x in hexdigits for x in attr[0:40]])
373        else:
374            return False
375
376
377    def check_attribute(self, name, attr):
378        # XXX proof soon
379        if isinstance(name, tuple):
380            raise abac_authorizer.bad_name(
381                    "ABAC doesn't understand three-names")
382        else:
383            # Convert non-string attributes to strings
384            if not isinstance(attr, basestring):
385                attr = "%s" % attr
386            # Attributes that start with a fedid only have the part of the
387            # attribute after the dot cleaned.  Others are completely cleaned
388            # and have the owner fedid attached.
389            if self.starts_with_fedid(attr):
390                r, a = attr.split('.',1)
391                a = "%s.%s" % ( r, self.clean_attr(a))
392            else: 
393                a = "%s.%s" % (self.fedid, self.clean_attr(attr))
394
395            a = str(a)
396            n = str("%s" % name)
397
398            self.lock.acquire()
399            # Sigh. Unicode vs swig and swig seems to lose.  Make sure
400            # everything we pass into ABAC is a str not a unicode.
401            rv, proof = self.context.query(a, n)
402            # XXX delete soon
403            if not rv and attr in self.globals: rv = True
404            self.lock.release()
405
406            return rv
407
408    def set_global_attribute(self, attr):
409        """
410        Set a global attribute.  All names, even those otherwise unknown to the
411        authorizer have this attribute.
412        """
413        self.lock.acquire()
414        self.globals.add(self.clean_attr(attr))
415        self.lock.release()
416
417    def unset_global_attribute(self, attr):
418        """
419        Remove a global attribute
420        """
421
422        self.lock.acquire()
423        self.globals.discard(self.clean_attr(attr))
424        self.lock.release()
425
426    def clone(self):
427        self.lock.acquire()
428        rv = abac_authorizer(me=self.me, key=self.key)
429        rv.globals = self.globals.copy()
430        rv.context = ABAC.Context(self.context)
431        self.lock.release()
432        return rv
433
434    def save(self, dir=None):
435        self.lock.acquire()
436        if dir:
437            self.save_dir = os.path.abspath(dir)
438        else:
439            dir = self.save_dir
440        if dir is None:
441            self.lock.release()
442            raise abac_authorizer.no_file_error("No load directory specified")
443        try:
444            if not os.access(dir, os.F_OK):
445                os.mkdir(dir)
446
447            f = open("%s/state" % dir, "w")
448            pickle.dump(self, f)
449            f.close()
450
451            if not os.access("%s/certs" %dir, os.F_OK):
452                os.mkdir("%s/certs" % dir)
453
454            # Clear the certs subdir
455            for fn in [ f for f in os.listdir("%s/certs" % dir) \
456                    if abac_authorizer.cred_file_re.match(f)]:
457                os.unlink('%s/certs/%s' % (dir, fn))
458
459            # Save the context
460            ii = 0
461            ai = 0
462            seenid = set()
463            seenattr = set()
464            for c in self.context.credentials():
465                id = c.issuer_cert()
466                attr = c.attribute_cert()
467                # NB: file naming conventions matter here.  The trailing_ID and
468                # _attr are required by ABAC.COntext.load_directory()
469                if id and id not in seenid:
470                    f = open("%s/certs/ID_%03d_ID.der" % (dir, ii), "w")
471                    f.write(id)
472                    f.close()
473                    ii += 1
474                    seenid.add(id)
475                if attr and attr not in seenattr:
476                    f = open("%s/certs/attr_%03d_attr.der" % (dir, ai), "w")
477                    f.write(attr)
478                    f.close()
479                    ai += 1
480                    seenattr.add(attr)
481        except EnvironmentError, e:
482            self.lock.release()
483            raise e
484        except pickle.PickleError, e:
485            self.lock.release()
486            raise e
487        self.lock.release()
488
489    def load(self, dir=None):
490        self.lock.acquire()
491        if dir:
492            self.save_dir = dir
493        else:
494            dir = self.save_dir
495        if dir is None:
496            self.lock.release()
497            raise abac_authorizer.no_file_error("No load directory specified")
498        try:
499            if os.access("%s/state" % dir, os.R_OK):
500                f = open("%s/state" % dir, "r")
501                st = pickle.load(f)
502                f.close()
503                # Copy the useful attributes from the pickled state
504                for a in ('globals', 'key', 'me', 'cert', 'fedid'):
505                    setattr(self, a, getattr(st, a, None))
506
507            # Initialize the new context with the new identity
508            self.context = ABAC.Context()
509            if self.me:
510                self.context.load_id_file(self.me)
511            self.context.load_directory("%s/certs" % dir)
512            self.save_dir = dir
513        except EnvironmentError, e:
514            self.lock.release()
515            raise e
516        except pickle.PickleError, e:
517            self.lock.release()
518            raise e
519        self.lock.release()
520
521    @staticmethod
522    def encode_credential(c):
523        return '%s <- %s' % (c.head().string(), c.tail().string())
524
525    def get_creds_for_principal(self, fid):
526        look_for = set(["%s" % fid])
527        found_attrs = set()
528        next_look = set()
529        found = set([])
530
531        self.lock.acquire()
532        while look_for:
533            for c in self.context.credentials():
534                tail = c.tail()
535                # XXX: This needs to be more aggressive for linked stuff
536                if tail.string() in look_for and c not in found:
537                    found.add(c)
538                    next_look.add(c.head().string())
539
540            look_for = next_look
541            next_look = set()
542        self.lock.release()
543       
544        return found
545
546    def __str__(self):
547
548        self.lock.acquire()
549        rv = "%s" % self.fedid
550        add = join([abac_authorizer.encode_credential(c)
551            for c in self.context.credentials()], '\n');
552        if add: rv += "\n%s" % add
553        self.lock.release()
554        return rv
555
Note: See TracBrowser for help on using the repository browser.