source: fedd/federation/authorizer.py @ 725c55d

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

Checkpoint - successful swap in and out

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