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 > 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;
}
}