#!/usr/local/bin/python import sys, os import re import subprocess from string import join from federation.fedid import fedid from optparse import OptionParser, OptionValueError 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 functiona 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 # (after the three-name and the attribute it maps to), the credential list to # add the new ABAC credential(s) that will be mapped into the loacl # credentials, the fedid of this entity, a dict mapping the local credentials # to ABAC credentials that are required to exercise those local rights and the # three-name (p, gp, gu) that is being mapped. def parse_emulab(l, creds, me, to_id, p, gp, gu, lr): ''' Parse the emulab (project, allocation_user, access_user) format. Access users are deprecates and allocation users used for both. This fuction collapses them. ''' right_side_str = '\s*,\s*\(\s*%s\s*,\s*%s\s*,\s*%s\s*\)' % \ (id_same_str, id_same_str,id_same_str) m = re.match(right_side_str, l) if m: project, user = m.group(1,2) # 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: project = 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 project and user: a = 'project_%s_user_%s' % (project, user) elif project: a = 'project_%s' % project elif user: a = 'user_%s' % user 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) in to_id: to_id[(project,user)].append(c) else: to_id[(project,user)] = [ 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 class access_opts(OptionParser): ''' 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): OptionParser.__init__(self, usage='%prog [opts] file [...]') self.add_option('--cert', dest='cert', default=None, help='my fedid as an X.509 certificate') self.add_option('--key', dest='key', default=None, help='key for the certificate') self.add_option('--dir', dest='dir', default=None, 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('--create-creds', action='store_true', dest='create_creds', default=False, help='create credentials for rules. Requires ' + \ '--cert, --key, and --dir to be given.') self.add_option('--file', dest='file', default=None, help='Access DB to parse. If this is present, ' + \ 'omit the positional filename') self.add_option('--mapfile', dest='map', default=None, help='File for the attribute to local authorization data') 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('--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, creddy='/usr/local/bin/creddy'): ''' Make the creddy calls to create the attributes from the list of credential objects in the creds parameter. ''' def attrs(r): ''' Convert an attribute into creddy --subject-id and --subject-role parameters ''' if r.principal and r.link and r.attr: return ['--subject-id=%s' % r.principal, '--subject-role=%s.%s' % (r.attr, r.link), ] elif r.principal and r.attr: return ['--subject-id=%s' % r.principal, '--subject-role=%s' %r.attr] elif r.principal: return ['--subject-id=%s' % r.prinicpal] else: raise parse_error('Attribute without a principal?') # main line of create_creds for i, c in enumerate(creds): cmd = [creddy, '--attribute', '--issuer=%s' % cert, '--key=%s' % key, '--role=%s' % c.attr, '--out=%s/cred%d_attr.der' % (dir, i)] for r in c.req: cmd.extend(attrs(r)) if debug: print join(cmd) else: rv = subprocess.call(cmd) if rv != 0: raise credential_error("%s: %d" % (join(cmd), rv)) # 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_-]*' path_str = '[a-zA-Z_/\.-]+' id_any_str = '(%s|)' % id_str id_same_str = '(%s|)' % id_str left_side_str = '\(\s*%s\s*,\s*%s\s*,\s*%s\s*\)' % \ (fedid_str, id_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() if opts.file: args.append(opts.file) # Validate arguments if len(args) < 1: sys.exit('No filenames given to parse') if opts.cert: try: me = fedid(file=opts.cert) except EnvironmentError, e: sys.exit('Bad --cert: %s (%s)' % (e.strerror, e.filename or '?!')) else: print >>sys.stderr, 'No --cert, using dummy fedid' me = fedid(hexstr='0123456789012345678901234567890123456789') if opts.key and not os.access(opts.key, os.R_OK): sys.exit('Cannot read key (%s)' % opts.key) if opts.dir: 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.delegate: delegation_link = 'acting_for' else: delegation_link = None mapper = opts.mapper # Do the parsing for fn in args: creds = set() to_id = { } try: 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.message)) f.close() 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 all([opts.cert, opts.key, opts.dir]): try: create_creds([c for c in creds if c.principal == me], opts.cert, opts.key, opts.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(): for a in set(["%s.%s" % (x.principal, x.attr) for x in c]): print >>f, "%s -> (%s)" % ( a, join(k, ', ')) except EnvironmentError, e: sys.exit("Cannot open %s: %s" % (e.filename or '!?', e.strerror))