Index: fedd/Makefile
===================================================================
--- fedd/Makefile	(revision 6ff0b91afedbb5c0b726a21c0c6cb3f24c66cebc)
+++ fedd/Makefile	(revision 6ff0b91afedbb5c0b726a21c0c6cb3f24c66cebc)
@@ -0,0 +1,12 @@
+
+GENERATED_MODS=fedd_services.py fedd_services_types.py
+WSDL_FILES=fedd.wsdl fedd_bindings.wsdl fedd_messages.wsdl fedd_types.xsd
+
+all:	${GENERATED_MODS}
+
+
+${GENERATED_MODS}:	${WSDL_FILES}
+	/usr/local/bin/wsdl2py --file fedd.wsdl --complexType
+
+clean:
+	rm -f ${GENERATED_MODS} *.pyc
Index: fedd/asn1_raw.py
===================================================================
--- fedd/asn1_raw.py	(revision 6ff0b91afedbb5c0b726a21c0c6cb3f24c66cebc)
+++ fedd/asn1_raw.py	(revision 6ff0b91afedbb5c0b726a21c0c6cb3f24c66cebc)
@@ -0,0 +1,158 @@
+# Read ASN.1/PEM X.509 certificates on stdin, parse each into plain text,
+# then build substrate from it
+import sys, string, base64
+from pyasn1.type import tag,namedtype,namedval,univ,constraint,char,useful
+from pyasn1.codec.der import decoder, encoder
+from pyasn1 import error
+
+from M2Crypto import EVP
+
+# Would be autogenerated from ASN.1 source by a ASN.1 parser
+# X.509 spec (rfc2459)
+
+MAX = 64  # XXX ?
+
+class DirectoryString(univ.Choice):
+    componentType = namedtype.NamedTypes(
+        namedtype.NamedType('teletexString', char.TeletexString().subtype(subtypeSpec=constraint.ValueSizeConstraint(1, MAX))),
+        namedtype.NamedType('printableString', char.PrintableString().subtype(subtypeSpec=constraint.ValueSizeConstraint(1, MAX))),
+        namedtype.NamedType('universalString', char.UniversalString().subtype(subtypeSpec=constraint.ValueSizeConstraint(1, MAX))),
+        namedtype.NamedType('utf8String', char.UTF8String().subtype(subtypeSpec=constraint.ValueSizeConstraint(1, MAX))),
+        namedtype.NamedType('bmpString', char.BMPString().subtype(subtypeSpec=constraint.ValueSizeConstraint(1, MAX))),
+        namedtype.NamedType('ia5String', char.IA5String().subtype(subtypeSpec=constraint.ValueSizeConstraint(1, MAX))) # hm, this should not be here!? XXX
+        )
+
+class AttributeValue(DirectoryString): pass
+
+class AttributeType(univ.ObjectIdentifier): pass
+
+class AttributeTypeAndValue(univ.Sequence):
+    componentType = namedtype.NamedTypes(
+        namedtype.NamedType('type', AttributeType()),
+        namedtype.NamedType('value', AttributeValue())
+        )
+
+class RelativeDistinguishedName(univ.SetOf):
+    componentType = AttributeTypeAndValue()
+
+class RDNSequence(univ.SequenceOf):
+    componentType = RelativeDistinguishedName()
+
+class Name(univ.Choice):
+    componentType = namedtype.NamedTypes(
+        namedtype.NamedType('', RDNSequence())
+        )
+                          
+class AlgorithmIdentifier(univ.Sequence):
+    componentType = namedtype.NamedTypes(
+        namedtype.NamedType('algorithm', univ.ObjectIdentifier()),
+        namedtype.OptionalNamedType('parameters', univ.Null())
+        # XXX syntax screwed?
+#        namedtype.OptionalNamedType('parameters', univ.ObjectIdentifier())
+        )
+
+class Extension(univ.Sequence):
+    componentType = namedtype.NamedTypes(
+        namedtype.NamedType('extnID', univ.ObjectIdentifier()),
+        namedtype.DefaultedNamedType('critical', univ.Boolean('False')),
+        namedtype.NamedType('extnValue', univ.OctetString())
+        )
+
+class Extensions(univ.SequenceOf):
+    componentType = Extension()
+    sizeSpec = univ.SequenceOf.sizeSpec + constraint.ValueSizeConstraint(1, MAX)
+
+class SubjectPublicKeyInfo(univ.Sequence):
+     componentType = namedtype.NamedTypes(
+         namedtype.NamedType('algorithm', AlgorithmIdentifier()),
+         namedtype.NamedType('subjectPublicKey', univ.BitString())
+         )
+
+class UniqueIdentifier(univ.BitString): pass
+
+class Time(univ.Choice):
+    componentType = namedtype.NamedTypes(
+        namedtype.NamedType('utcTime', useful.UTCTime()),
+        namedtype.NamedType('generalTime', useful.GeneralizedTime())
+        )
+    
+class Validity(univ.Sequence):
+    componentType = namedtype.NamedTypes(
+        namedtype.NamedType('notBefore', Time()),
+        namedtype.NamedType('notAfter', Time())
+        )
+
+class CertificateSerialNumber(univ.Integer): pass
+
+class Version(univ.Integer):
+    namedValues = namedval.NamedValues(
+        ('v1', 0), ('v2', 1), ('v3', 2)
+        )
+
+class TBSCertificate(univ.Sequence):
+    componentType = namedtype.NamedTypes(
+        namedtype.DefaultedNamedType('version', Version('v1', tagSet=Version.tagSet.tagExplicitly(tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 0)))),
+        namedtype.NamedType('serialNumber', CertificateSerialNumber()),
+        namedtype.NamedType('signature', AlgorithmIdentifier()),
+        namedtype.NamedType('issuer', Name()),
+        namedtype.NamedType('validity', Validity()),
+        namedtype.NamedType('subject', Name()),
+        namedtype.NamedType('subjectPublicKeyInfo', SubjectPublicKeyInfo()),
+        namedtype.OptionalNamedType('issuerUniqueID', UniqueIdentifier().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1))),
+        namedtype.OptionalNamedType('subjectUniqueID', UniqueIdentifier().subtype(implicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 2))),
+        namedtype.OptionalNamedType('extensions', Extensions().subtype(explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3)))
+        )
+
+class Certificate(univ.Sequence):
+    componentType = namedtype.NamedTypes(
+        namedtype.NamedType('tbsCertificate', TBSCertificate()),
+        namedtype.NamedType('signatureAlgorithm', AlgorithmIdentifier()),
+        namedtype.NamedType('signatureValue', univ.BitString())
+        )
+
+# end of ASN.1 data structures
+
+
+def get_bits(substrate):
+    certType = Certificate()
+    cert = decoder.decode(substrate, asn1Spec=certType)[0]
+    n = cert
+    for name in ('tbsCertificate', 'subjectPublicKeyInfo', 
+	    'subjectPublicKey'):
+	n = n.getComponentByName(name)
+	if n == None: raise RuntimeException("Bad cert")
+
+    b = []
+    for i in range(0, len(n), 8):
+	v = 0
+	for j in range(0, 8):
+	    v = (v << 1) + n[i+j]
+	b.append(v)
+
+    return str().join([chr(x) for x in b])
+
+
+def get_key_bits_from_file(fname):
+    stSpam, stHam, stDump = 0, 1, 2
+    state = stSpam
+
+    f = open(fname)
+
+    for certLine in f:
+	certLine = string.strip(certLine)
+	if state == stSpam:
+	    if certLine == '-----BEGIN CERTIFICATE-----':
+		substrate = ""
+		state = stHam
+		continue
+	if state == stHam:
+	    if certLine == '-----END CERTIFICATE-----':
+		state = stDump
+	    else:
+		substrate = substrate + base64.b64decode(certLine)
+	if state == stDump:
+	    f.close()
+	    return get_bits(substrate)
+
+def get_key_bits_from_cert(cert):
+    return get_bits(cert.as_der())
Index: fedd/fedd.py
===================================================================
--- fedd/fedd.py	(revision 6ff0b91afedbb5c0b726a21c0c6cb3f24c66cebc)
+++ fedd/fedd.py	(revision 6ff0b91afedbb5c0b726a21c0c6cb3f24c66cebc)
@@ -0,0 +1,146 @@
+#!/usr/local/bin/python
+
+import sys
+
+from BaseHTTPServer import BaseHTTPRequestHandler
+
+from ZSI import Fault, ParseException, FaultFromNotUnderstood, \
+    FaultFromZSIException, FaultFromException, ParsedSoap, SoapWriter
+
+from M2Crypto import SSL
+from M2Crypto.SSL.SSLServer import SSLServer
+
+from optparse import OptionParser
+
+from fedd_util import fedd_ssl_context, fedid
+from fedd_proj import new_feddservice
+
+# The SSL server here is based on the implementation described at
+# http://www.xml.com/pub/a/ws/2004/01/20/salz.html
+
+# Turn off the matching of hostname to certificate ID
+SSL.Connection.clientPostConnectionCheck = None
+
+class fedd_server(SSLServer):
+    def __init__(self, ME, handler, ssl_ctx, impl):
+	SSLServer.__init__(self, ME, handler, ssl_ctx)
+	self.impl = impl
+
+class fedd_handler(BaseHTTPRequestHandler):
+    server_version = "ZSI/2.0 fedd/0.1 " + BaseHTTPRequestHandler.server_version
+
+    namespace = 'http://www.isi.edu/faber/fedd.wsdl'
+    methods = {
+	    'RequestAccess': 'soap_RequestAccess',
+	    }
+
+
+    def send_xml(self, text, code=200):
+	"""Send an XML document as reply"""
+	self.send_response(code)
+	self.send_header('Content-type', 'text/xml; charset="utf-8"')
+	self.send_header('Content-Length', str(len(text)))
+	self.end_headers()
+	self.wfile.write(text)
+	self.wfile.flush()
+
+    def send_fault(self, f, code=500):
+	"""Send a SOAP encoded fault as reply"""
+	self.send_xml(f.AsSOAP(), code)
+
+    def check_headers(self, ps):
+	"""Send a fault for any required envelope headers"""
+	for (uri, localname) in ps.WhatMustIUnderstand():
+	    self.send_fault(FaultFromNotUnderstood(uri, lname, 'fedd'))
+	    return False
+	return  True
+
+    def check_method(self, ps):
+	"""Confirm that this class implements the namespace and SOAP method"""
+	root = ps.body_root
+	if root.namespaceURI != fedd_handler.namespace:
+	    self.send_fault(Fault(Fault.Client, 
+		'Unknown namespace "%s"' % root.namespaceURI))
+	    return False
+	if not fedd_handler.methods.has_key(root.localName):
+	    self.send_fault(Fault(Fault.Client, 'Unknown operation "%s"' % \
+		    root.localName))
+	    return False
+	return True
+
+    def do_POST(self):
+	"""Treat an HTTP POST request as a SOAP service call"""
+	try:
+	    cl = int(self.headers['content-length'])
+	    data = self.rfile.read(cl)
+	    ps = ParsedSoap(data)
+	except ParseException, e:
+	    self.send_fault(FaultFromZSIException(e))
+	    return
+	except Exception, e:
+	    self.send_fault(FaultFromException(e, 0, sys.exc_info()[2]))
+	    return
+	if not self.check_headers(ps): return
+	if not self.check_method(ps): return
+	try:
+	    # We know this will work because check_method passed above
+	    method = getattr(self.server.impl,
+		    fedd_handler.methods[ps.body_root.localName])
+	    resp = method(ps, fedid(cert=self.request.get_peer_cert()))
+	except Fault, f:
+	    self.send_fault(f)
+	    resp = None
+	
+	if resp != None:
+	    sw = SoapWriter()
+	    sw.serialize(resp)
+	    self.send_xml(str(sw))
+
+class fedd_opts(OptionParser):
+    """Encapsulate option processing in this class, rather than in main"""
+    def __init__(self):
+	OptionParser.__init__(self, usage="%prog [opts] (--help for details)",
+		version="0.1")
+
+	self.set_defaults(host="localhost", port=23235, debug=0)
+
+	self.add_option("-d", "--debug", action="count", dest="debug", 
+		help="Set debug.  Repeat for more information")
+	self.add_option("-f", "--configfile", action="store",
+		dest="configfile", help="Configuration file (required)")
+	self.add_option("-H", "--host", action="store", type="string",
+		dest="host", help="Hostname to listen on (default %default)")
+	self.add_option("-p", "--port", action="store", type="int",
+		dest="port", help="Port to listen on (default %default)")
+	self.add_option("--trace", action="store_const", dest="tracefile", 
+		const=sys.stderr, help="Print SOAP exchange to stderr")
+
+opts, args = fedd_opts().parse_args()
+
+if opts.configfile != None: 
+    try:
+	impl = new_feddservice(opts.configfile)
+    except RuntimeError, e:
+	sys.exit("Error configuring fedd: %s" % e.message)
+else: 
+    sys.exit("--configfile is required")
+
+SOAP_port = (opts.host, opts.port)
+
+if impl.cert_file == None:
+    sys.exit("Must supply certificate file (probably in config)")
+
+bad_password = True
+while bad_password:
+    try:
+	ctx = fedd_ssl_context(impl.cert_file, impl.trusted_certs, 
+		password=impl.cert_pwd)
+	bad_password = False
+    except SSL.SSLError, e:
+	if e.message == "bad decrypt" and impl.cert_pwd == None:
+	    bad_password = True
+	else:
+	    raise
+
+s = fedd_server(SOAP_port, fedd_handler, ctx, impl)
+s.serve_forever()
Index: fedd/fedd.wsdl
===================================================================
--- fedd/fedd.wsdl	(revision 6ff0b91afedbb5c0b726a21c0c6cb3f24c66cebc)
+++ fedd/fedd.wsdl	(revision 6ff0b91afedbb5c0b726a21c0c6cb3f24c66cebc)
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+<definitions name="fedd"
+  targetNamespace="http://www.isi.edu/faber/fedd.wsdl"
+  xmlns:tns="http://www.isi.edu/faber/fedd_messages"
+  xmlns:bns="http://www.isi.edu/faber/fedd_bindings"
+  xmlns:xsd1="http://www.isi.edu/faber/fedd_types"
+  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
+  xmlns="http://schemas.xmlsoap.org/wsdl/">
+  
+  <import namespace="http://www.isi.edu/faber/fedd_types" 
+    location="fedd_types.xsd"/>
+
+  <import namespace="http://www.isi.edu/faber/fedd_messages" 
+    location="fedd_messages.wsdl"/>
+
+  <import namespace="http://www.isi.edu/faber/fedd_bindings" 
+    location="fedd_bindings.wsdl"/>
+</definitions>
+
Index: fedd/fedd_bindings.wsdl
===================================================================
--- fedd/fedd_bindings.wsdl	(revision 6ff0b91afedbb5c0b726a21c0c6cb3f24c66cebc)
+++ fedd/fedd_bindings.wsdl	(revision 6ff0b91afedbb5c0b726a21c0c6cb3f24c66cebc)
@@ -0,0 +1,49 @@
+<?xml version="1.0"?>
+<definitions name="fedd"
+  targetNamespace="http://www.isi.edu/faber/fedd_bindings"
+  xmlns:tns="http://www.isi.edu/faber/fedd_messages"
+  xmlns:bns="http://www.isi.edu/faber/fedd_bindings"
+  xmlns:xsd1="http://www.isi.edu/faber/fedd_types"
+  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
+  xmlns="http://schemas.xmlsoap.org/wsdl/">
+
+  <binding name="feddBinding" type="tns:feddPortType">
+      <documentation>
+	These are really very straight ahead SOAP RPC bindings.  They're 1.1
+	SOAP and not much outside the defaults.  No interesting header packing
+	or fault configuration yet.
+      </documentation>
+    <soap:binding style="rpc" 
+      transport="http://schemas.xmlsoap.org/soap/http"/>
+      <operation name="RequestAccess">
+	<documentation>
+	  The bindings of this operation are straightforward SOAP RPC 1.1.
+	</documentation>
+	<soap:operation soapAction=""/> 
+	<input>
+	  <soap:body use="encoded" part="tns:RequestAccessRequestBody"
+	    namespace="http://www.isi.edu/faber/fedd.wsdl"
+	    encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
+	</input>
+	<output>
+	  <soap:body use="encoded" part="tns:RequestAccessResponseBody"
+	    namespace="http://www.isi.edu/faber/fedd.wsdl"
+	    encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
+	</output>
+	<fault>
+	  <soap:fault use="encoded"  name="tns:RequestAccessFault"
+	    namespace="http://www.isi.edu/faber/fedd.wsdl"
+	    encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
+	</fault>
+      </operation>
+    </binding>
+
+    <service name="feddService">
+      <documentation>
+	Fedd resource access service
+      </documentation>
+      <port name="feddPort" binding="bns:feddBinding">
+	<soap:address location="http://www.isi.edu/faber/fedd"/>
+      </port>
+    </service>
+</definitions>
Index: fedd/fedd_client.py
===================================================================
--- fedd/fedd_client.py	(revision 6ff0b91afedbb5c0b726a21c0c6cb3f24c66cebc)
+++ fedd/fedd_client.py	(revision 6ff0b91afedbb5c0b726a21c0c6cb3f24c66cebc)
@@ -0,0 +1,313 @@
+#!/usr/local/bin/python
+
+import sys
+import os
+import pwd
+
+from fedd_services import *
+
+from M2Crypto import SSL, X509
+import M2Crypto.httpslib
+
+from fedd_util import *
+
+from optparse import OptionParser, OptionValueError
+
+# Turn off the matching of hostname to certificate ID
+SSL.Connection.clientPostConnectionCheck = None
+
+class IDFormatException(RuntimeError): pass
+
+class access_method:
+    """Encapsulates an access method generically."""
+    (type_ssh, type_x509, type_pgp) = ('sshPubkey', 'X509', 'pgpPubkey')
+    default_type = type_ssh
+    def __init__(self, buf=None, type=None, file=None):
+	self.buf = buf
+
+	if type != None: self.type = type
+	else: self.type = access_method.default_type
+
+	if file != None:
+	    self.readfile(file)
+    
+    def readfile(self, file, type=None):
+	f = open(file, "r")
+	self.buf = f.read();
+	f.close()
+	if type == None:
+	    if self.type == None:
+		self.type = access_method.default_type
+	else:
+	    self.type = type;
+    
+class node_desc:
+    def __init__(self, image, hardware, count=1):
+	if getattr(image, "__iter__", None) == None:
+	    if image == None: self.image = [ ]
+	    else: self.image = [ image ]
+	else:
+	    self.image = image
+
+	if getattr(hardware, "__iter__", None) == None: 
+	    if hardware == None: self.hardware = [ ]
+	    else: self.hardware = [ hardware ]
+	else:
+	    self.hardware = hardware
+	if count != None: self.count = int(count)
+	else: self.count = 1
+
+class fedd_client_opts(OptionParser):
+    """Encapsulate option processing in this class, rather than in main"""
+    def __init__(self):
+	OptionParser.__init__(self, usage="%prog [opts] (--help for details)",
+		version="0.1")
+
+	self.set_defaults(url="https://localhost:23235", anonymous=False,
+		use_fedid=False, debug=0)
+
+	self.add_option("-a","--anonymous", action="store_true",
+		dest="anonymous", help="Do not include a user in the request")
+	self.add_option("-c","--cert", action="store", dest="cert",
+		type="string", help="my certificate file")
+	self.add_option("-d", "--debug", action="count", dest="debug", 
+		help="Set debug.  Repeat for more information")
+	self.add_option("-f","--useFedid", action="store_true",
+		dest="use_fedid",
+		help="Use a fedid derived from my certificate as user identity")
+	self.add_option("-k", "--sshKey", action="callback", type="string", 
+		callback=add_ssh_key, callback_args=(access_keys,),
+		help="ssh key for access (can be supplied more than once")
+	self.add_option("-K", "--x509Key", action="callback", type="string", 
+		callback=add_x509_cert, callback_args=(access_keys,),
+		help="X509 certificate for access " + \
+			"(can be supplied more than once")
+	self.add_option("-l","--label", action="store", dest="label",
+		type="string", help="Label for output")
+	self.add_option("-n", "--node", action="callback", type="string", 
+		callback=add_node_desc, callback_args=(node_descs,),
+		help="Node description: image:hardware[:count]")
+	self.add_option("-p", "--project", action="store", dest="project", 
+		type="string",
+		help="Use a project request with this project name")
+	self.add_option("-t", "--testbed", action="store", dest="testbed",
+		type="string",
+		help="Testbed identifier (URI) to contact (required)")
+	self.add_option("-T","--trusted", action="store", dest="trusted",
+		type="string", help="Trusted certificates (required)")
+	self.add_option("-u", "--url", action="store", dest="url",
+		type="string",
+		help="URL to connect to (default %default)")
+	self.add_option("-U", "--username", action="store", dest="user",
+		type="string", help="Use this username instead of the uid")
+	self.add_option("--trace", action="store_const", dest="tracefile", 
+		const=sys.stderr, help="Print SOAP exchange to stderr")
+
+
+
+def print_ID(id, out=sys.stdout, prefix=None, no_nl=False):
+    """Print an ID element (which may have many types) to as a string
+    
+    If out is given, that is the output destination. If prefix is given that
+    string is prepended.  If no_nl is given, the newline is not printed.
+    """
+    # This comprehension selects all accessors from id of the form
+    # get_element_*, calls those in turn and returns a list of non-None ones.
+    # For valid ID elements that will contain exactly one element, which we
+    # print.  Anything else is an error.
+    names = [ getattr(id, method)() for method in dir(id) \
+		    if method.startswith("get_element_") \
+			and callable(getattr(id, method)) 
+			and getattr(id,method)() != None ]
+    if len(names) == 1:
+	if prefix == None: print >>out, names[0],
+	else: print >>out, "%s%s" % (prefix,names[0]),
+	if not no_nl: print >>out
+    else:
+	raise IDFormatException()
+
+
+def print_users(ul, out=sys.stdout):
+    """
+    Print a list of users, with "User: " prepended, one to a line on out.
+    
+    @param ul sequence of user elements
+    @param out output object.  Defaults to sys.stdout
+    """
+    if ul != None:
+	for u in ul:
+	    print_ID(u.get_element_userID(), out, "User: ")
+
+
+def print_response_as_testbed(resp, label, out=sys.stdout):
+    """Print the response as input to the splitter script"""
+    emulab_data = { 
+	    "Boss: ": "get_element_boss",
+	    "OpsNode: ": "get_element_ops",
+	    "Domain: ": "get_element_domain",
+	    "FileServer: ": "get_element_fileServer",
+	    "EventServer: ": "get_element_eventServer",
+	    }
+    if (label != None): print >> out, "[%s]" % label
+    e = resp.get_element_emulab()
+    if ( e != None):
+	p = e.get_element_project()
+	if ( p != None ):
+	    print_ID(p.get_element_name(), out, "Project: ")
+	    print_users(p.get_element_user(), out)
+	for k, m in emulab_data.iteritems():
+	    method = getattr(e, m);
+	    if method != None and callable(method):
+		print >> out, "%s%s" % (k, method())
+	for a in e.get_element_fedAttr():
+	    print >>out, "%s: %s" % \
+		    (a.get_element_attribute(), a.get_element_value())
+def add_ssh_key(option, opt_str, value, parser, access_keys):
+    try:
+	access_keys.append(access_method(file=value,
+	    type=access_method.type_ssh))
+    except IOError, (errno, strerror):
+	raise OptionValueError("Cannot generate sshPubkey from %s: %s (%d)" %
+		(value,strerror,errno))
+
+def add_x509_cert(option, opt_str, value, parser, access_keys):
+    try:
+	access_keys.append(access_method(file=value,
+	    type=access_method.type_x509))
+    except IOError, (errno, strerror):
+	raise OptionValueError("Cannot read x509 cert from %s: %s (%d)" %
+		(value,strerror,errno))
+
+def add_node_desc(option, opt_str, value, parser, node_descs):
+    def none_if_zero(x):
+	if len(x) > 0: return x
+	else: return None
+
+    params = map(none_if_zero, value.split(":"));
+    
+    if len(params) < 4 and len(params) > 1:
+	node_descs.append(node_desc(*params))
+    else:
+	raise OptionValueError("Bad node description: %s" % value)
+
+def get_user_info(access_keys):
+    pw = pwd.getpwuid(os.getuid());
+    try_cert=None
+    user = None
+
+    if pw != None:
+	user = pw[0]
+	try_cert = "%s/.ssl/emulab.pem" % pw[5];
+	if not os.access(try_cert, os.R_OK):
+	    try_cert = None
+	if len(access_keys) == 0:
+	    for k in ["%s/.ssh/id_rsa.pub", "%s/.ssh/id_dsa.pub", 
+		    "%s/.ssh/identity.pub"]:
+		try_key = k % pw[5];
+		if os.access(try_key, os.R_OK):
+		    access_keys.append(access_method(file=try_key,
+			type=access_method.type_ssh))
+		    break
+    return (user, try_cert)
+
+access_keys = []
+node_descs = []
+proj = None
+
+# Process the options using the customized option parser defined above
+(opts, args) = fedd_client_opts().parse_args()
+
+if opts.testbed == None:
+    parser.error("--testbed is required")
+
+if opts.trusted != None:
+    if ( not os.access(opts.trusted, os.R_OK) ) :
+	sys.exit("Cannot read trusted certificates (%s)" % opts.trusted)
+else:
+    parser.error("--trusted is required")
+
+if opts.debug > 0: opts.tracefile=sys.stderr
+
+(user, cert) = get_user_info(access_keys)
+
+if opts.user: user = opts.user
+
+if opts.cert != None: cert = opts.cert
+
+if cert == None:
+    sys.exit("No certificate given (--cert) or found")
+
+if os.access(cert, os.R_OK):
+    fid = fedid(file=cert)
+    if opts.use_fedid == True:
+	user = fid
+else:
+    sys.exit("Cannot read certificate (%s)" % cert)
+
+context = None
+while context == None:
+    try:
+	context = fedd_ssl_context(cert, opts.trusted)
+    except SSL.SSLError, e:
+	# Yes, doing this on message type is not ideal.  The string comes from
+	# OpenSSL, so check there is this stops working.
+	if e.message == "bad decrypt": 
+	    print >>sys.stderr, "Bad Passphrase given."
+	else: raise
+
+loc = feddServiceLocator();
+port = loc.getfeddPortType(opts.url,
+	transport=M2Crypto.httpslib.HTTPSConnection, 
+	transdict={ 'ssl_context' : context },
+	tracefile=opts.tracefile)
+
+req = RequestAccessRequestMessage()
+
+msg = {
+	'allocID': pack_id('test alloc'),
+	'destinationTestbed': pack_id(opts.testbed),
+	'access' : [ { a.type: a.buf } for a in access_keys ],
+	}
+
+if len(node_descs) > 0:
+    msg['resources'] = { 
+	    'node': [ 
+		{ 
+		    'image':  n.image ,
+		    'hardware':  n.hardware,
+		    'count': n.count,
+		} for n in node_descs],
+	    }
+
+if opts.project != None:
+    if not opts.anonymous and user != None:
+	msg['project'] = {
+		'name': pack_id(opts.project),
+		'user': [ { 'userID': pack_id(user) } ],
+		}
+    else:
+	msg['project'] = { 'name': pack_id(opts.project) }
+else:
+    if not opts.anonymous and user != None:
+	msg['user'] = [ { 'userID': pack_id(user) } ]
+    else:
+	msg['user'] = [];
+
+req.set_element_RequestAccessRequestBody(
+	pack_soap(req, "RequestAccessRequestBody", msg))
+
+
+try:
+    resp = port.RequestAccess(req)
+except ZSI.FaultException, e:
+    sys.exit("Fault: %s" % e)
+
+if (resp != None):
+    resp_body = resp.get_element_RequestAccessResponseBody()
+    if ( resp_body != None): 
+	try:
+	    print_response_as_testbed(resp_body, opts.label)
+	except RuntimeError, e:
+	    sys.exit("Bad response. %s" % e.messgae)
+    else: sys.exit("No body in resonpse!?")
+else: sys.exit("No response?!?")
Index: fedd/fedd_messages.wsdl
===================================================================
--- fedd/fedd_messages.wsdl	(revision 6ff0b91afedbb5c0b726a21c0c6cb3f24c66cebc)
+++ fedd/fedd_messages.wsdl	(revision 6ff0b91afedbb5c0b726a21c0c6cb3f24c66cebc)
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<definitions name="fedd"
+  targetNamespace="http://www.isi.edu/faber/fedd_messages"
+  xmlns:tns="http://www.isi.edu/faber/fedd_messages"
+  xmlns:xsd1="http://www.isi.edu/faber/fedd_types"
+  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
+  xmlns="http://schemas.xmlsoap.org/wsdl/">
+
+  <message name="RequestAccessRequestMessage">
+    <part name="RequestAccessRequestBody" type="xsd1:requestType"/>
+  </message>
+
+  <message name="RequestAccessResponseMessage">
+    <part name="RequestAccessResponseBody" type="xsd1:responseType"/>
+  </message>
+
+
+  <portType name="feddPortType">
+    <operation name="RequestAccess">
+      <documentation>
+	Fill this in 
+      </documentation>
+      <input message="tns:RequestAccessRequestMessage"/>
+      <output message="tns:RequestAccessResponseMessage"/>
+      <fault name="RequestAccessFault" 
+	message="tns:RequestAccessResponseMessage"/>
+    </operation>
+  </portType>
+</definitions>
Index: fedd/fedd_proj.py
===================================================================
--- fedd/fedd_proj.py	(revision 6ff0b91afedbb5c0b726a21c0c6cb3f24c66cebc)
+++ fedd/fedd_proj.py	(revision 6ff0b91afedbb5c0b726a21c0c6cb3f24c66cebc)
@@ -0,0 +1,624 @@
+#!/usr/local/bin/python
+
+import os,sys
+
+from BaseHTTPServer import BaseHTTPRequestHandler
+from ZSI import *
+from M2Crypto import SSL
+from M2Crypto.SSL.SSLServer import SSLServer
+import M2Crypto.httpslib
+
+import re
+import random
+import string
+import subprocess
+import tempfile
+
+from fedd_services import *
+from fedd_util import *
+
+class fedd_proj:
+    """
+    The implementation of access control based on mapping users to projects.
+
+    Users can be mapped to existing projects or have projects created
+    dynamically.  This implements both direct requests and proxies.
+    """
+    # Attributes that can be parsed from the configuration file
+    bool_attrs = ("dynamic_projects", "project_priority")
+    emulab_attrs = ("boss", "ops", "domain", "fileserver", "eventserver")
+    id_attrs = ("testbed", "cert_file", "trusted_certs", "proxy",
+	    "proxy_trusted_certs", "cert_pwd")
+
+    class access_project:
+	"""
+	A project description used to grant access to this testbed.
+
+	The description includes a name and a list of node types to which the
+	project will be granted access.
+	"""
+	def __init__(self, name, nt):
+	    self.name = name
+	    self.node_types = list(nt)
+
+	def __repr__(self):
+	    if len(self.node_types) > 0:
+		return "access_proj('%s', ['%s'])" % \
+			(self.name, str("','").join(self.node_types))
+	    else:
+		return "access_proj('%s', [])" % self.name
+
+
+    class parse_error(RuntimeError):
+	"""Raised if the configuration file is unparsable"""
+	pass
+
+    def __init__(self, config=None):
+	"""
+	Initializer.  Parses a configuration if one is given.
+	"""
+
+	# Create instance attributes from the static lists
+	for a in fedd_proj.bool_attrs:
+	    setattr(self, a, False)
+
+	for a in fedd_proj.emulab_attrs + fedd_proj.id_attrs:
+	    setattr(self, a, None)
+
+	# Other attributes
+	self.attrs = {}
+	self.access = {}
+	self.fedid_category = {}
+	self.fedid_default = "user"
+	self.restricted = []
+	self.wap = '/usr/testbed/sbin/wap'
+	self.newproj = '/usr/testbed/sbin/newproj'
+	self.mkproj = '/usr/testbed/sbin/mkproj'
+	self.grantnodetype = '/usr/testbed/sbin/grantnodetype'
+
+	# Read the configuration
+	if config != None: 
+	    self.read_config(config)
+
+    def dump_state(self):
+	"""
+	Dump the state read from a configuration file.  Mostly for debugging.
+	"""
+	for a in fedd_proj.bool_attrs:
+	    print "%s: %s" % (a, getattr(self, a ))
+	for a in fedd_proj.emulab_attrs + fedd_proj.id_attrs:
+	    print "%s: %s" % (a, getattr(self, a))
+	for k, v in self.attrs.iteritems():
+	    print "%s %s" % (k, v)
+	print "Access DB:"
+	for k, v in self.access.iteritems():
+	    print "%s %s" % (k, v)
+	print "Trust DB:"
+	for k, v in self.fedid_category.iteritems():
+	    print "%s %s" % (k, v)
+	print "Restricted: %s" % str(',').join(sorted(self.restricted))
+
+    def get_id(self, id):
+	"""
+	Utility to get an object from the polymorphic IDType.
+	
+	Only fedids and usernames are currently understood.  If neither is
+	present None is returned.  If both are present (which is a bug) the
+	fedid is returned.
+	"""
+	if id == None: 
+	    return None
+	elif getattr(id, "get_element_fedid", None) != None:
+	    return fedid(id.get_element_fedid())
+	elif getattr(id, "get_element_username", None) != None:
+	    return id.get_element_username()
+	else:
+	    return None
+
+    def get_users(self, obj):
+	"""
+	Return a list of the IDs of the users in obj (accessed using get_id)
+	"""
+	if obj.get_element_user() != None:
+	    return [ self.get_id(u.get_element_userID()) \
+		    for u in obj.get_element_user() \
+			if u.get_element_userID() != None]
+	else:
+	    return None
+
+    def random_string(self, s, n=3):
+	"""Append n random ASCII characters to s and return the string"""
+	rv = s
+	for i in range(0,n):
+	    rv += random.choice(string.ascii_letters)
+	return rv
+
+    def write_attr_xml(self, file, root, lines):
+	"""
+	Write an emulab config file for a dynamic project.
+
+	Format is <root><attribute name=lines[0]>lines[1]</attribute></root>
+	"""
+	# Convert a pair to an attribute line
+	out_attr = lambda a,v : \
+		'<attribute name="%s"><value>%s</value></attribute>' % (a, v)
+
+	f = os.fdopen(file, "w")
+	f.write("<%s>\n" % root)
+	f.write("\n".join([out_attr(*l) for l in lines]))
+	f.write("</%s>\n" % root)
+	f.close()
+
+
+    def dynamic_project(self, found, ssh):
+	"""Create a dynamic project with ssh access"""
+	user_fields = [
+		("name", "Federation User %s" % found[1]),
+		("email", "%s-fed@isi.deterlab.net" % found[1]),
+		("password", self.random_string("", 8)),
+		("login", found[1]),
+		("address", "4676 Admiralty"),
+		("city", "Marina del Rey"),
+		("state", "CA"),
+		("zip", "90292"),
+		("country", "USA"),
+		("phone", "310-448-9190"),
+		("title", "None"),
+		("affiliation", "USC/ISI")
+	]
+
+	proj_fields = [
+		("name", found[0].name),
+		("short description", "dynamic federated project"),
+		("URL", "http://www.isi.edu/~faber"),
+		("funders", "USC/USU"),
+		("long description", "Federation access control"),
+		("public", "1"),
+		("num_pcs", "100"),
+		("linkedtous", "1")
+	]
+
+	# tempfiles for the parameter files
+	uf, userfile = tempfile.mkstemp(prefix="usr", suffix=".xml",
+		dir="/tmp")
+	pf, projfile = tempfile.mkstemp(prefix="proj", suffix=".xml",
+		dir="/tmp")
+
+	# A few more dynamic fields
+	for s in ssh:
+	    user_fields.append(("pubkey", s))
+	proj_fields.append(("newuser_xml", userfile))
+
+	# Write out the files
+	self.write_attr_xml(uf, "user", user_fields)
+	self.write_attr_xml(pf, "project", proj_fields)
+
+	# Generate the commands (only grantnodetype's are dynamic)
+	cmds = [
+		(self.wap, self.newproj, projfile),
+		(self.wap, self.mkproj, found[0].name)
+		]
+	for nt in found[0].node_types:
+	    cmds.append((self.wap, self.grantnodetype, '-p', found[0].name, nt))
+
+	# Create the projects
+	rc = 0
+	for cmd in cmds:
+	    if self.dynamic_projects:
+		try:
+		    rc = subprocess.call(cmd)
+		except OSerror, e:
+		    raise Fault(Fault.Server, 
+			    "Dynamic project subprocess creation error "+ \
+				    "[%s] (%s)" %  (cmd[1], e.strerror))
+	    else:
+		print >>sys.stdout, str(" ").join(cmd)
+
+	    if rc != 0: 
+		raise Fault(Fault.Server,
+			"Dynamic project subprocess error " +\
+				"[%s] (%d)" % (cmd[1], rc))
+	# Clean up tempfiles
+	os.unlink(userfile)
+	os.unlink(projfile)
+
+    def proxy_request(self, dt, req):
+	"""
+	Send req on to the real destination in dt and return the response
+
+	Req is just the requestType object.  This function re-wraps it.  It
+	also rethrows any faults.
+	"""
+	tc = self.proxy_trusted_certs or self.trusted_certs
+
+	# No retry loop here.  Proxy servers must correctly authenticate
+	# themselves without help
+	try:
+	    ctx = fedd_ssl_context(self.cert_file, tc, password=self.cert_pwd)
+	except SSL.SSLError:
+	    raise Fault(Fault.Server, "Server certificates misconfigured")
+
+	loc = feddServiceLocator();
+	port = loc.getfeddPortType(dt,
+		transport=M2Crypto.httpslib.HTTPSConnection, 
+		transdict={ 'ssl_context' : ctx })
+
+	# Reconstruct the full request message
+	msg = RequestAccessRequestMessage()
+	msg.set_element_RequestAccessRequestBody(req)
+
+	try:
+	    resp = port.RequestAccess(msg)
+	except ZSI.FaultException, e:
+	    raise e.fault
+	return resp
+
+    def permute_wildcards(self, a, p):
+	"""Return a copy of a with various fields wildcarded.
+
+	The bits of p control the wildcards.  A set bit is a wildcard
+	replacement with the lowest bit being user then project then testbed.
+	"""
+	if p & 1: user = ["<any>"]
+	else: user = a[2]
+	if p & 2: proj = "<any>"
+	else: proj = a[1]
+	if p & 4: tb = "<any>"
+	else: tb = a[0]
+
+	return (tb, proj, user)
+
+    def find_access(self, search):
+	"""
+	Search the access DB for a match on this tuple.  Return the matching
+	access tuple and the user that matched.
+	
+	NB, if the initial tuple fails to match we start inserting wildcards in
+	an order determined by self.project_priority.  Try the list of users in
+	order (when wildcarded, there's only one user in the list).
+	"""
+	if self.project_priority: perm = (0, 1, 2, 3, 4, 5, 6, 7)
+	else: perm = (0, 2, 1, 3, 4, 6, 5, 7)
+
+	for p in perm: 
+	    s = self.permute_wildcards(search, p)
+	    # s[2] is None on an anonymous, unwildcarded request
+	    if s[2] != None:
+		for u in s[2]:
+		    if self.access.has_key((s[0], s[1], u)):
+			return (self.access[(s[0], s[1], u)], u)
+	    else:
+		if self.access.has_key(s):
+		    return (self.access[s], None)
+	return None
+
+    def lookup_access(self, req, fid):
+	"""
+	Determine the allowed access for this request.  Return the access and
+	which fields are dynamic.
+
+	The fedid is needed to construct the request
+	"""
+	tb = None
+	project = None
+	user = None
+
+	principal_type = self.fedid_category.get(fid, self.fedid_default)
+
+	if principal_type == "testbed": tb = fid
+
+	p = req.get_element_project()
+	if p != None:
+	    if p.get_element_name() != None: 
+		project = self.get_id(p.get_element_name())
+	    user = self.get_users(p)
+	else:
+	    user = self.get_users(req)
+
+	# Now filter by prinicpal type
+	if principal_type == "user":
+	    if user != None:
+		fedids = [ u for u in user if isinstance(u, type(fid))]
+		if len(fedids) > 1:
+		    raise Fault(Fault.Client,
+			    "User asserting multiple fedids")
+		elif len(fedids) == 1 and fedids[0] != fid:
+		    raise Fault(Fault.Client, 
+			    "User asserting different fedid")
+	    project = None
+	    tb = None
+	elif principal_type == "project":
+	    if isinstance(project, type(fid)) and fid != project:
+		raise Fault(Fault.Client,
+			"Project asserting different fedid")
+	    tb = None
+
+	# Ready to look up access
+	print "Lookup %s %s %s: " % (tb, project, user)
+	found, user_match = self.find_access((tb, project, user))
+	print "Found: ", found
+	
+	if found == None: raise Fault(Fault.Server, "Access denied")
+
+	# resolve <dynamic> and <same> in found
+	dyn_proj = False
+	dyn_user = False
+
+	if found[0].name == "<same>":
+	    if project != None:
+		found[0].name = project
+	    else : 
+		raise Fault(Fault.Server, 
+			"Project matched <same> when no project given")
+	elif found[0].name == "<dynamic>":
+	    found[0].name = self.random_string("project", 3)
+	    dyn_proj = True
+
+	if found[1] == "<same>":
+	    if user_match == "<any>":
+		if user != None: found = (found[0], user[0])
+		else: raise Fault(Fault.Server,
+			"Matched <same> on anonymous request")
+	    else:
+		found = (found[0], user_match)
+	elif found[1] == "<dynamic>":
+	    found = (found[0], self.random_string("user", 4))
+	    dyn_user = True
+	
+	return found, (dyn_user, dyn_proj)
+
+    def build_response(self, resp, alloc_id, ap, ssh):
+	"""
+	Create the SOAP response.
+
+	Build the dictionary description of the response and use
+	fedd_utils.pack_soap to create the soap message.  NB that alloc_id is
+	a fedd_services_types.IDType_Holder pulled from the incoming message
+	"""
+	# Because alloc_id is already a fedd_services_types.IDType_Holder,
+	# there's no need to repack it
+	msg = { 
+		'allocID': alloc_id,
+		'emulab': { 
+		    'domain': self.domain,
+		    'boss': self.boss,
+		    'ops': self.ops,
+		    'fileServer': self.fileserver,
+		    'eventServer': self.eventserver,
+		    'project': {
+			'name': pack_id(ap[0].name),
+			'user': [ {
+			    'userID': pack_id(ap[1]),
+			    'access' : [ { 'sshPubkey': x } for x in ssh ],
+			    }
+			]
+		    }
+		},
+	    }
+	if len(self.attrs) > 0:
+	    msg['emulab']['fedAttr'] = \
+		[ { 'attribute': x, 'value' : y } \
+			for x,y in self.attrs.iteritems()]
+
+	resp.set_element_RequestAccessResponseBody(
+		pack_soap(resp, "RequestAccessResponseBody", msg))
+
+    def soap_RequestAccess(self, ps, fid):
+	req = ps.Parse(RequestAccessRequestMessage.typecode)
+	resp = RequestAccessResponseMessage()
+
+
+	if req == None: raise Fault(Fault.Client, "No request??")
+	req = req.get_element_RequestAccessRequestBody()
+	if req == None: raise Fault(Fault.Client, "No request body??")
+
+
+	dt = req.get_element_destinationTestbed()
+
+	if dt != None: dt = dt.get_element_uri()
+	
+	if dt == None or dt == self.testbed:
+	    # Request for this fedd
+	    found, dyn = self.lookup_access(req, fid)
+
+	    # Check for access to restricted nodes
+	    if req.get_element_resources() != None and \
+		    req.get_element_resources().get_element_node() != None:
+
+		resources = req.get_element_resources()
+		inaccessible = [ t for n in resources.get_element_node()\
+				if n.get_element_hardware() != None \
+				    for t in n.get_element_hardware()
+					if t in self.restricted and \
+						t not in found[0].node_types]
+		if len(inaccessible) > 0:
+		    raise Fault(Fault.Server, 
+			    "Access denied (nodetypes %s)" % \
+			    str(', ').join(inaccessible))
+
+	    ssh = [ x.get_element_sshPubkey() \
+		    for x in req.get_element_access() \
+			if x.get_element_sshPubkey() != None]
+
+	    if len(ssh) > 0: 
+		if dyn[1]: self.dynamic_project(found, ssh)
+		else: pass    # SSH key additions
+	    else:
+		raise Fault(Fault.Client, "SSH access parameters required")
+
+	    self.build_response(resp, req.get_element_allocID(), found, ssh)
+	    return resp
+	else:
+	    # Proxy the request
+	    return self.proxy_request(dt, req)
+
+
+    def read_trust(self, trust):
+	"""
+	Read a trust file that splits fedids into testbeds, users or projects
+
+	Format is:
+
+	[type]
+	fedid
+	fedid
+	default: type
+	"""
+	lineno = 0;
+	cat = None
+	cat_re = re.compile("\[(user|testbed|project)\]$", re.IGNORECASE)
+	fedid_re = re.compile("[" + string.hexdigits + "]+$")
+	default_re = re.compile("default:\s*(user|testbed|project)$", 
+		re.IGNORECASE)
+
+	f = open(trust, "r")
+	for line in f:
+	    lineno += 1
+	    line = line.strip()
+	    if len(line) == 0 or line.startswith("#"):
+		continue
+	    # Category line
+	    m = cat_re.match(line)
+	    if m != None:
+		cat = m.group(1).lower()
+		continue
+	    # Fedid line
+	    m = fedid_re.match(line)
+	    if m != None:
+		if cat != None:
+		    self.fedid_category[fedid(hexstr=m.string)] = cat
+		else:
+		    raise fedd_proj.parse_error(\
+			    "Bad fedid in trust file (%s) line: %d" % \
+			    (trust, lineno))
+		continue
+	    # default line
+	    m = default_re.match(line)
+	    if m != None:
+		self.fedid_default = m.group(1).lower()
+		continue
+	    # Nothing matched - bad line, raise exception
+	    f.close()
+	    raise fedd_proj.parse_error(\
+		    "Unparsable line in trustfile %s line %d" % (trust, lineno))
+	f.close()
+
+    def read_config(self, config):
+	"""
+	Read a configuration file and set internal parameters.
+
+	The format is more complex than one might hope.  The basic format is
+	attribute value pairs separated by colons(:) on a signle line.  The
+	attributes in bool_attrs, emulab_attrs and id_attrs can all be set
+	directly using the name: value syntax.  E.g. 
+	boss: hostname
+	sets self.boss to hostname.  In addition, there are access lines of the
+	form (tb, proj, user) -> (aproj, auser) that map the first tuple of
+	names to the second for access purposes.  Names in the key (left side)
+	can include "<NONE> or <ANY>" to act as wildcards or to require the
+	fields to be empty.  Similarly aproj or auser can be <SAME> or
+	<DYNAMIC> indicating that either the matching key is to be used or a
+	dynamic user or project will be created.  These names can also be
+	federated IDs (fedid's) if prefixed with fedid:.  Finally, the aproj
+	can be followed with a colon-separated list of node types to which that
+	project has access (or will have access if dynamic).
+	Testbed attributes outside the forms above can be given using the
+	format attribute: name value: value.  The name is a single word and the
+	value continues to the end of the line.  Empty lines and lines startin
+	with a # are ignored.
+
+	Parsing errors result in a parse_error exception being raised.
+	"""
+	lineno=0
+	name_expr = "["+string.ascii_letters + string.digits + "\.\-_]+"
+	fedid_expr = "fedid:[" + string.hexdigits + "]+"
+	key_name = "(<ANY>|<NONE>|"+fedid_expr + "|"+ name_expr + ")"
+	access_proj = "(<DYNAMIC>(?::" + name_expr +")*|"+ \
+		"<SAME>" + "(?::" + name_expr + ")*|" + \
+		fedid_expr + "(?::" + name_expr + ")*|" + \
+		name_expr + "(?::" + name_expr + ")*)"
+	access_name = "(<DYNAMIC>|<SAME>|" + fedid_expr + "|"+ name_expr + ")"
+
+	bool_re = re.compile('(' + '|'.join(fedd_proj.bool_attrs) + 
+		'):\s+(true|false)', re.IGNORECASE)
+	string_re = re.compile( "(" + \
+		'|'.join(fedd_proj.emulab_attrs + fedd_proj.id_attrs) + \
+		'):\s*(.*)', re.IGNORECASE)
+	attr_re = re.compile('attribute:\s*([\._\-a-z0-9]+)\s+value:\s*(.*)',
+		re.IGNORECASE)
+	access_re = re.compile('\('+key_name+'\s*,\s*'+key_name+'\s*,\s*'+
+		key_name+'\s*\)\s*->\s*\('+access_proj + '\s*,\s*' + 
+		access_name + '\s*\)', re.IGNORECASE)
+	trustfile_re = re.compile("trustfile:\s*(.*)", re.IGNORECASE)
+	restricted_re = re.compile("restricted:\s*(.*)", re.IGNORECASE)
+
+	def parse_name(n):
+	    if n.startswith('fedid:'): return fedid(n[len('fedid:'):])
+	    else: return n
+
+	f = open(config, "r");
+	for line in f:
+	    lineno += 1
+	    line = line.strip();
+	    if len(line) == 0 or line.startswith('#'):
+		continue
+
+	    # Boolean attribute line
+	    m = bool_re.match(line);
+	    if m != None:
+		attr, val = m.group(1,2)
+		setattr(self, attr.lower(), bool(val.lower() == "true"))
+		continue
+
+	    # String attribute line
+	    m = string_re.match(line)
+	    if m != None:
+		attr, val = m.group(1,2)
+		setattr(self, attr.lower(), val)
+		continue
+
+	    # Extended (attribute: x value: y) attribute line
+	    m = attr_re.match(line)
+	    if m != None:
+		attr, val = m.group(1,2)
+		self.attrs[attr] = val
+		continue
+
+	    # Access line (t, p, u) -> (ap, au) line
+	    m = access_re.match(line)
+	    if m != None:
+		access_key = tuple([ parse_name(x) for x in m.group(1,2,3)])
+		aps = m.group(4).split(":");
+		if aps[0] == 'fedid:':
+		    del aps[0]
+		    aps[0] = fedid(hexstr=aps[0])
+
+		au = m.group(5)
+		if au.startswith("fedid:"):
+		    au = fedid(hexstr=aus[len("fedid:"):])
+
+		access_val = (fedd_proj.access_project(aps[0], aps[1:]), au)
+
+		self.access[access_key] = access_val
+		continue
+
+	    # Trustfile inclusion
+	    m = trustfile_re.match(line)
+	    if m != None:
+		self.read_trust(m.group(1))
+		continue
+	    # Restricted node types
+
+	    m = restricted_re.match(line)
+	    if m != None:
+		self.restricted.append(m.group(1))
+		continue
+
+	    # Nothing matched to here: unknown line - raise exception
+	    f.close()
+	    raise fedd_proj.parse_error("Unknown statement at line %d of %s" % \
+		    (lineno, config))
+	f.close()
+
+def new_feddservice(configfile):
+    return fedd_proj(configfile)
Index: fedd/fedd_types.xsd
===================================================================
--- fedd/fedd_types.xsd	(revision 6ff0b91afedbb5c0b726a21c0c6cb3f24c66cebc)
+++ fedd/fedd_types.xsd	(revision 6ff0b91afedbb5c0b726a21c0c6cb3f24c66cebc)
@@ -0,0 +1,185 @@
+<?xml version="1.0"?>
+<xsd:schema targetNamespace="http://www.isi.edu/faber/fedd_types"
+  xmlns:tns="http://www.isi.edu/faber/fedd_types"
+  xmlns:xsd="http://www.w3.org/2000/10/XMLSchema"
+  xmlns="http://www.w3.org/2000/10/XMLSchema">
+
+  <xsd:complexType name="IDType">
+    <xsd:annotation>
+      <xsd:documentation>
+	The types of user IDs
+      </xsd:documentation>
+    </xsd:annotation>
+    <xsd:choice>
+      <xsd:element name="uuid" type="xsd:base64Binary"/>
+      <xsd:element name="fedid" type="xsd:base64Binary"/>
+      <xsd:element name="uri" type="xsd:string"/>
+      <xsd:element name="username" type="xsd:string"/>
+      <xsd:element name="kerberosUsername" type="xsd:string"/>
+    </xsd:choice>
+  </xsd:complexType>
+
+  <xsd:complexType name="nodeType">
+    <xsd:annotation>
+      <xsd:documentation>
+	Nodes are described in terms of their images or hardware requirements.
+	This description may be empty
+      </xsd:documentation>
+    </xsd:annotation>
+    <xsd:sequence>
+      <xsd:element name="image" type="xsd:string" minOccurs="0"
+	maxOccurs="unbounded"/>
+      <xsd:element name="hardware" type="xsd:string" minOccurs="0"
+	maxOccurs="unbounded"/>
+    </xsd:sequence>
+    <xsd:attribute name="count" type="xsd:int" use="optional"/>
+  </xsd:complexType>
+
+  <xsd:simpleType name="kindType">
+    <xsd:restriction base="xsd:string">
+      <xsd:enumeration value="max"/>
+      <xsd:enumeration value="average"/>
+    </xsd:restriction>
+  </xsd:simpleType>
+
+  <xsd:complexType name="capacityType">
+    <xsd:annotation>
+      <xsd:documentation>
+	Capacity value and type of cut
+      </xsd:documentation>
+    </xsd:annotation>
+    <xsd:sequence>
+      <xsd:element name="rate" type="xsd:double"/>
+      <xsd:element name="kind" type="tns:kindType"/>
+    </xsd:sequence>
+  </xsd:complexType>
+
+  <xsd:complexType name="accessType">
+    <xsd:annotation>
+      <xsd:documentation>
+	The types of credentails used for remote project access
+      </xsd:documentation>
+    </xsd:annotation>
+    <xsd:choice>
+      <xsd:element name="X509" type="xsd:base64Binary"/>
+      <xsd:element name="sshPubkey" type="xsd:base64Binary"/>
+      <xsd:element name="pgpPubkey" type="xsd:base64Binary"/>
+    </xsd:choice>
+  </xsd:complexType>
+
+  <xsd:complexType name="userType">
+    <xsd:annotation>
+      <xsd:documentation>
+	A user consists of one or both of an identifier and an access
+	credential. 
+      </xsd:documentation>
+    </xsd:annotation>
+    <xsd:sequence>
+      <xsd:element name="userID" type="tns:IDType" minOccurs="0"
+	maxOccurs="1"/>
+      <xsd:element name="access" type="tns:accessType" minOccurs="0"
+	maxOccurs="unbounded"/>
+    </xsd:sequence>
+  </xsd:complexType>
+
+  <xsd:complexType name="fedAttrType">
+    <xsd:annotation>
+      <xsd:documentation>
+	A general attribute value pair for passing federation parameters and
+	preferences
+      </xsd:documentation>
+    </xsd:annotation>
+    <xsd:sequence>
+      <xsd:element name="attribute" type="xsd:string"/>
+      <xsd:element name="value" type="xsd:string"/>
+    </xsd:sequence>
+  </xsd:complexType>
+
+
+  <xsd:complexType name="resourcesType">
+    <xsd:annotation>
+      <xsd:documentation>
+	An estimate of the resources than a federated experiment may require
+      </xsd:documentation>
+    </xsd:annotation>
+    <xsd:sequence>
+      <xsd:element name="node" type="tns:nodeType" minOccurs="0"
+	maxOccurs="unbounded" />
+      <xsd:element name="capacity" type="tns:capacityType" minOccurs="0"
+	maxOccurs="unbounded"/>
+    </xsd:sequence>
+  </xsd:complexType>
+
+  <xsd:complexType name="projectType">
+    <xsd:annotation>
+      <xsd:documentation>
+	A project name and access user
+      </xsd:documentation>
+    </xsd:annotation>
+    <xsd:sequence>
+      <xsd:element name="name" type="tns:IDType"/>
+      <xsd:element name="user" type="tns:userType" minOccurs="0"
+	maxOccurs="unbounded"/>
+    </xsd:sequence>
+  </xsd:complexType>
+
+  <xsd:complexType name="emulabType">
+    <xsd:annotation>
+      <xsd:documentation>
+	The information needed to swap an emulab experiment in
+      </xsd:documentation>
+    </xsd:annotation>
+    <xsd:sequence>
+      <xsd:element name="project" type="tns:projectType"/>
+      <xsd:element name="domain" type="xsd:string"/>
+      <xsd:element name="boss" type="xsd:string"/>
+      <xsd:element name="ops" type="xsd:string"/>
+      <xsd:element name="fileServer" type="xsd:string"/>
+      <xsd:element name="eventServer" type="xsd:string"/>
+      <xsd:element name="fedAttr" type="tns:fedAttrType" minOccurs="0"
+	maxOccurs="unbounded"/>
+    </xsd:sequence>
+  </xsd:complexType>
+
+  <xsd:complexType name="requestType">
+    <xsd:annotation>
+      <xsd:documentation>
+	The full on request type.
+      </xsd:documentation>
+    </xsd:annotation>
+    <xsd:sequence>
+      <xsd:element name="destinationTestbed" type="tns:IDType"
+	minOccurs="0" maxOccurs="1" />
+      <xsd:choice>
+	<xsd:element name="project" type="tns:projectType" minOccurs="0" 
+	  maxOccurs="1"/>
+	<xsd:element name="user" type="tns:userType" minOccurs="0"
+	  maxOccurs="unbounded"/>
+      </xsd:choice>
+      <xsd:element name="resources" type="tns:resourcesType" minOccurs="0" 
+	maxOccurs="1"/>
+      <xsd:element name="access" type="tns:accessType" minOccurs="1" 
+	maxOccurs="unbounded"/>
+      <xsd:element name="allocID" type="tns:IDType"/>
+      <xsd:element name="when" type="xsd:dateTime" minOccurs="0" maxOccurs="1"/>
+      <xsd:element name="until" type="xsd:dateTime" minOccurs="0" 
+	maxOccurs="1"/>
+    </xsd:sequence>
+  </xsd:complexType>
+
+  <xsd:complexType name="responseType">
+    <xsd:annotation>
+      <xsd:documentation>
+	The full on response type.
+      </xsd:documentation>
+    </xsd:annotation>
+    <xsd:sequence>
+      <xsd:element name="allocID" type="tns:IDType"/>
+      <xsd:element name="emulab" type="tns:emulabType"/>
+      <xsd:element name="when" type="xsd:dateTime" minOccurs="0" maxOccurs="1"/>
+      <xsd:element name="until" type="xsd:dateTime" minOccurs="0" 
+	maxOccurs="1"/>
+    </xsd:sequence>
+  </xsd:complexType>
+
+</xsd:schema>
Index: fedd/fedd_util.py
===================================================================
--- fedd/fedd_util.py	(revision 6ff0b91afedbb5c0b726a21c0c6cb3f24c66cebc)
+++ fedd/fedd_util.py	(revision 6ff0b91afedbb5c0b726a21c0c6cb3f24c66cebc)
@@ -0,0 +1,221 @@
+#!/usr/local/bin/python
+
+import sys
+
+from M2Crypto import SSL, X509, EVP
+from pyasn1.codec.der import decoder
+
+
+# The version of M2Crypto on users is pretty old and doesn't have all the
+# features that are useful.  The legacy code is somewhat more brittle than the
+# main line, but will work.
+if "as_der" not in dir(EVP.PKey):
+    from asn1_raw import get_key_bits_from_file, get_key_bits_from_cert
+    legacy = True
+else:
+    legacy = False
+
+class fedd_ssl_context(SSL.Context):
+    """
+    Simple wrapper around an M2Crypto.SSL.Context to initialize it for fedd.
+    """
+    def __init__(self, my_cert, trusted_certs=None, password=None):
+	"""
+	construct a fedd_ssl_context
+
+	@param my_cert: PEM file with my certificate in it
+	@param trusted_certs: PEM file with trusted certs in it (optional)
+	"""
+	SSL.Context.__init__(self)
+
+	# load_cert takes a callback to get a password, not a password, so if
+	# the caller provided a password, this creates a nonce callback using a
+	# lambda form.
+	if password != None and not callable(password):
+	    # This is cute.  password = lambda *args: password produces a
+	    # function object that returns itself rather than one that returns
+	    # the object itself.  This is because password is an object
+	    # reference and after the assignment it's a lambda.  So we assign
+	    # to a temp.
+	    pwd = password
+	    password =lambda *args: pwd
+
+	if password != None:
+	    self.load_cert(my_cert, callback=password)
+	else:
+	    self.load_cert(my_cert)
+	if trusted_certs != None: self.load_verify_locations(trusted_certs)
+	self.set_verify(SSL.verify_peer, 10)
+
+class fedid:
+    """
+    Wrapper around the federated ID from an X509 certificate. 
+    """
+    HASHSIZE=20
+    def __init__(self, bits=None, hexstr=None, cert=None, file=None):
+	if bits != None:
+	    self.set_bits(bits)
+	elif hexstr != None:
+	    self.set_hexstr(hexstr)
+	elif cert != None:
+	    self.set_cert(cert)
+	elif file != None:
+	    self.set_file(file)
+	else:
+	    self.buf = None
+
+    def __hash__(self):
+	return hash(self.buf)
+
+    def __eq__(self, other):
+	if isinstance(other, type(self)):
+	    return self.buf == other.buf
+	elif isinstance(other, type(str())):
+	    return self.buf == other;
+	else:
+	    return False
+
+    def __ne__(self, other): return not self.__eq__(other)
+
+    def __str__(self):
+	if self.buf != None:
+	    return str().join([ "%02x" % ord(x) for x in self.buf])
+	else: return ""
+
+    def __repr__(self):
+	return "fedid(hexstr='%s')" % self.__str__()
+
+    def pack_soap(self):
+	return self.buf
+
+    def digest_bits(self, bits):
+	"""Internal function.  Compute the fedid from bits and store in buf"""
+	d = EVP.MessageDigest('sha1')
+	d.update(bits)
+	self.buf = d.final()
+
+
+    def set_hexstr(self, hexstr):
+	h = hexstr.replace(':','')
+	self.buf= str().join([chr(int(h[i:i+2],16)) \
+		for i in range(0,2*fedid.HASHSIZE,2)])
+
+    def get_hexstr(self):
+	"""Return the hexstring representation of the fedid"""
+	return __str__(self)
+
+    def set_bits(self, bits):
+	"""Set the fedid to bits(a 160 bit buffer)"""
+	self.buf = bits
+
+    def get_bits(self):
+	"""Get the 160 bit buffer from the fedid"""
+	return self.buf
+
+    def set_file(self, file):
+	"""Get the fedid from a certificate file
+
+	Calculate the SHA1 hash over the bit string of the public key as
+	defined in RFC3280.
+	"""
+	self.buf = None
+	if legacy: self.digest_bits(get_key_bits_from_file(file))
+	else: self.set_cert(X509.load_cert(file))
+
+    def set_cert(self, cert):
+	"""Get the fedid from a certificate.
+
+	Calculate the SHA1 hash over the bit string of the public key as
+	defined in RFC3280.
+	"""
+
+	self.buf = None
+	if (cert != None):
+	    if legacy:
+		self.digest_bits(get_key_bits_from_cert(cert))
+	    else:
+		b = []
+		k = cert.get_pubkey()
+
+		# Getting the key was easy, but getting the bit string of the
+		# key requires a side trip through ASN.1
+		dec = decoder.decode(k.as_der())
+
+		# kv is a tuple of the bits in the key.  The loop below
+		# recombines these into bytes and then into a buffer for the
+		# SSL digest function.
+		kv =  dec[0].getComponentByPosition(1)
+		for i in range(0, len(kv), 8):
+		    v = 0
+		    for j in range(0, 8):
+			v = (v << 1) + kv[i+j]
+		    b.append(v)
+		# The comprehension turns b from a list of bytes into a buffer
+		# (string) of bytes
+		self.digest_bits(str().join([chr(x) for x in b]))
+
+def print_soap(soap, out=sys.stdout, level=0, show_all=False):
+    """Recursively print a soap element to out.
+
+    This is printed as pseudo XML with a <x></x> block around each element and
+    the element printed indented.  If show_all is true, even empty elements are
+    displayed.
+    """
+
+    indent = level* "\t"
+
+    methods = [ method for method in dir(soap) \
+	    if method.startswith("get_element") and \
+	    callable(getattr(soap, method))]
+    if len(methods) != 0:
+	for m in methods:
+	    element = getattr(soap, m)();
+	    if element != None or show_all:
+		# Element may be a list of repetitions of the same element. If
+		# so set elements to be that list, otherwise create a one entry
+		# tuple.  In either case we're going to iterate over elements.
+		if getattr(element,"__iter__", None) == None:
+		    elements = (element,)
+		else:
+		    elements = element
+		for e in elements:
+		    # attrs are the element attributes for this element and
+		    # they're printed in the framing XML rather than by the
+		    # recursive call.
+		    attrs = [ attr for attr in dir(e)\
+			    if attr.startswith("get_attribute") and \
+			    callable(getattr(e,attr))]
+		    print >> out, "%s<%s" % (indent, m[len("get_element_"):]),
+		    for a in attrs:
+			print >>out, ' %s="%s"' % \
+			    (a[len("get_attribute_"):], getattr(e,a)()),
+		    print >>out, ">"
+		    print_soap(e, out, level+1)
+		    print >>out, "%s</%s>" % (indent, m[len("get_element_"):])
+    else:
+	print >> out, "%s%s" % (indent, soap)
+
+def pack_id(id):
+    """
+    Return a dictionary with the field name set by the id type
+    """
+    if isinstance(id, type(fedid())): return { 'fedid': id }
+    elif id.startswith("http:") or id.startswith("https:"): return { 'uri': id }
+    else: return { 'username': id}
+
+def pack_soap(container, name, contents):
+    if getattr(contents, "__iter__", None) != None:
+	obj = getattr(container, "new_%s" % name, None)()
+	for e, v in contents.iteritems():
+	    assign = getattr(obj, "set_element_%s" % e, None) or \
+		    getattr(obj, "set_attribute_%s" % e, None)
+	    if isinstance(v, type(dict())):
+		assign(pack_soap(obj, e, v))
+	    elif getattr(v, "__iter__", None) != None:
+		assign([ pack_soap(obj, e, val ) for val in v])
+	    elif getattr(v, "pack_soap", None) != None:
+		assign(v.pack_soap())
+	    else:
+		assign(v)
+	return obj
+    else: return contents
