source: fedd/federation/skeleton_access.py @ e83f2f2

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

Move proofs around. Lots of changes, including fault handling.

  • Property mode set to 100644
File size: 10.5 KB
Line 
1#!/usr/local/bin/python
2
3import os,sys
4import re
5import string
6import copy
7import pickle
8import logging
9import random
10
11from util import *
12from fedid import fedid, generate_fedid
13from authorizer import authorizer, abac_authorizer
14from service_error import service_error
15from remote_service import xmlrpc_handler, soap_handler, service_caller
16
17import topdl
18
19from access import access_base
20from legacy_access import legacy_access
21
22# Make log messages disappear if noone configures a fedd logger.  This is
23# something of an incantation, but basically it creates a logger object
24# registered to fedd.access if no other module above us has.  It's an extra
25# belt for the suspenders.
26class nullHandler(logging.Handler):
27    def emit(self, record): pass
28
29fl = logging.getLogger("fedd.access")
30fl.addHandler(nullHandler())
31
32
33# The plug-in itself.
34class access(access_base, legacy_access):
35    """
36    This is a demonstration plug-in for fedd.  It responds to all the
37    experiment_control requests and keeps internal state.  The allocations it
38    makes are simple integers associated with each valid request.  It makes use
39    of the general routines in access.access_base.
40
41    Detailed comments in the code and info at
42    """
43
44    @staticmethod 
45    def parse_access_string(s):
46        """
47        Parse a parenthesized string from the access db by removing the parens.
48        If the string isn't in parens, we just return it with whitespace
49        trimmed in either case.
50        """
51        st = s.strip()
52        if st.startswith("(") and st.endswith(")"): return st[1:-1]
53        else: return st
54
55    def __init__(self, config=None, auth=None):
56        """
57        Initializer.  Pulls parameters out of the ConfigParser's access
58        section, and initializes simple internal state.  This version reads a
59        maximum integer to assign from the configuration file, while most other
60        configuration entries  are read by the base class. 
61
62        An access database in the cannonical format is also read as well as a
63        state database that is a hash of internal state.  Routines to
64        manipulate these are in the base class, but specializations appear
65        here.
66
67        The access database maps users to a simple string.
68        """
69
70        # Calling the base initializer, which reads canonical configuration
71        # information and initializes canonical members.
72        access_base.__init__(self, config, auth)
73        # Reading the maximum integer parameter from the configuration file
74        self.maxint = config.getint("access", "maxint") or 5
75        # The available integers
76        self.available_ints = set(range(0,self.maxint))
77
78        # authorization information
79        self.auth_type = config.get('access', 'auth_type') \
80                or 'legacy'
81        self.auth_dir = config.get('access', 'auth_dir')
82        accessdb = config.get("access", "accessdb")
83        # initialize the authorization system.  In each case we make a call to
84        # read the access database that maps from authorization information
85        # into local information.  The local information is parsed by the
86        # translator above.
87        if self.auth_type == 'legacy':
88            self.access = { }
89            if accessdb:
90                try:
91                    self.legacy_read_access(accessdb, self.parse_access_string)
92                except EnvironmentError, e:
93                    self.log.error("Cannot read %s: %s" % \
94                            (config.get("access", "accessdb"), e))
95                    raise e
96            # The base class initializer has read the state dictionary from the
97            # state file, if there is one.  The state variable includes
98            # information about each active allocation, keyed by the allocation
99            # identifier.  This loop extracts the owners stored with each
100            # allocation and associates an access attribute with them.  Each
101            # owner is allowed to access each thing they own.  This is a
102            # specialization of the state handling.  ABAC records this
103            # information explicitly so this loop only executes for legacy
104            # code.
105            self.state_lock.acquire()
106            for k in self.state.keys():
107                # Add the owners
108                for o in self.state[k].get('owners', []):
109                    self.auth.set_attribute(o, fedid(hexstr=k))
110                # The principal represented by the allocation itself is also
111                # allowed to make accesses.
112                self.auth.set_attribute(fedid(hexstr=k),fedid(hexstr=k))
113            self.state_lock.release()
114            # This access controller does not specialize the process of looking
115            # up local information.  This aliases the lookup_access method to
116            # be easier to read.
117            self.lookup_access = self.legacy_lookup_access_base
118        elif self.auth_type == 'abac':
119            #  Load the current authorization state
120            self.auth = abac_authorizer(load=self.auth_dir)
121            self.access = [ ]
122            if accessdb:
123                try:
124                    self.read_access(accessdb, self.parse_access_string)
125                except EnvironmentError, e:
126                    self.log.error("Cannot read %s: %s" % \
127                            (config.get("access", "accessdb"), e))
128                    raise e
129        else:
130            raise service_error(service_error.internal, 
131                    "Unknown auth_type: %s" % self.auth_type)
132
133        # Clean the state
134        self.state_lock.acquire()
135        for k in self.state.keys():
136            # Remove any allocated integers from the available ones
137            if 'integer' in self.state[k]:
138                self.available_ints.discard(self.state[k]['integer'])
139        self.state_lock.release()
140
141        # These dictionaries register the plug-in's local routines for handline
142        # these four messages with the server code above.  There's a version
143        # for SOAP and XMLRPC, depending on which interfaces the plugin
144        # supports.  There's rarely a technical reason not to support one or
145        # the other - the plugin code almost never deals with the transport -
146        # but if a plug-in writer wanted to disable XMLRPC, they could leave
147        # the self.xmlrpc_services dictionary empty.
148        self.soap_services = {\
149            'RequestAccess': soap_handler("RequestAccess", self.RequestAccess),
150            'ReleaseAccess': soap_handler("ReleaseAccess", self.ReleaseAccess),
151            'StartSegment': soap_handler("StartSegment", self.StartSegment),
152            'TerminateSegment': soap_handler("TerminateSegment", 
153                self.TerminateSegment),
154            }
155        self.xmlrpc_services =  {\
156            'RequestAccess': xmlrpc_handler('RequestAccess',
157                self.RequestAccess),
158            'ReleaseAccess': xmlrpc_handler('ReleaseAccess',
159                self.ReleaseAccess),
160            'StartSegment': xmlrpc_handler("StartSegment", self.StartSegment),
161            'TerminateSegment': xmlrpc_handler('TerminateSegment',
162                self.TerminateSegment),
163            }
164
165    # RequestAccess and ReleaseAccess come from the base class
166
167    def StartSegment(self, req, fid):
168        """
169        Start a segment.  In this simple skeleton, this means to parse the
170        request and assign an unassigned integer to it.  We store the integer
171        in the persistent state.
172        """
173        try:
174            req = req['StartSegmentRequestBody']
175            # Get the request topology.  If not present, a KeyError is thrown.
176            topref = req['segmentdescription']['topdldescription']
177            # The fedid of the allocation we're attaching resources to
178            auth_attr = req['allocID']['fedid']
179        except KeyError:
180            raise service_error(server_error.req, "Badly formed request")
181
182        # String version of the allocation ID for keying
183        aid = "%s" % auth_attr
184        # Authorization check
185        access_ok, proof = self.auth.check_attribute(fid, auth_attr, 
186                with_proof=True)
187        if not access_ok:
188            raise service_error(service_error.access, "Access denied", 
189                    proof=proof)
190        else:
191            # See if this is a replay of an earlier succeeded StartSegment -
192            # sometimes SSL kills 'em.  If so, replay the response rather than
193            # redoing the allocation.
194            self.state_lock.acquire()
195            retval = self.state[aid].get('started', None)
196            self.state_lock.release()
197            if retval:
198                self.log.warning(
199                        "[StartSegment] Duplicate StartSegment for %s: " \
200                                % aid + \
201                        "replaying response")
202                return retval
203
204        certfile = "%s/%s.pem" % (self.certdir, aid)
205
206        # Convert the topology into topdl data structures.  Again, the
207        # skeletion doesn't do anything with it, but this is how one parses a
208        # topology request.
209        if topref: topo = topdl.Topology(**topref)
210        else:
211            raise service_error(service_error.req, 
212                    "Request missing segmentdescription'")
213
214        # The attributes of the request.  Not used by this plug-in, but that's
215        # where they are.
216        attrs = req.get('fedAttr', [])
217
218        # Gather connection information.  Used to send messages to those
219        # waiting.
220        connInfo = req.get('connection', [])
221
222        # Do the assignment,  A more complex plug-in would interface to the
223        # facility here to create and configure the allocation.
224        if len(self.available_ints) > 0:
225            # NB: lock the data structure during allocation
226            self.state_lock.acquire()
227            assigned = random.choice([ i for i in self.available_ints])
228            self.available_ints.discard(assigned)
229            self.state[aid]['integer'] = assigned
230            self.write_state()
231            self.state_lock.release()
232            self.log.debug("[StartSegment] Allocated %d to %s" \
233                    % (assigned, aid))
234        else:
235            self.log.debug("[StartSegment] No remaining resources for %s" % aid)
236            raise service_error(service_error.federant, "No available integers")
237
238        # Save the information
239        self.state_lock.acquire()
240        # It's possible that the StartSegment call gets retried (!).
241        # if the 'started' key is in the allocation, we'll return it rather
242        # than redo the setup.  The integer allocation was saved when we made
243        # it.
244        self.state[aid]['started'] = { 
245                'allocID': req['allocID'],
246                'allocationLog': "Allocatation complete",
247                'segmentdescription': { 'topdldescription': topo.to_dict() },
248                'proof': proof.to_dict(),
249                }
250        retval = copy.deepcopy(self.state[aid]['started'])
251        self.write_state()
252        self.state_lock.release()
253
254        return retval
255
256    def TerminateSegment(self, req, fid):
257        """
258        Remove the resources associated with th eallocation and stop the music.
259        In this example, this simply means removing the integer we allocated.
260        """
261        # Gather the same access information as for Start Segment
262        try:
263            req = req['TerminateSegmentRequestBody']
264        except KeyError:
265            raise service_error(server_error.req, "Badly formed request")
266
267        auth_attr = req['allocID']['fedid']
268        aid = "%s" % auth_attr
269
270        self.log.debug("Terminate request for %s" %aid)
271        # Check authorization
272        access_ok, proof = self.auth.check_attribute(fid, auth_attr, 
273                with_proof=True)
274        if not access_ok:
275            raise service_error(service_error.access, "Access denied", 
276                    proof=proof)
277
278        # Authorized: remove the integer from the allocation.  A more complex
279        # plug in would interface with the underlying facility to turn off the
280        # experiment here.
281        self.state_lock.acquire()
282        if aid in self.state:
283            assigned = self.state[aid].get('integer', None)
284            self.available_ints.add(assigned)
285            if 'integer' in self.state[aid]:
286                del self.state[aid]['integer']
287            self.write_state()
288        self.state_lock.release()
289   
290        return { 'allocID': req['allocID'], 'proof': proof.to_dict() }
Note: See TracBrowser for help on using the repository browser.