source: fedd/federation/authorizer.py @ 27d964d

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

Translate attributes into something acceptable to libabac (or creddy)

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