How to Create a Web Service using GSI Authentication and Proxy Delegation
The following is a description of how to implement and deploy an Apache-Axis based web service using the GSI Authentication/Proxy Delegation and local-interface preserving model adopted by our group.
The security mechanism is described in more detail at ncsa-gsihttps.
Here we will focus on how to generate the service infrastructure, how to write the necessary adapter code for a specific service, and how to deploy the service.
Definitions:
IMPLEMENTATION |
the Java class or classes constituting the functionality of the service |
LOCAL INTERFACE |
the API and data objects used by the implementation; these are exposed to other classes, but not as remote objects |
REMOTE INTERFACE |
the service interface generated from the local interface by Axis which exports the latter as remote objects |
CLIENT ADAPTER |
a service client which uses the local rather than remote interface |
ENDPOINT ADAPTER |
the service skeleton which also translates from the remote interface back into the local interface of the implementation |
Dependencies:
We assume that the build process will be done through Eclipse, and that the user is familiar with that environment. The various phases of the build are handled by calls to the ncsa.services.build.ServiceBuilder
, run, however, as a Java Application (not as a plugin).
The following Java libraries are necessary for generating the service and client. For convenience, we have wrapped them as Eclipse plugins. Not included in this list are the Eclipse, Ant, and JUnit plugins, which should be part of the standard Eclipse distribution.
External Libraries / Plugins
LIBRARY |
INCLUDED IN PLUGIN |
---|---|
dom4j-1.5.2.jar |
org.dom4j |
commons-beanutils-1.7.0.jar |
org.apache.commons |
commons-cli-1.0.jar |
org.apache.commons |
commons-discovery-0.2.jar |
org.apache.commons |
commons-lang-2.0.jar |
org.apache.commons |
commons-logging-1.0.4.jar |
org.apache.commons |
commons-collections-3.1.jar |
org.apache.commons |
commons-codec-1.3.jar |
org.apache.commons.codec |
axis-1.2.1.jar |
org.apache.axis |
jaxrpc-1.1.jar |
org.apache.axis |
saaj-1.2.jar |
org.apache.axis |
log4j-1.2.7.jar |
org.apache.log4j |
spring-1.1.5.jar |
org.spring |
jmx.jar |
javax.management |
servlet-api.jar |
javax.servlet |
qname.jar |
com.ibm.wsdl |
wsdl4j.jar |
com.ibm.wsdl |
cryptix-asn-1.0.jar |
org.globus.jglobus |
cryptix-random-1.0.jar |
org.globus.jglobus |
cryptix-32-1.0.jar |
org.globus.jglobus |
jce-jdk13-120.jar |
org.globus.jglobus |
jgss-1.0.jar |
org.globus.jglobus |
puretls-1.0.jar |
org.globus.jglobus |
cog-jglobus-1.2.1 |
org.globus.jglobus |
cog-axis.jar |
cog.axis |
cog-url.jar |
cog.axis |
NCSA plugins
NCSA PLUGIN |
---|
ncsa.tools.common |
ncsa.tools.common.eclipse.descriptors |
ncsa.ca.certs |
ncsa.services.build |
ncsa.services.client |
ncsa.services.endpoint |
Optional (database)
If you wish to use the service builder to do database initialization, then you will also need:
LIBRARY |
INCLUDED IN PLUGIN |
---|---|
commons-dbcp-1.2.1.jar |
net.sf.hibernate |
aopalliance-1.0.jar |
net.sf.hibernate |
cglib-nodep-2.1-dev.jar |
net.sf.hibernate |
hibernate-2.1.7.jar |
net.sf.hibernate |
jta-1.2.jar |
net.sf.hibernate |
odmg-3.0.jar |
net.sf.hibernate |
along with some JDBC connector, such as mysql-jdbc-3.1.7.jar.
UML
The following diagram lays out the invariant structure of a typical service. Violet class names represent external packages; blue represents ncsa.services.client
or ncsa.services.endpoint
abstract classes or interfaces; red represents the classes or interfaces which need to be written or generated.
Step-by-step instructions.
Throughout the following, we use Example
to stand for the local interface class.
(1) Set up the plugin structure.
Create the plugins.
For the Example service, we recommend creating the following plugins:
PLUGIN |
CONTAINS |
DEPENDENCIES |
SUB-DIRECTORIES |
---|---|---|---|
|
|
- |
src |
|
|
|
src, + |
|
all the remote classes generated by |
|
src |
|
|
|
src, resources |
|
|
|
src, deploy |
The src
directories will contain pre-existing code only in the interface and impl plugins; after generating the wstypes
source code, you will write the client and endpoint classes.
The resources
directory is another source folder which will contain the .wsdl.
The deploy
directory is used to create the .war. It should have the following in it:
- The usual .jsp and .html files used by Axis (optional): fingerprint.jsp, happyaxis.jsp, index.html;
- A WEB-INF directory, set up in the usual manner (with classes and any other resource subdirectories necessary, along with the requisite configuration files, etc.); plus
- server-config.wsdd, containing the basic Axis deployment description
- web.xml, describing the web-apps (the Axis servlet) to Tomcat
Versions of the latter two can usually be copied and adapted from existing services.
Create a feature.
In addition, you will need to create a feature for building the service .war:
FEATURE |
CONTAINS |
DEPENDENCIES |
---|---|---|
|
the .xml files for generating a .war |
|
When you have finished expressing all the necessary dependencies for creating the .war file as the dependencies of the feature, select the feature.xml
file in the Eclipse editor, right click (control-click) and select "PDE Tools >> Create Ant Build File" to generate all the necessary build files for the plugins in your workspace on which this feature depends.
(2) Create a properties file to be used by the service builder.
The following template can be used. Replace the following references appropriately:
${name} |
e.g., |
${pname} |
e.g., |
${package} |
e.g., |
${urnpackage} |
e.g., |
${eclipse} |
the path to the installation |
${war.dir} |
output directory for the .war file |
service.name=${name}
wsdl.name=${pname}.wsdl
package.dir=${package}/service
endpoint.package=${package}.service.endpoint
client.plugin.path=${eclipse}/${package}.client
endpoint.plugin.path=${eclipse}/${package}.endpoint
wstypes.plugin.path=${eclipse}/${package}.wstypes
feature.build.path=${eclipse}/${package}.feature
war.path=${war.dir}/${pname}.war
build.dir=${eclipse}/ncsa.services.build/${package}
deploy.dir=${eclipse}/${package}.endpoint/deploy
server.config.path=${eclipse}/${package}.endpoint/deploy/WEB-INF/server-config.wsdd
impl.interface=Example
default.url=httpg://localhost:8443/${pname}/services/${name}
style=WRAPPED
namespace=urn:service.${urnpackage}
scope=application
verbose=true
includeStub=false
types.namespace=types.service.${urnpackage}
types.packages=hibernate.mappings.root=${eclipse}/${package}.impl/resources/mappings
hibernate.delimiter=:
hibernate.properties.file=${eclipse}/${package}.endpoint/deploy/WEB-INF/classes/datasource.properties
hibernate.output.file=${eclipse}/ncsa.services.build/schemas/${pname}.sql
hibernate.quiet=false
hibernate.text.only=false
hibernate.drop=false
hibernate.create=true
Notes
- The settings above are for generating wrapped (document-literal) wsdl from the local interface.
- The packages of all data types referenced by
Example
should be included undertypes.packages
using a comma-separated list (no spaces); these will all be mapped by the wsdl generator to the indicatedtypes.namespace
.
For an explanation of the Hibernate settings, see below.
Place this properties file in some convenient local directory.
(3) Create the service infrastructure.
Inside of Eclipse, select "Run", choosing the ncsa.services.build.ServiceBuilder
from the ncsa.services.build
plugin you have installed. Set the command-line arguments to:
- service <path to properties file>
and run it.
The following should happen:
- A .wsdl file is generated;
- From the .wsdl, the source code for the necessary remote types and interfaces is generated;
- The service description is added to the indicated server configuration (at the server.config.path, above);
- The source code is copied to the wstypes plugin;
- The .wsdl file is copied to the client plugin resources directory.
When this automatic generation completes, you will need to map the generated classes in the wstypes plugin as extensions to the ncsa.services.client.wstypes
extension point, to enable the client to run as a plugin.
The extension point is declared in the ncsa.services.client
plugin; the extensions should be declared in the wstypes plugin where they reside.
(4) Implement the specific service-layer endpoints.
This is the coding phase of the build; in essence, the connecting code between LOCAL --> REMOTE (client) and REMOTE --> LOCAL (endpoint) needs to be developed.
However, our ncsa.services.client
and ncsa.services.endpoint
plugins, along with the utilities in ncsa.tools.common
should go some way to facilitating, and lending uniformity to, the process.
Authentication on both the client and service side is handled by the abstract or delegate classes provided by these plugins, so there is no security work to do per se. In the case that your service needs to use the proxy delegated to it (e.g., in order to call another service or to do file transfers), then the endpoint needs to store the proxy so that the implementation has access to it. The abstract base class ncsa.service.endpoint.GSIDelegatingEndpoint
automatically does this via a static in-memory implementation (see further below).
We will treat the endpoint and client adapters separately, in that order. However, in both cases, the process is similar, and involves the following:
- Convert local to remote or remote to local objects (using a utility class);
- Invoke the analogous method on the delegate class.
Object conversion.
Axis has trouble with Java Collection
and Map
types, so objects destined to go over the wire should substitute arrays for these.
Given the exclusion of such untyped containers along with the fact that wstypes are merely cloned classes with a different package name, conversion from local to remote object and back can be handled entirely by reflection. This is done via the ncsa.tools.common.utils.bean.ImplicitBeanConverter
.
Endpoint Adapter.
This class should extend the ncsa.services.endpoint.GSIDelegatingEndpoint
class and implement the remote service interface (i.e., the wstypes interface generated from the local interface). For each method of the remote interface, it should convert any special data types (i.e., those in the wstypes plugin) to their local variants, and then use those to call the corresponding method on the class implementing the local interface. Outgoing return objects or specialized exceptions need to be converted back to their wire equivalents.
By way of example, we show here the endpoint adapter for a service which accepts submissions of workflow "ensembles" to be managed.
package ncsa.services.ensemble.service.endpoint; import ncsa.services.endpoint.GSIDelegatingEndpoint; import ncsa.services.ensemble.service.EnsembleBroker; import ncsa.services.ensemble.service.types.EnsembleDescriptor; import ncsa.services.ensemble.service.types.EnsembleHandle; import ncsa.tools.common.util.bean.ImplicitBeanConverter; public class EnsembleBrokerEndpoint extends GSIDelegatingEndpoint implements EnsembleBroker { // INPUT private ncsa.services.ensemble.EnsembleBroker impl; public void onInit() { super.onInit(); impl = ( ncsa.services.ensemble.EnsembleBroker )getWebApplicationContext() .getBean( "ensembleBrokerImpl" ); } public EnsembleHandle submit( EnsembleDescriptor descriptor, String instanceId ) { try { logger.debug( "extracting user info " ); String user = extractAuthorizationInfo(); logger.debug( "user: " + user ); descriptor.setUser( user ); ImplicitBeanConverter ibc = new ImplicitBeanConverter(); ncsa.services.ensemble.EnsembleDescriptor d = ( ncsa.services.ensemble.EnsembleDescriptor )ibc.convert ( descriptor, ncsa.services.ensemble.EnsembleDescriptor.class ); logger.debug( "submitting: " + d + " , " + instanceId ); ncsa.services.ensemble.EnsembleHandle h = impl.submit( d, instanceId ); logger.debug( "got: " + h ); return ( ncsa.services.ensemble.service.types.EnsembleHandle )ibc.convert ( h, ncsa.services.ensemble.service.types.EnsembleHandle.class ); } catch ( Throwable t ) { throw new RuntimeException( t ); } } public void updateNodeStatus( int in0, String in1 ) { impl.updateNodeStatus( new Integer( in0 ), in1 ); } }
In the case of submit
, the converter is used to translate the wire object ncsa.services.ensemble.service.types.EnsembleDescriptor
into its local equivalent ncsa.services.ensemble.EnsembleDescriptor
, and then to translate the local return value ncsa.services.ensemble.EnsembleHandle
into its wire equivalent ncsa.services.ensemble.service.types.EnsembleHandle
.
Note how no conversion is necessary in the case of the second method, since all types belong to java.lang
.
Security Context Information
As mentioned above, the base class stores the proxy in the in-memory repository. This happens via the call to extractAuthorizationInfo
, which does the following:
- retrieves the credential from the message context;
- retrieves the user's DN from the message context;
- uses the user's DN to find the user name on the host via the standard grid-map file (which should be in /etc/grid-security);
- stores the credential by mapping it to the user name;
- returns the user name.
NOTE: ncsa.services.endpoint.GSIDelegatingEndpoint
depends on the Spring framework mechanism; it accesses a Spring configuration file to get the static instance of the in-memory repository; we have assumed that the implementation classes will also access this static instance via dependency injection. Hence, it will be necessary to provide a Spring configuration file. (Refer to Spring documentation.) The file should contain this entry:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org./dtd/spring-beans.dtd"> <beans> ... <bean name="authenticationRepository" class="ncsa.services.endpoint.security.repositories.MemoryAuthenticationRepository"/> ... </beans>
Should you wish to replace this storage mechanism with something else, then the new class should be substituted for the MemoryAuthenticationRepository
in the bean definition; this class must implement ncsa.services.endpoint.security.IAuthenticationRepository
. (Notice also how the endpoint example above uses the Spring context to retrieve its implementation.)
Client Adapter.
This class should extend the ncsa.services.client.GSIServiceClientAdapter
class and implement the local interface. For each method of the local interface, it should convert any special types corresponding to those in the wstypes plugin to those remote variants, and then use those to call the superclass invoke
method. Any return objects or specialized exceptions need to be converted back to their local equivalents.
As illustration, we give the "ensemble service" client counterpart to the endpoint above:
package ncsa.services.ensemble.service.client; import java.rmi.RemoteException; import org.osgi.framework.Bundle; import ncsa.services.client.GSIServiceClientAdapter; import ncsa.services.ensemble.osgi.ClientPlugin; import ncsa.tools.common.util.bean.ImplicitBeanConverter; public class EnsembleBrokerClientAdapter extends GSIServiceClientAdapter implements ncsa.services.ensemble.EnsembleBroker { public EnsembleBrokerClientAdapter( String endpointURL ) throws Throwable { super( endpointURL ); } public ncsa.services.ensemble.EnsembleHandle submit( ncsa.services.ensemble.EnsembleDescriptor d, String instanceId ) { try { ncsa.services.ensemble.service.types.EnsembleDescriptor descriptor = ( ncsa.services.ensemble.service.types.EnsembleDescriptor ) new ImplicitBeanConverter().convert( d, ncsa.services.ensemble.service.types.EnsembleDescriptor.class ); Object handle = invoke( "submit", new Object[]{ descriptor, instanceId } ); return ( ncsa.services.ensemble.EnsembleHandle ) new ImplicitBeanConverter().convert( handle, ncsa.services.ensemble.EnsembleHandle.class ); } catch ( Throwable e ) { logger.error( "submit", e ); throw new RuntimeException( e ); } } public void updateNodeStatus( Integer sessionId, String status ) { try { if ( sessionId == null ) throw new RuntimeException( "no sessionId" ); invoke( "updateNodeStatus", new Object[]{ sessionId, status } ); } catch ( RemoteException e ) { logger.error( "updateNodeStatus", e ); throw new RuntimeException( e ); } } // GSI SERVICE CLIENT ADAPTER protected String getWSDLName() { return "ensemble.wsdl"; } protected Bundle getBundle() { ClientPlugin cp = ClientPlugin.getDefault(); if ( cp != null ) return cp.getBundle(); return null; } }
The procedure here is symmetrical to that in the endpoint code. There are also two protected methods which need to be implemented (along with a string constructor taking the endpoint url and calling the superclass constructor with it):
getWSDLName
is the name of the wsdl resource placed in the client .jar. This is used in order to initialize the base class (via a special WSDL helper class).getBundle
should return the OSGI (Eclipse) bundle for the client (this is necessary for running the client as a plugin).
(5) Build the service .war.
Inside of Eclipse, select "Run", choosing the ncsa.services.build.ServiceBuilder
from the ncsa.services.build
plugin you have installed. Set the command-line arguments to:
- war <path to properties file>
and run it.
This will build the .war file and place it in the location you indicated in the properties file.
NOTE
The JGlobus dependencies listed below should be placed in ${CATALINA_HOME}/common/lib, not in the individual service wars:
LIBRARY |
INCLUDED IN PLUGIN |
---|---|
cryptix-asn-1.0.jar |
org.globus.jglobus |
cryptix-random-1.0.jar |
org.globus.jglobus |
cryptix-32-1.0.jar |
org.globus.jglobus |
jce-jdk13-120.jar |
org.globus.jglobus |
jgss-1.0.jar |
org.globus.jglobus |
puretls-1.0.jar |
org.globus.jglobus |
cog-jglobus-1.2.1 |
org.globus.jglobus |
(6) Initialize the database (optional).
Most of the services we work with maintain state via a database. We have generally adopted the Spring/Hibernate mechanism for persisting Java objects to them.
As a matter of convenience, the service builder also contains a routine for initializing these Hibernate-based data stores.
The final properties in the file template given above are the necessary configuration settings for running this part of the builder. The first one points to the Hibernate mapping files defining the store (.hbm.xml
). A properties file used in the configuration is also indicated, along with various task settings.
Once you have these in place, the procedure is similiar to the other two invocations of the service builder:
Inside of Eclipse, select "Run", choosing the ncsa.services.build.ServiceBuilder
from the ncsa.services.build
plugin you have installed. Set the command-line arguments to:
- db-init <path to properties file>
and run it.
This will initialize the database from the mappings. (Note that the initial creation of the database and the establishment of permissions for it is not included in this process.)
For more information, see Hibernate Documentation.
The following command:
- all <path to properties file>
will run all three phases (source, war, db-init) in that order.
Note that all of these commands accept multiple properties file arguments.