source: fedd/access_to_abac.py @ 78f2668

axis_examplecompt_changesinfo-ops
Last change on this file since 78f2668 was de7cb08, checked in by Ted Faber <faber@…>, 14 years ago

use combo key

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