Wednesday, May 4, 2016

Adding custom admin service to run LDAP search filter from WSO2 Identity Server

From this post I'm going to explain how to create a custom admin service WSO2 Identity Server. For this I am using WSO2 Identity Server 5.0.0 with Service Pack 1 installed and going to create a admin service to run a LDAP search filter and get the results from the ReadWriteLDAPUserStoreManager.

First you need to created a maven project. Use the bellow pom file.

<?xml version="1.0" encoding="utf-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>org.wso2.carbon</groupId>
    <artifactId>custom.admin.service</artifactId>
    <version>4.2.3</version>
    <packaging>bundle</packaging>
    <name>WSO2 Carbon - OAuth</name>
    <description>A custom wso2 products or solution</description>
    <url>http://wso2.org</url>

    <dependencies>
        <dependency>
            <groupId>org.wso2.carbon</groupId>
            <artifactId>org.wso2.carbon.logging</artifactId>
            <version>4.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.wso2.carbon</groupId>
            <artifactId>org.wso2.carbon.core</artifactId>
            <version>4.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.wso2.carbon</groupId>
            <artifactId>org.wso2.carbon.user.core</artifactId>
            <version>4.2.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-scr-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-bundle-plugin</artifactId>
                <extensions>true</extensions>
                <configuration>
                    <instructions>
                        <Bundle-SymbolicName>
                            ${project.artifactId}
                        </Bundle-SymbolicName>
                        <Private-Package>
                            custom.admin.service.internal
                        </Private-Package>
                        <Import-Package>
                            org.apache.commons.logging.*; version="1.0.4",
                            *;resolution:=optional
                        </Import-Package>
                        <Export-Package>
                            custom.admin.service,
                        </Export-Package>
                        <Embed-Dependency>
                        </Embed-Dependency>
                        <DynamicImport-Package>*</DynamicImport-Package>
                    </instructions>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>wso2-nexus</id>
            <name>WSO2 internal Repository</name>
            <url>http://maven.wso2.org/nexus/content/groups/wso2-public/</url>
            <releases>
                <enabled>true</enabled>
                <updatePolicy>daily</updatePolicy>
                <checksumPolicy>ignore</checksumPolicy>
            </releases>
        </repository>
    </repositories>
</project>

You need to write a custom userstore manager to be able to run you LDAP search filter. This can easily done by extending ReadWriteLDAPUserStoreManager. Our new userstore manager is CustomUserStoreManager. We need to add the bellow constructor to our custom userstore manager.


public CustomUserStoreManager(org.wso2.carbon.user.api.RealmConfiguration realmConfig,
                                  Map<String, Object> properties,
                                  ClaimManager claimManager,
                                  ProfileConfigurationManager profileManager,
                                  UserRealm realm, Integer tenantId)
            throws UserStoreException {

        super(realmConfig, properties, claimManager, profileManager, realm, tenantId);

}

Then we need to add a method to run the search filter on LDAP.

public String[] runSearchFilter(String filter) throws UserStoreException {

    List<String> values = new ArrayList<String>();
    String userPropertyName = realmConfig
            .getUserStoreProperty(LDAPConstants.USER_NAME_ATTRIBUTE);

    String searchFilter = filter;

    DirContext dirContext = this.connectionSource.getContext();
    NamingEnumeration<?> answer = null;
    NamingEnumeration<?> attrs = null;
    try {
        answer = this.searchForUser(searchFilter, new String[] { userPropertyName }, dirContext);
        while (answer.hasMoreElements()) {
            SearchResult sr = (SearchResult) answer.next();
            Attributes attributes = sr.getAttributes();
            if (attributes != null) {
                Attribute attribute = attributes.get(userPropertyName);
                if (attribute != null) {
                    StringBuffer attrBuffer = new StringBuffer();
                    for (attrs = attribute.getAll(); attrs.hasMore();) {
                        String attr = (String) attrs.next();
                        if (attr != null && attr.trim().length() > 0) {
                            attrBuffer.append(attr + ",");
                        }
                    }
                    String propertyValue = attrBuffer.toString();
                    // Length needs to be more than one for a valid
                    // attribute, since we
                    // attach ",".
                    if (propertyValue != null && propertyValue.trim().length() > 1) {
                        propertyValue = propertyValue.substring(0, propertyValue.length() - 1);
                        values.add(propertyValue);
                    }
                }
            }
        }

    } catch (NamingException e) {
        throw new UserStoreException(e.getMessage(), e);
    } finally {
        // close the naming enumeration and free up resources
        JNDIUtil.closeNamingEnumeration(attrs);
        JNDIUtil.closeNamingEnumeration(answer);
        // close directory context
        JNDIUtil.closeContext(dirContext);
    }
    return values.toArray(new String[values.size()]);
}

Now you need to register the newly created userstore manager as a service. For this you need to create private package and add service component class. This private package need to be added to pom file as done above. Bellow is a sample code snippet.


import custom.admin.service.CustomUserStoreManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.osgi.service.component.ComponentContext;
import org.wso2.carbon.user.api.UserStoreManager;

/**
 * @scr.component name="custom.user.store.manager.dscomponent" immediate=true
 */
public class CustomUserStoreMgtDSComponent {
    private static Log log = LogFactory.getLog(CustomUserStoreMgtDSComponent.class);

    protected void activate(ComponentContext ctxt) {

        CustomUserStoreManager customUserStoreManager = new CustomUserStoreManager();
        ctxt.getBundleContext().registerService(UserStoreManager.class.getName(), customUserStoreManager, null);
        log.info("CustomUserStoreManager bundle activated successfully..");
    }

    protected void deactivate(ComponentContext ctxt) {
        if (log.isDebugEnabled()) {
            log.debug("Custom User Store Manager is deactivated ");
        }
    }
}

Now we need to create the admin service class. It retrieves the primary userstore manager for the super tenant and cast it to our new custom userstore manager and invoke our new method.

import org.wso2.carbon.context.PrivilegedCarbonContext;
import org.wso2.carbon.core.AbstractAdmin;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.user.api.UserRealm;
import org.wso2.carbon.user.core.common.AbstractUserStoreManager;
import org.wso2.carbon.user.core.service.RealmService;

public class CustomAdminService extends AbstractAdmin {

    protected Log log = LogFactory.getLog(CustomAdminService.class);

    public String[] filterUsers(String filter) throws Exception {
        //get super tenant context and get realm service which is an osgi service
        RealmService realmService = (RealmService)
                PrivilegedCarbonContext.getThreadLocalCarbonContext().getOSGiService(RealmService.class);
        if (realmService == null) {
            String error = "Can not obtain carbon realm service..";
            throw new Exception(error);
        }

        int tenantId = -1234;
        UserRealm userRealm = realmService.getTenantUserRealm(tenantId);
        if (userRealm == null || !(userRealm instanceof org.wso2.carbon.user.core.UserRealm)) {
            String error = "Can not obtain user realm for tenant carbon.super.";
            throw new Exception(error);
        }
        AbstractUserStoreManager userStoreManager = (AbstractUserStoreManager) userRealm.getUserStoreManager();
        if (userStoreManager instanceof CustomUserStoreManager) {
            return ((CustomUserStoreManager)userStoreManager).runSearchFilter(filter);
        }else {
            throw new Exception("Operation not supported");
        }
    }
}

Now we need to create services.xml. It should be in <project home>/src/main/resources/META-INF/ directory. Bellow is a sample services.xml file. This file is used to map the service name to the service class, add permission to the service and etc.


<serviceGroup>
 <service name="CustomAdminService" scope="transportsession">
     <transports>
            <transport>https</transport>
        </transports>
  <schema schemaNamespace="http://org.apache.axis2/xsd" elementFormDefaultQualified="true" />
  <description>OAuth administration related functionality.</description>
  <parameter name="ServiceClass">custom.admin.service.CustomAdminService</parameter>
  <parameter name="AuthorizationAction" locked="false">/permission/admin/manage</parameter>
        <parameter name="adminService" locked="true">true</parameter>
    </service>
 <parameter name="hiddenService" locked="true">true</parameter>
</serviceGroup>

Now our custom userstore manager is ready. Build the project using maven and add it to the <CARBON_HOME>/repository/components/dropins/ directory.

Finally you need to change the userstore manager class name in user-mgt.xml file. So we need to change bellow line in user-mgt.xml

<UserStoreManager class="org.wso2.carbon.user.core.ldap.ReadWriteLDAPUserStoreManager">

to

<UserStoreManager class="custom.admin.service.CustomUserStoreManager">

Now our custom component is ready. To view the wsdl of the service we need to change HideAdminServiceWSDLs in carbon.xml to true. Finally start the Identity Server and go to the bellow url to view the wsdl of the service.

https://localhost:9443/services/CustomAdminService?wsdl

You can invoke the admin service using SOAP client. Bellow is a sample SOAP request for the service. We need admin privileges to invoke this service.

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://org.apache.axis2/xsd">
   <soapenv:Header/>
   <soapenv:Body>
      <xsd:filterUsers>
         <!--Optional:-->
         <xsd:filter>(uid=user102)</xsd:filter>
      </xsd:filterUsers>
   </soapenv:Body>
</soapenv:Envelope>

You can pass any LDAP search filter to this service and it will return all the users that matches the search filter.

Source code for the service and userstore manager is available at https://github.com/madurangasiriwardena/custom-admin-service