source: fedd/federation/authorizer.py @ 85f5d11

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

Confusing print output fixed

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