#!/usr/local/bin/python
import os,sys
from BaseHTTPServer import BaseHTTPRequestHandler
from ZSI import *
from M2Crypto import SSL
from M2Crypto.m2xmlrpclib import SSL_Transport
from M2Crypto.SSL.SSLServer import SSLServer
import M2Crypto.httpslib
import xmlrpclib
import re
import random
import string
import subprocess
import tempfile
import copy
from fedd_services import *
from fedd_util import *
import parse_detail
if False:
_RequestAccessFault_typecode = Struct(pname=("http://www.isi.edu/faber/fedd.wsdl", 'RequestAccessFault'), ofwhat=[ns0.faultType_Def( pname=("http://www.isi.edu/faber/fedd.wsdl", "RequestAccessFaultBody"), aname="_RequestAccessFaultBody", minoccurs=0, maxoccurs=1, nillable=True, encoded=None, typed=True)], pyclass=None)
class RequestAccessFault:
typecode = _RequestAccessFault_typecode
__metaclass__ = pyclass_type
def __init__(self, code=0, str="str"):
self._RequestAccessFaultBody = None
RequestAccessFault.typecode.pyclass=RequestAccessFault
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")
# Used by the SOAP caller
soap_namespace = 'http://www.isi.edu/faber/fedd.wsdl'
soap_methods = { 'RequestAccess': 'soap_RequestAccess' }
xmlrpc_methods = { 'RequestAccess': 'xmlrpc_RequestAccess' }
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
# This is used to make the service error reporting independent of the
# transport. The XMLRPC and SOAP dispatchers will convert it into
# transport-specific errors
class service_error(RuntimeError):
access = 1
proxy= 2
req = 3
server_config = 4
internal = 5
code_str = {
access : "Access Denied",
proxy : "Proxy Error",
req : "Badly Formed Request",
server_config: "Server Configuration Error",
internal : "Internal Error"
}
str_code = dict([ (v, k) for k, v in code_str.iteritems() ])
client_errors = ( req )
server_errors = ( access, proxy, server_config, internal)
def __init__(self, code=None, desc=None, from_string=None):
self.code = code
self.desc = desc
if code == None:
self.set_code_from_string(from_string)
RuntimeError.__init__(self, desc)
def code_string(self, code=None):
code = code or self.code
return fedd_proj.service_error.code_str.get(code)
def set_code_from_string(self, errstr):
self.code = fedd_proj.service_error.str_code.get(errstr,
fedd_proj. service_error.internal)
return self.code
def is_client_error(self):
return self.code in fedd_proj.service_error.client_errors
def is_server_error(self):
return self.code in fedd_proj.service_error.server_errors
# Used to report errors parsing the configuration files, not in providing
# service
class parse_error(RuntimeError): 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 dict
"""
if obj.has_key('user'):
return [ unpack_id(u['userID']) \
for u in obj['user'] if u.has_key('userID') ]
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 lines[1]
"""
# Convert a pair to an attribute line
out_attr = lambda a,v : \
'%s' % (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 fedd_proj.service_error(\
fedd_proj.service_error.internal,
"Dynamic project subprocess creation error "+ \
"[%s] (%s)" % (cmd[1], e.strerror))
else:
print >>sys.stdout, str(" ").join(cmd)
if rc != 0:
raise fedd_proj.service_error(\
fedd_proj.service_error.internal,
"Dynamic project subprocess error " +\
"[%s] (%d)" % (cmd[1], rc))
# Clean up tempfiles
os.unlink(userfile)
os.unlink(projfile)
def strip_unicode(self, obj):
"""Loosly de-unicode an object"""
if isinstance(obj, dict):
for k in obj.keys():
obj[k] = self.strip_unicode(obj[k])
return obj
elif isinstance(obj, basestring):
return str(obj)
elif getattr(obj, "__iter__", None):
return [ self.strip_unicode(x) for x in obj]
else:
return obj
def proxy_xmlrpc_request(self, dt, req):
"""Send an XMLRPC proxy request. Called if the SOAP RPC fails"""
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 fedd_proj.service_error(fedd_proj.service_error.server_config,
"Server certificates misconfigured")
# Of all the dumbass things. The XMLRPC library in use here won't
# properly encode unicode strings, so we make a copy of req with the
# unicode objects converted. We also convert the destination testbed
# to a basic string if it isn't one already.
if isinstance(dt, str): url = dt
else: url = str(dt)
r = copy.deepcopy(req)
self.strip_unicode(r)
transport = SSL_Transport(ctx)
port = xmlrpclib.ServerProxy(url, transport=transport)
# Reconstruct the full request message
try:
resp = port.RequestAccess(
{ "RequestAccessRequestBody": r})
resp, method = xmlrpclib.loads(resp)
except xmlrpclib.Fault, f:
se = fedd_proj.service_error(None, f.faultString, f.faultCode)
raise se
except xmlrpclib.Error, e:
raise fedd_proj.service_error(fedd_proj.service_error.proxy,
"Remote XMLRPC Fault: %s" % e)
if resp[0].has_key('RequestAccessResponseBody'):
return resp[0]['RequestAccessResponseBody']
else:
raise fedd_proj.service_error(fedd_proj.service_error.proxy,
"Bad proxy response")
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 fedd_proj.service_error(fedd_proj.service_error.server_config,
"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(
pack_soap(msg, "RequestAccessRequestBody", req))
try:
resp = port.RequestAccess(msg)
except ZSI.ParseException, e:
raise fedd_proj.service_error(fedd_proj.service_error.proxy,
"Bad format message (XMLRPC??): %s" %
str(e))
r = unpack_soap(resp)
if r.has_key('RequestAccessResponseBody'):
return r['RequestAccessResponseBody']
else:
raise fedd_proj.service_error(fedd_proj.service_error.proxy,
"Bad proxy response")
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 = [""]
else: user = a[2]
if p & 2: proj = ""
else: proj = a[1]
if p & 4: tb = ""
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
if req.has_key('project'):
p = req['project']
if p.has_key('name'):
project = unpack_id(p['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 fedd_proj.service_error(service_error.req,
"User asserting multiple fedids")
elif len(fedids) == 1 and fedids[0] != fid:
raise fedd_proj.service_error(service_error.req,
"User asserting different fedid")
project = None
tb = None
elif principal_type == "project":
if isinstance(project, type(fid)) and fid != project:
raise fedd_proj.service_error(service_error.req,
"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 fedd_proj.service_error(fedd_proj.service_error.access,
"Access denied")
# resolve and in found
dyn_proj = False
dyn_user = False
if found[0].name == "":
if project != None:
found[0].name = project
else :
raise fedd_proj.service_error(\
fedd_proj.service_error.server_config,
"Project matched when no project given")
elif found[0].name == "":
found[0].name = self.random_string("project", 3)
dyn_proj = True
if found[1] == "":
if user_match == "":
if user != None: found = (found[0], user[0])
else: raise fedd_proj.service_error(\
fedd_proj.service_error.server_config,
"Matched on anonymous request")
else:
found = (found[0], user_match)
elif found[1] == "":
found = (found[0], self.random_string("user", 4))
dyn_user = True
return found, (dyn_user, dyn_proj)
def build_response(self, 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()]
return msg
def RequestAccess(self, req, fid):
if req.has_key('RequestAccessRequestBody'):
req = req['RequestAccessRequestBody']
else:
raise fedd_proj.service_error(service_error.req, "No request!?")
if req.has_key('destinationTestbed'):
dt = unpack_id(req['destinationTestbed'])
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.has_key('resources') and req['resources'].has_key('node'):
resources = req['resources']
inaccessible = [ t for n in resources['node'] \
if n.has_key('hardware') != None \
for t in n['hardware'] \
if t in self.restricted and \
t not in found[0].node_types]
if len(inaccessible) > 0:
raise fedd_proj.service_error(fedd_proj.service_error.access,
"Access denied (nodetypes %s)" % \
str(', ').join(inaccessible))
ssh = [ x['sshPubkey'] \
for x in req['access'] if x.has_key('sshPubkey')]
if len(ssh) > 0:
if dyn[1]: self.dynamic_project(found, ssh)
else: pass # SSH key additions
else:
raise fedd_proj.service_error(service_error.req,
"SSH access parameters required")
resp = self.build_response(req['allocID'], found, ssh)
return resp
else:
p_fault = None # Any SOAP failure (sent unless XMLRPC works)
try:
# Proxy the request using SOAP
return self.proxy_request(dt, req)
except fedd_proj.service_error, e:
if e.code == fedd_proj.service_error.proxy: p_fault = None
else: raise
except ZSI.FaultException, f:
p_fault = f.fault.detail[0]
# If we could not get a valid SOAP response to the request above,
# try the same address using XMLRPC and let any faults flow back
# out.
if p_fault == None:
return self.proxy_xmlrpc_request(dt, req)
else:
# Build the fault
body = p_fault.get_element_RequestAccessFaultBody()
if body != None:
raise service_error(body.get_element_code(),
body.get_element_desc());
else:
raise fedd_proj.service_error(\
fedd_proj.service_error.proxy,
"Undefined fault from proxy??");
def soap_RequestAccess(self, ps, fid):
req = ps.Parse(RequestAccessRequestMessage.typecode)
msg = self.RequestAccess(unpack_soap(req), fedid)
resp = RequestAccessResponseMessage()
resp.set_element_RequestAccessResponseBody(
pack_soap(resp, "RequestAccessResponseBody", msg))
return resp
def xmlrpc_RequestAccess(self, params, fid):
msg = self.RequestAccess(params[0], fedid)
if msg != None:
return xmlrpclib.dumps(({ "RequestAccessResponseBody": msg },))
else:
raise service_error(fedd_proj.service_error.internal,
"No response generated?!");
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 " or " to act as wildcards or to require the
fields to be empty. Similarly aproj or auser can be or
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 = "(||"+fedid_expr + "|"+ name_expr + ")"
access_proj = "((?::" + name_expr +")*|"+ \
"" + "(?::" + name_expr + ")*|" + \
fedid_expr + "(?::" + name_expr + ")*|" + \
name_expr + "(?::" + name_expr + ")*)"
access_name = "(||" + 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 soap_dispatch(self, method, req, fid):
if fedd_proj.soap_methods.has_key(method):
try:
return getattr(self, fedd_proj.soap_methods[method])(req, fid)
except fedd_proj.service_error, e:
de = ns0.faultType_Def(
(ns0.faultType_Def.schema,
"RequestAccessFaultBody")).pyclass()
de._code=e.code
de._errstr=e.code_string()
de._desc=e.desc
if e.is_server_error():
raise Fault(Fault.Server, e.code_string(), detail=de)
else:
raise Fault(Fault.Client, e.code_string(), detail=de)
else:
raise Fault(Fault.Client, "Unknown method: %s" % method)
def xmlrpc_dispatch(self, method, req, fid):
if fedd_proj.xmlrpc_methods.has_key(method):
try:
return getattr(self, fedd_proj.xmlrpc_methods[method])(req, fid)
except fedd_proj.service_error, e:
raise xmlrpclib.Fault(e.code_string(), e.desc)
else:
raise xmlrpclib.Fault(100, "Unknown method: %s" % method)
def new_feddservice(configfile):
return fedd_proj(configfile)