source: fedd/federation/authorizer.py @ 8cf2c507

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

Remove a race condition in the save member. The authorizer's lock was being nulled to work around a pickling issue (my limited understanding of pickling). The pickling process has been made cleaner, and as a result the save cleaner and safer.

  • Property mode set to 100644
File size: 14.5 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, hexdigits
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)
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    # Modify the pickling operations so that the context and lock are not
239    # pickled
240
241    def __getstate__(self):
242        d = self.__dict__.copy()
243        del d['lock']
244        del d['context']
245        return d
246
247    def __setstate__(self, d):
248        # Import everything from the pickle dict (except what we excluded in
249        # __getstate__)
250        self.__dict__.update(d)
251        # Initialize the unpicklables
252        self.context = ABAC.Context()
253        self.lock = Lock()
254
255    @staticmethod
256    def clean_attr(attr):
257        return abac_authorizer.clean_attr_re.sub('_', attr)
258
259
260    def import_credentials(self, file_list=None, data_list=None):
261        if data_list:
262            return any([self.import_credential(data=d) for d in data_list])
263        elif file_list:
264            return any([self.import_credential(file=f) for f in file_list])
265        else:
266            return False
267
268    def import_credential(self, file=None, data=None):
269        if data:
270            if self.context.load_id_chunk(data) != ABAC.ABAC_CERT_SUCCESS:
271                return self.context.load_attribute_chunk(data) == \
272                        ABAC.ABAC_CERT_SUCCESS
273            else:
274                return True
275        elif file:
276            if self.context.load_id_file(file) != ABAC.ABAC_CERT_SUCCESS:
277                return self.context.load_attribute_file(file) == \
278                        ABAC.ABAC_CERT_SUCCESS
279            else:
280                return True
281        else:
282            return False
283
284    def set_attribute(self, name=None, attr=None, cert=None):
285        if name and attr:
286            if isinstance(name, tuple):
287                raise abac_authorizer.bad_name(
288                        "ABAC doesn't understand three-names")
289            # Convert non-string attributes to strings
290            if not isinstance(attr, basestring):
291                attr = "%s" % attr
292
293            if self.me and self.key:
294                # Create a credential and insert it into context
295                # This will simplify when we have libcreddy
296                try:
297                    # create temp file
298                    f, fn = mkstemp()
299                    os.close(f)
300                except EnvironmentError, e:
301                    raise abac_authorizer.attribute_error(
302                            "Cannot create temp file: %s" %e)
303
304                # Create the attribute certificate with creddy
305                cmd = [self.creddy, '--attribute', '--issuer=%s' % self.me, 
306                    '--key=%s' % self.key, '--role=%s' % self.clean_attr(attr), 
307                    '--subject-id=%s' % name, '--out=%s' % fn]
308                rv = call(cmd)
309                if rv == 0:
310                    self.lock.acquire()
311                    # load it to context and remove the file
312                    rv = self.context.load_attribute_file(fn)
313                    self.lock.release()
314                    os.unlink(fn)
315                else:
316                    os.unlink(fn)
317                    raise abac_authorizer.attribute_error(
318                            "creddy returned %s" % rv)
319            else:
320                raise abac_authorizer.attribute_error(
321                        "Identity and key not specified on creation")
322        elif cert:
323            # Insert this credential into the context
324            self.lock.acquire()
325            self.context.load_attribute_chunk(cert)
326            self.lock.release()
327        else:
328            raise abac_authorizer.attribute_error(
329                    "Neither name/attr nor cert is set")
330
331    def unset_attribute(self, name, attr):
332        if isinstance(name, tuple):
333            raise abac_authorizer.bad_name(
334                    "ABAC doesn't understand three-names")
335        # Convert non-string attributes to strings
336        if not isinstance(attr, basestring):
337            attr = "%s" % attr
338        cattr = self.clean_attr(attr)
339        self.lock.acquire()
340        ctxt = ABAC.Context()
341        ids = set()
342        for c in self.context.credentials():
343            h = c.head()
344            t = c.tail()
345            if h.is_role() and t.is_principal():
346                if t.principal() == '%s' % name and \
347                        h.principal() == '%s' % self.fedid and \
348                        h.role_name() == cattr:
349                    continue
350
351            id = c.issuer_cert()
352            if id not in ids:
353                ctxt.load_id_chunk(id)
354                ids.add(id)
355            ctxt.load_attribute_chunk(c.attribute_cert())
356        self.context = ctxt
357        self.lock.release()
358
359    @staticmethod
360    def starts_with_fedid(attr):
361        """
362        Return true if the first 40 characters of the string are hex digits
363        followed by a dot.  False otherwise.  Used in check_attribute.
364        """
365        if attr.find('.') == 40:
366            return all([ x in hexdigits for x in attr[0:40]])
367        else:
368            return False
369
370
371    def check_attribute(self, name, attr):
372        # XXX proof soon
373        if isinstance(name, tuple):
374            raise abac_authorizer.bad_name(
375                    "ABAC doesn't understand three-names")
376        else:
377            # Convert non-string attributes to strings
378            if not isinstance(attr, basestring):
379                attr = "%s" % attr
380            # Attributes that start with a fedid only have the part of the
381            # attribute after the dot cleaned.  Others are completely cleaned
382            # and have the owner fedid attached.
383            if self.starts_with_fedid(attr):
384                r, a = attr.split('.',1)
385                a = "%s.%s" % ( r, self.clean_attr(a))
386            else: 
387                a = "%s.%s" % (self.fedid, self.clean_attr(attr))
388
389            a = str(a)
390            n = str("%s" % name)
391
392            self.lock.acquire()
393            # Sigh. Unicode vs swig and swig seems to lose.  Make sure
394            # everything we pass into ABAC is a str not a unicode.
395            rv, proof = self.context.query(a, n)
396            # XXX delete soon
397            if not rv and attr in self.globals: rv = True
398            self.lock.release()
399
400            return rv
401
402    def set_global_attribute(self, attr):
403        """
404        Set a global attribute.  All names, even those otherwise unknown to the
405        authorizer have this attribute.
406        """
407        self.lock.acquire()
408        self.globals.add(self.clean_attr(attr))
409        self.lock.release()
410
411    def unset_global_attribute(self, attr):
412        """
413        Remove a global attribute
414        """
415
416        self.lock.acquire()
417        self.globals.discard(self.clean_attr(attr))
418        self.lock.release()
419
420    def clone(self):
421        self.lock.acquire()
422        rv = abac_authorizer(me=self.me, key=self.key)
423        rv.globals = self.globals.copy()
424        rv.context = ABAC.Context(self.context)
425        self.lock.release()
426        return rv
427
428    def save(self, dir=None):
429        self.lock.acquire()
430        if dir:
431            self.save_dir = os.path.abspath(dir)
432        else:
433            dir = self.save_dir
434        if dir is None:
435            self.lock.release()
436            raise abac_authorizer.no_file_error("No load directory specified")
437        try:
438            if not os.access(dir, os.F_OK):
439                os.mkdir(dir)
440
441            f = open("%s/state" % dir, "w")
442            pickle.dump(self, f)
443            f.close()
444
445            if not os.access("%s/certs" %dir, os.F_OK):
446                os.mkdir("%s/certs" % dir)
447
448            # Clear the certs subdir
449            for fn in [ f for f in os.listdir("%s/certs" % dir) \
450                    if abac_authorizer.cred_file_re.match(f)]:
451                os.unlink('%s/certs/%s' % (dir, fn))
452
453            # Save the context
454            ii = 0
455            ai = 0
456            seenid = set()
457            seenattr = set()
458            for c in self.context.credentials():
459                id = c.issuer_cert()
460                attr = c.attribute_cert()
461                # NB: file naming conventions matter here.  The trailing_ID and
462                # _attr are required by ABAC.COntext.load_directory()
463                if id and id not in seenid:
464                    f = open("%s/certs/ID_%03d_ID.der" % (dir, ii), "w")
465                    f.write(id)
466                    f.close()
467                    ii += 1
468                    seenid.add(id)
469                if attr and attr not in seenattr:
470                    f = open("%s/certs/attr_%03d_attr.der" % (dir, ai), "w")
471                    f.write(attr)
472                    f.close()
473                    ai += 1
474                    seenattr.add(attr)
475        except EnvironmentError, e:
476            self.lock.release()
477            raise e
478        except pickle.PickleError, e:
479            self.lock.release()
480            raise e
481        self.lock.release()
482
483    def load(self, dir=None):
484        self.lock.acquire()
485        if dir:
486            self.save_dir = dir
487        else:
488            dir = self.save_dir
489        if dir is None:
490            self.lock.release()
491            raise abac_authorizer.no_file_error("No load directory specified")
492        try:
493            if os.access("%s/state" % dir, os.R_OK):
494                f = open("%s/state" % dir, "r")
495                st = pickle.load(f)
496                f.close()
497                # Copy the useful attributes from the pickled state
498                for a in ('globals', 'key', 'me', 'cert', 'fedid'):
499                    setattr(self, a, getattr(st, a, None))
500
501            # Initialize the new context with the new identity
502            self.context = ABAC.Context()
503            if self.me:
504                self.context.load_id_file(self.me)
505            self.context.load_directory("%s/certs" % dir)
506            self.save_dir = dir
507        except EnvironmentError, e:
508            self.lock.release()
509            raise e
510        except pickle.PickleError, e:
511            self.lock.release()
512            raise e
513        self.lock.release()
514
515    @staticmethod
516    def encode_credential(c):
517        return '%s <- %s' % (c.head().string(), c.tail().string())
518
519    def get_creds_for_principal(self, fid):
520        look_for = set(["%s" % fid])
521        found_attrs = set()
522        next_look = set()
523        found = set([])
524
525        self.lock.acquire()
526        while look_for:
527            for c in self.context.credentials():
528                tail = c.tail()
529                # XXX: This needs to be more aggressive for linked stuff
530                if tail.string() in look_for and c not in found:
531                    found.add(c)
532                    next_look.add(c.head().string())
533
534            look_for = next_look
535            next_look = set()
536        self.lock.release()
537       
538        return found
539
540    def __str__(self):
541
542        self.lock.acquire()
543        rv = "%s" % self.fedid
544        add = join([abac_authorizer.encode_credential(c)
545            for c in self.context.credentials()], '\n');
546        if add: rv += "\n%s" % add
547        self.lock.release()
548        return rv
549
Note: See TracBrowser for help on using the repository browser.