source: fedd/access_to_abac.py @ 8222e09

Last change on this file since 8222e09 was 8222e09, checked in by Ted Faber <faber@…>, 8 years ago

Better comment???

  • Property mode set to 100755
File size: 17.7 KB
Line 
1#!/usr/bin/env python
2
3import sys, os
4import re
5import subprocess
6import os.path
7
8from string import join
9from optparse import OptionParser, OptionValueError
10from tempfile import mkdtemp
11
12import Creddy
13
14from deter import fedid
15from federation.authorizer import abac_authorizer
16from federation.util import abac_split_cert, abac_pem_type, file_expanding_opts
17
18
19class attribute:
20    '''
21    Encapculate a principal/attribute/link tuple.
22    '''
23    bad_attr = re.compile('[^a-zA-Z0-9:_]+')
24    def __init__(self, p, a, l=None):
25        self.principal = p
26        self.attr = attribute.bad_attr.sub('_', a)
27        if l: self.link = attribute.bad_attr.sub('_', l)
28        else: self.link = None
29
30    def __str__(self):
31        if self.link:
32            return "%s.%s.%s" % (self.principal, self.attr, self.link)
33        elif self.attr:
34            return "%s.%s" % (self.principal, self.attr)
35        else:
36            return "%s" % self.principal
37
38class credential:
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    '''
44    bad_attr = re.compile('[^a-zA-Z0-9:_]+')
45    def __init__(self, p, a, req):
46        self.principal = p
47        if isinstance(a, (tuple, list, set)) and len(a) == 1:
48            self.attr = credential.bad_attr.sub('_', a[0])
49        else:
50            self.attr = credential.bad_attr.sub('_', a)
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, 
56                    join(["%s" % r for r in self.req], ' & '))
57        else:
58            return "%s.%s <- %s" % (self.principal, self.attr, self.req)
59
60# Mappinng generation function and the access parser throw these when there is
61# a parsing problem.
62class parse_error(RuntimeError): pass
63
64# Error creating a credential
65class credential_error(RuntimeError): pass
66
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
72#    * the unparsed remainder of the line in l.  This is everything on the
73#       input line from the first comma after the -> to the end of the line.
74#    * the current list of credentials to be issued.  This is a list of
75#      credential objects assigned from this principal (me) to principals
76#      making requests.  They are derived from the three-name and delegation
77#      credentials.  This routine adds any credentials that will be mapped to
78#      local access control information to creds.
79#    * me is this princpal - the access controller
80#    * to_id is a dict that maps the local name access control information into
81#      a list of credentials that imply the principal should be mapped to them.
82#      For example, parse_emulab is assigning (project, user, cert, key) to
83#      principals.  The key of to_id is that tuple and it maps to a list of
84#      ABAC attributes.  If an access controller wants to see if a caller can
85#      use a 4-tuple of credentials, it tries to prove that the caller has one
86#      of those attributes.
87#    * p is the principal that assigned the project and group.  It is the name
88#       of an external experiment controller.
89#    * gp is the group asserted by the experiment controller
90#    * gu is the user asserted by the experiment controller
91#    * lr is the linking role used for delegation.  If present credentials
92#    should be created with it.  If it is None the credential construator will
93#    ignore it.  It is hard to go wrong just passing it to the credential
94#    constructor.
95#
96# These functions map an assertion of (testbed, project, user) into the
97# local parameters (local project, password, etc.) that triple gives the caller
98# the caller to.  These local parameters are parsed out of l.  Those
99# credentials are the keys to the to_id dict that will become the abac map.
100# The c = credential... and creds.add(c)  lines
101# in parse_emulab can be taken as boilerplate for creating ABAC credentials.
102# Each ABAC credential created by that boilerplate should be added to to_id,
103# keyed by the local credentials.
104def parse_emulab(l, creds, me, to_id, p, gp, gu, lr):
105    '''
106    Parse the emulab (project, allocation_user, cert_file) format.
107    '''
108    right_side_str = '\s*,\s*\(\s*%s\s*,\s*%s\s*,\s*(%s)\s*\)' % \
109            (proj_same_str, id_same_str,path_str)
110
111    m = re.match(right_side_str, l)
112    if m:
113        project, user, cert = m.group(1,2,3)
114        # Resolve "<same>"s in project and user
115        if project == '<same>':
116            if gp  is not None:
117                project = gp
118            else:
119                raise parse_error("Project cannot be decisively mapped: %s" % l)
120        if user == '<same>':
121            if gu is not None:
122                user = gu
123            else:
124                raise parse_error("User cannot be decisively mapped: %s" % l)
125
126        # Create a semi-mnemonic name for the destination credential (the one
127        # that will be mapped to the local attributes
128        if gp and gu:
129            a = 'project_%s_user_%s' % (gp, gu)
130        elif gp:
131            a = 'project_%s' % gp
132        elif gu:
133            a = 'user_%s' % gu
134        else:
135            raise parse_error("No mapping for %s/%s!?" % (gp, gu))
136
137        # Store the creds and map entries
138        c = credential(me, a, 
139                [attribute(p, x, lr) for x in (gp, gu) if x is not None])
140        creds.add(c)
141        if (project, user,cert) in to_id: to_id[(project,user,cert)].append(c)
142        else: to_id[(project,user,cert)] = [ c ]
143    else:
144        raise parse_error("Badly formatted local mapping: %s" % l)
145
146
147def parse_protogeni(l, creds, me, to_id, p, gp, gu, lr):
148    '''
149    Parse the protoGENI (cert, user, user_key, cert_pw) format.
150    '''
151    right_side_str = '\s*,\s*\(\s*(%s)\s*,\s*(%s)\s*,\s*(%s)\s*,\s*(%s)\s*\)' \
152            % (path_str, id_str, path_str, id_str)
153
154    m = re.match(right_side_str, l)
155    if m:
156        cert, user, key, pw = m.group(1,2,3,4)
157        # The credential is formed from just the path (with / mapped to _) and
158        # the username.
159        acert = re.sub('/', '_', cert)
160
161        a = "cert_%s_user_%s" % (acert, user)
162
163        # Store em
164        c = credential(me, a, 
165                [attribute(p, x, lr) for x in (gp, gu) if x is not None])
166        creds.add(c)
167        if (cert, user, key, pw) in to_id: 
168            to_id[(cert, user, key, pw)].append(c)
169        else: 
170            to_id[(cert, user, key, pw)] = [ c ]
171    else:
172        raise parse_error("Badly formatted local mapping: %s" % l)
173
174def parse_dragon(l, creds, me, to_id, p, gp, gu, lr):
175    '''
176    Parse the dragon (repository_name) version.
177    '''
178    right_side_str = '\s*,\s*\(\s*(%s)\s*\)' % \
179            (id_str)
180
181    m = re.match(right_side_str, l)
182    if m:
183        repo= m.group(1)
184        c = credential(me, 'repo_%s' % repo, 
185                [attribute(p, x, lr) for x in (gp, gu) if x is not None])
186        creds.add(c)
187        if repo in to_id: to_id[repo].append(c)
188        else: to_id[repo] = [ c ]
189    else:
190        raise parse_error("Badly formatted local mapping: %s" % l)
191
192def parse_skel(l, creds, me, to_id, p, gp, gu, lr):
193    '''
194    Parse the skeleton (local_attr) version.
195    '''
196    right_side_str = '\s*,\s*\(\s*(%s)\s*\)' % \
197            (id_str)
198
199    m = re.match(right_side_str, l)
200    if m:
201        lattr = m.group(1)
202        c = credential(me, 'lattr_%s' % lattr, 
203                [attribute(p, x, lr) for x in (gp, gu) if x is not None])
204        creds.add(c)
205        if lattr in to_id: to_id[lattr].append(c)
206        else: to_id[lattr] = [ c ]
207    else:
208        raise parse_error("Badly formatted local mapping: %s" % l)
209
210# internal plug-ins have no local attributes.
211def parse_internal(l, creds, me, to_id, p, gp, gu, lr): pass
212
213
214def parse_access(fn, mapper, delegation_link):
215    """
216    Parse the access file, calling out to the mapper to parse specific
217    credential types.  Mappers are above this code.
218    """
219    creds = set()
220    to_id = { }
221    f = open(fn, "r")
222    for i, l in enumerate(f):
223        try:
224            if comment_re.match(l):
225                continue
226            else:
227                m =  line_re.match(l)
228                if m:
229                    p, da = m.group(1, 4)
230                    gp, gu = m.group(2, 3)
231                    if gp == '<any>': gp = None
232                    if gu == '<any>': gu = None
233
234                    creds.add(credential(me, da, 
235                            [attribute(p, x, delegation_link) \
236                                    for x in (gp, gu) \
237                                        if x is not None]))
238                    if m.group(5) and mapper:
239                        mapper(m.group(5), creds, me, to_id, p, gp, gu,
240                                delegation_link)
241                else:
242                    raise parse_error('Syntax error')
243        except parse_error, e:
244            f.close()
245            raise parse_error('Error on line %d of %s: %s' % \
246                    (i, fn, e))
247    f.close()
248
249    return creds, to_id
250
251
252
253class access_opts(file_expanding_opts):
254    '''
255    Parse the options for this program.  Most are straightforward, but the
256    mapper uses a callback to convert from a string to a local mapper function.
257    '''
258    # Valid mappers
259    mappers = { 
260            'emulab': parse_emulab, 
261            'dragon': parse_dragon,
262            'internal': parse_internal,
263            'skel': parse_skel,
264            'protogeni': parse_protogeni,
265            }
266
267    @staticmethod
268    def parse_mapper(opt, s, val, parser, dest):
269        if val in access_opts.mappers:
270            setattr(parser.values, dest, access_opts.mappers[val])
271        else:
272            raise OptionValueError('%s must be one of %s' % \
273                    (s, join(access_opts.mappers.keys(), ', ')))
274
275    def __init__(self):
276        file_expanding_opts.__init__(self, usage='%prog [opts] file [...]')
277        self.add_option('--cert', dest='cert', default=None,
278                type='str', action='callback', callback=self.expand_file,
279                help='my fedid as an X.509 certificate')
280        self.add_option('--key', dest='key', default=None,
281                type='str', action='callback', callback=self.expand_file,
282                help='key for the certificate')
283        self.add_option('--dir', dest='dir', default=None,
284                type='str', action='callback', callback=self.expand_file,
285                help='Output directory for credentials')
286        self.add_option('--type', action='callback', nargs=1, type='str',
287                callback=access_opts.parse_mapper, 
288                callback_kwargs = { 'dest': 'mapper'}, 
289                help='Type of access file to parse.  One of %s. ' %\
290                        join(access_opts.mappers.keys(), ', ') + \
291                        'Omit for generic parsing.')
292        self.add_option('--quiet', dest='quiet', action='store_true', 
293                default=False,
294                help='Do not print credential to local attribute map')
295        self.add_option('--no_create_creds', action='store_false', 
296                dest='create_creds', default=True,
297                help='Do not create credentials for rules.')
298        self.add_option('--file', dest='file', default=None,
299                type='str', action='callback', callback=self.expand_file,
300                help='Access DB to parse.  If this is present, ' + \
301                        'omit the positional filename')
302        self.add_option('--mapfile', dest='map', default=None,
303                type='str', action='callback', callback=self.expand_file,
304                help='File for the attribute to local authorization data')
305        self.add_option('--update', action='store_const', const=True,
306                dest='update_authorizer', default=False, 
307                help='Add the generated policy to an existing authorizer')
308        self.add_option('--no-delegate', action='store_false', dest='delegate',
309                default=True,
310                help='do not accept delegated attributes with the ' +\
311                        'acting_for linking role')
312        self.add_option('--fed-root', dest='root', 
313                help='add a rule to accept federated users from facilities ' +\
314                        'recognized by ROOT.  This is a certificate file')
315        self.add_option('--fed-tuple', dest='ftuple', 
316                help='a tuple into which to map federated ' + \
317                        'users about which we know nothing else.')
318        self.add_option('--no_auth', action='store_false', dest='create_auth', 
319                default=True, help='do not create a full ABAC authorizer')
320        self.add_option('--debug', action='store_true', dest='debug', 
321                default=False, help='Just print actions')
322        self.set_defaults(mapper=None)
323
324def create_creds(creds, cert, key, dir, debug=False):
325    '''
326    Make the the attributes from the list of credential
327    objects in the creds parameter.
328    '''
329    cfiles = []
330    for i, c in enumerate(creds):
331        cid = Creddy.ID(cert)
332        cid.load_privkey(key)
333        cattr = Creddy.Attribute(cid, c.attr, 3600 * 24 * 365 * 10)
334        for r in c.req:
335            if r.principal and r.link and r.attr:
336                cattr.linking_role(r.principal, r.attr, r.link)
337            elif r.principal and r.attr:
338                cattr.role(r.principal, r.attr)
339            elif r.principal:
340                cattr.principal(r.principal)
341            else:
342                raise parse_error('Attribute without a principal?')
343        cattr.bake()
344        fn = '%s/cred%d_attr.der' % (dir, i)
345        cattr.write_name(fn)
346        cfiles.append(fn)
347    return cfiles
348
349
350def clear_dir(dir):
351    for path, dirs, files in os.walk(dir, topdown=False):
352        for f in files: os.unlink(os.path.join(path, f))
353        for d in dirs: os.rmdir(os.path.join(path, d))
354
355# Regular expressions and parts thereof for parsing
356comment_re = re.compile('^\s*#|^$')
357fedid_str = 'fedid:([0-9a-fA-F]{40})'
358id_str = '[a-zA-Z][\w_-]*'
359proj_str = '[a-zA-Z][\w_:/-]*'
360path_str = '[a-zA-Z0-9_/\.-]+'
361id_any_str = '(%s|<any>)' % id_str
362proj_any_str = '(%s|<any>)' % proj_str
363id_same_str = '(%s|<same>)' % id_str
364proj_same_str = '(%s|<same>)' % proj_str
365left_side_str = '\(\s*%s\s*,\s*%s\s*,\s*%s\s*\)' % \
366        (fedid_str, proj_any_str, id_any_str)
367right_side_str = '(%s)(\s*,\s*\(.*\))?' % (id_str)
368line_re = re.compile('%s\s*->\s*%s' % (left_side_str, right_side_str))
369
370p = access_opts()
371opts, args = p.parse_args()
372
373cert, key = None, None
374delete_certs = False
375delete_creds = False
376
377if opts.file:
378    args.append(opts.file)
379
380# Validate arguments
381if len(args) < 1:
382    sys.exit('No filenames given to parse')
383
384if opts.key:
385    if not os.access(opts.key, os.R_OK):
386        key = opts.key
387    else:
388        sys.exit('Cannot read key (%s)' % opts.key)
389
390if opts.dir:
391    if not os.access(opts.dir, os.F_OK):
392        try:
393            os.mkdir(opts.dir, 0700)
394        except EnvironmentError, e:
395            sys.exit("Cannot create %s: %s" % (e.filename, e.strerror))
396    if not os.path.isdir(opts.dir):
397        sys.exit('%s is not a directory' % opts.dir)
398    elif not os.access(opts.dir, os.W_OK):
399        sys.exit('%s is not writable' % opts.dir)
400
401if opts.create_auth:
402    creds_dir = mkdtemp()
403    delete_creds = True
404    auth_dir = opts.dir
405    if not os.path.isabs(auth_dir):
406        sys.exit('Authorizer path must be absolute')
407else:
408    creds_dir = opts.dir
409    auth_dir = None
410
411if opts.delegate: delegation_link = 'acting_for'
412else: delegation_link = None
413
414if not opts.mapper and (opts.map or opts.debug):
415    print >>sys.stderr, "No --type specified, mapping file will be empty."
416
417if opts.cert: 
418    try:
419        me = fedid(file=opts.cert) 
420    except EnvironmentError, e:
421        sys.exit('Bad --cert: %s (%s)' % (e.strerror, e.filename or '?!'))
422
423    if not opts.key:
424        if abac_pem_type(opts.cert) == 'both':
425            key, cert = abac_split_cert(opts.cert)
426            delete_certs = True
427    else:
428        cert = opts.cert
429else: 
430    print >>sys.stderr, 'No --cert, using dummy fedid'
431    me = fedid(hexstr='0123456789012345678901234567890123456789')
432    cert = None
433
434fed_to_id =  { }
435if any((opts.root, opts.ftuple)) and not all ((opts.root, opts.ftuple)):
436    sys.exit('Either both or neither of --fed-root and ' + \
437            '--fed-project must be specified')
438elif opts.root:
439    try:
440        root_fedid = fedid(file=opts.root)
441    except EnvironmentError, e:
442        sys.exit('Bad --root: %s (%s)' % (e.strerror, e.filename or '?!'))
443
444    fed_tuple = tuple(opts.ftuple.split(','))
445    fed_someuser_cred = \
446            credential(me, 'some_feduser', 
447                    [attribute(root_fedid.get_hexstr(), 
448                        'fedfacility', 'feduser')])
449    fed_user_cred = \
450            credential(me, 'default_feduser', 
451                    [attribute(me.get_hexstr(), 
452                        'some_feduser', 'acting_for')])
453    fed_access_cred = \
454            credential(me, 'access', 
455                    [attribute(me.get_hexstr(), 'default_feduser')])
456   
457    fed_to_id[fed_tuple] = [fed_user_cred]
458
459else:
460    # No fed-root or fed-tuple
461    fed_access_cred = None
462    fed_user_cred = None
463    fed_someuser_cred = None
464
465credfiles = []
466   
467# The try block makes sure that credentials split into tmp files are deleted
468try:
469    # Do the parsing
470    for fn in args:
471        try:
472            creds, to_id = parse_access(fn, opts.mapper, delegation_link)
473        except parse_error, e:
474            print >> sys.stderr, "%s" % e
475            continue
476        except EnvironmentError, e:
477            print >>sys.stderr, "File error %s: %s" % \
478                    (e.filename or '!?', e.strerror) 
479            continue
480
481        # Credential output
482        if opts.create_creds:
483            if fed_access_cred and fed_user_cred and fed_someuser_cred:
484                creds.add(fed_access_cred)
485                creds.add(fed_user_cred)
486                creds.add(fed_someuser_cred)
487            if all([cert, key, opts.dir]):
488                try:
489                    credfiles = create_creds(
490                            [c for c in creds if c.principal == me],
491                            cert, key, creds_dir, opts.debug)
492                except credential_error, e:
493                    sys.exit('Credential creation failed: %s' % e)
494            else:
495                print >>sys.stderr, 'Cannot create credentials.  ' + \
496                        'Missing parameter'
497
498        # Local map output
499        if opts.map or opts.debug:
500            try:
501                if opts.map and opts.map != '-' and not opts.debug:
502                    f = open(opts.map, 'w')
503                else:
504                    f = sys.stdout
505                for k, c in to_id.items() + fed_to_id.items():
506                    # Keys are either a single string or a tuple of them; join
507                    # the tuples into a comma-separated string.
508                    if isinstance(k, basestring): rhs = k
509                    else: rhs = join(k, ', ')
510
511                    for a in set(["%s.%s" % (x.principal, x.attr) for x in c]):
512                        print >>f, "%s -> (%s)" % (a, rhs)
513            except EnvironmentError, e:
514                sys.exit("Cannot open %s: %s" % (e.filename or '!?', 
515                    e.strerror))
516
517        # Create an authorizer if requested.
518        if opts.create_auth:
519            try:
520                # Pass in the options rather than the potentially split key
521                # because abac_authorizer will split it and store it
522                # internally.  The opts.cert may get split twice, but we won't
523                # lose one.
524                if opts.update_authorizer:
525                    operation = 'updat'
526                    a = abac_authorizer(load=auth_dir)
527                    a.import_credentials(file_list=credfiles)
528                    a.save()
529                else:
530                    clear_dir(auth_dir)
531                    operation = 'creat'
532                    a = abac_authorizer(key=opts.key, me=opts.cert, 
533                            certs=creds_dir, save=auth_dir)
534                    a.save(auth_dir)
535            except EnvironmentError, e:
536                sys.exit("Can't create or write %s: %s" % \
537                        (e.filename, e.strerror))
538            except abac_authorizer.bad_cert_error, e:
539                sys.exit("Error %sing authorizer: %s" % (operation, e))
540
541finally:
542    try:
543        if delete_certs:
544            if cert: os.unlink(cert)
545            if key: os.unlink(key)
546        if delete_creds and creds_dir:
547            clear_dir(creds_dir)
548            os.rmdir(creds_dir)
549    except EnvironmentError, e:
550        sys.exit("Can't remove %s: %s" % ( e.filename, e.strerror))
Note: See TracBrowser for help on using the repository browser.