source: fedd/federation/authorizer.py @ 09b1e9d

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

Cleaner save format

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