source: fedd/federation/authorizer.py @ 25f66c3

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

typo

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