source: fedd/federation/authorizer.py @ 6031c9d

compt_changesinfo-ops
Last change on this file since 6031c9d was 5097195, checked in by Ted Faber <faber@…>, 13 years ago

Make sure that the file access attributes do not begin with an
underscore. (Unsure why this wasn't needed before).

  • Property mode set to 100644
File size: 15.2 KB
Line 
1#/usr/local/bin/python
2
3from tempfile import mkstemp
4from subprocess import call
5from threading import Lock
6
7from string import join, hexdigits
8
9from fedid import fedid
10from remote_service import service_caller
11from service_error import service_error
12from util import abac_pem_type, abac_split_cert
13from proof import proof
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, with_proof=False):
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            if with_proof: return True, proof("me", name, attr)
133            else: return True
134
135        if isinstance(name, tuple):
136            for p in range(0,8):
137                lookup = ( tup(name, 0, p), tup(name,1, p), tup(name,2,p))
138                if self.attrs.has_key(lookup):
139                    if attr in self.attrs[lookup]:
140                        if with_proof: return True, proof("me", name, attr)
141                        else: return True
142                # Drop through
143                if with_proof: return False, proof("me", name, attr)
144                else: return False
145        else:
146            if with_proof:
147                return attr in self.attrs.get(self.auth_name(name), set()), \
148                        proof("me", name, attr)
149            else:
150                return attr in self.attrs.get(self.auth_name(name), set())
151
152    def set_global_attribute(self, attr):
153        """
154        Set a global attribute.  All names, even those otherwise unknown to the
155        authorizer have this attribute.
156        """
157        self.globals.add(attr)
158
159    def unset_global_attribute(self, attr):
160        """
161        Remove a global attribute
162        """
163
164        self.globals.discard(attr)
165
166    def import_credentials(self, file_list=None, data_list=None):
167        return False
168
169    def __str__(self):
170        rv = ""
171        rv += "attrs %s\n" % self.attrs
172        rv += "globals %s" % self.globals
173        return rv
174
175    def clone(self):
176        rv = authorizer()
177        rv.attrs = self.attrs.copy()
178        rv.globals = self.globals.copy()
179        return rv
180
181    def save(self, fn=None):
182        if fn:
183            f = open(fn, "w")
184            pickle.dump(self, f)
185            f.close()
186
187    def load(self, fn=None):
188        if fn:
189            f = open(fn, "r")
190            a = pickle.load(f)
191            f.close()
192            self.attrs = a.attrs
193            self.globals = a.globals
194       
195
196class abac_authorizer(authorizer_base):
197    """
198    Use the ABAC authorization system to make attribute decisions.
199    """
200
201    clean_attr_re = re.compile('[^A-Za-z0-9_]+')
202    clean_prefix_attr_re = re.compile('^_+')
203    cred_file_re = re.compile('.*\.der$')
204    bad_name = authorizer_base.bad_name
205    attribute_error = authorizer_base.attribute_error
206    class no_file_error(RuntimeError): pass
207    class bad_cert_error(RuntimeError): pass
208
209    def __init__(self, certs=None, me=None, key=None, load=None, save=None):
210        self.creddy = '/usr/local/bin/creddy'
211        self.globals = set()
212        self.lock = Lock()
213        self.me = me
214        self.save_dir = load or save
215        if self.save_dir:
216            self.save_dir = os.path.abspath(self.save_dir)
217        # If the me parameter is a combination certificate, split it into the
218        # abac_authorizer save directory (if any) for use with creddy.
219        if self.me is not None and abac_pem_type(self.me) == 'both':
220            if self.save_dir:
221                keyfile="%s/key.pem" % self.save_dir
222                certfile = "%s/cert.pem" % self.save_dir
223
224                # Clear a spot for the new key and cert files.
225                for fn in (keyfile, certfile):
226                    if os.access(fn, os.F_OK):
227                        os.unlink(fn)
228
229                self.key, self.me = abac_split_cert(self.me, keyfile, certfile)
230            else:
231                raise abac_authorizer.bad_cert_error("Combination " + \
232                        "certificate and nowhere to split it");
233        else:
234            self.key = key
235        self.context = ABAC.Context()
236        if me:
237            self.fedid = fedid(file=self.me)
238            rv = self.context.load_id_file(self.me)
239            if rv != 0:
240                raise abac_authorizer.bad_name(
241                        'Cannot load identity from %s' % me)
242        else:
243            self.fedid = None
244
245        if isinstance(certs, basestring):
246            certs = [ certs ] 
247
248        for dir in certs or []:
249            self.context.load_directory(dir)
250
251        if load:
252            self.load(load)
253
254    # Modify the pickling operations so that the context and lock are not
255    # pickled
256
257    def __getstate__(self):
258        d = self.__dict__.copy()
259        del d['lock']
260        del d['context']
261        return d
262
263    def __setstate__(self, d):
264        # Import everything from the pickle dict (except what we excluded in
265        # __getstate__)
266        self.__dict__.update(d)
267        # Initialize the unpicklables
268        self.context = ABAC.Context()
269        self.lock = Lock()
270
271    @staticmethod
272    def clean_attr(attr):
273        a = abac_authorizer.clean_attr_re.sub('_', attr)
274        return abac_authorizer.clean_prefix_attr_re.sub('', a)
275
276
277    def import_credentials(self, file_list=None, data_list=None):
278        if data_list:
279            return any([self.import_credential(data=d) for d in data_list])
280        elif file_list:
281            return any([self.import_credential(file=f) for f in file_list])
282        else:
283            return False
284
285    def import_credential(self, file=None, data=None):
286        if data:
287            if self.context.load_id_chunk(data) != ABAC.ABAC_CERT_SUCCESS:
288                return self.context.load_attribute_chunk(data) == \
289                        ABAC.ABAC_CERT_SUCCESS
290            else:
291                return True
292        elif file:
293            if self.context.load_id_file(file) != ABAC.ABAC_CERT_SUCCESS:
294                return self.context.load_attribute_file(file) == \
295                        ABAC.ABAC_CERT_SUCCESS
296            else:
297                return True
298        else:
299            return False
300
301    def set_attribute(self, name=None, attr=None, cert=None):
302        if name and attr:
303            if isinstance(name, tuple):
304                raise abac_authorizer.bad_name(
305                        "ABAC doesn't understand three-names")
306            # Convert non-string attributes to strings
307            if not isinstance(attr, basestring):
308                attr = "%s" % attr
309
310            if self.me and self.key:
311                # Create a credential and insert it into context
312                # This will simplify when we have libcreddy
313                try:
314                    # create temp file
315                    f, fn = mkstemp()
316                    os.close(f)
317                except EnvironmentError, e:
318                    raise abac_authorizer.attribute_error(
319                            "Cannot create temp file: %s" %e)
320
321                # Create the attribute certificate with creddy
322                cmd = [self.creddy, '--attribute', '--issuer=%s' % self.me, 
323                    '--key=%s' % self.key, '--role=%s' % self.clean_attr(attr), 
324                    '--subject-id=%s' % name, '--out=%s' % fn]
325                rv = call(cmd)
326                if rv == 0:
327                    self.lock.acquire()
328                    # load it to context and remove the file
329                    rv = self.context.load_attribute_file(fn)
330                    self.lock.release()
331                    os.unlink(fn)
332                else:
333                    os.unlink(fn)
334                    raise abac_authorizer.attribute_error(
335                            "creddy returned %s" % rv)
336            else:
337                raise abac_authorizer.attribute_error(
338                        "Identity and key not specified on creation")
339        elif cert:
340            # Insert this credential into the context
341            self.lock.acquire()
342            self.context.load_attribute_chunk(cert)
343            self.lock.release()
344        else:
345            raise abac_authorizer.attribute_error(
346                    "Neither name/attr nor cert is set")
347
348    def unset_attribute(self, name, attr):
349        if isinstance(name, tuple):
350            raise abac_authorizer.bad_name(
351                    "ABAC doesn't understand three-names")
352        # Convert non-string attributes to strings
353        if not isinstance(attr, basestring):
354            attr = "%s" % attr
355        cattr = self.clean_attr(attr)
356        self.lock.acquire()
357        ctxt = ABAC.Context()
358        ids = set()
359        for c in self.context.credentials():
360            h = c.head()
361            t = c.tail()
362            if h.is_role() and t.is_principal():
363                if t.principal() == '%s' % name and \
364                        h.principal() == '%s' % self.fedid and \
365                        h.role_name() == cattr:
366                    continue
367
368            id = c.issuer_cert()
369            if id not in ids:
370                ctxt.load_id_chunk(id)
371                ids.add(id)
372            ctxt.load_attribute_chunk(c.attribute_cert())
373        self.context = ctxt
374        self.lock.release()
375
376    @staticmethod
377    def starts_with_fedid(attr):
378        """
379        Return true if the first 40 characters of the string are hex digits
380        followed by a dot.  False otherwise.  Used in check_attribute.
381        """
382        if attr.find('.') == 40:
383            return all([ x in hexdigits for x in attr[0:40]])
384        else:
385            return False
386
387
388    def check_attribute(self, name, attr, with_proof=False):
389        if isinstance(name, tuple):
390            raise abac_authorizer.bad_name(
391                    "ABAC doesn't understand three-names")
392        else:
393            # Convert non-string attributes to strings
394            if not isinstance(attr, basestring):
395                attr = "%s" % attr
396            # Attributes that start with a fedid only have the part of the
397            # attribute after the dot cleaned.  Others are completely cleaned
398            # and have the owner fedid attached.
399            if self.starts_with_fedid(attr):
400                r, a = attr.split('.',1)
401                a = "%s.%s" % ( r, self.clean_attr(a))
402            else: 
403                a = "%s.%s" % (self.fedid, self.clean_attr(attr))
404
405            a = str(a)
406            n = str("%s" % name)
407
408            self.lock.acquire()
409            # Sigh. Unicode vs swig and swig seems to lose.  Make sure
410            # everything we pass into ABAC is a str not a unicode.
411            rv, p = self.context.query(a, n)
412            # XXX delete soon
413            if not rv and attr in self.globals: 
414                rv = True
415                p = None
416            self.lock.release()
417            if with_proof: return rv, proof(self.fedid, name, a, p)
418            else: return rv
419
420    def set_global_attribute(self, attr):
421        """
422        Set a global attribute.  All names, even those otherwise unknown to the
423        authorizer have this attribute.
424        """
425        self.lock.acquire()
426        self.globals.add(self.clean_attr(attr))
427        self.lock.release()
428
429    def unset_global_attribute(self, attr):
430        """
431        Remove a global attribute
432        """
433
434        self.lock.acquire()
435        self.globals.discard(self.clean_attr(attr))
436        self.lock.release()
437
438    def clone(self):
439        self.lock.acquire()
440        rv = abac_authorizer(me=self.me, key=self.key)
441        rv.globals = self.globals.copy()
442        rv.context = ABAC.Context(self.context)
443        self.lock.release()
444        return rv
445
446    def save(self, dir=None):
447        self.lock.acquire()
448        if dir:
449            self.save_dir = os.path.abspath(dir)
450        else:
451            dir = self.save_dir
452        if dir is None:
453            self.lock.release()
454            raise abac_authorizer.no_file_error("No load directory specified")
455        try:
456            if not os.access(dir, os.F_OK):
457                os.mkdir(dir)
458
459            f = open("%s/state" % dir, "w")
460            pickle.dump(self, f)
461            f.close()
462
463            if not os.access("%s/certs" %dir, os.F_OK):
464                os.mkdir("%s/certs" % dir)
465
466            # Clear the certs subdir
467            for fn in [ f for f in os.listdir("%s/certs" % dir) \
468                    if abac_authorizer.cred_file_re.match(f)]:
469                os.unlink('%s/certs/%s' % (dir, fn))
470
471            # Save the context
472            ii = 0
473            ai = 0
474            seenid = set()
475            seenattr = set()
476            for c in self.context.credentials():
477                id = c.issuer_cert()
478                attr = c.attribute_cert()
479                # NB: file naming conventions matter here.  The trailing_ID and
480                # _attr are required by ABAC.COntext.load_directory()
481                if id and id not in seenid:
482                    f = open("%s/certs/ID_%03d_ID.der" % (dir, ii), "w")
483                    f.write(id)
484                    f.close()
485                    ii += 1
486                    seenid.add(id)
487                if attr and attr not in seenattr:
488                    f = open("%s/certs/attr_%03d_attr.der" % (dir, ai), "w")
489                    f.write(attr)
490                    f.close()
491                    ai += 1
492                    seenattr.add(attr)
493        except EnvironmentError, e:
494            self.lock.release()
495            raise e
496        except pickle.PickleError, e:
497            self.lock.release()
498            raise e
499        self.lock.release()
500
501    def load(self, dir=None):
502        self.lock.acquire()
503        if dir:
504            self.save_dir = dir
505        else:
506            dir = self.save_dir
507        if dir is None:
508            self.lock.release()
509            raise abac_authorizer.no_file_error("No load directory specified")
510        try:
511            if os.access("%s/state" % dir, os.R_OK):
512                f = open("%s/state" % dir, "r")
513                st = pickle.load(f)
514                f.close()
515                # Copy the useful attributes from the pickled state
516                for a in ('globals', 'key', 'me', 'cert', 'fedid'):
517                    setattr(self, a, getattr(st, a, None))
518
519            # Initialize the new context with the new identity
520            self.context = ABAC.Context()
521            if self.me:
522                self.context.load_id_file(self.me)
523            self.context.load_directory("%s/certs" % dir)
524            self.save_dir = dir
525        except EnvironmentError, e:
526            self.lock.release()
527            raise e
528        except pickle.PickleError, e:
529            self.lock.release()
530            raise e
531        self.lock.release()
532
533    @staticmethod
534    def encode_credential(c):
535        return '%s <- %s' % (c.head().string(), c.tail().string())
536
537    def get_creds_for_principal(self, fid):
538        look_for = set(["%s" % fid])
539        found_attrs = set()
540        next_look = set()
541        found = set([])
542
543        self.lock.acquire()
544        while look_for:
545            for c in self.context.credentials():
546                tail = c.tail()
547                # XXX: This needs to be more aggressive for linked stuff
548                if tail.string() in look_for and c not in found:
549                    found.add(c)
550                    next_look.add(c.head().string())
551
552            look_for = next_look
553            next_look = set()
554        self.lock.release()
555       
556        return found
557
558    def __str__(self):
559
560        self.lock.acquire()
561        rv = "%s" % self.fedid
562        add = join([abac_authorizer.encode_credential(c)
563            for c in self.context.credentials()], '\n');
564        if add: rv += "\n%s" % add
565        self.lock.release()
566        return rv
567
Note: See TracBrowser for help on using the repository browser.