source: fedd/federation/authorizer.py @ 2628e5d

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

Slight rearrangement, add a call to initialize the library and the loadfile
parameter for initialization.

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