source: fedd/access_to_abac.py @ 0a49bd7

axis_examplecompt_changesinfo-ops
Last change on this file since 0a49bd7 was 3fa4328, checked in by Ted Faber <faber@…>, 14 years ago

More sane defaults.

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