source: fedd/access_to_abac.py @ a80a4a7

Last change on this file since a80a4a7 was 67fa1cf, checked in by Ted Faber <faber@…>, 11 years ago

MOve over to ABAC 0.1.4

  • Property mode set to 100755
File size: 16.8 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
[67fa1cf]12import ABAC
[cd360a0]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
[794325b]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 local
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    '''
[f77a256]79    Parse the emulab (project, allocation_user, cert_file) format.
[af25848]80    '''
[f77a256]81    right_side_str = '\s*,\s*\(\s*%s\s*,\s*%s\s*,\s*(%s)\s*\)' % \
82            (proj_same_str, id_same_str,path_str)
[6cc5c81]83
84    m = re.match(right_side_str, l)
85    if m:
[f77a256]86        project, user, cert = m.group(1,2,3)
[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)
[f77a256]114        if (project, user,cert) in to_id: to_id[(project,user,cert)].append(c)
115        else: to_id[(project,user,cert)] = [ 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)
[794325b]182def parse_starbed(l,creds,me, to_id,p,gp, gu, lr):
183        '''
184        Parse the starbed credientials
185        '''
186        right_side_str = '\s*,\s*\(\s*(%s)\s*,\s*(%s)\s*,\s*(%s)\s*\)' % \
187                (id_str,id_str,id_str)
188        m = re.match(right_side_str,l)
189        if m:
190                user,passwd,project = m.group(1,2,3)
191                if gp and gu:
192                        a = 'project_%s_user_%s' % (gp, gu)
193                elif gp:
194                        a = 'project_%s' % gp
195                elif gu:
196                         a = 'user_%s' % gu
197                else:
198                        raise parse_error("No mapping for %s/%s!?" % (gp, gu))
199                c = credential(me, a, 
200                        [attribute(p, x, lr) for x in (gp, gu) if x is not None])
201                creds.add(c)
202                if (user,passwd,project) in to_id: to_id[(user,passwd,project)].append(c)
203                else: to_id[(user,passwd,project)] = [ c ]
204        else:
205                raise parse_error("Badly formatted local mapping: %s" % l)     
[af25848]206# internal plug-ins have no local attributes.
[87807f42]207def parse_internal(l, creds, me, to_id, p, gp, gu, lr): pass
[6cc5c81]208
209
[3fa4328]210def parse_access(fn, mapper, delegation_link):
[d894c21]211    """
212    Parse the access file, calling out to the mapper to parse specific
213    credential types.  Mappers are above this code.
214    """
215    creds = set()
216    to_id = { }
217    f = open(fn, "r")
218    for i, l in enumerate(f):
219        try:
220            if comment_re.match(l):
221                continue
222            else:
223                m =  line_re.match(l)
224                if m:
225                    p, da = m.group(1, 4)
226                    gp, gu = m.group(2, 3)
227                    if gp == '<any>': gp = None
228                    if gu == '<any>': gu = None
229
230                    creds.add(credential(me, da, 
231                            [attribute(p, x, delegation_link) \
232                                    for x in (gp, gu) \
233                                        if x is not None]))
234                    if m.group(5) and mapper:
235                        mapper(m.group(5), creds, me, to_id, p, gp, gu,
236                                delegation_link)
237                else:
238                    raise parse_error('Syntax error')
239        except parse_error, e:
240            f.close()
241            raise parse_error('Error on line %d of %s: %s' % \
[b931822]242                    (i, fn, e))
[d894c21]243    f.close()
244
245    return creds, to_id
246
247
248
[62f3dd9]249class access_opts(file_expanding_opts):
[af25848]250    '''
251    Parse the options for this program.  Most are straightforward, but the
252    mapper uses a callback to convert from a string to a local mapper function.
253    '''
254    # Valid mappers
[9cce15a]255    mappers = { 
256            'emulab': parse_emulab, 
257            'dragon': parse_dragon,
258            'internal': parse_internal,
259            'skel': parse_skel,
[5a721ed]260            'protogeni': parse_protogeni,
[794325b]261            'starbed' : parse_starbed,
[9cce15a]262            }
[6cc5c81]263
264    @staticmethod
265    def parse_mapper(opt, s, val, parser, dest):
266        if val in access_opts.mappers:
267            setattr(parser.values, dest, access_opts.mappers[val])
268        else:
269            raise OptionValueError('%s must be one of %s' % \
[87807f42]270                    (s, join(access_opts.mappers.keys(), ', ')))
[6cc5c81]271
272    def __init__(self):
[62f3dd9]273        file_expanding_opts.__init__(self, usage='%prog [opts] file [...]')
[6cc5c81]274        self.add_option('--cert', dest='cert', default=None,
[62f3dd9]275                type='str', action='callback', callback=self.expand_file,
[6cc5c81]276                help='my fedid as an X.509 certificate')
277        self.add_option('--key', dest='key', default=None,
[62f3dd9]278                type='str', action='callback', callback=self.expand_file,
[6cc5c81]279                help='key for the certificate')
[5a721ed]280        self.add_option('--dir', dest='dir', default=None,
[62f3dd9]281                type='str', action='callback', callback=self.expand_file,
[5a721ed]282                help='Output directory for credentials')
[6cc5c81]283        self.add_option('--type', action='callback', nargs=1, type='str',
284                callback=access_opts.parse_mapper, 
285                callback_kwargs = { 'dest': 'mapper'}, 
286                help='Type of access file to parse.  One of %s. ' %\
[87807f42]287                        join(access_opts.mappers.keys(), ', ') + \
[6cc5c81]288                        'Omit for generic parsing.')
[5a721ed]289        self.add_option('--quiet', dest='quiet', action='store_true', 
290                default=False,
291                help='Do not print credential to local attribute map')
[3fa4328]292        self.add_option('--no_create_creds', action='store_false', 
293                dest='create_creds', default=True,
294                help='Do not create credentials for rules.')
[87807f42]295        self.add_option('--file', dest='file', default=None,
[62f3dd9]296                type='str', action='callback', callback=self.expand_file,
[87807f42]297                help='Access DB to parse.  If this is present, ' + \
298                        'omit the positional filename')
299        self.add_option('--mapfile', dest='map', default=None,
[62f3dd9]300                type='str', action='callback', callback=self.expand_file,
[87807f42]301                help='File for the attribute to local authorization data')
[bfbaa85]302        self.add_option('--update', action='store_const', const=True,
303                dest='update_authorizer', default=False, 
304                help='Add the generated policy to an existing authorizer')
[87807f42]305        self.add_option('--no-delegate', action='store_false', dest='delegate',
306                default=True,
307                help='do not accept delegated attributes with the ' +\
308                        'acting_for linking role')
[5529264]309        self.add_option('--fed-root', dest='root', 
310                help='add a rule to accept federated users from facilities ' +\
311                        'recognized by ROOT.  This is a certificate file')
312        self.add_option('--fed-tuple', dest='ftuple', 
313                help='a tuple into which to map federated ' + \
314                        'users about which we know nothing else.')
[3fa4328]315        self.add_option('--no_auth', action='store_false', dest='create_auth', 
316                default=True, help='do not create a full ABAC authorizer')
[87807f42]317        self.add_option('--debug', action='store_true', dest='debug', 
318                default=False, help='Just print actions')
[6cc5c81]319        self.set_defaults(mapper=None)
320
[bfbaa85]321def create_creds(creds, cert, key, dir, debug=False):
[af25848]322    '''
[cd360a0]323    Make the the attributes from the list of credential
[af25848]324    objects in the creds parameter.
325    '''
[bfbaa85]326    cfiles = []
[5a721ed]327    for i, c in enumerate(creds):
[67fa1cf]328        cid = ABAC.ID(cert)
[cd360a0]329        cid.load_privkey(key)
[67fa1cf]330        cattr = ABAC.Attribute(cid, c.attr, 3600 * 24 * 365 * 10)
[5a721ed]331        for r in c.req:
[cd360a0]332            if r.principal and r.link and r.attr:
333                cattr.linking_role(r.principal, r.attr, r.link)
334            elif r.principal and r.attr:
335                cattr.role(r.principal, r.attr)
336            elif r.principal:
337                cattr.principal(r.principal)
338            else:
339                raise parse_error('Attribute without a principal?')
340        cattr.bake()
[bfbaa85]341        fn = '%s/cred%d_attr.der' % (dir, i)
[67fa1cf]342        cattr.write_file(fn)
[bfbaa85]343        cfiles.append(fn)
344    return cfiles
345
[6cc5c81]346
[d894c21]347def clear_dir(dir):
348    for path, dirs, files in os.walk(dir, topdown=False):
349        for f in files: os.unlink(os.path.join(path, f))
350        for d in dirs: os.rmdir(os.path.join(path, d))
351
[af25848]352# Regular expressions and parts thereof for parsing
[6cc5c81]353comment_re = re.compile('^\s*#|^$')
354fedid_str = 'fedid:([0-9a-fA-F]{40})'
[9cce15a]355id_str = '[a-zA-Z][\w_-]*'
[b931822]356proj_str = '[a-zA-Z][\w_:/-]*'
[f77a256]357path_str = '[a-zA-Z0-9_/\.-]+'
[6cc5c81]358id_any_str = '(%s|<any>)' % id_str
[f3898f7]359proj_any_str = '(%s|<any>)' % proj_str
[6cc5c81]360id_same_str = '(%s|<same>)' % id_str
[f3898f7]361proj_same_str = '(%s|<same>)' % proj_str
[6cc5c81]362left_side_str = '\(\s*%s\s*,\s*%s\s*,\s*%s\s*\)' % \
[f3898f7]363        (fedid_str, proj_any_str, id_any_str)
[6cc5c81]364right_side_str = '(%s)(\s*,\s*\(.*\))?' % (id_str)
365line_re = re.compile('%s\s*->\s*%s' % (left_side_str, right_side_str))
366
367p = access_opts()
368opts, args = p.parse_args()
369
[de7cb08]370cert, key = None, None
371delete_certs = False
[d894c21]372delete_creds = False
[de7cb08]373
[87807f42]374if opts.file:
375    args.append(opts.file)
376
[af25848]377# Validate arguments
[6cc5c81]378if len(args) < 1:
379    sys.exit('No filenames given to parse')
380
[de7cb08]381if opts.key:
382    if not os.access(opts.key, os.R_OK):
383        key = opts.key
384    else:
385        sys.exit('Cannot read key (%s)' % opts.key)
[5a721ed]386
387if opts.dir:
[c324ad3]388    if not os.access(opts.dir, os.F_OK):
389        try:
390            os.mkdir(opts.dir, 0700)
391        except EnvironmentError, e:
392            sys.exit("Cannot create %s: %s" % (e.filename, e.strerror))
[5a721ed]393    if not os.path.isdir(opts.dir):
394        sys.exit('%s is not a directory' % opts.dir)
395    elif not os.access(opts.dir, os.W_OK):
396        sys.exit('%s is not writable' % opts.dir)
397
[d894c21]398if opts.create_auth:
399    creds_dir = mkdtemp()
400    delete_creds = True
401    auth_dir = opts.dir
[4909fcf]402    if not os.path.isabs(auth_dir):
403        sys.exit('Authorizer path must be absolute')
[d894c21]404else:
405    creds_dir = opts.dir
406    auth_dir = None
407
[87807f42]408if opts.delegate: delegation_link = 'acting_for'
409else: delegation_link = None
410
[a0e20ac]411if not opts.mapper and (opts.map or opts.debug):
412    print >>sys.stderr, "No --type specified, mapping file will be empty."
[6cc5c81]413
[de7cb08]414if opts.cert: 
[6cc5c81]415    try:
[de7cb08]416        me = fedid(file=opts.cert) 
[6cc5c81]417    except EnvironmentError, e:
[de7cb08]418        sys.exit('Bad --cert: %s (%s)' % (e.strerror, e.filename or '?!'))
[6cc5c81]419
[de7cb08]420    if not opts.key:
421        if abac_pem_type(opts.cert) == 'both':
422            key, cert = abac_split_cert(opts.cert)
423            delete_certs = True
424    else:
425        cert = opts.cert
426else: 
427    print >>sys.stderr, 'No --cert, using dummy fedid'
428    me = fedid(hexstr='0123456789012345678901234567890123456789')
429    cert = None
430
[5529264]431fed_to_id =  { }
432if any((opts.root, opts.ftuple)) and not all ((opts.root, opts.ftuple)):
433    sys.exit('Either both or neither of --fed-root and ' + \
434            '--fed-project must be specified')
435elif opts.root:
436    try:
437        root_fedid = fedid(file=opts.root)
438    except EnvironmentError, e:
439        sys.exit('Bad --root: %s (%s)' % (e.strerror, e.filename or '?!'))
440
441    fed_tuple = tuple(opts.ftuple.split(','))
442    fed_someuser_cred = \
443            credential(me, 'some_feduser', 
444                    [attribute(root_fedid.get_hexstr(), 
445                        'fedfacility', 'feduser')])
446    fed_user_cred = \
447            credential(me, 'default_feduser', 
448                    [attribute(me.get_hexstr(), 
449                        'some_feduser', 'acting_for')])
450    fed_access_cred = \
451            credential(me, 'access', 
452                    [attribute(me.get_hexstr(), 'default_feduser')])
453   
454    fed_to_id[fed_tuple] = [fed_user_cred]
455
456else:
457    # No fed-root or fed-tuple
458    fed_access_cred = None
459    fed_user_cred = None
460    fed_someuser_cred = None
[bfbaa85]461
462credfiles = []
[5529264]463   
[de7cb08]464# The try block makes sure that credentials split into tmp files are deleted
465try:
466    # Do the parsing
467    for fn in args:
[87807f42]468        try:
[3fa4328]469            creds, to_id = parse_access(fn, opts.mapper, delegation_link)
[de7cb08]470        except parse_error, e:
471            print >> sys.stderr, "%s" % e
472            continue
[87807f42]473        except EnvironmentError, e:
[de7cb08]474            print >>sys.stderr, "File error %s: %s" % \
475                    (e.filename or '!?', e.strerror) 
476            continue
477
478        # Credential output
479        if opts.create_creds:
[5529264]480            if fed_access_cred and fed_user_cred and fed_someuser_cred:
481                creds.add(fed_access_cred)
482                creds.add(fed_user_cred)
483                creds.add(fed_someuser_cred)
[de7cb08]484            if all([cert, key, opts.dir]):
485                try:
[bfbaa85]486                    credfiles = create_creds(
487                            [c for c in creds if c.principal == me],
[d894c21]488                            cert, key, creds_dir, opts.debug)
[de7cb08]489                except credential_error, e:
490                    sys.exit('Credential creation failed: %s' % e)
491            else:
[d894c21]492                print >>sys.stderr, 'Cannot create credentials.  ' + \
493                        'Missing parameter'
[de7cb08]494
495        # Local map output
496        if opts.map or opts.debug:
497            try:
498                if opts.map and opts.map != '-' and not opts.debug:
499                    f = open(opts.map, 'w')
500                else:
501                    f = sys.stdout
[5529264]502                for k, c in to_id.items() + fed_to_id.items():
[5d7f1e8]503                    # Keys are either a single string or a tuple of them; join
504                    # the tuples into a comma-separated string.
505                    if isinstance(k, basestring): rhs = k
506                    else: rhs = join(k, ', ')
507
[de7cb08]508                    for a in set(["%s.%s" % (x.principal, x.attr) for x in c]):
[5d7f1e8]509                        print >>f, "%s -> (%s)" % (a, rhs)
[de7cb08]510            except EnvironmentError, e:
[d894c21]511                sys.exit("Cannot open %s: %s" % (e.filename or '!?', 
512                    e.strerror))
513
514        # Create an authorizer if requested.
515        if opts.create_auth:
516            try:
517                # Pass in the options rather than the potentially split key
518                # because abac_authorizer will split it and store it
519                # internally.  The opts.cert may get split twice, but we won't
520                # lose one.
[bfbaa85]521                if opts.update_authorizer:
522                    operation = 'updat'
523                    a = abac_authorizer(load=auth_dir)
524                    a.import_credentials(file_list=credfiles)
525                    a.save()
526                else:
527                    clear_dir(auth_dir)
528                    operation = 'creat'
529                    a = abac_authorizer(key=opts.key, me=opts.cert, 
530                            certs=creds_dir, save=auth_dir)
531                    a.save(auth_dir)
[d894c21]532            except EnvironmentError, e:
[5d7f1e8]533                sys.exit("Can't create or write %s: %s" % \
534                        (e.filename, e.strerror))
[d894c21]535            except abac_authorizer.bad_cert_error, e:
[bfbaa85]536                sys.exit("Error %sing authorizer: %s" % (operation, e))
[d894c21]537
[de7cb08]538finally:
[d894c21]539    try:
540        if delete_certs:
541            if cert: os.unlink(cert)
542            if key: os.unlink(key)
543        if delete_creds and creds_dir:
544            clear_dir(creds_dir)
545            os.rmdir(creds_dir)
546    except EnvironmentError, e:
547        sys.exit("Can't remove %s: %s" % ( e.filename, e.strerror))
Note: See TracBrowser for help on using the repository browser.