Index: fedd/federation/deter_impl.py
===================================================================
--- fedd/federation/deter_impl.py	(revision 181aeb4705ccf35512b1d84210f06a0ce879f835)
+++ fedd/federation/deter_impl.py	(revision 7888aeef779fdb33135707fc370dd03542e6ceb8)
@@ -5,4 +5,5 @@
 import protogeni_access
 import deter_internal_access
+import skeleton_access
 
 from experiment_control import experiment_control_local
@@ -80,4 +81,6 @@
 		    self.access = deter_internal_access.access(config,
 			    self.auth)
+		elif self.access_type == "skel":
+		    self.access = skeleton_access.access(config, self.auth)
 		else:
 		    raise RuntimeError("Unknown access_type: %s" % \
Index: fedd/federation/skeleton_access.py
===================================================================
--- fedd/federation/skeleton_access.py	(revision 7888aeef779fdb33135707fc370dd03542e6ceb8)
+++ fedd/federation/skeleton_access.py	(revision 7888aeef779fdb33135707fc370dd03542e6ceb8)
@@ -0,0 +1,355 @@
+#!/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'] }
