source: fedd/federation/authorizer.py @ 822d31b

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

fix legacy mode

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