source: fedd/federation/authorizer.py @ 962ea25

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

checkpoint: new works pretty well

  • Property mode set to 100644
File size: 12.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 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 import_credentials(self, file_list=None, data_list=None):
155        return False
156
157    def __str__(self):
158        rv = ""
159        rv += "attrs %s\n" % self.attrs
160        rv += "globals %s" % self.globals
161        return rv
162
163    def clone(self):
164        rv = authorizer()
165        rv.attrs = self.attrs.copy()
166        rv.globals = self.globals.copy()
167        return rv
168
169    def save(self, fn):
170        f = open(fn, "w")
171        pickle.dump(self, f)
172        f.close()
173
174    def load(self, fn):
175        f = open(fn, "r")
176        a = pickle.load(f)
177        f.close()
178        self.attrs = a.attrs
179        self.globals = a.globals
180       
181
182class abac_authorizer(authorizer_base):
183    """
184    Use the ABAC authorization system to make attribute decisions.
185    """
186
187    clean_attr_re = re.compile('[^A-Za-z0-9_]+')
188    cred_file_re = re.compile('.*\.der$')
189    bad_name = authorizer_base.bad_name
190    attribute_error = authorizer_base.attribute_error
191    class no_file(RuntimeError): pass
192
193    def __init__(self, certs=None, me=None, key=None, load=None):
194        self.creddy = '/usr/local/bin/creddy'
195        self.globals = set()
196        self.lock = Lock()
197        self.me = me
198        self.key = key
199        self.context = ABAC.Context()
200        if me:
201            self.fedid = fedid(file=self.me)
202            self.context.load_id_file(self.me)
203        else:
204            self.fedid = None
205
206        if isinstance(certs, basestring):
207            certs = [ certs ] 
208
209        for dir in certs or []:
210            self.context.load_directory(dir)
211
212        if load:
213            self.save_dir = load
214            self.load(load)
215        else:
216            self.save_dir = None
217
218    @staticmethod
219    def clean_attr(attr):
220        return abac_authorizer.clean_attr_re.sub('_', attr)
221
222    def import_credentials(self, file_list=None, data_list=None):
223        if data_list:
224            return any([self.import_credential(data=d) for d in data_list])
225        elif file_list:
226            return any([self.import_credential(file=f) for f in file_list])
227        else:
228            return False
229
230    def import_credential(self, file=None, data=None):
231        if data:
232            if self.context.load_id_chunk(data) != ABAC.ABAC_CERT_SUCCESS:
233                return self.context.load_attribute_chunk(data) == \
234                        ABAC.ABAC_CERT_SUCCESS
235            else:
236                return True
237        elif file:
238            if self.context.load_id_file(file) != ABAC.ABAC_CERT_SUCCESS:
239                return self.context.load_attribute_file(file) == \
240                        ABAC.ABAC_CERT_SUCCESS
241            else:
242                return True
243        else:
244            return False
245
246    def set_attribute(self, name=None, attr=None, cert=None):
247        if name and attr:
248            if isinstance(name, tuple):
249                raise abac_authorizer.bad_name(
250                        "ABAC doesn't understand three-names")
251            # Convert non-string attributes to strings
252            if not isinstance(attr, basestring):
253                attr = "%s" % attr
254            if self.me and self.key:
255                # Create a credential and insert it into context
256                # This will simplify when we have libcreddy
257                try:
258                    # create temp file
259                    f, fn = mkstemp()
260                    os.close(f)
261                except EnvironmentError, e:
262                    raise abac_authorizer.attribute_error(
263                            "Cannot create temp file: %s" %e)
264
265                # Create the attribute certificate with creddy
266                cmd = [self.creddy, '--attribute', '--issuer=%s' % self.me, 
267                    '--key=%s' % self.key, '--role=%s' % self.clean_attr(attr), 
268                    '--subject-id=%s' % name, '--out=%s' % fn]
269                rv = call(cmd)
270                if rv == 0:
271                    self.lock.acquire()
272                    # load it to context and remove the file
273                    rv = self.context.load_attribute_file(fn)
274                    self.lock.release()
275                    os.unlink(fn)
276                else:
277                    os.unlink(fn)
278                    raise abac_authorizer.attribute_error(
279                            "creddy returned %s" % rv)
280            else:
281                raise abac_authorizer.attribute_error(
282                        "Identity and key not specified on creation")
283        elif cert:
284            # Insert this credential into the context
285            self.lock.acquire()
286            self.context.load_attribute_chunk(cert)
287            self.lock.release()
288        else:
289            raise abac_authorizer.attribute_error(
290                    "Neither name/attr nor cert is set")
291
292    def unset_attribute(self, name, attr):
293        if isinstance(name, tuple):
294            raise abac_authorizer.bad_name(
295                    "ABAC doesn't understand three-names")
296        # Convert non-string attributes to strings
297        if not isinstance(attr, basestring):
298            attr = "%s" % attr
299        cattr = self.clean_attr(attr)
300        self.lock.acquire()
301        ctxt = ABAC.Context()
302        ids = set()
303        for c in self.context.credentials():
304            h = c.head()
305            t = c.tail()
306            if h.is_role() and t.is_principal():
307                if t.principal() == '%s' % name and \
308                        h.principal() == '%s' % self.fedid and \
309                        h.role_name() == cattr:
310                    continue
311
312            id = c.issuer_cert()
313            if id not in ids:
314                ctxt.load_id_chunk(id)
315                ids.add(id)
316            ctxt.load_attribute_chunk(c.attribute_cert())
317        self.context = ctxt
318        self.lock.release()
319
320
321    def check_attribute(self, name, attr):
322        # XXX proof soon
323        if isinstance(name, tuple):
324            raise abac_authorizer.bad_name(
325                    "ABAC doesn't understand three-names")
326        else:
327            # Convert non-string attributes to strings
328            if not isinstance(attr, basestring):
329                attr = "%s" % attr
330            # Naked attributes are attested by this principal
331            if attr.find('.') == -1: 
332                a = "%s.%s" % (self.fedid, self.clean_attr(attr))
333            else: 
334                r, a = attr.split('.',1)
335                a = "%s.%s" ( r, self.clean_attr(a))
336
337            self.lock.acquire()
338            rv, proof = self.context.query(a, "%s" % name)
339            # XXX delete soon
340            if not rv and attr in self.globals: rv = True
341            self.lock.release()
342
343            return rv
344
345    def set_global_attribute(self, attr):
346        """
347        Set a global attribute.  All names, even those otherwise unknown to the
348        authorizer have this attribute.
349        """
350        self.lock.acquire()
351        self.globals.add(self.clean_attr(attr))
352        self.lock.release()
353
354    def unset_global_attribute(self, attr):
355        """
356        Remove a global attribute
357        """
358
359        self.lock.acquire()
360        self.globals.discard(self.clean_attr(attr))
361        self.lock.release()
362
363    def clone(self):
364        self.lock.acquire()
365        rv = abac_authorizer(me=self.me, key=self.key)
366        rv.globals = self.globals.copy()
367        rv.context = ABAC.Context(self.context)
368        self.lock.release()
369        return rv
370
371    def save(self, dir=None):
372        self.lock.acquire()
373        if dir:
374            self.save_dir = dir
375        else:
376            dir = self.save_dir
377        if dir is None:
378            self.lock.release()
379            raise abac_authorizer.no_file_error("No load directory specified")
380        try:
381            if not os.access(dir, os.F_OK):
382                os.mkdir(dir)
383            # These are unpicklable, so set them aside
384            context = self.context
385            lock = self.lock
386            self.context = None
387            self.lock = None
388
389            f = open("%s/state" % dir, "w")
390            pickle.dump(self, f)
391            f.close()
392
393            if not os.access("%s/certs" %dir, os.F_OK):
394                os.mkdir("%s/certs" % dir)
395            seenid = set()
396            seenattr = set()
397
398            #restore unpicklable state
399            self.context = context
400            self.lock = lock
401            #remove old certs
402            for fn in [ f for f in os.listdir("%s/certs" % dir) \
403                    if abac_authorizer.cred_file_re.match(f)]:
404                os.unlink('%s/certs/%s' % (dir, fn))
405            ii = 0
406            ai = 0
407            for c in self.context.credentials():
408                id = c.issuer_cert()
409                attr = c.attribute_cert()
410                # NB: file naming conventions matter here.  The trailing_ID and
411                # _attr are required by ABAC.COntext.load_directory()
412                if id and id not in seenid:
413                    f = open("%s/certs/ID_%03d_ID.der" % (dir, ii), "w")
414                    f.write(id)
415                    f.close()
416                    ii += 1
417                    seenid.add(id)
418                if attr and attr not in seenattr:
419                    f = open("%s/certs/attr_%03d_attr.der" % (dir, ai), "w")
420                    f.write(attr)
421                    f.close()
422                    ai += 1
423                    seenattr.add(attr)
424        except EnvironmentError, e:
425            # If we've mislaid self.lock, release lock (they're the same object)
426            if self.lock: self.lock.release()
427            elif lock: lock.release()
428            raise e
429        except pickle.PickleError, e:
430            # If we've mislaid self.lock, release lock (they're the same object)
431            if self.lock: self.lock.release()
432            elif lock: lock.release()
433            raise e
434        self.lock.release()
435
436    def load(self, dir=None):
437        self.lock.acquire()
438        if dir:
439            self.save_dir = dir
440        else:
441            dir = self.save_dir
442        if dir is None:
443            self.lock.release()
444            raise abac_authorizer.no_file_error("No load directory specified")
445        try:
446            if os.access("%s/state" % dir, os.R_OK):
447                f = open("%s/state" % dir, "r")
448                st = pickle.load(f)
449                f.close()
450                # Cpoy the useful attributes from the pickled state
451                for a in ('globals', 'key', 'me', 'cert', 'fedid'):
452                    setattr(self, a, getattr(st, a, None))
453
454            # Initialize the new context with the new identity
455            self.context = ABAC.Context()
456            if self.me:
457                self.context.load_id_file(self.me)
458            self.context.load_directory("%s/certs" % dir)
459            self.save_dir = dir
460        except EnvironmentError, e:
461            self.lock.release()
462            raise e
463        except pickle.PickleError, e:
464            self.lock.release()
465            raise e
466        self.lock.release()
467
468    def __str__(self):
469        def encode_role(r):
470            if r.is_principal(): 
471                return "%s" % r.principal()
472            elif r.is_role(): 
473                return "%s.%s" % (r.principal(), r.role_name())
474            elif r.is_linking():
475                return "%s.%s.%s" % \
476                        (r.principal(), r.linking_role(), r.role_name())
477
478        self.lock.acquire()
479        rv = "%s" % self.fedid
480        for c in self.context.credentials():
481            rv += '\n%s <- %s' % (encode_role(c.head()), encode_role(c.tail()))
482        self.lock.release()
483        return rv
484
Note: See TracBrowser for help on using the repository browser.