source: fedd/access_to_abac.py @ 547aa3b

axis_examplecompt_changesinfo-ops
Last change on this file since 547aa3b was 547aa3b, checked in by Ted Faber <faber@…>, 13 years ago

various fixes to abac tools to work with the new library

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