source: fedd/federation/authorizer.py @ 1d73342

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

Looks like internal works now.

Had to add default entries to the access list to accomodate that, and discovered that ABAC requires strings - not unicode.

Moved lookup_access into the aceess class as most should be able to use it directly now.

  • Property mode set to 100644
File size: 14.5 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                self.key, self.me = abac_split_cert(self.me,
212                        keyfile="%s/key.pem" % self.save_dir, 
213                        certfile = "%s/cert.pem" % self.save_dir)
214            else:
215                raise abac_authorizer.bad_cert_error("Combination " + \
216                        "certificate and nowhere to split it");
217        else:
218            self.key = key
219        self.context = ABAC.Context()
220        if me:
221            self.fedid = fedid(file=self.me)
222            rv = self.context.load_id_file(self.me)
223            if rv != 0:
224                raise abac_authorizer.bad_name(
225                        'Cannot load identity from %s' % me.cert)
226        else:
227            self.fedid = None
228
229        if isinstance(certs, basestring):
230            certs = [ certs ] 
231
232        for dir in certs or []:
233            self.context.load_directory(dir)
234
235        if load:
236            self.load(load)
237
238    @staticmethod
239    def clean_attr(attr):
240        return abac_authorizer.clean_attr_re.sub('_', attr)
241
242
243    def import_credentials(self, file_list=None, data_list=None):
244        if data_list:
245            return any([self.import_credential(data=d) for d in data_list])
246        elif file_list:
247            return any([self.import_credential(file=f) for f in file_list])
248        else:
249            return False
250
251    def import_credential(self, file=None, data=None):
252        if data:
253            if self.context.load_id_chunk(data) != ABAC.ABAC_CERT_SUCCESS:
254                return self.context.load_attribute_chunk(data) == \
255                        ABAC.ABAC_CERT_SUCCESS
256            else:
257                return True
258        elif file:
259            if self.context.load_id_file(file) != ABAC.ABAC_CERT_SUCCESS:
260                return self.context.load_attribute_file(file) == \
261                        ABAC.ABAC_CERT_SUCCESS
262            else:
263                return True
264        else:
265            return False
266
267    def set_attribute(self, name=None, attr=None, cert=None):
268        if name and attr:
269            if isinstance(name, tuple):
270                raise abac_authorizer.bad_name(
271                        "ABAC doesn't understand three-names")
272            # Convert non-string attributes to strings
273            if not isinstance(attr, basestring):
274                attr = "%s" % attr
275
276            if self.me and self.key:
277                # Create a credential and insert it into context
278                # This will simplify when we have libcreddy
279                try:
280                    # create temp file
281                    f, fn = mkstemp()
282                    os.close(f)
283                except EnvironmentError, e:
284                    raise abac_authorizer.attribute_error(
285                            "Cannot create temp file: %s" %e)
286
287                # Create the attribute certificate with creddy
288                cmd = [self.creddy, '--attribute', '--issuer=%s' % self.me, 
289                    '--key=%s' % self.key, '--role=%s' % self.clean_attr(attr), 
290                    '--subject-id=%s' % name, '--out=%s' % fn]
291                rv = call(cmd)
292                if rv == 0:
293                    self.lock.acquire()
294                    # load it to context and remove the file
295                    rv = self.context.load_attribute_file(fn)
296                    self.lock.release()
297                    os.unlink(fn)
298                else:
299                    os.unlink(fn)
300                    raise abac_authorizer.attribute_error(
301                            "creddy returned %s" % rv)
302            else:
303                raise abac_authorizer.attribute_error(
304                        "Identity and key not specified on creation")
305        elif cert:
306            # Insert this credential into the context
307            self.lock.acquire()
308            self.context.load_attribute_chunk(cert)
309            self.lock.release()
310        else:
311            raise abac_authorizer.attribute_error(
312                    "Neither name/attr nor cert is set")
313
314    def unset_attribute(self, name, attr):
315        if isinstance(name, tuple):
316            raise abac_authorizer.bad_name(
317                    "ABAC doesn't understand three-names")
318        # Convert non-string attributes to strings
319        if not isinstance(attr, basestring):
320            attr = "%s" % attr
321        cattr = self.clean_attr(attr)
322        self.lock.acquire()
323        ctxt = ABAC.Context()
324        ids = set()
325        for c in self.context.credentials():
326            h = c.head()
327            t = c.tail()
328            if h.is_role() and t.is_principal():
329                if t.principal() == '%s' % name and \
330                        h.principal() == '%s' % self.fedid and \
331                        h.role_name() == cattr:
332                    continue
333
334            id = c.issuer_cert()
335            if id not in ids:
336                ctxt.load_id_chunk(id)
337                ids.add(id)
338            ctxt.load_attribute_chunk(c.attribute_cert())
339        self.context = ctxt
340        self.lock.release()
341
342    @staticmethod
343    def starts_with_fedid(attr):
344        """
345        Return true if the first 40 characters of the string are hex digits
346        followed by a dot.  False otherwise.  Used in check_attribute.
347        """
348        if attr.find('.') == 40:
349            return all([ x in hexdigits for x in attr[0:40]])
350        else:
351            return False
352
353
354    def check_attribute(self, name, attr):
355        # XXX proof soon
356        if isinstance(name, tuple):
357            raise abac_authorizer.bad_name(
358                    "ABAC doesn't understand three-names")
359        else:
360            # Convert non-string attributes to strings
361            if not isinstance(attr, basestring):
362                attr = "%s" % attr
363            # Attributes that start with a fedid only have the part of the
364            # attribute after the dot cleaned.  Others are completely cleaned
365            # and have the owner fedid attached.
366            if self.starts_with_fedid(attr):
367                r, a = attr.split('.',1)
368                a = "%s.%s" % ( r, self.clean_attr(a))
369            else: 
370                a = "%s.%s" % (self.fedid, self.clean_attr(attr))
371
372            a = str(a)
373            n = str("%s" % name)
374
375            self.lock.acquire()
376            # Sigh. Unicode vs swig and swig seems to lose.  Make sure
377            # everything we pass into ABAC is a str not a unicode.
378            rv, proof = self.context.query(a, n)
379            # XXX delete soon
380            if not rv and attr in self.globals: rv = True
381            self.lock.release()
382
383            return rv
384
385    def set_global_attribute(self, attr):
386        """
387        Set a global attribute.  All names, even those otherwise unknown to the
388        authorizer have this attribute.
389        """
390        self.lock.acquire()
391        self.globals.add(self.clean_attr(attr))
392        self.lock.release()
393
394    def unset_global_attribute(self, attr):
395        """
396        Remove a global attribute
397        """
398
399        self.lock.acquire()
400        self.globals.discard(self.clean_attr(attr))
401        self.lock.release()
402
403    def clone(self):
404        self.lock.acquire()
405        rv = abac_authorizer(me=self.me, key=self.key)
406        rv.globals = self.globals.copy()
407        rv.context = ABAC.Context(self.context)
408        self.lock.release()
409        return rv
410
411    def save(self, dir=None):
412        self.lock.acquire()
413        if dir:
414            self.save_dir = os.path.abspath(dir)
415        else:
416            dir = self.save_dir
417        if dir is None:
418            self.lock.release()
419            raise abac_authorizer.no_file_error("No load directory specified")
420        try:
421            if not os.access(dir, os.F_OK):
422                os.mkdir(dir)
423            # These are unpicklable, so set them aside
424            context = self.context
425            lock = self.lock
426            self.context = None
427            self.lock = None
428
429            f = open("%s/state" % dir, "w")
430            pickle.dump(self, f)
431            f.close()
432
433            if not os.access("%s/certs" %dir, os.F_OK):
434                os.mkdir("%s/certs" % dir)
435            seenid = set()
436            seenattr = set()
437
438            #restore unpicklable state
439            self.context = context
440            self.lock = lock
441            #remove old certs
442            for fn in [ f for f in os.listdir("%s/certs" % dir) \
443                    if abac_authorizer.cred_file_re.match(f)]:
444                os.unlink('%s/certs/%s' % (dir, fn))
445            ii = 0
446            ai = 0
447            for c in self.context.credentials():
448                id = c.issuer_cert()
449                attr = c.attribute_cert()
450                # NB: file naming conventions matter here.  The trailing_ID and
451                # _attr are required by ABAC.COntext.load_directory()
452                if id and id not in seenid:
453                    f = open("%s/certs/ID_%03d_ID.der" % (dir, ii), "w")
454                    f.write(id)
455                    f.close()
456                    ii += 1
457                    seenid.add(id)
458                if attr and attr not in seenattr:
459                    f = open("%s/certs/attr_%03d_attr.der" % (dir, ai), "w")
460                    f.write(attr)
461                    f.close()
462                    ai += 1
463                    seenattr.add(attr)
464        except EnvironmentError, e:
465            # If we've mislaid self.lock, release lock (they're the same object)
466            if self.lock: self.lock.release()
467            elif lock: lock.release()
468            raise e
469        except pickle.PickleError, e:
470            # If we've mislaid self.lock, release lock (they're the same object)
471            if self.lock: self.lock.release()
472            elif lock: lock.release()
473            raise e
474        self.lock.release()
475
476    def load(self, dir=None):
477        self.lock.acquire()
478        if dir:
479            self.save_dir = dir
480        else:
481            dir = self.save_dir
482        if dir is None:
483            self.lock.release()
484            raise abac_authorizer.no_file_error("No load directory specified")
485        try:
486            if os.access("%s/state" % dir, os.R_OK):
487                f = open("%s/state" % dir, "r")
488                st = pickle.load(f)
489                f.close()
490                # Copy the useful attributes from the pickled state
491                for a in ('globals', 'key', 'me', 'cert', 'fedid'):
492                    setattr(self, a, getattr(st, a, None))
493
494            # Initialize the new context with the new identity
495            self.context = ABAC.Context()
496            if self.me:
497                self.context.load_id_file(self.me)
498            self.context.load_directory("%s/certs" % dir)
499            self.save_dir = dir
500        except EnvironmentError, e:
501            self.lock.release()
502            raise e
503        except pickle.PickleError, e:
504            self.lock.release()
505            raise e
506        self.lock.release()
507
508    @staticmethod
509    def encode_credential(c):
510        return '%s <- %s' % (c.head().string(), c.tail().string())
511
512    def get_creds_for_principal(self, fid):
513        look_for = set(["%s" % fid])
514        found_attrs = set()
515        next_look = set()
516        found = set([])
517
518        self.lock.acquire()
519        while look_for:
520            for c in self.context.credentials():
521                tail = c.tail()
522                # XXX: This needs to be more aggressive for linked stuff
523                if tail.string() in look_for and c not in found:
524                    found.add(c)
525                    next_look.add(c.head().string())
526
527            look_for = next_look
528            next_look = set()
529        self.lock.release()
530       
531        return found
532
533    def __str__(self):
534
535        self.lock.acquire()
536        rv = "%s" % self.fedid
537        add = join([abac_authorizer.encode_credential(c)
538            for c in self.context.credentials()], '\n');
539        if add: rv += "\n%s" % add
540        self.lock.release()
541        return rv
542
Note: See TracBrowser for help on using the repository browser.