source: fedd/access_to_abac.py @ 8222e09

Last change on this file since 8222e09 was 8222e09, checked in by Ted Faber <faber@…>, 12 years ago

Better comment???

  • Property mode set to 100755
File size: 17.7 KB
RevLine 
[2e46f35]1#!/usr/bin/env python
[6cc5c81]2
3import sys, os
4import re
[87807f42]5import subprocess
[d894c21]6import os.path
[6cc5c81]7
[87807f42]8from string import join
[d894c21]9from optparse import OptionParser, OptionValueError
10from tempfile import mkdtemp
11
[cd360a0]12import Creddy
13
[6bedbdba]14from deter import fedid
[d894c21]15from federation.authorizer import abac_authorizer
[62f3dd9]16from federation.util import abac_split_cert, abac_pem_type, file_expanding_opts
[6cc5c81]17
[87807f42]18
[6cc5c81]19class attribute:
[af25848]20    '''
[87807f42]21    Encapculate a principal/attribute/link tuple.
[af25848]22    '''
[b931822]23    bad_attr = re.compile('[^a-zA-Z0-9:_]+')
[87807f42]24    def __init__(self, p, a, l=None):
[6cc5c81]25        self.principal = p
[87807f42]26        self.attr = attribute.bad_attr.sub('_', a)
27        if l: self.link = attribute.bad_attr.sub('_', l)
28        else: self.link = None
[6cc5c81]29
30    def __str__(self):
[87807f42]31        if self.link:
32            return "%s.%s.%s" % (self.principal, self.attr, self.link)
33        elif self.attr:
[af25848]34            return "%s.%s" % (self.principal, self.attr)
35        else:
36            return "%s" % self.principal
[6cc5c81]37
38class credential:
[af25848]39    '''
40    A Credential, that is the requisites (as attributes) and the assigned
41    attribute (as principal, attr).   If req is iterable, the requirements are
42    an intersection/conjunction.
43    '''
[b931822]44    bad_attr = re.compile('[^a-zA-Z0-9:_]+')
[6cc5c81]45    def __init__(self, p, a, req):
46        self.principal = p
47        if isinstance(a, (tuple, list, set)) and len(a) == 1:
[87807f42]48            self.attr = credential.bad_attr.sub('_', a[0])
[6cc5c81]49        else:
[87807f42]50            self.attr = credential.bad_attr.sub('_', a)
[6cc5c81]51        self.req = req
52
53    def __str__(self):
54        if isinstance(self.req, (tuple, list, set)):
55            return "%s.%s <- %s" % (self.principal, self.attr, 
[87807f42]56                    join(["%s" % r for r in self.req], ' & '))
[6cc5c81]57        else:
58            return "%s.%s <- %s" % (self.principal, self.attr, self.req)
59
[353db8c]60# Mappinng generation function and the access parser throw these when there is
[af25848]61# a parsing problem.
[6cc5c81]62class parse_error(RuntimeError): pass
63
[87807f42]64# Error creating a credential
65class credential_error(RuntimeError): pass
66
[af25848]67# Functions to parse the individual access maps as well as an overall function
68# to parse generic ones.  The specific ones create a credential to local
69# attributes mapping and the global one creates the access policy credentials.
70
[8222e09]71#  All the local parsing functions get
72#    * the unparsed remainder of the line in l.  This is everything on the
73#       input line from the first comma after the -> to the end of the line.
74#    * the current list of credentials to be issued.  This is a list of
75#      credential objects assigned from this principal (me) to principals
76#      making requests.  They are derived from the three-name and delegation
77#      credentials.  This routine adds any credentials that will be mapped to
78#      local access control information to creds.
79#    * me is this princpal - the access controller
80#    * to_id is a dict that maps the local name access control information into
81#      a list of credentials that imply the principal should be mapped to them.
82#      For example, parse_emulab is assigning (project, user, cert, key) to
83#      principals.  The key of to_id is that tuple and it maps to a list of
84#      ABAC attributes.  If an access controller wants to see if a caller can
85#      use a 4-tuple of credentials, it tries to prove that the caller has one
86#      of those attributes.
87#    * p is the principal that assigned the project and group.  It is the name
88#       of an external experiment controller.
89#    * gp is the group asserted by the experiment controller
90#    * gu is the user asserted by the experiment controller
91#    * lr is the linking role used for delegation.  If present credentials
92#    should be created with it.  If it is None the credential construator will
93#    ignore it.  It is hard to go wrong just passing it to the credential
94#    constructor.
95#
96# These functions map an assertion of (testbed, project, user) into the
97# local parameters (local project, password, etc.) that triple gives the caller
98# the caller to.  These local parameters are parsed out of l.  Those
99# credentials are the keys to the to_id dict that will become the abac map.
100# The c = credential... and creds.add(c)  lines
101# in parse_emulab can be taken as boilerplate for creating ABAC credentials.
102# Each ABAC credential created by that boilerplate should be added to to_id,
103# keyed by the local credentials.
[87807f42]104def parse_emulab(l, creds, me, to_id, p, gp, gu, lr):
[af25848]105    '''
[f77a256]106    Parse the emulab (project, allocation_user, cert_file) format.
[af25848]107    '''
[f77a256]108    right_side_str = '\s*,\s*\(\s*%s\s*,\s*%s\s*,\s*(%s)\s*\)' % \
109            (proj_same_str, id_same_str,path_str)
[6cc5c81]110
111    m = re.match(right_side_str, l)
112    if m:
[f77a256]113        project, user, cert = m.group(1,2,3)
[af25848]114        # Resolve "<same>"s in project and user
[6cc5c81]115        if project == '<same>':
116            if gp  is not None:
117                project = gp
118            else:
119                raise parse_error("Project cannot be decisively mapped: %s" % l)
120        if user == '<same>':
121            if gu is not None:
[c324ad3]122                user = gu
[6cc5c81]123            else:
124                raise parse_error("User cannot be decisively mapped: %s" % l)
[af25848]125
126        # Create a semi-mnemonic name for the destination credential (the one
127        # that will be mapped to the local attributes
[c324ad3]128        if gp and gu:
129            a = 'project_%s_user_%s' % (gp, gu)
130        elif gp:
131            a = 'project_%s' % gp
132        elif gu:
133            a = 'user_%s' % gu
[6cc5c81]134        else:
135            raise parse_error("No mapping for %s/%s!?" % (gp, gu))
136
[af25848]137        # Store the creds and map entries
[6cc5c81]138        c = credential(me, a, 
[87807f42]139                [attribute(p, x, lr) for x in (gp, gu) if x is not None])
[6cc5c81]140        creds.add(c)
[f77a256]141        if (project, user,cert) in to_id: to_id[(project,user,cert)].append(c)
142        else: to_id[(project,user,cert)] = [ c ]
[6cc5c81]143    else:
144        raise parse_error("Badly formatted local mapping: %s" % l)
145
146
[87807f42]147def parse_protogeni(l, creds, me, to_id, p, gp, gu, lr):
[af25848]148    '''
149    Parse the protoGENI (cert, user, user_key, cert_pw) format.
150    '''
[5a721ed]151    right_side_str = '\s*,\s*\(\s*(%s)\s*,\s*(%s)\s*,\s*(%s)\s*,\s*(%s)\s*\)' \
152            % (path_str, id_str, path_str, id_str)
153
154    m = re.match(right_side_str, l)
155    if m:
156        cert, user, key, pw = m.group(1,2,3,4)
[af25848]157        # The credential is formed from just the path (with / mapped to _) and
158        # the username.
[5a721ed]159        acert = re.sub('/', '_', cert)
160
161        a = "cert_%s_user_%s" % (acert, user)
[af25848]162
163        # Store em
[5a721ed]164        c = credential(me, a, 
[87807f42]165                [attribute(p, x, lr) for x in (gp, gu) if x is not None])
[5a721ed]166        creds.add(c)
167        if (cert, user, key, pw) in to_id: 
168            to_id[(cert, user, key, pw)].append(c)
169        else: 
170            to_id[(cert, user, key, pw)] = [ c ]
171    else:
172        raise parse_error("Badly formatted local mapping: %s" % l)
173
[87807f42]174def parse_dragon(l, creds, me, to_id, p, gp, gu, lr):
[af25848]175    '''
176    Parse the dragon (repository_name) version.
177    '''
[6cc5c81]178    right_side_str = '\s*,\s*\(\s*(%s)\s*\)' % \
179            (id_str)
180
181    m = re.match(right_side_str, l)
182    if m:
183        repo= m.group(1)
184        c = credential(me, 'repo_%s' % repo, 
[87807f42]185                [attribute(p, x, lr) for x in (gp, gu) if x is not None])
[6cc5c81]186        creds.add(c)
187        if repo in to_id: to_id[repo].append(c)
188        else: to_id[repo] = [ c ]
189    else:
190        raise parse_error("Badly formatted local mapping: %s" % l)
[9cce15a]191
[87807f42]192def parse_skel(l, creds, me, to_id, p, gp, gu, lr):
[af25848]193    '''
194    Parse the skeleton (local_attr) version.
195    '''
196    right_side_str = '\s*,\s*\(\s*(%s)\s*\)' % \
197            (id_str)
198
199    m = re.match(right_side_str, l)
200    if m:
201        lattr = m.group(1)
202        c = credential(me, 'lattr_%s' % lattr, 
[87807f42]203                [attribute(p, x, lr) for x in (gp, gu) if x is not None])
[af25848]204        creds.add(c)
205        if lattr in to_id: to_id[lattr].append(c)
206        else: to_id[lattr] = [ c ]
207    else:
208        raise parse_error("Badly formatted local mapping: %s" % l)
[9cce15a]209
[af25848]210# internal plug-ins have no local attributes.
[87807f42]211def parse_internal(l, creds, me, to_id, p, gp, gu, lr): pass
[6cc5c81]212
213
[3fa4328]214def parse_access(fn, mapper, delegation_link):
[d894c21]215    """
216    Parse the access file, calling out to the mapper to parse specific
217    credential types.  Mappers are above this code.
218    """
219    creds = set()
220    to_id = { }
221    f = open(fn, "r")
222    for i, l in enumerate(f):
223        try:
224            if comment_re.match(l):
225                continue
226            else:
227                m =  line_re.match(l)
228                if m:
229                    p, da = m.group(1, 4)
230                    gp, gu = m.group(2, 3)
231                    if gp == '<any>': gp = None
232                    if gu == '<any>': gu = None
233
234                    creds.add(credential(me, da, 
235                            [attribute(p, x, delegation_link) \
236                                    for x in (gp, gu) \
237                                        if x is not None]))
238                    if m.group(5) and mapper:
239                        mapper(m.group(5), creds, me, to_id, p, gp, gu,
240                                delegation_link)
241                else:
242                    raise parse_error('Syntax error')
243        except parse_error, e:
244            f.close()
245            raise parse_error('Error on line %d of %s: %s' % \
[b931822]246                    (i, fn, e))
[d894c21]247    f.close()
248
249    return creds, to_id
250
251
252
[62f3dd9]253class access_opts(file_expanding_opts):
[af25848]254    '''
255    Parse the options for this program.  Most are straightforward, but the
256    mapper uses a callback to convert from a string to a local mapper function.
257    '''
258    # Valid mappers
[9cce15a]259    mappers = { 
260            'emulab': parse_emulab, 
261            'dragon': parse_dragon,
262            'internal': parse_internal,
263            'skel': parse_skel,
[5a721ed]264            'protogeni': parse_protogeni,
[9cce15a]265            }
[6cc5c81]266
267    @staticmethod
268    def parse_mapper(opt, s, val, parser, dest):
269        if val in access_opts.mappers:
270            setattr(parser.values, dest, access_opts.mappers[val])
271        else:
272            raise OptionValueError('%s must be one of %s' % \
[87807f42]273                    (s, join(access_opts.mappers.keys(), ', ')))
[6cc5c81]274
275    def __init__(self):
[62f3dd9]276        file_expanding_opts.__init__(self, usage='%prog [opts] file [...]')
[6cc5c81]277        self.add_option('--cert', dest='cert', default=None,
[62f3dd9]278                type='str', action='callback', callback=self.expand_file,
[6cc5c81]279                help='my fedid as an X.509 certificate')
280        self.add_option('--key', dest='key', default=None,
[62f3dd9]281                type='str', action='callback', callback=self.expand_file,
[6cc5c81]282                help='key for the certificate')
[5a721ed]283        self.add_option('--dir', dest='dir', default=None,
[62f3dd9]284                type='str', action='callback', callback=self.expand_file,
[5a721ed]285                help='Output directory for credentials')
[6cc5c81]286        self.add_option('--type', action='callback', nargs=1, type='str',
287                callback=access_opts.parse_mapper, 
288                callback_kwargs = { 'dest': 'mapper'}, 
289                help='Type of access file to parse.  One of %s. ' %\
[87807f42]290                        join(access_opts.mappers.keys(), ', ') + \
[6cc5c81]291                        'Omit for generic parsing.')
[5a721ed]292        self.add_option('--quiet', dest='quiet', action='store_true', 
293                default=False,
294                help='Do not print credential to local attribute map')
[3fa4328]295        self.add_option('--no_create_creds', action='store_false', 
296                dest='create_creds', default=True,
297                help='Do not create credentials for rules.')
[87807f42]298        self.add_option('--file', dest='file', default=None,
[62f3dd9]299                type='str', action='callback', callback=self.expand_file,
[87807f42]300                help='Access DB to parse.  If this is present, ' + \
301                        'omit the positional filename')
302        self.add_option('--mapfile', dest='map', default=None,
[62f3dd9]303                type='str', action='callback', callback=self.expand_file,
[87807f42]304                help='File for the attribute to local authorization data')
[bfbaa85]305        self.add_option('--update', action='store_const', const=True,
306                dest='update_authorizer', default=False, 
307                help='Add the generated policy to an existing authorizer')
[87807f42]308        self.add_option('--no-delegate', action='store_false', dest='delegate',
309                default=True,
310                help='do not accept delegated attributes with the ' +\
311                        'acting_for linking role')
[5529264]312        self.add_option('--fed-root', dest='root', 
313                help='add a rule to accept federated users from facilities ' +\
314                        'recognized by ROOT.  This is a certificate file')
315        self.add_option('--fed-tuple', dest='ftuple', 
316                help='a tuple into which to map federated ' + \
317                        'users about which we know nothing else.')
[3fa4328]318        self.add_option('--no_auth', action='store_false', dest='create_auth', 
319                default=True, help='do not create a full ABAC authorizer')
[87807f42]320        self.add_option('--debug', action='store_true', dest='debug', 
321                default=False, help='Just print actions')
[6cc5c81]322        self.set_defaults(mapper=None)
323
[bfbaa85]324def create_creds(creds, cert, key, dir, debug=False):
[af25848]325    '''
[cd360a0]326    Make the the attributes from the list of credential
[af25848]327    objects in the creds parameter.
328    '''
[bfbaa85]329    cfiles = []
[5a721ed]330    for i, c in enumerate(creds):
[cd360a0]331        cid = Creddy.ID(cert)
332        cid.load_privkey(key)
333        cattr = Creddy.Attribute(cid, c.attr, 3600 * 24 * 365 * 10)
[5a721ed]334        for r in c.req:
[cd360a0]335            if r.principal and r.link and r.attr:
336                cattr.linking_role(r.principal, r.attr, r.link)
337            elif r.principal and r.attr:
338                cattr.role(r.principal, r.attr)
339            elif r.principal:
340                cattr.principal(r.principal)
341            else:
342                raise parse_error('Attribute without a principal?')
343        cattr.bake()
[bfbaa85]344        fn = '%s/cred%d_attr.der' % (dir, i)
345        cattr.write_name(fn)
346        cfiles.append(fn)
347    return cfiles
348
[6cc5c81]349
[d894c21]350def clear_dir(dir):
351    for path, dirs, files in os.walk(dir, topdown=False):
352        for f in files: os.unlink(os.path.join(path, f))
353        for d in dirs: os.rmdir(os.path.join(path, d))
354
[af25848]355# Regular expressions and parts thereof for parsing
[6cc5c81]356comment_re = re.compile('^\s*#|^$')
357fedid_str = 'fedid:([0-9a-fA-F]{40})'
[9cce15a]358id_str = '[a-zA-Z][\w_-]*'
[b931822]359proj_str = '[a-zA-Z][\w_:/-]*'
[f77a256]360path_str = '[a-zA-Z0-9_/\.-]+'
[6cc5c81]361id_any_str = '(%s|<any>)' % id_str
[f3898f7]362proj_any_str = '(%s|<any>)' % proj_str
[6cc5c81]363id_same_str = '(%s|<same>)' % id_str
[f3898f7]364proj_same_str = '(%s|<same>)' % proj_str
[6cc5c81]365left_side_str = '\(\s*%s\s*,\s*%s\s*,\s*%s\s*\)' % \
[f3898f7]366        (fedid_str, proj_any_str, id_any_str)
[6cc5c81]367right_side_str = '(%s)(\s*,\s*\(.*\))?' % (id_str)
368line_re = re.compile('%s\s*->\s*%s' % (left_side_str, right_side_str))
369
370p = access_opts()
371opts, args = p.parse_args()
372
[de7cb08]373cert, key = None, None
374delete_certs = False
[d894c21]375delete_creds = False
[de7cb08]376
[87807f42]377if opts.file:
378    args.append(opts.file)
379
[af25848]380# Validate arguments
[6cc5c81]381if len(args) < 1:
382    sys.exit('No filenames given to parse')
383
[de7cb08]384if opts.key:
385    if not os.access(opts.key, os.R_OK):
386        key = opts.key
387    else:
388        sys.exit('Cannot read key (%s)' % opts.key)
[5a721ed]389
390if opts.dir:
[c324ad3]391    if not os.access(opts.dir, os.F_OK):
392        try:
393            os.mkdir(opts.dir, 0700)
394        except EnvironmentError, e:
395            sys.exit("Cannot create %s: %s" % (e.filename, e.strerror))
[5a721ed]396    if not os.path.isdir(opts.dir):
397        sys.exit('%s is not a directory' % opts.dir)
398    elif not os.access(opts.dir, os.W_OK):
399        sys.exit('%s is not writable' % opts.dir)
400
[d894c21]401if opts.create_auth:
402    creds_dir = mkdtemp()
403    delete_creds = True
404    auth_dir = opts.dir
[4909fcf]405    if not os.path.isabs(auth_dir):
406        sys.exit('Authorizer path must be absolute')
[d894c21]407else:
408    creds_dir = opts.dir
409    auth_dir = None
410
[87807f42]411if opts.delegate: delegation_link = 'acting_for'
412else: delegation_link = None
413
[a0e20ac]414if not opts.mapper and (opts.map or opts.debug):
415    print >>sys.stderr, "No --type specified, mapping file will be empty."
[6cc5c81]416
[de7cb08]417if opts.cert: 
[6cc5c81]418    try:
[de7cb08]419        me = fedid(file=opts.cert) 
[6cc5c81]420    except EnvironmentError, e:
[de7cb08]421        sys.exit('Bad --cert: %s (%s)' % (e.strerror, e.filename or '?!'))
[6cc5c81]422
[de7cb08]423    if not opts.key:
424        if abac_pem_type(opts.cert) == 'both':
425            key, cert = abac_split_cert(opts.cert)
426            delete_certs = True
427    else:
428        cert = opts.cert
429else: 
430    print >>sys.stderr, 'No --cert, using dummy fedid'
431    me = fedid(hexstr='0123456789012345678901234567890123456789')
432    cert = None
433
[5529264]434fed_to_id =  { }
435if any((opts.root, opts.ftuple)) and not all ((opts.root, opts.ftuple)):
436    sys.exit('Either both or neither of --fed-root and ' + \
437            '--fed-project must be specified')
438elif opts.root:
439    try:
440        root_fedid = fedid(file=opts.root)
441    except EnvironmentError, e:
442        sys.exit('Bad --root: %s (%s)' % (e.strerror, e.filename or '?!'))
443
444    fed_tuple = tuple(opts.ftuple.split(','))
445    fed_someuser_cred = \
446            credential(me, 'some_feduser', 
447                    [attribute(root_fedid.get_hexstr(), 
448                        'fedfacility', 'feduser')])
449    fed_user_cred = \
450            credential(me, 'default_feduser', 
451                    [attribute(me.get_hexstr(), 
452                        'some_feduser', 'acting_for')])
453    fed_access_cred = \
454            credential(me, 'access', 
455                    [attribute(me.get_hexstr(), 'default_feduser')])
456   
457    fed_to_id[fed_tuple] = [fed_user_cred]
458
459else:
460    # No fed-root or fed-tuple
461    fed_access_cred = None
462    fed_user_cred = None
463    fed_someuser_cred = None
[bfbaa85]464
465credfiles = []
[5529264]466   
[de7cb08]467# The try block makes sure that credentials split into tmp files are deleted
468try:
469    # Do the parsing
470    for fn in args:
[87807f42]471        try:
[3fa4328]472            creds, to_id = parse_access(fn, opts.mapper, delegation_link)
[de7cb08]473        except parse_error, e:
474            print >> sys.stderr, "%s" % e
475            continue
[87807f42]476        except EnvironmentError, e:
[de7cb08]477            print >>sys.stderr, "File error %s: %s" % \
478                    (e.filename or '!?', e.strerror) 
479            continue
480
481        # Credential output
482        if opts.create_creds:
[5529264]483            if fed_access_cred and fed_user_cred and fed_someuser_cred:
484                creds.add(fed_access_cred)
485                creds.add(fed_user_cred)
486                creds.add(fed_someuser_cred)
[de7cb08]487            if all([cert, key, opts.dir]):
488                try:
[bfbaa85]489                    credfiles = create_creds(
490                            [c for c in creds if c.principal == me],
[d894c21]491                            cert, key, creds_dir, opts.debug)
[de7cb08]492                except credential_error, e:
493                    sys.exit('Credential creation failed: %s' % e)
494            else:
[d894c21]495                print >>sys.stderr, 'Cannot create credentials.  ' + \
496                        'Missing parameter'
[de7cb08]497
498        # Local map output
499        if opts.map or opts.debug:
500            try:
501                if opts.map and opts.map != '-' and not opts.debug:
502                    f = open(opts.map, 'w')
503                else:
504                    f = sys.stdout
[5529264]505                for k, c in to_id.items() + fed_to_id.items():
[5d7f1e8]506                    # Keys are either a single string or a tuple of them; join
507                    # the tuples into a comma-separated string.
508                    if isinstance(k, basestring): rhs = k
509                    else: rhs = join(k, ', ')
510
[de7cb08]511                    for a in set(["%s.%s" % (x.principal, x.attr) for x in c]):
[5d7f1e8]512                        print >>f, "%s -> (%s)" % (a, rhs)
[de7cb08]513            except EnvironmentError, e:
[d894c21]514                sys.exit("Cannot open %s: %s" % (e.filename or '!?', 
515                    e.strerror))
516
517        # Create an authorizer if requested.
518        if opts.create_auth:
519            try:
520                # Pass in the options rather than the potentially split key
521                # because abac_authorizer will split it and store it
522                # internally.  The opts.cert may get split twice, but we won't
523                # lose one.
[bfbaa85]524                if opts.update_authorizer:
525                    operation = 'updat'
526                    a = abac_authorizer(load=auth_dir)
527                    a.import_credentials(file_list=credfiles)
528                    a.save()
529                else:
530                    clear_dir(auth_dir)
531                    operation = 'creat'
532                    a = abac_authorizer(key=opts.key, me=opts.cert, 
533                            certs=creds_dir, save=auth_dir)
534                    a.save(auth_dir)
[d894c21]535            except EnvironmentError, e:
[5d7f1e8]536                sys.exit("Can't create or write %s: %s" % \
537                        (e.filename, e.strerror))
[d894c21]538            except abac_authorizer.bad_cert_error, e:
[bfbaa85]539                sys.exit("Error %sing authorizer: %s" % (operation, e))
[d894c21]540
[de7cb08]541finally:
[d894c21]542    try:
543        if delete_certs:
544            if cert: os.unlink(cert)
545            if key: os.unlink(key)
546        if delete_creds and creds_dir:
547            clear_dir(creds_dir)
548            os.rmdir(creds_dir)
549    except EnvironmentError, e:
550        sys.exit("Can't remove %s: %s" % ( e.filename, e.strerror))
Note: See TracBrowser for help on using the repository browser.