2010/01/06
6 Jan, 2010

Content Filtering in Data Services with User Roles

  • Anjana Fernando
  • Software Architect - WSO2
Content Filtering in Data Services with User Roles

 

Applies To

WSO2 Data Services Server 2.2.0

 

Table of Contents 

Introduction

Some data service invocation results may contain sensitive information where a specific group of users should not be given access, or simply, the data is not relevant to them. In this situation, a content filtering mechanism would be useful where specific data sections are only accessible by the given type of users. 
 
The following use case revolves around a toy company 'East Coast Toys'. The company manufactures toys to many countries and in need of a software system to manage their stocks. They take on a service oriented architecture approach and decide to create a separate data layer by using a data services solution. The initial requirement from data services is to simply list all the toys the company produces. It has two main divisions which has access to the product information, which are,
  • Sales Division
  • Finance Division

The company's database contains very detailed data about its products where the buying price is also mentioned. But this data must be moderated where the sales division must not see the buying price, but the finance division is allowed to access it. This requirement can be achieved in WSO2 Data-Services with the usage of 'user roles'. The following sections will take you through a step by step guide into how this functionality can be applied in helping 'East Coast Toys' in fulfilling their requirements.

Prerequisites

  1. Download WSO2 Data-Services latest build from https://wso2.com/products/data-services-server/.
  2. Download and install Apache Ant from https://ant.apache.org/ (optional: for the re-deployment of samples).
  3. Install WSO2 Data-Services as a stand-alone server (Install location will be referred to as DS_HOME here after).
  4. Start the server (Run DS_HOME/bin/wso2server.bat | wso2server.sh).
  5. Open a web browser and navigate to https://localhost:9443/carbon.
  6. For first time use, login using the default credentials: username=admin, password=admin.

Setting up the Server

In this section, the steps will be shown on how to create the necessary data service, and the users who would be accessing it.

Step 1 – Create Users

For this demonstration, we will be creating two new users. 'tucker' and 'smith', who works for the sales and finance divisions respectively. Click the 'User Management' under the 'Configure' section in the menu. You will be presented with a screen similar to shown in Figure 1.1.

Add User

Figure 1.1: User List Screen

Click 'Add New User' and fill in the fields as shown below.Click 'Add New User' and fill in the fields as shown below.

User Name tucker
Password secret_1
 
Add New User Screen

Figure 1.2: Add New User Screen

After the data is entered, click finish to create the user.

By following the same steps, add another user with the following fields.

User Name smith
Password secret_2

User List After New User Added

Figure 1.3: User List After New User Added

Step 2 – Create New Roles and Assign them to Users

Under 'User Management', click Roles, and you will be presented with a screen as shown in Figure 2.1.

Roles List Screen

Figure 2.1: Roles List Screen

Click 'Add New Role' and set the 'Role Name' as 'sales'. After that, click 'Next', and you will be presented with a screen as shown in Figure 2.2 to select the permissions. In this instance, we will just give all the permissions available by clicking 'Select All'.

Select Role Permissions Screen

Figure 2.2: Select Role Permissions Screen

Click 'Next' in the 'Add Role' page, and you will arrive in the 'Select user to add to Role' page. Here you can select a desired user who will be added the current role. In the search box, type 'tucker' and click 'Search', shortly the 'tucker' will be visible in the list. Click the check box in front of it, and click 'Finish' to assign the role to the user.

Adding the Role to an User

Figure 2.3: Adding the Role to an User

Following the same steps, add another role with the name 'finance' and add the user 'smith' to the role.

After following the previous steps, the roles list will contain the newly created roles, as shown in Figure 2.4.

Roles List After New Role Added

Figure 2.4: Roles List After New Role Added

Now the necessary users and their roles are set up, we are ready to define the data service the users will be consuming.

Step 3 – Edit Data Service Query

Click 'List' under 'Services' section, to list the currently deployed services and you should be presented with a screen similar to that shown in Figure 3.1.Click 'List' under 'Services' section, to list the currently deployed services and you should be presented with a screen similar to that shown in Figure 3.1.

Deployed Services List

Figure 3.1: Deployed Services List

These services will be deployed by default with the WSO2 Data Services solution. Here, the 'DataServiceSample1' sample will be used to model the company's requirements.

Note: If the sample services are not deployed by default they can be deployed by navigating to the directory 'DS_HOME/samples/DataService/' and running the 'ant' command.

Click on the 'DataServiceSample1' service and follow the data service editing wizard up to the query listing page, as shown in Figure 3.2.

Query List

Figure 3.2: Query List

Edit the 'productsSQL' query, which will result in the screen shown in Figure 3.3.

Edit Query Screen

Figure 3.3: Edit Query Screen

Here the list of output mappings can be seen in the query editing page. The output mappings represent the elements of the resultant XML of the data service call. The 'Allowed User Roles' field controls the access to this data for a specific user. 

This behavior is simply enforced by checking the service invoker's assigned user roles. If the user has the required roles of an output mapping, then that data section will be returned, otherwise it will not be returned with the result.

The criteria for filtering data for the 'productsSQL' query will be as follows.

  • All fields will be visible to users with the "finance" role.
  • All fields except the 'buyPrice' field will be visible to users with the "sales" role.
  • Other users who doesn't have the roles "sales" nor "finance" will not see any of the fields.

Click on 'Edit' on the output fields as mentioned above and select the respective user roles as mentioned above, Figure 3.4 shows the assignment of user roles to the 'buyPrice' field.

Note: For an output field to be visible to all users, you can choose not to select any user roles from the list, or select the special 'everyone' user role which is inherent in all the users.

Setting Allowed Roles for an Output Field

Figure 3.4: Setting Allowed Roles for an Output Field

After all the required roles have been set for the result entries the Edit Query page should resemble Figure 3.5.

Edit Query Page with the Required Roles SetEdit Query Page with the Required Roles Set

Figure 3.5: Edit Query Page with the Required Roles Set

Step 4 – Enable User Authentication for the Data Service

In order for the service to identify the user who is sending requests security must be enabled for it and a method for authentication must be provided. Here we will be enabling UsernameToken for authenticating the current user. 

At the services list, click on the data service we were currently editing, and it will take you to the service dashboard, as seen in Figure 4.1.

Service DashboardService Dashboard

Figure 4.1: Service DashboardService Dashboard

Click on security, and in the next screen, set 'Yes' to enable security. There you will be given options to select the type of authentication. Under the 'Basic Scenarios' section, select 'UsernameToken'. These steps are shown in Figure 4.2.

Set User Authentication Method Screen

Figure 4.2: Set User Authentication Method Screen

Click 'Next' and it will take you to a screen where you should specify the user groups that are allowed to access the service. For now, we will select 'everyone' which will allow all user groups to access the service, after that click 'Finish'.

Security - Select User Groups Screen

Figure 4.3: Security - Select User Groups Screen

Consuming the Service

This section shows the possible ways, the above defined data service can be consumed. In earlier stages when testing a service, the built-in 'tryit' can be used. But in a production environment, a client application must be written. The upcoming sections present both approaches.

Tryit

In the services list, click on the 'Try this service' link at the 'DataServiceSample1''s entry. There you will be taken to a screen where you will be presented with fields to enter user credentials and choose the operation to be invoked. The operation to invoke is 'productsInfo' which uses our modified user roles aware query. The operations are invoked separately with the following users.
 

  • smith

Data Service Result with User 'smith'

Figure 5.1: Data Service Result with User 'smith'

In Figure 5.1, we see that all the information about a product is shown, this is bacause, the user 'smith' has the 'finance' role, and all the fields in product information support that specific role.

  • tucker

Data Service Result with User 'tucker'

Figure 5.2: Data Service Result with User 'tucker'

Invoking the service with the user 'tucker', all the fields except the 'buyPrice' is returning. The buyPrice field is not available to this user, because he does not have the 'finance' role, which is required for that field.

  • admin

Data Service Result with User 'admin'

Figure 5.3: Data Service Result with User 'admin'

The user 'admin' does not have any of the required roles for product information, thus the data services result contains empty elements.

Code Generated Java Client

A client stub can be automatically generated using the 'WSDL2Code' tool. This can be accessed by navigating to the service dashboard and clicking on the 'Generate Client' link. There you will be given the option to change several parameters used in the code generation. Here we will be using ADB (Axis2 Data Binding) as the data binding mechanism for the service schema types. Below table contains some of the useful parameters and its values that can be provided.

-p org.example.ds1
-d adb
-ns2p https://ws.wso2.org/dataservice/data_service_sample1=org.example.types.ds1
-u

[checked]

-uw [checked]

WSDL2Code Tool

Figure 5.4: WSDL2Code Tool

After the necessary fields are filled, click the 'Generate' button, where you will be presented with a zip file which contains the generated client code.
The generated code will contain a package 'org.example.ds1'. We will be adding a new class 'Client' which will use the generated stubs. The source code for the client class is listed below.

Client.java

package org.example.ds1;

import java.io.File;

import org.apache.axiom.om.impl.builder.StAXOMBuilder;
import org.apache.axis2.client.Options;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.context.ConfigurationContextFactory;
import org.apache.neethi.Policy;
import org.apache.neethi.PolicyEngine;
import org.apache.rampart.RampartMessageData;
import org.wso2.ws.dataservice.Product;

public class Client {

	private static Policy loadPolicy(String xmlPath) throws Exception {
		StAXOMBuilder builder = new StAXOMBuilder(xmlPath);
		return PolicyEngine.getPolicy(builder.getDocumentElement());
	}
	
	public static void main(String[] args) throws Exception {
		/* change the following variables suitably */
		String ds_home = "/home/laf/dev/bin/wso2dataservices-2.2.0";
		String policyFilePath = "/home/laf/Desktop/policy.xml";
		String epr = "https://localhost:9443/services/DataServiceSample1";
		
		/* set security settings */
		System.setProperty("javax.net.ssl.trustStore", 
				ds_home + File.separator + "resources" + File.separator + "security" + File.separator + "wso2carbon.jks"); 
		System.setProperty("javax.net.ssl.trustStorePassword", "wso2carbon");
		
		ConfigurationContext ctx = 
			ConfigurationContextFactory.createConfigurationContextFromFileSystem(null, null);
		
		DataServiceSample1Stub stub = new DataServiceSample1Stub(ctx, epr);
		stub._getServiceClient().engageModule("rampart");
		
		Options options = stub._getServiceClient().getOptions();
		options.setProperty(RampartMessageData.KEY_RAMPART_POLICY, loadPolicy(policyFilePath));
		stub._getServiceClient().setOptions(options);
		
		/* invoke service */
		Product[] products = stub.productsInfo();
		System.out.println("Customers:-");
		for (Product product : products) {
			System.out.println("\t---------------");
			System.out.println("\tProduct Code: " + product.getProductCode());
			System.out.println("\tMSRP: " + product.getMSRP());
			System.out.println("\tProduct Line: " + product.getProductLine());
			System.out.println("\tProduct Name: " + product.getProductName());
			System.out.println("\tProduct Scale: " + product.getProductScale());
			System.out.println("\tProduct Vendor: " + product.getProductVendor());
			System.out.println("\tBuy Price: " + product.getBuyPrice());
		}
	}	
}




The variable 'ds_home' and 'policyFilePath' paths must be set suitably for the user's environment. For enabling security, Rampart requires the user to provide a policy file, there a password callback handler class is defined. The source code for the password callback handler class is stated below.

package org.example.ds1;

import java.io.IOException;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;

import org.apache.ws.security.WSPasswordCallback;

public class PWCBHandler implements CallbackHandler {

	public void handle(Callback[] callbacks) throws IOException,
			UnsupportedCallbackException {
		for (Callback cb : callbacks) {
			WSPasswordCallback pwcb = (WSPasswordCallback) cb;
			String id = pwcb.getIdentifer();
			if ("tucker".equals(id)) {
				pwcb.setPassword("secret_1");
			} else if ("smith".equals(id)) {
				pwcb.setPassword("secret_2");
			} else if ("admin".equals(id)) {
				pwcb.setPassword("admin");
			}
		}
	}

}

The policy file contains the fully qualified name of the password callback class as 'org.example.ds1.PWCBHandler'. This file can be downloaded from the attachments section at the end of the article.

After the necessary files are in place, we will modify the auto generated ant build file – build.xml, to add a new build target to run the newly created client class. In the build file, add the following lines.

    <target name="run" depends="jar.client"> 
        <java fork="true" classname="org.example.ds1.Client"> 
            <classpath> 
                <path refid="axis2.class.path"></path> 
                <path location="${lib}/${name}-test-client.jar"></path> 
            </classpath> 
        </java> 
    </target> 

Setting the Classpath

Before running the ant tool for compiling the code and running it, we should set the classpath pointing to the Axis2 libraries. The ant build file uses an environment variable 'AXIS2_HOME' which it expects to point to a directory which contains the Axis2 library jar files. The expected value for AXIS2_HOME is DS_HOME/repository (replace DS_HOME appropriately).

e.g. :-

Unix/Linux:

# export AXIS2_HOME=/home/laf/home/laf/dev/bin/wso2dataservices-2.2.0/repository

Windows:

# set AXIS2_HOME=c:\wso2dataservices-2.2.0\repository

After modifying the build.xml and setting AXIS2_HOME, run the command 'ant run' to run the client. The output should resemble the following.

 

Buildfile: build.xml

init:

pre.compile.test:
     [echo] Stax Availability= true
     [echo] Axis2 Availability= true

compile.src:

jar.client:

run:
     [java] log4j:WARN No appenders could be found for logger (org.apache.axis2.util.Loader).
     [java] log4j:WARN Please initialize the log4j system properly.
     [java] Customers:-
     [java] 	---------------
     [java] 	Product Code: S10_1678
     [java] 	MSRP: 95.7
     [java] 	Product Line: Motorcycles
     [java] 	Product Name: 1969 Harley Davidson Ultimate Chopper
     [java] 	Product Scale: 1:10
     [java] 	Product Vendor: Min Lin Diecast
     [java] 	Buy Price: NaN
     [java] 	---------------
.
.
.
     [java] 	Product Code: S72_1253
     [java] 	MSRP: 49.66
     [java] 	Product Line: Planes
     [java] 	Product Name: Boeing X-32A JSF
     [java] 	Product Scale: 1:72
     [java] 	Product Vendor: Motor City Art Classics
     [java] 	Buy Price: NaN
     [java] 	---------------
     [java] 	Product Code: S72_3212
     [java] 	MSRP: 54.6
     [java] 	Product Line: Ships
     [java] 	Product Name: Pont Yacht
     [java] 	Product Scale: 1:72
     [java] 	Product Vendor: Unimax Art Galleries
     [java] 	Buy Price: NaN

BUILD SUCCESSFUL

Notice that the 'Buy Price' field always returns as 'NaN' in the results which stands for 'Not a Number'. This is because the user 'tucker' who is mentioned in the policy.xml does not have the rights to access that field in the data service. You can change the user in the policy file and see the changes in the result.

Summary

In this article, we have discussed how a data service result can be moderated according to the user who is accessing it by using WSO2 Data Services Server. First, we demonstrated how the server was configured to support this functionality by creating the necessary users and roles and then how the data service results were mapped appropriately to the user roles. And finally, this functionality is put in action by consuming the data service via a custom Java client application.

 

Author
Anjana Fernando. Software Engineer. WSO2 Inc. anjana at wso2 dot com

 

 

 

About Author

  • Anjana Fernando
  • Software Architect
  • WSO2 Inc.