Saturday, September 12. 2015
SPMLv2 - Part II: XSD profile
In the previous entry the SPMLv2 provisioning standard was introduced and its DSML profile explained. The DSML profile manages object classes and attributes in a very similar way to an LDAP server. Basic CRUD methods were shown in a little PoC that implemented the core SPML capabilities with a demo server and client. Both ends were developed using the outdated Java library openspml. This library is very old and I am sure that now it would be developed in a very different way. Besides openspml does not cover the XSD profile that the SPML standard also provides. This is the second entry of the SPML series and it will extend the openspml library to minimally cover the XSD profile. The basic CRUD examples that were explained in the previous entry will be repeated with this new profile.
If you remember, the SPML server exposes several targets and each target should manage the data interchanged using one of the schema profiles. In the XSD profile the data is exchanged using XML language, data which is compliant to a XSD file previously defined. That schema file is accessible through the initial listTargets operation.
As I said in the previous introduction the openspml library is too old to implement the XSD profile (the XSD profile was added to the standard later than the DSML one and the library was never updated to include it). So I was forced to add some little changes in the library to add that second profile. The library manages a interface called OpenContentElement to transform any object to XML language. (As you see the library is very old and it does not manage current frameworks like JAXB. In general it uses DOM/Xerces for everything.) So I added a XSDDocElement class that wraps any DOM XML object exchanged storing the DOM document itself as an OpenContentElement. This way any XML data that is sent or received in the XSD profile is maintained as a Java object with the XSDDocElement. In openspml the library itself manages its own unmarshallers in order to understand the data interchanged. The library uses an interface called OCEUnmarshaller (Open Content Element unmarshaller) to give the possibility to be extended to new profiles. By default there is a DSMLUnmarshaller to support the DSML profile. So I added a XSDUnmarshaller that, following the same idea, converts the data to a XSDDocElement. I basically used the namespace of the XSD element to guess that the data is a XSD XML chunk. Finally I created the XSDProfileRegistrar which is a registration class for my new XSD unmarshaller. I know my implementation is a bit sloppy but I just wanted to show in a quick way how the XSD profile works.
In this second entry I will also show some parts of my server implementation. Remember that the server is based on the openspml provided Servlet and a simple SPMLExecutor I developed. This executor just maps a Request class from openspml (ListTargetRequest, AddRequest, LookupRequest,...) to the real executor that handles this kind of requests. This way my server project added the executors to cover the core capabilities of the SPML framework.
ListTargets
As in the previous entry the listEntries method should be called first by the client. There is no difference with the DSML profile except that in this example the profile is set to the XSD URI.
ListTargetsRequest ltr = new ListTargetsRequest();
ltr.setRequestID("ltr1");
ltr.setProfile(new URI(XSDProfileRegistrar.PROFILE_URI_STRING));
ltr.setExecutionMode(ExecutionMode.SYNCHRONOUS);
Response res = client.send(ltr);
The executor implemented for this operation just creates the response loading a fixed response with the DSML, XSD or both schema. Depending the URI requested by the client the correct prefixed XML is returned.
ListTargetsResponse res = null;
try {
XMLUnmarshaller unmarshaller = new ReflectiveDOMXMLUnmarshaller();
if (req.getProfile() == null) {
res = (ListTargetsResponse) unmarshaller.unmarshall(TARGET_DEFINITION_BOTH);
} else if (XSDProfileRegistrar.PROFILE_URI_STRING.equals(req.getProfile().toString())) {
res = (ListTargetsResponse) unmarshaller.unmarshall(TARGET_DEFINITION_XSD);
} else if (DSMLProfileRegistrar.PROFILE_URI_STRING.equals(req.getProfile().toString())) {
res = (ListTargetsResponse) unmarshaller.unmarshall(TARGET_DEFINITION_DSML);
} else {
SpmlUtils.fillError(res, ErrorCode.UNSUPPORTED_PROFILE,
"Profile '" + req.getProfile() + "' is not supported");
}
res.setRequestID(req.getRequestID());
} catch (UnknownSpml2TypeException e) {
e.printStackTrace();
res = new ListTargetsResponse(new String[] {e.getMessage()}, StatusCode.FAILURE,
TARGET_DEFINITION_XSD, ErrorCode.CUSTOM_ERROR, null);
}
return res;
In the XSD case the returned schema is the XSD definition file of the different entities the target manages. In my little PoC the XSD schema is just a single element with the definition for the user type.
<listTargetsRequest xmlns='urn:oasis:names:tc:SPML:2:0' requestID='ltr1' executionMode='synchronous' profile='urn:oasis:names:tc:SPML:2.0:profiles:XSD'/>
<listTargetsResponse xmlns='urn:oasis:names:tc:SPML:2:0' status='success' requestID='ltr1'>
<target targetID='ddbb-spml-xsd' profile='urn:oasis:names:tc:SPML:2.0:profiles:XSD'>
<schema>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://www.w3.org/2001/XMLSchema" xmlns:spml="urn:oasis:names:tc:SPML:2:0" elementFormDefault="qualified" targetNamespace="urn:ddbb-spml-dsml:user" version="1.0">
<element name="user">
<complexType>
<sequence>
<element maxOccurs="1" minOccurs="1" name="uid" type="string"/>
<element maxOccurs="1" minOccurs="0" name="password" type="string"/>
<element maxOccurs="1" minOccurs="1" name="cn" type="string"/>
<element maxOccurs="1" minOccurs="0" name="description" type="string"/>
<element maxOccurs="unbounded" minOccurs="0" name="role" type="string"/>
</sequence>
</complexType>
</element>
</xs:schema>
<supportedSchemaEntity entityName='user'/>
</schema>
<capabilities/>
</target>
</listTargetsResponse>
Add
The add request for the XSD profile just sends the correct XML data for the desired element to be created specifying the XSD target returned by the previous listTargets. Instead of the attributes and values sent with the DSML profile, the XSD profile just send a complete XML that represents a supported entity. In this profile the openspml client just needs to add the User object to the Extensible class. For the XSD profile the User class has been modified to implement the OpenContentElement interface. Any user object can be transformed to XML using JAXB, that resulting XML is the data sent to the server.
AddRequest ar = new AddRequest();
ar.setRequestID("ar1");
ar.setTargetId("ddbb-spml-xsd");
ar.setExecutionMode(ExecutionMode.SYNCHRONOUS);
Extensible data = new Extensible();
User u = new User();
u.setUsername("ricky");
u.setPassword("ricky123");
u.setCommonName("Ricardo Martin");
u.setDescription("me");
u.getRoles().add("Admin");
u.getRoles().add("User");
data.addOpenContentElement(u);
ar.setData(data);
Response res = client.send(ar);
In the server end, the add executor receives the data and it is converted back to a user object. Depending the target specified the user object is created using DSML attributes or the DOM document. Once the user is recovered it is inserted in the ddbb and the data added to the response (again, depending the profile, DSML attributes or XML data is appended).
AddResponse res = new AddResponse();
res.setStatus(StatusCode.SUCCESS);
if (SpmlUtils.checkSynch(req, res) && SpmlUtils.checkTargetId(req.getTargetId(), res)) {
try {
User u = new User();
if (ListTargetsExecutor.TARGET_ID_DSML.equals(req.getTargetId())) {
// convert the data in a User
u = SpmlUtils.spml2User(u, req.getData());
} else {
// convert from DOC to User
List docs = (List) req.getData(
).getOpenContentElements(XSDDocElement.class);
if (docs.size() == 1) {
u = SpmlUtils.doc2User(docs.get(0).getDocument());
} else {
SpmlUtils.fillError(res, ErrorCode.MALFORMED_REQUEST,
"The XML data is not properly received");
}
}
// if the psoId is set assign the id
if (req.getPsoID() != null && req.getPsoID().getID() != null) {
u.setUsername(req.getPsoID().getID());
}
if (SpmlUtils.checkUser(u, res, true)) {
// add the user normally
um.create(u);
if (ReturnData.IDENTIFIER.equals(req.getReturnData())) {
res.setPso(SpmlUtils.user2Spml(u, false,
ListTargetsExecutor.TARGET_ID_DSML.equals(req.getTargetId())));
} else if (req.getReturnData() == null
|| ReturnData.DATA.equals(req.getReturnData())
|| ReturnData.EVERYTHING.equals(req.getReturnData())) {
res.setPso(SpmlUtils.user2Spml(u, true,
ListTargetsExecutor.TARGET_ID_DSML.equals(req.getTargetId())));
}
}
} catch (Exception e) {
e.printStackTrace();
SpmlUtils.fillErrorException(res, e);
}
}
return res;
Finally the conversation is as follows (please pay attention to the XML data used instead of the DSML attributes).
<addRequest xmlns='urn:oasis:names:tc:SPML:2:0' requestID='ar1' executionMode='synchronous' targetId='ddbb-spml-xsd' returnData='everything'>
<data>
<usr:user xmlns:usr="urn:ddbb-spml-dsml:user">
<usr:uid>ricky</usr:uid>
<usr:password>ricky123</usr:password>
<usr:cn>Ricardo Martin</usr:cn>
<usr:description>me</usr:description>
<usr:role>User</usr:role>
<usr:role>Admin</usr:role>
</usr:user>
</data>
</addRequest>
<addResponse xmlns='urn:oasis:names:tc:SPML:2:0' status='success' requestID='ar1'>
<pso>
<psoID ID='ricky' targetID='ddbb-spml-xsd'/>
<data>
<usr:user xmlns:usr="urn:ddbb-spml-dsml:user">
<usr:uid>ricky</usr:uid>
<usr:password>ricky123</usr:password>
<usr:cn>Ricardo Martin</usr:cn>
<usr:description>me</usr:description>
<usr:role>User</usr:role>
<usr:role>Admin</usr:role>
</usr:user>
</data>
</pso>
</addResponse>
LookupRequest
The lookup request is only different in the response, because the data is returned as XML instead of DSML attributes. The request is created setting the psoID to be read but using the XSD target.
LookupRequest lr = new LookupRequest();
lr.setRequestID("lr1");
lr.setExecutionMode(ExecutionMode.SYNCHRONOUS);
PSOIdentifier psoId = new PSOIdentifier("ricky", null, "ddbb-spml-xsd");
lr.setPsoID(psoId);
lr.setReturnData(ReturnData.DATA);
Response res = client.send(lr);
The server gets the psoID to retrieve the user object from the ddbb. Then, depending the profile specified in the pso, the returned data is constructed using DSML attributes or XML.
LookupResponse res = new LookupResponse();
res.setStatus(StatusCode.SUCCESS);
if (SpmlUtils.checkSynch(req, res) && SpmlUtils.checkPSO(req, res)) {
try {
User u = um.read(req.getPsoID().getID());
if (u != null) {
if (ReturnData.IDENTIFIER.equals(req.getReturnData())) {
res.setPso(SpmlUtils.user2Spml(u, false,
ListTargetsExecutor.TARGET_ID_DSML.equals(req.getPsoID().getTargetID())));
} else if (ReturnData.DATA.equals(req.getReturnData())
|| ReturnData.EVERYTHING.equals(req.getReturnData())) {
res.setPso(SpmlUtils.user2Spml(u, true,
ListTargetsExecutor.TARGET_ID_DSML.equals(req.getPsoID().getTargetID())));
}
} else {
SpmlUtils.fillError(res, ErrorCode.INVALID_IDENTIFIER,
"The user does not exist in the ddbb");
}
} catch (Exception e) {
e.printStackTrace();
SpmlUtils.fillErrorException(res, e);
}
}
return res;
The conversation is similar to the previous entry except that the returned pso contains the XML data corresponding to the user object looked up.
<lookupRequest xmlns='urn:oasis:names:tc:SPML:2:0' requestID='lr1' executionMode='synchronous' returnData='data'>
<psoID ID='ricky' targetID='ddbb-spml-xsd'/>
</lookupRequest>
<lookupResponse xmlns='urn:oasis:names:tc:SPML:2:0' status='success' requestID='lr1'>
<pso>
<psoID ID='ricky' targetID='ddbb-spml-xsd'/>
<data>
<usr:user xmlns:usr="urn:ddbb-spml-dsml:user">
<usr:uid>ricky</usr:uid>
<usr:cn>Ricardo Martin</usr:cn>
<usr:description>me</usr:description>
<usr:role>User</usr:role>
<usr:role>Admin</usr:role>
</usr:user>
</data>
</pso>
</lookupResponse>
Modify
A modify request contains several modification operations but, in the XSD profile, a XPath expression is used to define where the operation take place. So the modify (as in DSML) can add, delete or replace a part of the XML document but the expression marks where that operation takes place. This is an example of how to construct a complex modify that adds a new role Other, remove role Admin and replaces the common name.
ModifyRequest mr = new ModifyRequest();
mr.setRequestID("mr1");
mr.setExecutionMode(ExecutionMode.SYNCHRONOUS);
PSOIdentifier psoId = new PSOIdentifier("ricky", null, "ddbb-spml-xsd");
mr.setPsoID(psoId);
// add role Other
Modification wrapper = new Modification();
wrapper.setModificationMode(ModificationMode.ADD);
Selection sel = new Selection();
sel.setNamespaceURI("http://www.w3.org/TR/xpath20");
sel.setPath("/user");
sel.addOpenContentElement(new XSDDocElement(
"<usr:role xmlns:usr=\"urn:ddbb-spml-dsml:user\">Other</usr:role>"));
wrapper.setComponent(sel);
mr.addModification(wrapper);
// remove role Admin
wrapper = new Modification();
wrapper.setModificationMode(ModificationMode.DELETE);
sel = new Selection();
sel.setNamespaceURI("http://www.w3.org/TR/xpath20");
sel.setPath("/user/role[text()='Admin']");
wrapper.setComponent(sel);
mr.addModification(wrapper);
// replace cn
wrapper = new Modification();
wrapper.setModificationMode(ModificationMode.REPLACE);
sel = new Selection();
sel.setNamespaceURI("http://www.w3.org/TR/xpath20");
sel.setPath("/usr:user/usr:cn");
sel.addOpenContentElement(new XSDDocElement(
"<usr:cn xmlns:usr=\"urn:ddbb-spml-dsml:user\">Ricardo Martin Camarero</usr:cn>"));
wrapper.setComponent(sel);
mr.addModification(wrapper);
Response res = client.send(mr);
In this point I have two comments. First I have coded the server in such a way that the xpath expression is evaluated with the default prefix for the namespace (last prefixed expression /usr:user/usr:cn) and not considering them (the previous examples, for example /user/role[text()='Admin'] to select the Admin role). Second the data can be a fragment or a complete user XML document (the examples just show three fragments), so it is complicated to parse that information in the XSDDocElement. I have decided to use the namespace (which I think is not in the standard). That is why this implementation is not very trustable. The server part in this request is more complicated, the current user is read from the ddbb and then the modifications are applied to that user. In the case of DSML the different values are added, deleted or replaced but in the case of XSD the user is transformed into a DOM document, the xpath element retrieved and the DOM elements sent in the request applied (deleted, added or replaced). The resulting user is updated in the ddbb and returned again using DSML or XSD.
ModifyResponse res = new ModifyResponse();
res.setStatus(StatusCode.SUCCESS);
if (SpmlUtils.checkSynch(req, res) && SpmlUtils.checkPSO(req, res) &&
SpmlUtils.checkTargetId(req.getPsoID().getTargetID(), res)) {
try {
User u = um.read(req.getPsoID().getID());
if (u == null) {
SpmlUtils.fillError(res, ErrorCode.INVALID_IDENTIFIER, "The user does not exist in the ddbb");
} else {
if (ListTargetsExecutor.TARGET_ID_DSML.equals(req.getPsoID().getTargetID())) {
// DSML modification
u = SpmlUtils.spmlDsml2User(u, req.getModifications());
} else {
// XSD modification
u = SpmlUtils.spmlXsd2User(u, req.getModifications(), res);
}
if (StatusCode.SUCCESS.equals(res.getStatus())) {
if (SpmlUtils.checkUser(u, res, false)) {
// add the user normally
if (um.update(u)) {
res.setStatus(StatusCode.SUCCESS);
if (ReturnData.IDENTIFIER.equals(req.getReturnData())) {
res.setPso(SpmlUtils.user2Spml(u, false,
ListTargetsExecutor.TARGET_ID_DSML.equals(req.getPsoID().getTargetID())));
} else if (ReturnData.DATA.equals(req.getReturnData())
|| ReturnData.EVERYTHING.equals(req.getReturnData())) {
res.setPso(SpmlUtils.user2Spml(u, true,
ListTargetsExecutor.TARGET_ID_DSML.equals(req.getPsoID().getTargetID())));
}
} else {
SpmlUtils.fillError(res, ErrorCode.INVALID_IDENTIFIER, "The user does not exist in the ddbb");
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
SpmlUtils.fillErrorException(res, e);
}
}
return res;
The communication for the previous example between both is presented.
<modifyRequest xmlns='urn:oasis:names:tc:SPML:2:0' requestID='mr1' executionMode='synchronous' returnData='everything'>
<psoID ID='ricky' targetID='ddbb-spml-xsd'/>
<modification modificationMode='add'>
<component path='/user' namespaceURI='http://www.w3.org/TR/xpath20'>
<usr:role xmlns:usr="urn:ddbb-spml-dsml:user">Other</usr:role>
</component>
</modification>
<modification modificationMode='delete'>
<component path="/user/role[text()='Admin']" namespaceURI='http://www.w3.org/TR/xpath20'/>
</modification>
<modification modificationMode='replace'>
<component path='/usr:user/usr:cn' namespaceURI='http://www.w3.org/TR/xpath20'>
<usr:cn xmlns:usr="urn:ddbb-spml-dsml:user">Ricardo Martin Camarero</usr:cn>
</component>
</modification>
</modifyRequest>
<modifyResponse xmlns='urn:oasis:names:tc:SPML:2:0' status='success' requestID='mr1'>
<pso>
<psoID ID='ricky' targetID='ddbb-spml-xsd'/>
<data>
<usr:user xmlns:usr="urn:ddbb-spml-dsml:user">
<usr:uid>ricky</usr:uid>
<usr:cn>Ricardo Martin Camarero</usr:cn>
<usr:description>me</usr:description>
<usr:role>User</usr:role>
<usr:role>Other</usr:role>
</usr:user>
</data>
</pso>
</modifyResponse>
As you can see the example presents the three possible operations but obviously the whole modification (sending a complete user) can always be executed sending a replace modification with the full XML for the user and specifying the XPath to point to root user element (/user in my PoC example).
Delete
The delete operation has no difference between profiles because no data is exchanged (just the pso identifier is needed). So the client part is exactly the same but specifying the XSD target.
DeleteRequest dr = new DeleteRequest();
dr.setRequestID("dr1");
dr.setExecutionMode(ExecutionMode.SYNCHRONOUS);
PSOIdentifier psoId = new PSOIdentifier("ricky", null, "ddbb-spml-xsd");
dr.setPsoID(psoId);
Response res = client.send(dr);
In the server implementation the executor has also no difference between profiles. The user is just deleted in the ddbb using the manager.
DeleteResponse res = new DeleteResponse();
res.setStatus(StatusCode.SUCCESS);
if (SpmlUtils.checkSynch(req, res) || SpmlUtils.checkPSO(req, res)) {
try {
if (!um.delete(req.getPsoID().getID())) {
SpmlUtils.fillError(res, ErrorCode.INVALID_IDENTIFIER,
"The user does not exist in the ddbb");
}
} catch (Exception e) {
e.printStackTrace();
SpmlUtils.fillErrorException(res, e);
}
}
return res;
The conversation is the same than in the previous entry.
<deleteRequest xmlns='urn:oasis:names:tc:SPML:2:0' requestID='dr1' executionMode='synchronous' recursive='false'>
<psoID ID='ricky' targetID='ddbb-spml-xsd'/>
</deleteRequest>
<deleteResponse xmlns='urn:oasis:names:tc:SPML:2:0' status='success' requestID='dr1'/>
This is the second entry about the SPMLv2 provisioning standard. The entry has explained the XSD profile in which the data is sent in XML language defined by a XSD schema exchanged in the initial listTargets operation. The openspml library has been slightly modified in order to marshall and unmarshall the new XML data. You can download both projects from the following links: the modified openspml library with XSD support and the SpmlServlet project which I have used to present the test cases. In general SPMLv2 is a good standard that can be suitable for a lot of provisioning software but, sadly, it is quite unknown and rarely included (only some identity solutions are exposing SPML in the last years). The openspml library is quite old and developing a new implementation (based on JAXB or any new modern technology) would be much better. If I have time I will try to start the development of a new SPMLv2 library from scratch, using JAXB I suppose that all XML management should be quite easier.
Regards!
Comments