source: fedd/federation/authorizer.py @ 3bf0b3c

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

Looks like this works pretty well in testing now, including setting attributes.

Thread safety added.

  • Property mode set to 100644
File size: 9.7 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    def __init__(self, certs=None, me=None, key=None):
186        self.bad_name = authorizer_base.bad_name
187        self.attribute_error = authorizer_base.attribute_error
188        self.globals = set()
189        self.creddy = '/usr/local/bin/creddy'
190        self.me = me
191        self.key = key
192        self.context = ABAC.Context()
193        self.lock = Lock()
194
195        if me:
196            self.fedid = fedid(file=self.me)
197            self.context.load_id_file(self.me)
198
199        for dir in certs or []:
200            self.context.load_directory(dir)
201
202    def set_attribute(self, name=None, attr=None, cert=None):
203        if name and attr:
204            if isinstance(name, tuple):
205                raise self.bad_name("ABAC doesn't understand three-names")
206            if self.me and self.key:
207                # Create a credential and insert it into context
208                # This will simplify when we have libcreddy
209                try:
210                    # create temp file
211                    f, fn = mkstemp()
212                    os.close(f)
213                except EnvironmentError, e:
214                    raise self.attribute_error(
215                            "Cannot create temp file: %s" %e)
216
217                # Create the attribute certificate with creddy
218                cmd = [self.creddy, '--attribute', '--issuer=%s' % self.me, 
219                    '--key=%s' % self.key, '--role=%s' % attr, 
220                    '--subject-id=%s' % name, '--out=%s' % fn]
221                rv = call(cmd)
222                if rv == 0:
223                    self.lock.acquire()
224                    # load it to context and remove the file
225                    rv = self.context.load_attribute_file(fn)
226                    self.lock.release()
227                    os.unlink(fn)
228                else:
229                    os.unlink(fn)
230                    raise self.attribute_error("creddy returned %s" % rv)
231            else:
232                raise self.attribute_error(
233                        "Identity and key not specified on creation")
234        elif cert:
235            # Insert this credential into the context
236            self.lock.acquire()
237            self.context.load_attribute_chunk(cert)
238            self.lock.release()
239        else:
240            raise self.attribute_error("Neither name/attr nor cert is set")
241
242    def check_attribute(self, name, attr):
243        # XXX proof soon
244        if isinstance(name, tuple):
245            raise self.bad_name("ABAC doesn't understand three-names")
246        else:
247           
248            # Naked attributes are attested by this principal
249            if attr.find('.') == -1: a = "%s.%s" % (self.fedid, attr)
250            else: a = attr
251
252            self.lock.acquire()
253            proof, rv = self.context.query(a, name)
254            # XXX delete soon
255            if not rv and attr in self.globals: rv = True
256            self.lock.release()
257
258            return rv
259
260    def set_global_attribute(self, attr):
261        """
262        Set a global attribute.  All names, even those otherwise unknown to the
263        authorizer have this attribute.
264        """
265        self.lock.acquire()
266        self.globals.add(attr)
267        self.lock.release()
268
269    def unset_global_attribute(self, attr):
270        """
271        Remove a global attribute
272        """
273
274        self.lock.acquire()
275        self.globals.discard(attr)
276        self.lock.release()
277
278    def clone(self):
279        self.lock.acquire()
280        rv = abac_authorizer(me=self.me, key=self.key)
281        rv.globals = self.globals.copy()
282        rv.context = ABAC.Context(self.context)
283        self.lock.release()
284        return rv
285
286    def save(self, dir):
287        self.lock.acquire()
288        try:
289            if not os.access(dir, os.F_OK):
290                os.mkdir(dir)
291
292            f = open("%s/globals" % dir, "w")
293            pickle.dump(self.globals, f)
294            f.close()
295
296            if self.me and self.key:
297                f = open("%s/me" % dir, "w")
298                pickle.dump(self.me, f)
299                f.close()
300                f = open("%s/key" % dir, "w")
301                pickle.dump(self.key, f)
302                f.close()
303            if not os.access("%s/certs" %dir, os.F_OK):
304                os.mkdir("%s/certs" % dir)
305            seenit = set()
306            #remove old certs
307            for fn in [ f for f in os.listdir("%s/certs" % dir) \
308                    if re.match('.*\.der$', f)]:
309                os.unlink('%s/certs/%s' % (dir, fn))
310            ii = 0
311            ai = 0
312            for c in self.context.credentials():
313                id = c.issuer_cert()
314                attr = c.attribute_cert()
315                # NB: file naming conventions matter here.  The trailing_ID and
316                # _attr are required by ABAC.COntext.load_directory()
317                if id not in seenit:
318                    f = open("%s/certs/ID_%03d_ID.der" % (dir, ii), "w")
319                    print >>f, id
320                    f.close()
321                    ii += 1
322                    seenit.add(id)
323                if attr:
324                    f = open("%s/certs/attr_%03d_attr.der" % (dir, ai), "w")
325                    print >>f, attr
326                    f.close()
327                    ai += 1
328        except EnvironmentError, e:
329            self.lock.release()
330            raise e
331        except pickle.PickleError, e:
332            self.lock.release()
333            raise e
334        self.lock.release()
335
336    def load(self, dir):
337        self.lock.acquire()
338        try:
339            if os.access("%s/me" % dir, os.R_OK):
340                f = open("%s/me" % dir, "r")
341                self.me = pickle.load(f)
342                f.close()
343                if self.me:
344                    self.fedid = fedid(file=self.me)
345            else:
346                self.me = None
347            if os.access("%s/key" % dir, os.R_OK):
348                f = open("%s/key" % dir, "r")
349                self.key = pickle.load(f)
350                f.close()
351            else:
352                self.key = None
353            f = open("%s/globals" % dir, "r")
354            self.globals = pickle.load(f)
355            f.close()
356            self.context = ABAC.Context()
357            self.context.load_directory("%s/certs" % dir)
358        except EnvironmentError, e:
359            self.lock.release()
360            raise e
361        except pickle.PickleError, e:
362            self.lock.release()
363            raise e
364        self.lock.release()
365
366    def __str__(self):
367        def encode_role(r):
368            if r.is_principal(): 
369                return "%s" % r.principal()
370            elif r.is_role(): 
371                return "%s.%s" % (r.principal(), r.role_name())
372            elif r.is_linking():
373                return "%s.%s.%s" % \
374                        (r.principal(), r.linking_role(), r.role_name())
375
376        self.lock.acquire()
377        rv = "%s" % self.fedid
378        for c in self.context.credentials():
379            rv += '\n%s <- %s' % (encode_role(c.head()), encode_role(c.tail()))
380        self.lock.release()
381        return rv
382
Note: See TracBrowser for help on using the repository browser.