source: fedd/federation/authorizer.py @ c573278

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

Checkpoint. Still lots to do

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