Skip to content

enigma007-rgb/Sailpoint-IIQ-CustomConnector

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 

Repository files navigation


package sailpoint.plugin;

/*
 * =============================================================================
 * CUSTOM CONNECTOR DEMO - SAILPOINT INTEGRATION & ARCHITECTURE
 * =============================================================================
 *
 * SAILPOINT CONTEXT:
 * SailPoint (IdentityIQ / IdentityNow) is an identity governance platform. It
 * maintains a central view of identities (accounts, entitlements, groups) and
 * connects to multiple "sources" (AD, LDAP, HR, applications) via CONNECTORS.
 * This class is one such connector—it implements the SailPoint Open Connector
 * (openconnector) API so the platform can talk to a target system in a uniform way.
 *
 * WHERE THIS CONNECTOR FITS:
 * - SailPoint Server/Cloud  →  Connector framework  →  This connector  →  Target system
 * In this DEMO the "target system" is an in-memory Java map (no real DB/API).
 * In production, a connector would call REST APIs, LDAP, JDBC, etc., to talk to
 * the real system (e.g. Workday, ServiceNow, custom app).
 *
 * SAILPOINT OPERATIONS THAT DRIVE THIS CODE:
 * 1. AGGREGATION (identity/account discovery):
 *    SailPoint runs aggregation tasks per source. It sets objectType (account or
 *    group), then calls discoverSchema() and iterate(filter). Each Map we return
 *    becomes a resource object; SailPoint links them to identities and builds
 *    the identity warehouse. read() is used when it needs a single account/group.
 *
 * 2. PROVISIONING (creating/updating/deleting in the target):
 *    When a request is approved (e.g. "add user to application X"), SailPoint
 *    invokes create(), update(), or delete() with a native identifier and a list
 *    of Item (attribute changes). Our connector applies these to the in-memory
 *    store; a real connector would call the target system's API or directory.
 *
 * 3. LIFECYCLE OPERATIONS:
 *    Enable/Disable (enable(), disable()), Unlock (unlock()), and Set Password
 *    (setPassword()) are invoked when SailPoint executes lifecycle or password
 *    policies or provisioning plans that include these operations.
 *
 * 4. CONNECTOR SETUP & CONFIGURATION:
 *    When an admin configures a "Source" in SailPoint, the UI uses testConnection()
 *    to validate the connection and discoverSchema() to show which attributes
 *    are available. getSupportedObjectTypes() and getSupportedFeatures() drive
 *    what the UI allows (e.g. "Account" and "Group" types, "Create Account", etc.).
 *
 * 5. AUTHENTICATION:
 *    authenticate(identity, password) is used when SailPoint needs to verify
 *    credentials against this source (e.g. forwarded login, or verification).
 *
 * TYPICAL FLOW (sequence SailPoint uses):
 * 1. SailPoint creates the connector: new CustomConnectorDemo(config, log).
 * 2. testConnection() — validate "connection" (we print config inputs).
 * 3. getSupportedObjectTypes() / getSupportedFeatures() — UI and engine know what we support.
 * 4. discoverSchema() — attribute list for the selected object type (account/group).
 * 5. AGGREGATION: iterate(filter) for accounts, then for groups; SailPoint builds identities.
 * 6. PROVISIONING / LIFECYCLE: create(), update(), delete(), enable(), disable(), unlock(), setPassword().
 * 7. authenticate() when credential check is needed.
 * 8. close() when the connector is no longer needed.
 *
 * DATA FLOW (this demo only):
 * - accounts map: key = username (native ID), value = Map of attributes.
 * - groups map: key = group name (native ID), value = Map of attributes.
 * - getObjectsMap() returns the right map based on objectType (account vs group).
 * - read() returns a COPY by default so SailPoint cannot corrupt our store; for
 *   update operations we use read(id, true) to get the real object and modify it.
 */

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import openconnector.AbstractConnector; 
import openconnector.AuthenticationFailedException;
import openconnector.ConnectorConfig;
import openconnector.ConnectorException;
import openconnector.ExpiredPasswordException;
import openconnector.Filter;
import openconnector.FilteredIterator;
import openconnector.Item;
import openconnector.Log;
import openconnector.ObjectAlreadyExistsException;
import openconnector.ObjectNotFoundException;
import openconnector.Permission;
import openconnector.Result;
import openconnector.Schema;
import openconnector.SystemOutLog;



/**
 * SailPoint Open Connector implementation for demo/testing. Extends AbstractConnector
 * (openconnector API) so IdentityIQ/IdentityNow can use this class as a "Source"
 * for aggregation (discover accounts/groups) and provisioning (create/update/delete,
 * enable/disable, set password). Target system is in-memory only; see file header
 * for SailPoint architecture and integration flow.
 */
public class CustomConnectorDemo extends AbstractConnector {

	    ////////////////////////////////////////////////////////////////////////////
	    // INNER CLASSES
	    //
	    ////////////////////////////////////////////////////////////////////////////

	    /**
	     * Wraps another iterator and returns a COPY of each map from next().
	     * Prevents callers (e.g. SailPoint aggregation) from mutating our
	     * internal accounts/groups maps when they process iterated results.
	     */
	    private class CopyIterator implements Iterator<Map<String,Object>> {
	        
	        private Iterator<Map<String,Object>> it;
	        
	        /** Stores the underlying iterator (e.g. FilteredIterator). */
	        public CopyIterator(Iterator<Map<String,Object>> it) {
	            this.it = it;
	        }
	        
	        /** Delegates to the underlying iterator. */
	        public boolean hasNext() {
	            return this.it.hasNext();
	        }

	        /** Returns a copy of the next map so our cache is not modified. */
	        public Map<String,Object> next() {
	            return copy(this.it.next());
	        }
	        
	        /** Delegates remove to the underlying iterator. */
	        public void remove() {
	            this.it.remove();
	        }
	    }
	    

	    ////////////////////////////////////////////////////////////////////////////
	    // Attribute names used in account and group maps (and in schema).
	    ////////////////////////////////////////////////////////////////////////////

	    public static final String ATTR_USERNAME = "username";
	    public static final String ATTR_FIRSTNAME = "firstname";
	    public static final String ATTR_LASTNAME = "lastname";
	    public static final String ATTR_EMAIL = "email";
	    public static final String ATTR_GROUPS = "groups";

	    public static final String ATTR_DISABLED = "disabled";
	    public static final String ATTR_LOCKED = "locked";
	    public static final String ATTR_PASSWORD = "password";
	    public static final String ATTR_PASSWORD_OPTIONS = "passwordOptions";
	    public static final String ATTR_PASSWORD_HISTORY = "passwordHistory";
	    
	    public static final String GROUP_ATTR_NAME = "name";
	    public static final String GROUP_ATTR_DESCRIPTION = "description";

	    
	    ////////////////////////////////////////////////////////////////////////////
	    //
	    // STATIC FIELDS
	    //
	    ////////////////////////////////////////////////////////////////////////////

	    private static Map<String,Map<String,Object>> accounts =
	        new HashMap<String,Map<String,Object>>();

	    private static Map<String,Map<String,Object>> groups =
	        new HashMap<String,Map<String,Object>>();

	    static {
	        // Setup some accounts and groups.
	        accounts.put("Catherine.Simmons", createAccount("Catherine.Simmons", "Catherine", "Simmons", "[email protected]", "secret", "group1", "group2"));
	        accounts.put("Aaron.Nichols", createAccount("Aaron.Nichols", "Aaron", "Nichols", "[email protected]", "secret", "group1"));
	        accounts.put("deleteMe", createAccount("deleteMe", "Delete", "Me", "[email protected]", "secret", "group1"));
	        
	        groups.put("group1", createGroup("group1", "Description of Group 1"));
	        groups.put("group2", createGroup("group2", "Description of Group 2"));
	    }

	    /**
	     * Builds a new account map (username, firstname, lastname, email, password, groups).
	     * Used by the static initializer to seed demo data; can also be used by tests.
	     * Does NOT add the account to the store—caller must put it in the accounts map.
	     */
	    public static Map<String,Object> createAccount(String username, String firstname,
	                                                   String lastname, String email,
	                                                   String password, String... groups) {
	        Map<String,Object> acct = new HashMap<String,Object>();
	        acct.put(ATTR_USERNAME, username);
	        acct.put(ATTR_FIRSTNAME, firstname);
	        acct.put(ATTR_LASTNAME, lastname);
	        acct.put(ATTR_EMAIL, email);
	        acct.put(ATTR_PASSWORD, password);

	        List<String> grpList = null;
	        if (null != groups) {
	            grpList = new ArrayList<String>(Arrays.asList(groups));
	        }
	        acct.put(ATTR_GROUPS, grpList);

	        return acct;
	    }
	    
	    /**
	     * Builds a new group map (name, description). Used by the static initializer
	     * to seed demo groups; does not add to the store—caller must put in groups map.
	     */
	    public static Map<String,Object> createGroup(String name, String desc) {
	        Map<String,Object> group = new HashMap<String,Object>();
	        group.put(GROUP_ATTR_NAME, name);
	        group.put(GROUP_ATTR_DESCRIPTION, desc);
	        return group;
	    }
	    
	    /**
	     * Debug helper: prints all accounts and groups to System.out. Useful for
	     * unit tests or manual verification; not used by SailPoint at runtime.
	     */
	    public static void dump() {
	        System.out.println(accounts);
	        System.out.println(groups);
	    }

	    ////////////////////////////////////////////////////////////////////////////
	    //
	    // CONSTRUCTORS
	    //
	    ////////////////////////////////////////////////////////////////////////////

	    /** Default constructor; SailPoint typically uses the config+log constructor. */
	    public CustomConnectorDemo() {
	        super();
	    }

	    /**
	     * Constructor used by SailPoint: supplies configuration (e.g. input1, input2)
	     * and a Log for connector output. Base class stores config and sets objectType.
	     * 
	     * @param  config  Connector configuration (host, credentials, custom keys like input1/input2).
	     * @param  log     Log instance for connector messages.
	     */
	    public CustomConnectorDemo(ConnectorConfig config, Log log) {
	        super(config, log);
	    }

	    ////////////////////////////////////////////////////////////////////////////
	    //
	    // LIFECYCLE
	    //
	    ////////////////////////////////////////////////////////////////////////////
	    
	    /**
	     * Called when the connector is no longer needed. We use no external resources
	     * (only in-memory maps), so this is a no-op. Real connectors would close
	     * connections, release handles, etc.
	     */
	    public void close() {
	        // No-op.
	    }

	    /**
	     * Called by SailPoint to verify the connection to the target system. We do not
	     * connect to anything; we read config keys "input1" and "input2" and print them
	     * so you can confirm the connector config is passed correctly. In production,
	     * this would typically attempt a real connection and throw on failure.
	     */
	    public void testConnection() {

		String input1 = config.getString("input1");
		String input2 = config.getString("input2");
		System.out.println("Testing connector:\ninput1 = " + input1 + "\ninput2 = " + input2); 

	    }
	    
	    /**
	     * Tells SailPoint which operations are supported for the given object type
	     * (e.g. account or group). We declare all features (Create, Read, Update,
	     * Delete, Enable, Disable, Unlock, SetPassword, Authenticate, etc.) for
	     * both types. Real connectors return only what the target system supports.
	     */
	    public List<Feature> getSupportedFeatures(String objectType) {
	        return Arrays.asList(Feature.values());
	    }

	    /**
	     * Returns the list of object types this connector manages. We add GROUP to the
	     * default ACCOUNT type from the base class. SailPoint uses this to know it can
	     * aggregate and provision both accounts and groups through this connector.
	     */
	    public List<String> getSupportedObjectTypes() {
	        // Add group support to the default account support.
	        List<String> types = super.getSupportedObjectTypes();
	        types.add(OBJECT_TYPE_GROUP);
	        return types;
	    }


	    ////////////////////////////////////////////////////////////////////////////
	    //
	    // BASIC CRUD
	    //
	    ////////////////////////////////////////////////////////////////////////////

	    /**
	     * Returns the in-memory store (accounts or groups) for the current object type.
	     * objectType is set by the framework based on which object type is being
	     * aggregated or provisioned. Used by create/read/update/delete/iterate.
	     */
	    private Map<String,Map<String,Object>> getObjectsMap()
	        throws ConnectorException {

	        if (OBJECT_TYPE_ACCOUNT.equals(this.objectType)) {
	            return accounts;
	        }
	        else if (OBJECT_TYPE_GROUP.equals(this.objectType)) {
	            return groups;
	        }
	        throw new ConnectorException("Unhandled object type: " + this.objectType);
	    }
	    
	    /**
	     * Creates a new account or group in the store with the given native identifier
	     * and attributes (items). If an object with that ID already exists, throws
	     * ObjectAlreadyExistsException. Otherwise builds a new map from items, stores it,
	     * and returns a Committed result with the new object. Called during provisioning.
	     */
	    public Result create(String nativeIdentifier, List<Item> items)
	        throws ConnectorException, ObjectAlreadyExistsException {

	        Result result = new Result(Result.Status.Committed);

	        Object existing = read(nativeIdentifier);
	        if (null != existing) {
	            throw new ObjectAlreadyExistsException(nativeIdentifier);
	        }

	        Map<String,Object> object = new HashMap<String,Object>();
	        object.put(getIdentityAttribute(), nativeIdentifier);
	        if (items != null) {
	            for (Item item : items)
	                object.put(item.getName(), item.getValue());
	        }
	        getObjectsMap().put(nativeIdentifier, object);
	        result.setObject(object);

	        return result;
	    }

	    /**
	     * Reads a single account or group by native identifier. Returns a COPY of the
	     * object so callers cannot mutate our store. Used for aggregation lookups and
	     * provisioning reads. Delegates to read(id, false).
	     */
	    public Map<String,Object> read(String nativeIdentifier)
	        throws ConnectorException, IllegalArgumentException {
	        return read(nativeIdentifier, false);
	    }

	    /**
	     * Internal read: returns the object from the store. If forUpdate is false,
	     * returns a copy to protect the cache; if true (used by update/enable/disable/
	     * setPassword), returns the actual map so we can modify it in place.
	     */
	    private Map<String,Object> read(String nativeIdentifier, boolean forUpdate)
	        throws ConnectorException, IllegalArgumentException {

	        if (null == nativeIdentifier) {
	            throw new IllegalArgumentException("nativeIdentifier is required");
	        }
	        
	        Map<String,Object> obj = getObjectsMap().get(nativeIdentifier);

	        // If we're not updating, create a copy so the cache won't get corrupted.
	        return (forUpdate) ? obj : copy(obj);
	    }
	    
	    /**
	     * Returns a shallow copy of the given map, or null if the input is null.
	     * Used to avoid exposing internal maps to callers. Note: nested lists (e.g.
	     * groups) are not deep-cloned; modifying them in the copy can affect the store.
	     */
	    private Map<String,Object> copy(Map<String,Object> obj) {
	        // Should do a deeper clone here for nested collections.
	        return (null != obj) ? new HashMap<String,Object>(obj) : null;
	    }
	    
	    /**
	     * Returns an iterator over all accounts or all groups (depending on objectType),
	     * filtered by the SailPoint filter and returning a copy of each map. Used during
	     * aggregation: SailPoint calls this to discover all identities/groups. We copy
	     * the list of values to avoid concurrent modification; we wrap in FilteredIterator
	     * to apply the framework filter, then CopyIterator so each returned map is a copy.
	     * Real connectors would push the filter to the target (e.g. LDAP filter).
	     */
	    public Iterator<Map<String,Object>> iterate(Filter filter)
	        throws ConnectorException {
	        
	        // Return the iterator on a copy of the list to avoid concurrent mod
	        // exceptions if entries are added/removed while iterating.
	        Iterator<Map<String,Object>> it =
	            new ArrayList<Map<String,Object>>(getObjectsMap().values()).iterator();

	        // Note: FilteredIterator should not be used for most connectors.
	        // Instead, the filter should be converted to something that can be
	        // used to filter results natively (eg - an LDAP search filter, etc...)
	        // Wrap this in a CopyIterator so the cache won't get corrupted.
	        return new CopyIterator(new FilteredIterator(it, filter));
	    }

	    /**
	     * Updates an existing account or group. Loads the object in "for update" mode,
	     * then applies each Item according to its operation: Add (append to list, e.g.
	     * add to groups), Remove (remove from list), Set (replace attribute value).
	     * getAsList() (from base class) normalizes single values to a list for multivalued
	     * handling. Called during provisioning when SailPoint sends attribute changes.
	     */
	    public Result update(String nativeIdentifier, List<Item> items)
	        throws ConnectorException, ObjectNotFoundException {

	        Result result = new Result(Result.Status.Committed);

	        Map<String,Object> existing = read(nativeIdentifier, true);
	        if (null == existing) {
	            throw new ObjectNotFoundException(nativeIdentifier);
	        }
	        
	        if (items != null) {
	            for (Item item : items) {
	                String name = item.getName();
	                Object value = item.getValue();
	                Item.Operation op = item.getOperation();

	                switch (op) {
	                case Add: {
	                    List<Object> currentList = getAsList(existing.get(name));
	                    List<Object> values = getAsList(value);
	                    currentList.addAll(values);
	                    existing.put(name, currentList);
	                }
	                break;

	                case Remove: {
	                    List<Object> currentList = getAsList(existing.get(name));
	                    List<Object> values = getAsList(value);
	                    currentList.removeAll(values);
	                    if (currentList.isEmpty())
	                        existing.remove(name);
	                    else
	                        existing.put(name, currentList);
	                }
	                break;
	                
	                case Set: {
	                    existing.put(name, value);
	                }
	                break;
	                
	                default:
	                    throw new IllegalArgumentException("Unknown operation: " + op);
	                }
	            }
	        }

	        return result;
	    }

	    /**
	     * Deletes the account or group with the given native identifier. Removes the
	     * entry from the appropriate store; throws ObjectNotFoundException if not found.
	     * The options map is echoed into the result as "key:value" strings for unit
	     * tests that verify round-trip of options. Called during de-provisioning.
	     */
	    public Result delete(String nativeIdentitifer, Map<String,Object> options)
	        throws ConnectorException, ObjectNotFoundException {

	        Result result = new Result(Result.Status.Committed);

	        Object removed = getObjectsMap().remove(nativeIdentitifer);
	        if (null == removed) {
	            throw new ObjectNotFoundException(nativeIdentitifer);
	        }
	        
	        // Echo options into result so unittests can confirm round trip.
	        if ( options != null ) {
	            Iterator<String> keys = options.keySet().iterator();
	            if ( keys != null ) {
	                while ( keys.hasNext() ) {
	                    String key = keys.next();
	                    result.add(key + ":" + options.get(key));
	                }
	            }
	        }
	        return result;
	    }
	    
	    ////////////////////////////////////////////////////////////////////////////
	    //
	    // EXTENDED OPERATIONS
	    //
	    ////////////////////////////////////////////////////////////////////////////

	    /**
	     * Enables an account: sets the "disabled" attribute to false. Used when a user
	     * is re-enabled in the target system. Reads the object for update and modifies
	     * it in place; returns Committed.
	     */
	    public Result enable(String nativeIdentifier, Map<String,Object> options)
	        throws ConnectorException, ObjectNotFoundException {

	        Result result = new Result(Result.Status.Committed);

	        Map<String,Object> obj = read(nativeIdentifier, true);
	        if (null == obj) {
	            throw new ObjectNotFoundException(nativeIdentifier);
	        }

	        obj.put(ATTR_DISABLED, false);

	        return result;
	    }

	    /**
	     * Disables an account: sets the "disabled" attribute to true. Used when a user
	     * is disabled in the target system. Reads the object for update and modifies
	     * it in place; returns Committed.
	     */
	    public Result disable(String nativeIdentifier, Map<String,Object> options)
	        throws ConnectorException, ObjectNotFoundException {

	        Result result = new Result(Result.Status.Committed);

	        Map<String,Object> obj = read(nativeIdentifier, true);
	        if (null == obj) {
	            throw new ObjectNotFoundException(nativeIdentifier);
	        }

	        obj.put(ATTR_DISABLED, true);

	        return result;
	    }

	    /**
	     * Unlocks an account: sets the "locked" attribute to false. Used when an
	     * admin unlocks a locked account in the target system. Reads for update
	     * and modifies in place; returns Committed.
	     */
	    public Result unlock(String nativeIdentifier, Map<String,Object> options)
	        throws ConnectorException, ObjectNotFoundException {

	        Result result = new Result(Result.Status.Committed);

	        Map<String,Object> obj = read(nativeIdentifier, true);
	        if (null == obj) {
	            throw new ObjectNotFoundException(nativeIdentifier);
	        }

	        obj.put(ATTR_LOCKED, false);

	        return result;
	    }

	    /**
	     * Sets the password for the given account. Updates ATTR_PASSWORD to newPassword;
	     * if expiration is non-null, stores it in options and saves options under
	     * ATTR_PASSWORD_OPTIONS (used by authenticate() to enforce expiration). If
	     * currentPassword is provided, appends it to ATTR_PASSWORD_HISTORY (e.g. for
	     * password history policy). Called during password change or reset provisioning.
	     */
	    public Result setPassword(String nativeIdentifier, String newPassword,
	                              String currentPassword, Date expiration,
	                              Map<String,Object> options)
	        throws ConnectorException, ObjectNotFoundException {

	        Result result = new Result(Result.Status.Committed);

	        Map<String,Object> obj = read(nativeIdentifier, true);
	        if (null == obj) {
	            throw new ObjectNotFoundException(nativeIdentifier);
	        }

	        obj.put(ATTR_PASSWORD, newPassword);

	        // Store expiration in options so authenticate() can check it.
	        if (expiration != null) {
	            if (options == null)
	                options = new HashMap<String,Object>();
	            options.put(ARG_EXPIRATION, expiration);
	        }
	        obj.put(ATTR_PASSWORD_OPTIONS, options);
	        
	        if (null != currentPassword) {
	            @SuppressWarnings("unchecked")
	            List<String> history = (List<String>) obj.get(ATTR_PASSWORD_HISTORY);
	            if (null == history) {
	                history = new ArrayList<String>();
	                obj.put(ATTR_PASSWORD_HISTORY, history);
	            }
	            history.add(currentPassword);
	        }

	        return result;
	    }

	    ////////////////////////////////////////////////////////////////////////////
	    //
	    // ADDITIONAL FEATURES
	    //
	    ////////////////////////////////////////////////////////////////////////////

	    /**
	     * Verifies that the given identity (username) and password match an account in
	     * the store. If the account does not exist, throws ObjectNotFoundException. If
	     * the password does not match, throws AuthenticationFailedException. If the
	     * password matches but has expired (expiration date in ATTR_PASSWORD_OPTIONS is
	     * before now), throws ExpiredPasswordException. On success returns the account
	     * map. Used for login or credential verification.
	     */
	    public Map<String,Object> authenticate(String identity, String password)
	        throws ConnectorException, ObjectNotFoundException,
	               AuthenticationFailedException, ExpiredPasswordException {

	        Map<String,Object> obj = read(identity);
	        if (null == obj) {
	            throw new ObjectNotFoundException(identity);
	        }

	        String actualPassword = (String) obj.get(ATTR_PASSWORD);

	        // If the password matches, check the expiration if one is set.
	        if ((null != actualPassword) && actualPassword.equals(password)) {
	            @SuppressWarnings("unchecked")
	            Map<String,Object> passwordsOptions =
	                (Map<String,Object>) obj.get(ATTR_PASSWORD_OPTIONS);
	            if (null != passwordsOptions) {
	                Date expiration = (Date) passwordsOptions.get(ARG_EXPIRATION);
	                if ((null != expiration) && expiration.before(new Date())) {
	                    throw new ExpiredPasswordException(identity);
	                }
	            }
	        }
	        else {
	            throw new AuthenticationFailedException();
	        }

	        return obj;
	    }

	    /**
	     * Returns the schema (list of attributes and types) for the current object type.
	     * SailPoint calls this to know which attributes exist for accounts vs groups.
	     * Account schema: username, firstname, lastname, email, groups (multivalued),
	     * disabled, locked, password (secret). Group schema: name, description. Used
	     * during connector configuration and aggregation.
	     */
	    public Schema discoverSchema() {
	        Schema schema = new Schema();
	        
	        if (OBJECT_TYPE_ACCOUNT.equals(this.objectType)) {
	            schema.addAttribute(ATTR_USERNAME);
	            schema.addAttribute(ATTR_FIRSTNAME);
	            schema.addAttribute(ATTR_LASTNAME);
	            schema.addAttribute(ATTR_EMAIL);
	            schema.addAttribute(ATTR_GROUPS, Schema.Type.STRING, true);
	            schema.addAttribute(ATTR_DISABLED, Schema.Type.BOOLEAN);
	            schema.addAttribute(ATTR_LOCKED, Schema.Type.BOOLEAN);
	            schema.addAttribute(ATTR_PASSWORD, Schema.Type.SECRET);
	        }
	        else {
	            schema.addAttribute(GROUP_ATTR_NAME);
	            schema.addAttribute(GROUP_ATTR_DESCRIPTION);
	        }

	        return schema;
	    }

	 
	
	}

===========================================================================


package sailpoint.iiq.connector;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import openconnector.AbstractConnector;
import openconnector.ConnectorConfig;
import openconnector.ConnectorException;
import openconnector.Filter;
import openconnector.Item;
import openconnector.ObjectNotFoundException;
import openconnector.Result;
import openconnector.Result.Status;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import sailpoint.iiq.object.Resources;
import sailpoint.iiq.object.User;
import sailpoint.tools.Base64;

import com.google.gson.Gson;

/**
 * SailPoint IdentityIQ (IIQ) connector that uses the SCIM v2 REST API.
 *
 * -----------------------------------------------------------------------------
 * FLOW DIAGRAM (Visual)
 * -----------------------------------------------------------------------------
 *
 *                              +------------------+
 *                              |   IIQ / Caller   |
 *                              +--------+---------+
 *                                       |
 *                                       v
 *  +------------------------------------------------------------------------------------------+
 *  | configure(config, log)                                                                   |
 *  |   --> baseUrl, user, password, pageSize, accountFilter                                   |
 *  |   --> setAuthorization() --> "Basic " + Base64(user:password)                            |
 *  +------------------------------------------------------------------------------------------+
 *                                       |
 *                     +-----------------+-----------------+
 *                     v                 v                 v
 *            +----------------+ +-------------+ +------------------------+
 *            | testConnection | |  iterate()  | | read(id) / provisioning |
 *            +-------+--------+ +------+------+ +-----------+-------------+
 *                    |                 |                    |
 *                    v                 v                    v
 *            +----------------+ +------+------+    +-----------------------+
 *            | GET /Resource  | | UserIterator|    | create: POST /Users   |
 *            | Types/         | |     |       |    | update: GET+PUT /Users|
 *            | --> 2xx? throw | +------+------+    | delete: DELETE /Users |
 *            +----------------+        |           | disable: GET+PUT      |
 *                                      v           | enable:  GET+PUT      |
 *                             +--------+--------+  | setPassword: GET+PUT  |
 *                             |PagedUsersIterator| +----------+-----------+
 *                             |  next():          |            |
 *                             |  GET /Users?      |            v
 *                             |   startIndex,     |  +---------------------+
 *                             |   count, filter   |  | getResultFromResponse|
 *                             +--------+---------+  | --> Result(Committed |
 *                                      |            |      or Failed)      |
 *                                      v            +----------------------+
 *                             +----------------+
 *                             | Resources JSON |
 *                             | --> List<User> |
 *                             +--------+-------+
 *                                      |
 *                                      v
 *                             +----------------+
 *                             | user.getMap()  |
 *                             | (one per next())|
 *                             +----------------+
 *
 * -----------------------------------------------------------------------------
 * STEP-BY-STEP FLOW (Summary)
 * -----------------------------------------------------------------------------
 *
 * 1. STARTUP
 *    configure(config, log)
 *       -> Read host, user, password, pageSize, accountFilter from config
 *       -> setAuthorization(user, password) builds Basic auth header
 *
 * 2. CONNECTION TEST (optional)
 *    testConnection()
 *       -> GET baseUrl/scim/v2/ResourceTypes/
 *       -> Throw ConnectorException if no response or non-2xx
 *
 * 3. AGGREGATION (list all users)
 *    iterate(filter)
 *       -> Returns UserIterator(pageSize, accountFilter)
 *    UserIterator.hasNext() -> true if buffer has users OR PagedUsersIterator has more pages
 *    UserIterator.next()    -> If buffer empty: PagedUsersIterator.next() fetches next page
 *                            -> Returns one user as Map, buffer shrinks until refill
 *    PagedUsersIterator.next()
 *       -> GET .../scim/v2/Users?startIndex=&count=&sortOrder=ascending[&filter=]
 *       -> Parse JSON -> Resources -> List<User>; set totalResults, advance currentIndex
 *
 * 4. READ ONE USER
 *    read(id) -> GET .../scim/v2/Users/{id} -> User -> user.getMap()
 *    evaluateResponse() throws if response null or !isSuccessful()
 *
 * 5. PROVISIONING
 *    create(id, items)   -> Build User(id), user.modify(items), POST .../scim/v2/Users
 *    update(id, items)   -> GET user -> user.modify(items) -> PUT .../scim/v2/Users/{id}
 *    delete(id, options) -> DELETE .../scim/v2/Users/{id}
 *    disable(id, options)-> GET user -> user.setActive(false) -> PUT
 *    enable(id, options) -> GET user -> user.setActive(true)  -> PUT
 *    setPassword(...)    -> GET user -> user.setPassword(...) -> PUT
 *    All use getResultFromResponse(response, request) for Result (Committed/Failed).
 *
 * 6. HELPERS
 *    evaluateResponse()      -> throw ConnectorException if null or !success
 *    getResultFromResponse() -> Result Committed (2xx) or Failed + message
 *    setAuthorization()      -> "Basic " + Base64(user:password)
 * -----------------------------------------------------------------------------
 */
public class IIQConnector extends AbstractConnector {

	private static Log log = LogFactory.getLog( IIQConnector.class );
	
	/*
	 * Args for the Application definition.
	 */
	public static final String ARGS_URL = "host";
    public static final String ARGS_USER = "user";
    public static final String ARGS_PASSWORD = "password";
    public static final String ARGS_PAGE_SIZE = "pageSize";
    public static final String ARGS_ACCOUNT_FILTER = "accountFilter";
  
    /*
     * Default HTTP Headers
     */
    private static final String HTTP_HEADER_AUTHORIZATION_KEY = "authorization";
    private static final String HTTP_HEADER_CACHE_CONTROL_KEY = "cache-control";
    private static final String HTTP_HEADER_CACHE_CONTROL_VALUE = "no-cache";
    private static final String HTTP_HEADER_CONTENT_TYPE_KEY = "content-type";
    private static final String HTTP_HEADER_CONTENT_TYPE_VALUE = "application/json";
    
    /*
     * Query Parameters
     */
    private static final String QUERY_PARAM_START_INDEX = "startIndex";
    private static final String QUERY_PARAM_COUNT = "count";
    private static final String QUERY_PARAM_SORT_ORDER = "sortOrder";
    private static final String QUERY_PARAM_FILTER = "filter";
    
    /*
     * SCIM HTTP Endpoints
     */
    public static final String ENDPOINT_SCIM_USERS = "/scim/v2/Users/";
    public static final String ENDPOINT_SCIM_RESOURCE_TYPES = "/scim/v2/ResourceTypes/";
	
    /*
     * Defaults
     */
    public static final String DEFAULT_BASE_URL = "http://localhost:8080/identityiq";
    
    public static final int DEFAULT_PAGE_SIZE = 100;
    
    private static final String DEFAULT_BASIC_AUTH = "Basic c3BhZG1pbjphZG1pbg==";
    
    /*
     * Instance variables
     */
    String baseUrl = DEFAULT_BASE_URL;
    
    String authorization = DEFAULT_BASIC_AUTH;
    
    String user = "spadmin";
    
    String password = "admin";
    
    int pageSize = DEFAULT_PAGE_SIZE;
    
    String accountFilter = null;
    
    Gson gson = new Gson();
    
    /*
	 * This is an HTTP client, which we will re-use.
	 */
	OkHttpClient client = new OkHttpClient();
    
	/**
	 * Called once at startup to apply connector settings from the IIQ Application definition.
	 * Reads host, user, password, pageSize, accountFilter and builds Basic auth header.
	 */
	@Override
	public void configure( ConnectorConfig config, openconnector.Log log) {
		
		super.configure( config, (openconnector.Log) log ); 
		
		this.baseUrl = StringUtils.removeEnd( config.getString( ARGS_URL ), "/" );
		
		this.user = config.getString( ARGS_USER );
		
		this.password = config.getString( ARGS_PASSWORD );
		
		this.authorization = setAuthorization( user, password );
		
		this.pageSize = Integer.parseInt( config.getString( ARGS_PAGE_SIZE ) );
		
		this.accountFilter = config.getString( ARGS_ACCOUNT_FILTER );
		
		this.gson = new Gson();
		
	}
    
    /**
     * Declares that this connector supports the "account" object type (users).
     */
    @Override
    public List<String> getSupportedObjectTypes() {
    	List<String> types = super.getSupportedObjectTypes();
        types.add( OBJECT_TYPE_ACCOUNT );
        return types;
    }

    /**
     * Declares which provisioning features are supported for the given object type:
     * create, update, delete, enable, setPassword.
     */
    public List<Feature> getSupportedFeatures( String objectType ) {
        List<Feature> features = super.getSupportedFeatures( objectType );
        features.add( Feature.CREATE );
        features.add( Feature.UPDATE );
        features.add (Feature.DELETE );
        features.add( Feature.ENABLE );
        features.add( Feature.SET_PASSWORD );
        return features;
    }
	
    /**
     * Verifies that the SCIM server is reachable by sending a GET to /scim/v2/ResourceTypes/
     * with the configured credentials. Throws ConnectorException if the request fails or returns non-2xx.
     */
    @Override
    public void testConnection() throws ConnectorException {
    	
    	OkHttpClient client = new OkHttpClient();
    		
        try {
        	// ResourceTypes is a lightweight endpoint used only to check connectivity
        	Request request = new Request.Builder()
	  			.url( baseUrl + ENDPOINT_SCIM_RESOURCE_TYPES )
	  			.get()
	  			.addHeader( HTTP_HEADER_AUTHORIZATION_KEY, authorization )
	  			.addHeader( HTTP_HEADER_CACHE_CONTROL_KEY, HTTP_HEADER_CACHE_CONTROL_VALUE )
	  			.build();

        	Response response = client.newCall( request ).execute();
        	
        	if ( response == null )
        		throw new ConnectorException( "No response received from web-service call." );
        	else if ( !response.isSuccessful() )
        		throw new ConnectorException( "Received " + response.message() + " [" + response.code() + "] for request [" + request + "]" );
        	      
        } catch ( Exception e ) {
        
        	if ( e.getCause() != null) {
                throw new ConnectorException("Test Connection Failed: " + e.getCause() );
            } else {
                throw new ConnectorException("Test Connection Failed: " + e.getMessage() );
            }
        } 
    }
    
    /**
     * Returns an iterator over all accounts (users) from the SCIM server.
     * Flow: caller repeatedly calls hasNext() and next(); next() uses PagedUsersIterator
     * to fetch pages of users and returns one user (as Map) per next() call.
     */
    @Override
	public Iterator<Map<String, Object>> iterate( Filter filter ) throws ConnectorException, UnsupportedOperationException {
		return new UserIterator( pageSize, accountFilter );
	}
    
    /**
     * Iterates over users one-by-one. Buffers a page of users from PagedUsersIterator
     * and hands them out on each next() until the buffer is empty, then fetches the next page.
     */
    private class UserIterator implements Iterator<Map<String, Object>> {
	
    	List<User> users = new ArrayList<User>();
    	
    	PagedUsersIterator usersIterator = new PagedUsersIterator( 1, DEFAULT_PAGE_SIZE, null );
    	
		@SuppressWarnings("unused")
		UserIterator() {
    		usersIterator = new PagedUsersIterator( DEFAULT_PAGE_SIZE, null );
		}
    	
       	UserIterator( int pageSize, String accountFilter ) {
       		usersIterator = new PagedUsersIterator( pageSize, accountFilter );
    	}
	
    	/** True if we still have a user in the buffer or if PagedUsersIterator has more pages. */
    	@Override
    	public boolean hasNext() {
		
    		if ( users == null )
    			users = new ArrayList<User>();
    		
    		if ( !users.isEmpty() )
    			return true;
    		// Buffer empty: check if there are more pages to fetch
    		return usersIterator.hasNext();
    	}

    	/**
    	 * Returns the next user as a Map. If the buffer is empty, fetches the next page
    	 * from SCIM (via PagedUsersIterator), then returns the first user from the buffer.
    	 */
    	@Override
    	public Map<String, Object> next() {
    		
    		User user = null; 
    		
    		if ( users != null ) {
    			// Refill buffer from next page when empty
    			if ( users.isEmpty() ) {
    				List<User> nextPageOfUsers = usersIterator.next();
    				if ( nextPageOfUsers != null && !nextPageOfUsers.isEmpty() )
    					users.addAll( nextPageOfUsers );
    			}
    			if ( !users.isEmpty() )
    				user = users.remove( 0 );
    		}
    		
    		log.debug( "Users size: " + users.size() );
    		log.debug( "Returning object: " + user );
    		
    		return ( user != null ) ? user.getMap() : new HashMap<String,Object>();
    	}
	}
    
    /**
     * Fetches users from SCIM in pages. Each next() performs one GET /scim/v2/Users
     * with startIndex and count; totalResults from the response drives hasNext().
     */
    private class PagedUsersIterator implements Iterator<List<User>> {
    	
    	/*
    	 * Page Size, defaults to 100 records per page.
    	 */
    	int pageSize = 100;
    	
    	/*
    	 * This is the current index; We start at 1.
    	 */
    	int currentIndex = 1;
    	
    	/*
    	 * This is the total number of records we know about, used for hasNext().
    	 * -1 means that the totalResults has not been set yet.
    	 * 0 or more means that the totalResults
    	 */
    	int totalResults = -1;
    	
    	/*
    	 * Account filter string
    	 */
    	String accountFilter = null;
    	
    	@SuppressWarnings("unused")
		PagedUsersIterator ( ) {
    		this.currentIndex = 1;
    		this.pageSize = DEFAULT_PAGE_SIZE;
    		this.accountFilter = null;
    	}
    	
    	PagedUsersIterator ( int pageSize, String accountFilter  ) {
    		
    		this.currentIndex = 1;
    		
    		if ( pageSize < 1 ) {
    			log.error( "Paged Users Iterator: Bad page size detected. Falling back to default of " + DEFAULT_PAGE_SIZE + ".");
    			this.pageSize = DEFAULT_PAGE_SIZE;
    		} else {
    			this.pageSize = pageSize;
    		}
    		
    		this.accountFilter = accountFilter;   		
    	}
    	
    	PagedUsersIterator ( int startIndex, int pageSize, String accountFilter ) {
    		
    		this.currentIndex = startIndex;
    		
    		if ( pageSize < 1 ) {
    			log.error( "Paged Users Iterator: Bad page size detected. Falling back to default of " + DEFAULT_PAGE_SIZE + ".");
    			this.pageSize = DEFAULT_PAGE_SIZE;
    		} else {
    			this.pageSize = pageSize;
    		}
    		
    		this.accountFilter = accountFilter;
    	}
	
    	/** True until we have fetched all pages (currentIndex &gt; totalResults). totalResults=-1 means unknown, so we assume more. */
    	@Override
    	public boolean hasNext() {
		
    		if ( totalResults == -1 )
    			return true;
    		else 
    			return currentIndex <= totalResults;
    	}

    	/**
    	 * Performs one GET to /scim/v2/Users with startIndex, count, optional filter;
    	 * parses JSON into Resources, extracts list of User, updates totalResults and currentIndex.
    	 */
    	@SuppressWarnings("unchecked")
		@Override
    	public List<User> next() {
    		
    		log.info( "Paged Users Iterator: Fetching next page with settings currentIndex["+currentIndex+"], pageSize["+pageSize+"], totalResults["+totalResults+"], accountFilter["+accountFilter+"]" );
    		
    		OkHttpClient client = new OkHttpClient();
        		
        	List<User> users = null;
        	
        	try {
                // Build URL with SCIM pagination and optional filter
        		HttpUrl.Builder urlBuilder = HttpUrl.parse( baseUrl + ENDPOINT_SCIM_USERS ).newBuilder();
        		urlBuilder.addQueryParameter( QUERY_PARAM_START_INDEX, Integer.toString( currentIndex ) );
        		urlBuilder.addQueryParameter( QUERY_PARAM_COUNT, Integer.toString( pageSize ) );
        		urlBuilder.addQueryParameter( QUERY_PARAM_SORT_ORDER, "ascending" );
        		
        		if ( accountFilter != null )
        			urlBuilder.addQueryParameter( QUERY_PARAM_FILTER, accountFilter );
        		
        		Request request = new Request.Builder()
    	  			.url( urlBuilder.build() )
            		.get()
    	  			.addHeader( HTTP_HEADER_AUTHORIZATION_KEY, authorization )
    	  			.addHeader( HTTP_HEADER_CACHE_CONTROL_KEY, HTTP_HEADER_CACHE_CONTROL_VALUE )
    	  			.build();
        		  	
            	Response response = client.newCall( request ).execute();
            	
            	evaluateResponse( response, request );
            	
            	String jsonString = response.body().string();
        		
            	Resources resources = gson.fromJson( jsonString, Resources.class );
            	
            	/*
            	 * This is the paged set of users which we will be returning.
            	 */
            	users = (List<User>) resources.getResources();
            	
            	/*
            	 * Update the totalResults based what our query indicates. 
            	 * We'll do a null check here to make sure we have a resource object.
            	 */
            	totalResults = (resources != null ) ? resources.getTotalResults() : 0;
            	
            	/*
            	 * Update the currentIndex for next iterations.
            	 */
            	currentIndex = currentIndex + pageSize;
            	
            } catch ( Exception e ) {
            
            	if ( e.getCause() != null) {
                    throw new ConnectorException("Account Aggregation Failed: " + e.getCause() );
                } else {
                    throw new ConnectorException("Account Aggregation Failed: " + e.getMessage() );
                }
            }
        	
        	/*
        	 * Return our list of users, if we have any.
        	 * Note: this can return null.
        	 */
        	return users;
    	}
    }
	
    /**
     * Fetches a single user by ID. Performs GET /scim/v2/Users/{id}, parses JSON to User, returns user as Map.
     */
    @Override
    public Map<String, Object> read ( String id ) throws ConnectorException, ObjectNotFoundException, UnsupportedOperationException {
        
    	OkHttpClient client = new OkHttpClient();
        
    	Map<String, Object> returnObject = null;
        
    	try {
            
        	Request request = new Request.Builder()
	  			.url( baseUrl + ENDPOINT_SCIM_USERS + id )
	  			.get()
	  			.addHeader( HTTP_HEADER_AUTHORIZATION_KEY, authorization )
	  			.addHeader( HTTP_HEADER_CACHE_CONTROL_KEY, HTTP_HEADER_CACHE_CONTROL_VALUE )
	  			.build();

        	Response response = client.newCall( request ).execute();
        	
        	evaluateResponse( response, request );
        	
        	String jsonString = response.body().string();
    		
        	User user = gson.fromJson( jsonString, User.class );
        	
        	returnObject = user.getMap();
        	
        } catch ( Exception e ) {
        
        	if ( e.getCause() != null) {
                throw new ConnectorException("Account Aggregation Failed: " + e.getCause() );
            } else {
                throw new ConnectorException("Account Aggregation Failed: " + e.getMessage() );
            }
        }
    	
        return returnObject;
    }
	
    /**
     * Creates a new user in IIQ. Builds a User from id and items, serializes to JSON, POSTs to /scim/v2/Users.
     */
    @Override
    public Result create( String id, List<Item> items ) {

    	Result result = new Result( Result.Status.Committed );
        
    	OkHttpClient client = new OkHttpClient();
        
    	Response response = null;
        
    	try {
        	// Step 1: Build in-memory User and apply provisioning items
        	User user = new User();
        	user.setUserName( id );
        	user.modify( items );
        	
        	log.debug( gson.toJson( user ) );
        	
        	/*
        	 * Step 2. Submit the newly created user for creation.
        	 */
        	MediaType mediaType = MediaType.parse( HTTP_HEADER_CONTENT_TYPE_VALUE );
        	RequestBody body = RequestBody.create( mediaType, gson.toJson( user ) );
        	Request request = new Request.Builder()
        	  .url( baseUrl + ENDPOINT_SCIM_USERS )
        	  .post( body )
        	  .addHeader( HTTP_HEADER_AUTHORIZATION_KEY, authorization )
	  		  .addHeader( HTTP_HEADER_CACHE_CONTROL_KEY, HTTP_HEADER_CACHE_CONTROL_VALUE )
	  		  .addHeader( HTTP_HEADER_CONTENT_TYPE_KEY, HTTP_HEADER_CONTENT_TYPE_VALUE )
        	  .build();
        	
        	response = client.newCall( request ).execute();
        	
        	log.debug( "IIQ Connector: Create called with request [" + request + "] and received response [" + response + "]" );
        	
        	result = getResultFromResponse( response, request );
         
        } catch (Exception e) {
            result = new Result( Result.Status.Failed );
            result.add( e.getMessage() );
        } finally {
        	if ( response != null && response.body() != null )
        		response.body().close();
        }
        
        return result;
    }

    /**
     * Updates an existing user. GETs current user by id, applies items with user.modify(), PUTs back to /scim/v2/Users/{id}.
     */
    @Override
    public Result update( String id, List<Item> items ) {
    	
    	Result result = new Result( Result.Status.Committed );
        
    	OkHttpClient client = new OkHttpClient();
    	
    	try {
        	
        	/* 
        	 * Step 1. Get existing User object.
        	 */
          	Request request = new Request.Builder()
    	  			.url( baseUrl + ENDPOINT_SCIM_USERS + id )
    	  			.get()
    	  			.addHeader( HTTP_HEADER_AUTHORIZATION_KEY, authorization )
    	  			.addHeader( HTTP_HEADER_CACHE_CONTROL_KEY, HTTP_HEADER_CACHE_CONTROL_VALUE )
    	  			.build();

            Response response = client.newCall( request ).execute();
            	
            result = getResultFromResponse( response, request );
            	
            String jsonString = response.body().string();
        	
            /*
             * Step 2. Parse the existing object, and modify it according to the plan.
             */
            User user = gson.fromJson( jsonString, User.class );
        	user.modify( items );
        	
        	/*
        	 * Step 3. Submit the modified User to the system.
        	 */
        	MediaType mediaType = MediaType.parse( HTTP_HEADER_CONTENT_TYPE_VALUE );
        	RequestBody body = RequestBody.create( mediaType, gson.toJson( user ) );
        	request = new Request.Builder()
        	  .url( baseUrl + ENDPOINT_SCIM_USERS + id )
        	  .put( body )
        	  .addHeader( HTTP_HEADER_AUTHORIZATION_KEY, authorization )
	  		  .addHeader( HTTP_HEADER_CACHE_CONTROL_KEY, HTTP_HEADER_CACHE_CONTROL_VALUE )
	  		  .addHeader( HTTP_HEADER_CONTENT_TYPE_KEY, HTTP_HEADER_CONTENT_TYPE_VALUE )
        	  .build();
        	
        	response = client.newCall( request ).execute();
        	
        	log.debug( "IIQ Connector: Update called with request [" + request + "] and received response [" + response + "]" );
        	
        	result = getResultFromResponse( response, request );
        
        } catch (Exception e) {
            result = new Result( Result.Status.Failed );
            result.add( e.getMessage() );
            e.printStackTrace();
        } 
        
        return result;
    }
	
    /**
     * Deletes a user in IIQ. Sends DELETE /scim/v2/Users/{id}.
     */
    @Override
    public Result delete( String id, Map<String, Object> options ) throws ConnectorException, ObjectNotFoundException, UnsupportedOperationException {
        
    	Result result = new Result( Result.Status.Committed );
        
    	OkHttpClient client = new OkHttpClient();
        
        try {
        
        	Request request = new Request.Builder()
  			.url( baseUrl + ENDPOINT_SCIM_USERS + id )
  			.delete( null )
  			.addHeader( HTTP_HEADER_AUTHORIZATION_KEY, authorization )
	  		.addHeader( HTTP_HEADER_CACHE_CONTROL_KEY, HTTP_HEADER_CACHE_CONTROL_VALUE )
  			.build();

        	Response response = client.newCall( request ).execute();
        	
        	log.debug( "IIQ Connector: Delete called with request [" + request + "] and received response [" + response + "]" );
        	
        	result = getResultFromResponse( response, request );
        
        } catch (Exception e) {
            result = new Result( Result.Status.Failed );
            result.add( e );
        } 
        
        return result;
    }
    
    
    
    /**
     * Disables a user account. GETs user, sets active=false, PUTs back to /scim/v2/Users/{id}.
     */
    @Override
	public Result disable( String id, Map<String, Object> options ) throws ConnectorException, ObjectNotFoundException, UnsupportedOperationException {
   	
    	Result result = new Result( Result.Status.Committed );
        
    	OkHttpClient client = new OkHttpClient();
        
    	try {
        	// Step 1: Fetch current user
          	Request request = new Request.Builder()
    	  			.url( baseUrl + ENDPOINT_SCIM_USERS + id )
    	  			.get()
    	  			.addHeader( HTTP_HEADER_AUTHORIZATION_KEY, authorization )
    	  			.addHeader( HTTP_HEADER_CACHE_CONTROL_KEY, HTTP_HEADER_CACHE_CONTROL_VALUE )
    	  			.build();

            Response response = client.newCall( request ).execute();
            	
            result = getResultFromResponse( response, request );
            	
            String jsonString = response.body().string();
        	
            /*
             * Step 2. Parse the existing object, and modify it according to the plan.
             */
            User user = gson.fromJson( jsonString, User.class );
        	user.setActive( false );
        	
        	/*
        	 * Step 3. Submit the modified User to the system.
        	 */
        	MediaType mediaType = MediaType.parse( HTTP_HEADER_CONTENT_TYPE_VALUE );
        	RequestBody body = RequestBody.create( mediaType, gson.toJson( user ) );
        	request = new Request.Builder()
        	  .url( baseUrl + ENDPOINT_SCIM_USERS + id )
        	  .put( body )
        	  .addHeader( HTTP_HEADER_AUTHORIZATION_KEY, authorization )
	  		  .addHeader( HTTP_HEADER_CACHE_CONTROL_KEY, HTTP_HEADER_CACHE_CONTROL_VALUE )
	  		  .addHeader( HTTP_HEADER_CONTENT_TYPE_KEY, HTTP_HEADER_CONTENT_TYPE_VALUE )
        	  .build();
        	
        	response = client.newCall( request ).execute();
        	
        	log.debug( "IIQ Connector: Disable called with request [" + request + "] and received response [" + response + "]" );
        	
        	result = getResultFromResponse( response, request );
        
        } catch (Exception e) {
            result = new Result( Result.Status.Failed );
            result.add( e );
        } 
        
    	log.debug( result );
    	
        return result;
	}

	/**
	 * Enables a user account. GETs user, sets active=true, PUTs back to /scim/v2/Users/{id}.
	 */
	@Override
	public Result enable( String id, Map<String, Object> options ) throws ConnectorException, ObjectNotFoundException, UnsupportedOperationException {
   	
		Result result = new Result( Result.Status.Committed );
        
    	OkHttpClient client = new OkHttpClient();
        
    	try {
        	// Step 1: Fetch current user
          	Request request = new Request.Builder()
    	  			.url( baseUrl + ENDPOINT_SCIM_USERS + id )
    	  			.get()
    	  			.addHeader( HTTP_HEADER_AUTHORIZATION_KEY, authorization )
    	  			.addHeader( HTTP_HEADER_CACHE_CONTROL_KEY, HTTP_HEADER_CACHE_CONTROL_VALUE )
    	  			.build();

            Response response = client.newCall( request ).execute();
            	
            result = getResultFromResponse( response, request );
            	
            String jsonString = response.body().string();
        	
            /*
             * Step 2. Parse the existing object, and modify it according to the plan.
             */
            User user = gson.fromJson( jsonString, User.class );
        	user.setActive( true );
        	
        	/*
        	 * Step 3. Submit the modified User to the system.
        	 */
        	MediaType mediaType = MediaType.parse( HTTP_HEADER_CONTENT_TYPE_VALUE );
        	RequestBody body = RequestBody.create( mediaType, gson.toJson( user ) );
        	request = new Request.Builder()
        	  .url( baseUrl + ENDPOINT_SCIM_USERS + id )
        	  .put( body )
        	  .addHeader( HTTP_HEADER_AUTHORIZATION_KEY, authorization )
	  		  .addHeader( HTTP_HEADER_CACHE_CONTROL_KEY, HTTP_HEADER_CACHE_CONTROL_VALUE )
	  		  .addHeader( HTTP_HEADER_CONTENT_TYPE_KEY, HTTP_HEADER_CONTENT_TYPE_VALUE )
        	  .build();
        	
        	response = client.newCall( request ).execute();
        	
        	log.debug( "IIQ Connector: Enable called with request [" + request + "] and received response [" + response + "]" );
        	
        	result = getResultFromResponse( response, request );
        
        } catch (Exception e) {
            result = new Result( Result.Status.Failed );
            result.add( e );
        } 
        
        return result;
	}

	@Override
    public Result setPassword( String id, String newPassword, String currentPassword, Date expiration, Map<String, Object> options ) throws ConnectorException, ObjectNotFoundException, UnsupportedOperationException {
        
    	Result result = new Result( Result.Status.Committed );
        
    	OkHttpClient client = new OkHttpClient();
    	
    	try {
        	
           	/* 
        	 * Step 1. Get existing User object.
        	 */
          	Request request = new Request.Builder()
    	  			.url( baseUrl + ENDPOINT_SCIM_USERS + id )
    	  			.get()
    	  			.addHeader( HTTP_HEADER_AUTHORIZATION_KEY, authorization )
    	  			.addHeader( HTTP_HEADER_CACHE_CONTROL_KEY, HTTP_HEADER_CACHE_CONTROL_VALUE )
    	  			.build();

            Response response = client.newCall( request ).execute();
            	
            result = getResultFromResponse( response, request );
            	
            String jsonString = response.body().string();
        	
            /*
             * Step 2. Parse the existing object, and modify it according to the plan.
             */
            User user = gson.fromJson( jsonString, User.class );
        	user.setPassword( currentPassword );
        	
        	/*
        	 * Step 3. Submit the modified User to the system.
        	 */
        	MediaType mediaType = MediaType.parse( HTTP_HEADER_CONTENT_TYPE_VALUE );
        	RequestBody body = RequestBody.create( mediaType, gson.toJson( user ) );
        	request = new Request.Builder()
        	  .url( baseUrl + ENDPOINT_SCIM_USERS + id )
        	  .put( body )
        	  .addHeader( HTTP_HEADER_AUTHORIZATION_KEY, authorization )
	  		  .addHeader( HTTP_HEADER_CACHE_CONTROL_KEY, HTTP_HEADER_CACHE_CONTROL_VALUE )
	  		  .addHeader( HTTP_HEADER_CONTENT_TYPE_KEY, HTTP_HEADER_CONTENT_TYPE_VALUE )
        	  .build();
        	
        	response = client.newCall( request ).execute();
        	
        	result = getResultFromResponse( response, request );
        
        } catch (Exception e) {
            result = new Result( Result.Status.Failed );
            result.add( e );
        } 
        
        return result;
    }
       
    /**
     * Throws ConnectorException if response is null or not successful (non-2xx). Used after execute() to fail fast.
     */
	private void evaluateResponse( Response response, Request request ) throws ConnectorException {
		
		if ( response != null ) {
			
			if ( !response.isSuccessful() ) {
				throw new ConnectorException( "Error: Received " + response.message() + " [" + response.code() + "] for request [" + request + "]" );
			}
			
		} else {
			throw new ConnectorException( "Error: No response received from web-service call." );
		}
		
	}
	
	/**
	 * Maps HTTP response to connector Result: Committed if success, Failed with message otherwise.
	 */
	private Result getResultFromResponse( Response response, Request request ) {
		
		Result result = new Result();
		
		if ( response != null ) {
			
			if ( response.isSuccessful() ) {
				result.setStatus( Status.Committed );
			} else {
				result.setStatus( Status.Failed );
				result.add( "Error: Received " + response.message() + " [" + response.code() + "] for request [" + request + "]" );
			}
			
		} else {
			result.setStatus( Status.Failed );
			result.add( "Error: No response received from web-service call." );
		}
		
		return result;
				
	}
	
	/**
	 * Builds Basic auth header from user and password (user:password Base64-encoded) and stores in this.authorization.
	 */
	private String setAuthorization ( String user, String password ) {
		
		String authString = user + ":" + password;
		this.authorization = "Basic " + Base64.encodeBytes( authString.getBytes() );
		return this.authorization;

	}

}


About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors