#!/usr/bin/env python import sys, os import re import subprocess import os.path from string import join from optparse import OptionParser, OptionValueError from tempfile import mkdtemp import Creddy from deter import fedid from federation.authorizer import abac_authorizer from federation.util import abac_split_cert, abac_pem_type, file_expanding_opts class attribute: ''' Encapculate a principal/attribute/link tuple. ''' bad_attr = re.compile('[^a-zA-Z0-9:_]+') def __init__(self, p, a, l=None): self.principal = p self.attr = attribute.bad_attr.sub('_', a) if l: self.link = attribute.bad_attr.sub('_', l) else: self.link = None def __str__(self): if self.link: return "%s.%s.%s" % (self.principal, self.attr, self.link) elif self.attr: return "%s.%s" % (self.principal, self.attr) else: return "%s" % self.principal class credential: ''' A Credential, that is the requisites (as attributes) and the assigned attribute (as principal, attr). If req is iterable, the requirements are an intersection/conjunction. ''' bad_attr = re.compile('[^a-zA-Z0-9:_]+') def __init__(self, p, a, req): self.principal = p if isinstance(a, (tuple, list, set)) and len(a) == 1: self.attr = credential.bad_attr.sub('_', a[0]) else: self.attr = credential.bad_attr.sub('_', a) self.req = req def __str__(self): if isinstance(self.req, (tuple, list, set)): return "%s.%s <- %s" % (self.principal, self.attr, join(["%s" % r for r in self.req], ' & ')) else: return "%s.%s <- %s" % (self.principal, self.attr, self.req) # Mappinng generation function and the access parser throw these when there is # a parsing problem. class parse_error(RuntimeError): pass # Error creating a credential class credential_error(RuntimeError): pass # Functions to parse the individual access maps as well as an overall function # to parse generic ones. The specific ones create a credential to local # attributes mapping and the global one creates the access policy credentials. # All the local parsing functions get # * the unparsed remainder of the line in l. This is everything on the # input line from the first comma after the -> to the end of the line. # * the current list of credentials to be issued. This is a list of # credential objects assigned from this principal (me) to principals # making requests. They are derived from the three-name and delegation # credentials. This routine adds any credentials that will be mapped to # local access control information to creds. # * me is this princpal - the access controller # * to_id is a dict that maps the local name access control information into # a list of credentials that imply the principal should be mapped to them. # For example, parse_emulab is assigning (project, user, cert, key) to # principals. The key of to_id is that tuple and it maps to a list of # ABAC attributes. If an access controller wants to see if a caller can # use a 4-tuple of credentials, it tries to prove that the caller has one # of those attributes. # * p is the principal that assigned the project and group. It is the name # of an external experiment controller. # * gp is the group asserted by the experiment controller # * gu is the user asserted by the experiment controller # * lr is the linking role used for delegation. If present credentials # should be created with it. If it is None the credential construator will # ignore it. It is hard to go wrong just passing it to the credential # constructor. # # These functions map an assertion of (testbed, project, user) into the # local parameters (local project, password, etc.) that triple gives the caller # the caller to. These local parameters are parsed out of l. Those # credentials are the keys to the to_id dict that will become the abac map. # The c = credential... and creds.add(c) lines # in parse_emulab can be taken as boilerplate for creating ABAC credentials. # Each ABAC credential created by that boilerplate should be added to to_id, # keyed by the local credentials. def parse_emulab(l, creds, me, to_id, p, gp, gu, lr): ''' Parse the emulab (project, allocation_user, cert_file) format. ''' right_side_str = '\s*,\s*\(\s*%s\s*,\s*%s\s*,\s*(%s)\s*\)' % \ (proj_same_str, id_same_str,path_str) m = re.match(right_side_str, l) if m: project, user, cert = m.group(1,2,3) # Resolve ""s in project and user if project == '': if gp is not None: project = gp else: raise parse_error("Project cannot be decisively mapped: %s" % l) if user == '': if gu is not None: user = gu else: raise parse_error("User cannot be decisively mapped: %s" % l) # Create a semi-mnemonic name for the destination credential (the one # that will be mapped to the local attributes if gp and gu: a = 'project_%s_user_%s' % (gp, gu) elif gp: a = 'project_%s' % gp elif gu: a = 'user_%s' % gu else: raise parse_error("No mapping for %s/%s!?" % (gp, gu)) # Store the creds and map entries c = credential(me, a, [attribute(p, x, lr) for x in (gp, gu) if x is not None]) creds.add(c) if (project, user,cert) in to_id: to_id[(project,user,cert)].append(c) else: to_id[(project,user,cert)] = [ c ] else: raise parse_error("Badly formatted local mapping: %s" % l) def parse_protogeni(l, creds, me, to_id, p, gp, gu, lr): ''' Parse the protoGENI (cert, user, user_key, cert_pw) format. ''' right_side_str = '\s*,\s*\(\s*(%s)\s*,\s*(%s)\s*,\s*(%s)\s*,\s*(%s)\s*\)' \ % (path_str, id_str, path_str, id_str) m = re.match(right_side_str, l) if m: cert, user, key, pw = m.group(1,2,3,4) # The credential is formed from just the path (with / mapped to _) and # the username. acert = re.sub('/', '_', cert) a = "cert_%s_user_%s" % (acert, user) # Store em c = credential(me, a, [attribute(p, x, lr) for x in (gp, gu) if x is not None]) creds.add(c) if (cert, user, key, pw) in to_id: to_id[(cert, user, key, pw)].append(c) else: to_id[(cert, user, key, pw)] = [ c ] else: raise parse_error("Badly formatted local mapping: %s" % l) def parse_dragon(l, creds, me, to_id, p, gp, gu, lr): ''' Parse the dragon (repository_name) version. ''' right_side_str = '\s*,\s*\(\s*(%s)\s*\)' % \ (id_str) m = re.match(right_side_str, l) if m: repo= m.group(1) c = credential(me, 'repo_%s' % repo, [attribute(p, x, lr) for x in (gp, gu) if x is not None]) creds.add(c) if repo in to_id: to_id[repo].append(c) else: to_id[repo] = [ c ] else: raise parse_error("Badly formatted local mapping: %s" % l) def parse_skel(l, creds, me, to_id, p, gp, gu, lr): ''' Parse the skeleton (local_attr) version. ''' right_side_str = '\s*,\s*\(\s*(%s)\s*\)' % \ (id_str) m = re.match(right_side_str, l) if m: lattr = m.group(1) c = credential(me, 'lattr_%s' % lattr, [attribute(p, x, lr) for x in (gp, gu) if x is not None]) creds.add(c) if lattr in to_id: to_id[lattr].append(c) else: to_id[lattr] = [ c ] else: raise parse_error("Badly formatted local mapping: %s" % l) # internal plug-ins have no local attributes. def parse_internal(l, creds, me, to_id, p, gp, gu, lr): pass def parse_access(fn, mapper, delegation_link): """ Parse the access file, calling out to the mapper to parse specific credential types. Mappers are above this code. """ creds = set() to_id = { } f = open(fn, "r") for i, l in enumerate(f): try: if comment_re.match(l): continue else: m = line_re.match(l) if m: p, da = m.group(1, 4) gp, gu = m.group(2, 3) if gp == '': gp = None if gu == '': gu = None creds.add(credential(me, da, [attribute(p, x, delegation_link) \ for x in (gp, gu) \ if x is not None])) if m.group(5) and mapper: mapper(m.group(5), creds, me, to_id, p, gp, gu, delegation_link) else: raise parse_error('Syntax error') except parse_error, e: f.close() raise parse_error('Error on line %d of %s: %s' % \ (i, fn, e)) f.close() return creds, to_id class access_opts(file_expanding_opts): ''' Parse the options for this program. Most are straightforward, but the mapper uses a callback to convert from a string to a local mapper function. ''' # Valid mappers mappers = { 'emulab': parse_emulab, 'dragon': parse_dragon, 'internal': parse_internal, 'skel': parse_skel, 'protogeni': parse_protogeni, } @staticmethod def parse_mapper(opt, s, val, parser, dest): if val in access_opts.mappers: setattr(parser.values, dest, access_opts.mappers[val]) else: raise OptionValueError('%s must be one of %s' % \ (s, join(access_opts.mappers.keys(), ', '))) def __init__(self): file_expanding_opts.__init__(self, usage='%prog [opts] file [...]') self.add_option('--cert', dest='cert', default=None, type='str', action='callback', callback=self.expand_file, help='my fedid as an X.509 certificate') self.add_option('--key', dest='key', default=None, type='str', action='callback', callback=self.expand_file, help='key for the certificate') self.add_option('--dir', dest='dir', default=None, type='str', action='callback', callback=self.expand_file, help='Output directory for credentials') self.add_option('--type', action='callback', nargs=1, type='str', callback=access_opts.parse_mapper, callback_kwargs = { 'dest': 'mapper'}, help='Type of access file to parse. One of %s. ' %\ join(access_opts.mappers.keys(), ', ') + \ 'Omit for generic parsing.') self.add_option('--quiet', dest='quiet', action='store_true', default=False, help='Do not print credential to local attribute map') self.add_option('--no_create_creds', action='store_false', dest='create_creds', default=True, help='Do not create credentials for rules.') self.add_option('--file', dest='file', default=None, type='str', action='callback', callback=self.expand_file, help='Access DB to parse. If this is present, ' + \ 'omit the positional filename') self.add_option('--mapfile', dest='map', default=None, type='str', action='callback', callback=self.expand_file, help='File for the attribute to local authorization data') self.add_option('--update', action='store_const', const=True, dest='update_authorizer', default=False, help='Add the generated policy to an existing authorizer') self.add_option('--no-delegate', action='store_false', dest='delegate', default=True, help='do not accept delegated attributes with the ' +\ 'acting_for linking role') self.add_option('--fed-root', dest='root', help='add a rule to accept federated users from facilities ' +\ 'recognized by ROOT. This is a certificate file') self.add_option('--fed-tuple', dest='ftuple', help='a tuple into which to map federated ' + \ 'users about which we know nothing else.') self.add_option('--no_auth', action='store_false', dest='create_auth', default=True, help='do not create a full ABAC authorizer') self.add_option('--debug', action='store_true', dest='debug', default=False, help='Just print actions') self.set_defaults(mapper=None) def create_creds(creds, cert, key, dir, debug=False): ''' Make the the attributes from the list of credential objects in the creds parameter. ''' cfiles = [] for i, c in enumerate(creds): cid = Creddy.ID(cert) cid.load_privkey(key) cattr = Creddy.Attribute(cid, c.attr, 3600 * 24 * 365 * 10) for r in c.req: if r.principal and r.link and r.attr: cattr.linking_role(r.principal, r.attr, r.link) elif r.principal and r.attr: cattr.role(r.principal, r.attr) elif r.principal: cattr.principal(r.principal) else: raise parse_error('Attribute without a principal?') cattr.bake() fn = '%s/cred%d_attr.der' % (dir, i) cattr.write_name(fn) cfiles.append(fn) return cfiles def clear_dir(dir): for path, dirs, files in os.walk(dir, topdown=False): for f in files: os.unlink(os.path.join(path, f)) for d in dirs: os.rmdir(os.path.join(path, d)) # Regular expressions and parts thereof for parsing comment_re = re.compile('^\s*#|^$') fedid_str = 'fedid:([0-9a-fA-F]{40})' id_str = '[a-zA-Z][\w_-]*' proj_str = '[a-zA-Z][\w_:/-]*' path_str = '[a-zA-Z0-9_/\.-]+' id_any_str = '(%s|)' % id_str proj_any_str = '(%s|)' % proj_str id_same_str = '(%s|)' % id_str proj_same_str = '(%s|)' % proj_str left_side_str = '\(\s*%s\s*,\s*%s\s*,\s*%s\s*\)' % \ (fedid_str, proj_any_str, id_any_str) right_side_str = '(%s)(\s*,\s*\(.*\))?' % (id_str) line_re = re.compile('%s\s*->\s*%s' % (left_side_str, right_side_str)) p = access_opts() opts, args = p.parse_args() cert, key = None, None delete_certs = False delete_creds = False if opts.file: args.append(opts.file) # Validate arguments if len(args) < 1: sys.exit('No filenames given to parse') if opts.key: if not os.access(opts.key, os.R_OK): key = opts.key else: sys.exit('Cannot read key (%s)' % opts.key) if opts.dir: if not os.access(opts.dir, os.F_OK): try: os.mkdir(opts.dir, 0700) except EnvironmentError, e: sys.exit("Cannot create %s: %s" % (e.filename, e.strerror)) if not os.path.isdir(opts.dir): sys.exit('%s is not a directory' % opts.dir) elif not os.access(opts.dir, os.W_OK): sys.exit('%s is not writable' % opts.dir) if opts.create_auth: creds_dir = mkdtemp() delete_creds = True auth_dir = opts.dir if not os.path.isabs(auth_dir): sys.exit('Authorizer path must be absolute') else: creds_dir = opts.dir auth_dir = None if opts.delegate: delegation_link = 'acting_for' else: delegation_link = None if not opts.mapper and (opts.map or opts.debug): print >>sys.stderr, "No --type specified, mapping file will be empty." if opts.cert: try: me = fedid(file=opts.cert) except EnvironmentError, e: sys.exit('Bad --cert: %s (%s)' % (e.strerror, e.filename or '?!')) if not opts.key: if abac_pem_type(opts.cert) == 'both': key, cert = abac_split_cert(opts.cert) delete_certs = True else: cert = opts.cert else: print >>sys.stderr, 'No --cert, using dummy fedid' me = fedid(hexstr='0123456789012345678901234567890123456789') cert = None fed_to_id = { } if any((opts.root, opts.ftuple)) and not all ((opts.root, opts.ftuple)): sys.exit('Either both or neither of --fed-root and ' + \ '--fed-project must be specified') elif opts.root: try: root_fedid = fedid(file=opts.root) except EnvironmentError, e: sys.exit('Bad --root: %s (%s)' % (e.strerror, e.filename or '?!')) fed_tuple = tuple(opts.ftuple.split(',')) fed_someuser_cred = \ credential(me, 'some_feduser', [attribute(root_fedid.get_hexstr(), 'fedfacility', 'feduser')]) fed_user_cred = \ credential(me, 'default_feduser', [attribute(me.get_hexstr(), 'some_feduser', 'acting_for')]) fed_access_cred = \ credential(me, 'access', [attribute(me.get_hexstr(), 'default_feduser')]) fed_to_id[fed_tuple] = [fed_user_cred] else: # No fed-root or fed-tuple fed_access_cred = None fed_user_cred = None fed_someuser_cred = None credfiles = [] # The try block makes sure that credentials split into tmp files are deleted try: # Do the parsing for fn in args: try: creds, to_id = parse_access(fn, opts.mapper, delegation_link) except parse_error, e: print >> sys.stderr, "%s" % e continue except EnvironmentError, e: print >>sys.stderr, "File error %s: %s" % \ (e.filename or '!?', e.strerror) continue # Credential output if opts.create_creds: if fed_access_cred and fed_user_cred and fed_someuser_cred: creds.add(fed_access_cred) creds.add(fed_user_cred) creds.add(fed_someuser_cred) if all([cert, key, opts.dir]): try: credfiles = create_creds( [c for c in creds if c.principal == me], cert, key, creds_dir, opts.debug) except credential_error, e: sys.exit('Credential creation failed: %s' % e) else: print >>sys.stderr, 'Cannot create credentials. ' + \ 'Missing parameter' # Local map output if opts.map or opts.debug: try: if opts.map and opts.map != '-' and not opts.debug: f = open(opts.map, 'w') else: f = sys.stdout for k, c in to_id.items() + fed_to_id.items(): # Keys are either a single string or a tuple of them; join # the tuples into a comma-separated string. if isinstance(k, basestring): rhs = k else: rhs = join(k, ', ') for a in set(["%s.%s" % (x.principal, x.attr) for x in c]): print >>f, "%s -> (%s)" % (a, rhs) except EnvironmentError, e: sys.exit("Cannot open %s: %s" % (e.filename or '!?', e.strerror)) # Create an authorizer if requested. if opts.create_auth: try: # Pass in the options rather than the potentially split key # because abac_authorizer will split it and store it # internally. The opts.cert may get split twice, but we won't # lose one. if opts.update_authorizer: operation = 'updat' a = abac_authorizer(load=auth_dir) a.import_credentials(file_list=credfiles) a.save() else: clear_dir(auth_dir) operation = 'creat' a = abac_authorizer(key=opts.key, me=opts.cert, certs=creds_dir, save=auth_dir) a.save(auth_dir) except EnvironmentError, e: sys.exit("Can't create or write %s: %s" % \ (e.filename, e.strerror)) except abac_authorizer.bad_cert_error, e: sys.exit("Error %sing authorizer: %s" % (operation, e)) finally: try: if delete_certs: if cert: os.unlink(cert) if key: os.unlink(key) if delete_creds and creds_dir: clear_dir(creds_dir) os.rmdir(creds_dir) except EnvironmentError, e: sys.exit("Can't remove %s: %s" % ( e.filename, e.strerror))