source: fedd/federation/authorizer.py @ 36f642f

compt_changesinfo-ops
Last change on this file since 36f642f was 36f642f, checked in by Ted Faber <faber@…>, 8 years ago

Save split keys in abac_authorizers fixes #12

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