source: fedd/access_to_abac.py @ 0dc62df

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

Simple support for experiments in groups. Closes #32

  • Property mode set to 100755
File size: 14.3 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
[6cc5c81]14from federation.fedid 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    '''
[87807f42]23    bad_attr = re.compile('[^a-zA-Z0-9_]+')
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    '''
[87807f42]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
71#  All the local parsing functions get the unparsed remainder of the line
72#  (after the three-name and the attribute it maps to), the credential list to
73#  add the new ABAC credential(s) that will be mapped into the loacl
74#  credentials, the fedid of this entity, a dict mapping the local credentials
75#  to ABAC credentials that are required to exercise those local rights and the
76#  three-name (p, gp, gu) that is being mapped.
[87807f42]77def parse_emulab(l, creds, me, to_id, p, gp, gu, lr):
[af25848]78    '''
79    Parse the emulab (project, allocation_user, access_user) format.  Access
80    users are deprecates and allocation users used for both.  This fuction
81    collapses them.
82    '''
[6cc5c81]83    right_side_str = '\s*,\s*\(\s*%s\s*,\s*%s\s*,\s*%s\s*\)' % \
[f3898f7]84            (proj_same_str, id_same_str,id_same_str)
[6cc5c81]85
86    m = re.match(right_side_str, l)
87    if m:
88        project, user = m.group(1,2)
[af25848]89        # Resolve "<same>"s in project and user
[6cc5c81]90        if project == '<same>':
91            if gp  is not None:
92                project = gp
93            else:
94                raise parse_error("Project cannot be decisively mapped: %s" % l)
95        if user == '<same>':
96            if gu is not None:
[c324ad3]97                user = gu
[6cc5c81]98            else:
99                raise parse_error("User cannot be decisively mapped: %s" % l)
[af25848]100
101        # Create a semi-mnemonic name for the destination credential (the one
102        # that will be mapped to the local attributes
[c324ad3]103        if gp and gu:
104            a = 'project_%s_user_%s' % (gp, gu)
105        elif gp:
106            a = 'project_%s' % gp
107        elif gu:
108            a = 'user_%s' % gu
[6cc5c81]109        else:
110            raise parse_error("No mapping for %s/%s!?" % (gp, gu))
111
[af25848]112        # Store the creds and map entries
[6cc5c81]113        c = credential(me, a, 
[87807f42]114                [attribute(p, x, lr) for x in (gp, gu) if x is not None])
[6cc5c81]115        creds.add(c)
[5a721ed]116        if (project, user) in to_id: to_id[(project,user)].append(c)
117        else: to_id[(project,user)] = [ c ]
[6cc5c81]118    else:
119        raise parse_error("Badly formatted local mapping: %s" % l)
120
121
[87807f42]122def parse_protogeni(l, creds, me, to_id, p, gp, gu, lr):
[af25848]123    '''
124    Parse the protoGENI (cert, user, user_key, cert_pw) format.
125    '''
[5a721ed]126    right_side_str = '\s*,\s*\(\s*(%s)\s*,\s*(%s)\s*,\s*(%s)\s*,\s*(%s)\s*\)' \
127            % (path_str, id_str, path_str, id_str)
128
129    m = re.match(right_side_str, l)
130    if m:
131        cert, user, key, pw = m.group(1,2,3,4)
[af25848]132        # The credential is formed from just the path (with / mapped to _) and
133        # the username.
[5a721ed]134        acert = re.sub('/', '_', cert)
135
136        a = "cert_%s_user_%s" % (acert, user)
[af25848]137
138        # Store em
[5a721ed]139        c = credential(me, a, 
[87807f42]140                [attribute(p, x, lr) for x in (gp, gu) if x is not None])
[5a721ed]141        creds.add(c)
142        if (cert, user, key, pw) in to_id: 
143            to_id[(cert, user, key, pw)].append(c)
144        else: 
145            to_id[(cert, user, key, pw)] = [ c ]
146    else:
147        raise parse_error("Badly formatted local mapping: %s" % l)
148
[87807f42]149def parse_dragon(l, creds, me, to_id, p, gp, gu, lr):
[af25848]150    '''
151    Parse the dragon (repository_name) version.
152    '''
[6cc5c81]153    right_side_str = '\s*,\s*\(\s*(%s)\s*\)' % \
154            (id_str)
155
156    m = re.match(right_side_str, l)
157    if m:
158        repo= m.group(1)
159        c = credential(me, 'repo_%s' % repo, 
[87807f42]160                [attribute(p, x, lr) for x in (gp, gu) if x is not None])
[6cc5c81]161        creds.add(c)
162        if repo in to_id: to_id[repo].append(c)
163        else: to_id[repo] = [ c ]
164    else:
165        raise parse_error("Badly formatted local mapping: %s" % l)
[9cce15a]166
[87807f42]167def parse_skel(l, creds, me, to_id, p, gp, gu, lr):
[af25848]168    '''
169    Parse the skeleton (local_attr) version.
170    '''
171    right_side_str = '\s*,\s*\(\s*(%s)\s*\)' % \
172            (id_str)
173
174    m = re.match(right_side_str, l)
175    if m:
176        lattr = m.group(1)
177        c = credential(me, 'lattr_%s' % lattr, 
[87807f42]178                [attribute(p, x, lr) for x in (gp, gu) if x is not None])
[af25848]179        creds.add(c)
180        if lattr in to_id: to_id[lattr].append(c)
181        else: to_id[lattr] = [ c ]
182    else:
183        raise parse_error("Badly formatted local mapping: %s" % l)
[9cce15a]184
[af25848]185# internal plug-ins have no local attributes.
[87807f42]186def parse_internal(l, creds, me, to_id, p, gp, gu, lr): pass
[6cc5c81]187
188
[3fa4328]189def parse_access(fn, mapper, delegation_link):
[d894c21]190    """
191    Parse the access file, calling out to the mapper to parse specific
192    credential types.  Mappers are above this code.
193    """
194    creds = set()
195    to_id = { }
196    f = open(fn, "r")
197    for i, l in enumerate(f):
198        try:
199            if comment_re.match(l):
200                continue
201            else:
202                m =  line_re.match(l)
203                if m:
204                    p, da = m.group(1, 4)
205                    gp, gu = m.group(2, 3)
206                    if gp == '<any>': gp = None
207                    if gu == '<any>': gu = None
208
209                    creds.add(credential(me, da, 
210                            [attribute(p, x, delegation_link) \
211                                    for x in (gp, gu) \
212                                        if x is not None]))
213                    if m.group(5) and mapper:
214                        mapper(m.group(5), creds, me, to_id, p, gp, gu,
215                                delegation_link)
216                else:
217                    raise parse_error('Syntax error')
218        except parse_error, e:
219            f.close()
220            raise parse_error('Error on line %d of %s: %s' % \
221                    (i, fn, e.message))
222    f.close()
223
224    return creds, to_id
225
226
227
[62f3dd9]228class access_opts(file_expanding_opts):
[af25848]229    '''
230    Parse the options for this program.  Most are straightforward, but the
231    mapper uses a callback to convert from a string to a local mapper function.
232    '''
233    # Valid mappers
[9cce15a]234    mappers = { 
235            'emulab': parse_emulab, 
236            'dragon': parse_dragon,
237            'internal': parse_internal,
238            'skel': parse_skel,
[5a721ed]239            'protogeni': parse_protogeni,
[9cce15a]240            }
[6cc5c81]241
242    @staticmethod
243    def parse_mapper(opt, s, val, parser, dest):
244        if val in access_opts.mappers:
245            setattr(parser.values, dest, access_opts.mappers[val])
246        else:
247            raise OptionValueError('%s must be one of %s' % \
[87807f42]248                    (s, join(access_opts.mappers.keys(), ', ')))
[6cc5c81]249
250    def __init__(self):
[62f3dd9]251        file_expanding_opts.__init__(self, usage='%prog [opts] file [...]')
[6cc5c81]252        self.add_option('--cert', dest='cert', default=None,
[62f3dd9]253                type='str', action='callback', callback=self.expand_file,
[6cc5c81]254                help='my fedid as an X.509 certificate')
255        self.add_option('--key', dest='key', default=None,
[62f3dd9]256                type='str', action='callback', callback=self.expand_file,
[6cc5c81]257                help='key for the certificate')
[5a721ed]258        self.add_option('--dir', dest='dir', default=None,
[62f3dd9]259                type='str', action='callback', callback=self.expand_file,
[5a721ed]260                help='Output directory for credentials')
[6cc5c81]261        self.add_option('--type', action='callback', nargs=1, type='str',
262                callback=access_opts.parse_mapper, 
263                callback_kwargs = { 'dest': 'mapper'}, 
264                help='Type of access file to parse.  One of %s. ' %\
[87807f42]265                        join(access_opts.mappers.keys(), ', ') + \
[6cc5c81]266                        'Omit for generic parsing.')
[5a721ed]267        self.add_option('--quiet', dest='quiet', action='store_true', 
268                default=False,
269                help='Do not print credential to local attribute map')
[3fa4328]270        self.add_option('--no_create_creds', action='store_false', 
271                dest='create_creds', default=True,
272                help='Do not create credentials for rules.')
[87807f42]273        self.add_option('--file', dest='file', default=None,
[62f3dd9]274                type='str', action='callback', callback=self.expand_file,
[87807f42]275                help='Access DB to parse.  If this is present, ' + \
276                        'omit the positional filename')
277        self.add_option('--mapfile', dest='map', default=None,
[62f3dd9]278                type='str', action='callback', callback=self.expand_file,
[87807f42]279                help='File for the attribute to local authorization data')
280        self.add_option('--no-delegate', action='store_false', dest='delegate',
281                default=True,
282                help='do not accept delegated attributes with the ' +\
283                        'acting_for linking role')
[3fa4328]284        self.add_option('--no_auth', action='store_false', dest='create_auth', 
285                default=True, help='do not create a full ABAC authorizer')
[87807f42]286        self.add_option('--debug', action='store_true', dest='debug', 
287                default=False, help='Just print actions')
[6cc5c81]288        self.set_defaults(mapper=None)
289
[87807f42]290def create_creds(creds, cert, key, dir, debug=False, 
291        creddy='/usr/local/bin/creddy'):
[af25848]292    '''
[cd360a0]293    Make the the attributes from the list of credential
[af25848]294    objects in the creds parameter.
295    '''
[5a721ed]296    for i, c in enumerate(creds):
[cd360a0]297        cid = Creddy.ID(cert)
298        cid.load_privkey(key)
299        cattr = Creddy.Attribute(cid, c.attr, 3600 * 24 * 365 * 10)
[5a721ed]300        for r in c.req:
[cd360a0]301            if r.principal and r.link and r.attr:
302                cattr.linking_role(r.principal, r.attr, r.link)
303            elif r.principal and r.attr:
304                cattr.role(r.principal, r.attr)
305            elif r.principal:
306                cattr.principal(r.principal)
307            else:
308                raise parse_error('Attribute without a principal?')
309        cattr.bake()
310        cattr.write_name('%s/cred%d_attr.der' % (dir, i))
[6cc5c81]311
[d894c21]312def clear_dir(dir):
313    for path, dirs, files in os.walk(dir, topdown=False):
314        for f in files: os.unlink(os.path.join(path, f))
315        for d in dirs: os.rmdir(os.path.join(path, d))
316
[af25848]317# Regular expressions and parts thereof for parsing
[6cc5c81]318comment_re = re.compile('^\s*#|^$')
319fedid_str = 'fedid:([0-9a-fA-F]{40})'
[9cce15a]320id_str = '[a-zA-Z][\w_-]*'
[f3898f7]321proj_str = '[a-zA-Z][\w_/-]*'
[5a721ed]322path_str = '[a-zA-Z_/\.-]+'
[6cc5c81]323id_any_str = '(%s|<any>)' % id_str
[f3898f7]324proj_any_str = '(%s|<any>)' % proj_str
[6cc5c81]325id_same_str = '(%s|<same>)' % id_str
[f3898f7]326proj_same_str = '(%s|<same>)' % proj_str
[6cc5c81]327left_side_str = '\(\s*%s\s*,\s*%s\s*,\s*%s\s*\)' % \
[f3898f7]328        (fedid_str, proj_any_str, id_any_str)
[6cc5c81]329right_side_str = '(%s)(\s*,\s*\(.*\))?' % (id_str)
330line_re = re.compile('%s\s*->\s*%s' % (left_side_str, right_side_str))
331
332p = access_opts()
333opts, args = p.parse_args()
334
[de7cb08]335cert, key = None, None
336delete_certs = False
[d894c21]337delete_creds = False
[de7cb08]338
[87807f42]339if opts.file:
340    args.append(opts.file)
341
[af25848]342# Validate arguments
[6cc5c81]343if len(args) < 1:
344    sys.exit('No filenames given to parse')
345
[de7cb08]346if opts.key:
347    if not os.access(opts.key, os.R_OK):
348        key = opts.key
349    else:
350        sys.exit('Cannot read key (%s)' % opts.key)
[5a721ed]351
352if opts.dir:
[c324ad3]353    if not os.access(opts.dir, os.F_OK):
354        try:
355            os.mkdir(opts.dir, 0700)
356        except EnvironmentError, e:
357            sys.exit("Cannot create %s: %s" % (e.filename, e.strerror))
[5a721ed]358    if not os.path.isdir(opts.dir):
359        sys.exit('%s is not a directory' % opts.dir)
360    elif not os.access(opts.dir, os.W_OK):
361        sys.exit('%s is not writable' % opts.dir)
362
[d894c21]363if opts.create_auth:
364    creds_dir = mkdtemp()
365    delete_creds = True
366    auth_dir = opts.dir
[4909fcf]367    if not os.path.isabs(auth_dir):
368        sys.exit('Authorizer path must be absolute')
[d894c21]369else:
370    creds_dir = opts.dir
371    auth_dir = None
372
[87807f42]373if opts.delegate: delegation_link = 'acting_for'
374else: delegation_link = None
375
[a0e20ac]376if not opts.mapper and (opts.map or opts.debug):
377    print >>sys.stderr, "No --type specified, mapping file will be empty."
[6cc5c81]378
[de7cb08]379if opts.cert: 
[6cc5c81]380    try:
[de7cb08]381        me = fedid(file=opts.cert) 
[6cc5c81]382    except EnvironmentError, e:
[de7cb08]383        sys.exit('Bad --cert: %s (%s)' % (e.strerror, e.filename or '?!'))
[6cc5c81]384
[de7cb08]385    if not opts.key:
386        if abac_pem_type(opts.cert) == 'both':
387            key, cert = abac_split_cert(opts.cert)
388            delete_certs = True
389    else:
390        cert = opts.cert
391else: 
392    print >>sys.stderr, 'No --cert, using dummy fedid'
393    me = fedid(hexstr='0123456789012345678901234567890123456789')
394    cert = None
395
396# The try block makes sure that credentials split into tmp files are deleted
397try:
398    # Do the parsing
399    for fn in args:
[87807f42]400        try:
[3fa4328]401            creds, to_id = parse_access(fn, opts.mapper, delegation_link)
[de7cb08]402        except parse_error, e:
403            print >> sys.stderr, "%s" % e
404            continue
405
[87807f42]406        except EnvironmentError, e:
[de7cb08]407            print >>sys.stderr, "File error %s: %s" % \
408                    (e.filename or '!?', e.strerror) 
409            continue
410
411        # Credential output
412        if opts.create_creds:
413            if all([cert, key, opts.dir]):
414                try:
415                    create_creds([c for c in creds if c.principal == me],
[d894c21]416                            cert, key, creds_dir, opts.debug)
[de7cb08]417                except credential_error, e:
418                    sys.exit('Credential creation failed: %s' % e)
419            else:
[d894c21]420                print >>sys.stderr, 'Cannot create credentials.  ' + \
421                        'Missing parameter'
[de7cb08]422
423        # Local map output
424        if opts.map or opts.debug:
425            try:
426                if opts.map and opts.map != '-' and not opts.debug:
427                    f = open(opts.map, 'w')
428                else:
429                    f = sys.stdout
430                for k, c in to_id.items():
[5d7f1e8]431                    # Keys are either a single string or a tuple of them; join
432                    # the tuples into a comma-separated string.
433                    if isinstance(k, basestring): rhs = k
434                    else: rhs = join(k, ', ')
435
[de7cb08]436                    for a in set(["%s.%s" % (x.principal, x.attr) for x in c]):
[5d7f1e8]437                        print >>f, "%s -> (%s)" % (a, rhs)
[de7cb08]438            except EnvironmentError, e:
[d894c21]439                sys.exit("Cannot open %s: %s" % (e.filename or '!?', 
440                    e.strerror))
441
442        # Create an authorizer if requested.
443        if opts.create_auth:
444            clear_dir(auth_dir)
445            try:
446                # Pass in the options rather than the potentially split key
447                # because abac_authorizer will split it and store it
448                # internally.  The opts.cert may get split twice, but we won't
449                # lose one.
450                a = abac_authorizer(key=opts.key, me=opts.cert, 
451                        certs=creds_dir, save=auth_dir)
452                a.save(auth_dir)
453            except EnvironmentError, e:
[5d7f1e8]454                sys.exit("Can't create or write %s: %s" % \
455                        (e.filename, e.strerror))
[d894c21]456            except abac_authorizer.bad_cert_error, e:
457                sys.exit("Error creating authorizer: %s" % e)
458
[de7cb08]459finally:
[d894c21]460    try:
461        if delete_certs:
462            if cert: os.unlink(cert)
463            if key: os.unlink(key)
464        if delete_creds and creds_dir:
465            clear_dir(creds_dir)
466            os.rmdir(creds_dir)
467    except EnvironmentError, e:
468        sys.exit("Can't remove %s: %s" % ( e.filename, e.strerror))
Note: See TracBrowser for help on using the repository browser.