#!/usr/local/bin/python import os,sys import re import string import copy import pickle import logging import random from util import * from fedid import fedid, generate_fedid from authorizer import authorizer from service_error import service_error from remote_service import xmlrpc_handler, soap_handler, service_caller import topdl from access import access_base # Make log messages disappear if noone configures a fedd logger. This is # something of an incantation, but basically it creates a logger object # registered to fedd.access if no other module above us has. It's an extra # belt for the suspenders. class nullHandler(logging.Handler): def emit(self, record): pass fl = logging.getLogger("fedd.access") fl.addHandler(nullHandler()) # The plug-in itself. class access(access_base): """ This is a demonstration plug-in for fedd. It responds to all the experiment_control requests and keeps internal state. The allocations it makes are simple integers associated with each valid request. It makes use of the general routines in access.access_base. Detailed comments in the code and info at """ @staticmethod def parse_access_string(s): """ Parse a parenthesized string from the access db by removing the parens. If the string isn't in parens, we just return it with whitespace trimmed in either case. """ st = s.strip() if st.startswith("(") and st.endswith(")"): return st[1:-1] else: return st def __init__(self, config=None, auth=None): """ Initializer. Pulls parameters out of the ConfigParser's access section, and initializes simple internal state. This version reads a maximum integer to assign from the configuration file, while most other configuration entries are read by the base class. An access database in the cannonical format is also read as well as a state database that is a hash of internal state. Routines to manipulate these are in the base class, but specializations appear here. The access database maps users to a simple string. Use the sample at. """ # Calling the base initializer, which reads canonical configuration # information and initializes canonical members. access_base.__init__(self, config, auth) # Reading the maximum integer parameter from the configuration file self.maxint = config.getint("access", "maxint") or 5 # The available integers self.available_ints = set(range(0,self.maxint)) # This will contain the local data from a successful access request. # Keys are the valid three-names, and values are the locally # interpreted data. self.access = { } # Read the access values. We read the accessDB in the derived class so # that the derived class can specialize the reading of access info. In # this case, we gather the strings in the access db into self.access # using the translator above. if config.has_option("access", "accessdb"): try: self.read_access(config.get("access", "accessdb"), self.parse_access_string) except EnvironmentError, e: self.log.error("Cannot read %s: %s" % \ (config.get("access", "accessdb"), e)) raise e # The base class initializer has read the state dictionary from the # state file, if there is one. The state variable includes information # about each active allocation, keyed by the allocation identifier. # This loop extracts the owners stored with each allocation and # associates an access attribute with them. Each owner is allowed to # access each thing they own. This is a specialization of the state # handling. self.state_lock.acquire() for k in self.state.keys(): # Add the owners for o in self.state[k].get('owners', []): self.auth.set_attribute(o, fedid(hexstr=k)) # The principal represented by the allocation itself is also # allowed to make accesses. self.auth.set_attribute(fedid(hexstr=k),fedid(hexstr=k)) # Remove any allocated integers from the available ones if 'integer' in self.state[k]: self.available_ints.discard(self.state[k]['integer']) self.state_lock.release() # This access controller does not specialize the process of looking up # local information. This aliases the lookup_access method to be # easier to read. self.lookup_access = self.lookup_access_base # These dictionaries register the plug-in's local routines for handline # these four messages with the server code above. There's a version # for SOAP and XMLRPC, depending on which interfaces the plugin # supports. There's rarely a technical reason not to support one or # the other - the plugin code almost never deals with the transport - # but if a plug-in writer wanted to disable XMLRPC, they could leave # the self.xmlrpc_services dictionary empty. self.soap_services = {\ 'RequestAccess': soap_handler("RequestAccess", self.RequestAccess), 'ReleaseAccess': soap_handler("ReleaseAccess", self.ReleaseAccess), 'StartSegment': soap_handler("StartSegment", self.StartSegment), 'TerminateSegment': soap_handler("TerminateSegment", self.TerminateSegment), } self.xmlrpc_services = {\ 'RequestAccess': xmlrpc_handler('RequestAccess', self.RequestAccess), 'ReleaseAccess': xmlrpc_handler('ReleaseAccess', self.ReleaseAccess), 'StartSegment': xmlrpc_handler("StartSegment", self.StartSegment), 'TerminateSegment': xmlrpc_handler('TerminateSegment', self.TerminateSegment), } def RequestAccess(self, req, fid): """ Handle an access request. Success here maps the requester into the local access control space and establishes state about that user keyed to a fedid. We also save a copy of the certificate underlying that fedid so this allocation can access configuration information and shared parameters on the experiment controller. """ # The dance to get into the request body if req.has_key('RequestAccessRequestBody'): req = req['RequestAccessRequestBody'] else: raise service_error(service_error.req, "No request!?") # Base class lookup routine. If this fails, it throws a service # exception denying access that triggers a fault response back to the # caller. found, match = self.lookup_access(req, fid) self.log.info( "[RequestAccess] Access granted to %s with local creds %s" % \ (match, found)) # Make a fedid for this allocation allocID, alloc_cert = generate_fedid(subj="alloc", log=self.log) aid = unicode(allocID) # Store the data about this allocation: self.state_lock.acquire() self.state[aid] = { } self.state[aid]['user'] = found self.state[aid]['owners'] = [ fid ] self.write_state() self.state_lock.release() # Authorize the creating fedid and the principal representing the # allocation to manipulate it. self.auth.set_attribute(fid, allocID) self.auth.set_attribute(allocID, allocID) # Create a directory to stash the certificate in, ans stash it. try: f = open("%s/%s.pem" % (self.certdir, aid), "w") print >>f, alloc_cert f.close() except EnvironmentError, e: raise service_error(service_error.internal, "Can't open %s/%s : %s" % (self.certdir, aid, e)) self.log.debug('[RequestAccess] Returning allocation ID: %s' % allocID) return { 'allocID': { 'fedid': allocID } } def ReleaseAccess(self, req, fid): """ Release the allocation granted earlier. Access to the allocation is checked and if valid, the state and cached certificate are destroyed. """ # The dance to get into the request body if req.has_key('ReleaseAccessRequestBody'): req = req['ReleaseAccessRequestBody'] else: raise service_error(service_error.req, "No request!?") # Pull a key out of the request. One can request to delete an # allocation by a local human readable name or by a fedid. This finds # both choices. try: if 'localname' in req['allocID']: auth_attr = aid = req['allocID']['localname'] elif 'fedid' in req['allocID']: aid = unicode(req['allocID']['fedid']) auth_attr = req['allocID']['fedid'] else: raise service_error(service_error.req, "Only localnames and fedids are understood") except KeyError: raise service_error(service_error.req, "Badly formed request") self.log.debug("[ReleaseAccess] deallocation requested for %s", aid) # Confirm access if not self.auth.check_attribute(fid, auth_attr): self.log.debug("[ReleaseAccess] deallocation denied for %s", aid) raise service_error(service_error.access, "Access Denied") # If there is an allocation in the state, delete it. Note the locking. self.state_lock.acquire() if aid in self.state: self.log.debug("[ReleaseAccess] Found allocation for %s" %aid) del self.state[aid] self.write_state() self.state_lock.release() # And remove the access cert cf = "%s/%s.pem" % (self.certdir, aid) self.log.debug("[ReleaseAccess] Removing %s" % cf) os.remove(cf) return { 'allocID': req['allocID'] } else: self.state_lock.release() raise service_error(service_error.req, "No such allocation") def StartSegment(self, req, fid): """ Start a segment. In this simple skeleton, this means to parse the request and assign an unassigned integer to it. We store the integer in the persistent state. """ try: req = req['StartSegmentRequestBody'] # Get the request topology. If not present, a KeyError is thrown. topref = req['segmentdescription']['topdldescription'] # The fedid of the allocation we're attaching resources to auth_attr = req['allocID']['fedid'] except KeyError: raise service_error(server_error.req, "Badly formed request") # String version of the allocation ID for keying aid = "%s" % auth_attr # Authorization check if not self.auth.check_attribute(fid, auth_attr): raise service_error(service_error.access, "Access denied") else: # See if this is a replay of an earlier succeeded StartSegment - # sometimes SSL kills 'em. If so, replay the response rather than # redoing the allocation. self.state_lock.acquire() retval = self.state[aid].get('started', None) self.state_lock.release() if retval: self.log.warning( "[StartSegment] Duplicate StartSegment for %s: " \ % aid + \ "replaying response") return retval certfile = "%s/%s.pem" % (self.certdir, aid) # Convert the topology into topdl data structures. Again, the # skeletion doesn't do anything with it, but this is how one parses a # topology request. if topref: topo = topdl.Topology(**topref) else: raise service_error(service_error.req, "Request missing segmentdescription'") # The attributes of the request. Not used by this plug-in, but that's # where they are. attrs = req.get('fedAttr', []) # Gather connection information. Used to send messages to those # waiting. connInfo = req.get('connection', []) # Do the assignment, A more complex plug-in would interface to the # facility here to create and configure the allocation. if len(self.available_ints) > 0: # NB: lock the data structure during allocation self.state_lock.acquire() assigned = random.choice([ i for i in self.available_ints]) self.available_ints.discard(assigned) self.state[aid]['integer'] = assigned self.write_state() self.state_lock.release() self.log.debug("[StartSegment] Allocated %d to %s" \ % (assigned, aid)) else: self.log.debug("[StartSegment] No remaining resources for %s" % aid) raise service_error(service_error.federant, "No available integers") # Save the information self.state_lock.acquire() # It's possible that the StartSegment call gets retried (!). # if the 'started' key is in the allocation, we'll return it rather # than redo the setup. The integer allocation was saved when we made # it. self.state[aid]['started'] = { 'allocID': req['allocID'], 'allocationLog': "Allocatation complete", 'segmentdescription': { 'topdldescription': topo.to_dict() } } retval = copy.deepcopy(self.state[aid]['started']) self.write_state() self.state_lock.release() return retval def TerminateSegment(self, req, fid): """ Remove the resources associated with th eallocation and stop the music. In this example, this simply means removing the integer we allocated. """ # Gather the same access information as for Start Segment try: req = req['TerminateSegmentRequestBody'] except KeyError: raise service_error(server_error.req, "Badly formed request") auth_attr = req['allocID']['fedid'] aid = "%s" % auth_attr self.log.debug("Terminate request for %s" %aid) # Check authorization if not self.auth.check_attribute(fid, auth_attr): raise service_error(service_error.access, "Access denied") # Authorized: remove the integer from the allocation. A more complex # plug in would interface with the underlying facility to turn off the # experiment here. self.state_lock.acquire() if aid in self.state: assigned = self.state[aid].get('integer', None) self.available_ints.add(assigned) if 'integer' in self.state[aid]: del self.state[aid]['integer'] self.write_state() self.state_lock.release() return { 'allocID': req['allocID'] }