/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2011-2015 ForgeRock AS. */ package org.forgerock.openidm.provisioner.openicf.impl; import static org.forgerock.json.JsonValue.array; import static org.forgerock.json.JsonValue.json; import static org.forgerock.json.JsonValue.object; import static org.forgerock.json.resource.Responses.newActionResponse; import static org.forgerock.json.resource.Responses.newQueryResponse; import static org.forgerock.json.resource.Responses.newResourceResponse; import static org.forgerock.util.promise.Promises.newResultPromise; import static org.identityconnectors.framework.common.objects.filter.FilterBuilder.and; import static org.identityconnectors.framework.common.objects.filter.FilterBuilder.contains; import static org.identityconnectors.framework.common.objects.filter.FilterBuilder.containsAllValues; import static org.identityconnectors.framework.common.objects.filter.FilterBuilder.endsWith; import static org.identityconnectors.framework.common.objects.filter.FilterBuilder.equalTo; import static org.identityconnectors.framework.common.objects.filter.FilterBuilder.greaterThan; import static org.identityconnectors.framework.common.objects.filter.FilterBuilder.greaterThanOrEqualTo; import static org.identityconnectors.framework.common.objects.filter.FilterBuilder.lessThan; import static org.identityconnectors.framework.common.objects.filter.FilterBuilder.lessThanOrEqualTo; import static org.identityconnectors.framework.common.objects.filter.FilterBuilder.not; import static org.identityconnectors.framework.common.objects.filter.FilterBuilder.or; import static org.identityconnectors.framework.common.objects.filter.FilterBuilder.startsWith; import java.io.IOException; import java.io.Serializable; import java.lang.reflect.Array; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicReference; import org.apache.commons.lang3.StringUtils; import org.apache.felix.scr.annotations.Activate; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.ConfigurationPolicy; import org.apache.felix.scr.annotations.Deactivate; import org.apache.felix.scr.annotations.Properties; import org.apache.felix.scr.annotations.Property; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.ReferencePolicy; import org.apache.felix.scr.annotations.Service; import org.forgerock.guava.common.base.Predicate; import org.forgerock.guava.common.collect.FluentIterable; import org.forgerock.services.context.Context; import org.forgerock.http.routing.UriRouterContext; import org.forgerock.json.crypto.JsonCryptoException; import org.forgerock.json.JsonPointer; import org.forgerock.json.JsonValue; import org.forgerock.json.JsonValueException; import org.forgerock.json.resource.ActionRequest; import org.forgerock.json.resource.ActionResponse; import org.forgerock.json.resource.BadRequestException; import org.forgerock.json.resource.CollectionResourceProvider; import org.forgerock.json.resource.ConflictException; import org.forgerock.json.resource.CreateRequest; import org.forgerock.json.resource.DeleteRequest; import org.forgerock.json.resource.ForbiddenException; import org.forgerock.json.resource.InternalServerErrorException; import org.forgerock.json.resource.NotFoundException; import org.forgerock.json.resource.NotSupportedException; import org.forgerock.json.resource.PatchOperation; import org.forgerock.json.resource.PatchRequest; import org.forgerock.json.resource.QueryFilters; import org.forgerock.json.resource.QueryRequest; import org.forgerock.json.resource.QueryResourceHandler; import org.forgerock.json.resource.QueryResponse; import org.forgerock.json.resource.ReadRequest; import org.forgerock.json.resource.Request; import org.forgerock.json.resource.RequestHandler; import org.forgerock.json.resource.Requests; import org.forgerock.json.resource.ResourceResponse; import org.forgerock.json.resource.ResourceException; import org.forgerock.json.resource.ServiceUnavailableException; import org.forgerock.json.resource.SingletonResourceProvider; import org.forgerock.json.resource.UpdateRequest; import org.forgerock.json.resource.http.HttpContext; import org.forgerock.openidm.audit.util.ActivityLogger; import org.forgerock.openidm.audit.util.NullActivityLogger; import org.forgerock.openidm.audit.util.RouterActivityLogger; import org.forgerock.openidm.audit.util.Status; import org.forgerock.openidm.config.enhanced.EnhancedConfig; import org.forgerock.openidm.core.ServerConstants; import org.forgerock.openidm.crypto.CryptoService; import org.forgerock.openidm.provisioner.ProvisionerService; import org.forgerock.openidm.provisioner.SimpleSystemIdentifier; import org.forgerock.openidm.provisioner.SystemIdentifier; import org.forgerock.openidm.provisioner.openicf.ConnectorInfoProvider; import org.forgerock.openidm.provisioner.openicf.ConnectorReference; import org.forgerock.openidm.provisioner.openicf.OperationHelper; import org.forgerock.openidm.provisioner.openicf.commons.AttributeMissingException; import org.forgerock.openidm.provisioner.openicf.commons.ConnectorUtil; import org.forgerock.openidm.provisioner.openicf.commons.ObjectClassInfoHelper; import org.forgerock.openidm.provisioner.openicf.commons.OperationOptionInfoHelper; import org.forgerock.openidm.provisioner.openicf.internal.SystemAction; import org.forgerock.openidm.provisioner.openicf.syncfailure.SyncFailureHandler; import org.forgerock.openidm.provisioner.openicf.syncfailure.SyncFailureHandlerFactory; import org.forgerock.openidm.provisioner.openicf.syncfailure.SyncHandlerException; import org.forgerock.openidm.router.IDMConnectionFactory; import org.forgerock.openidm.router.RouteBuilder; import org.forgerock.openidm.router.RouteEntry; import org.forgerock.openidm.router.RouterRegistry; import org.forgerock.openidm.smartevent.EventEntry; import org.forgerock.openidm.smartevent.Publisher; import org.forgerock.openidm.util.ContextUtil; import org.forgerock.openidm.util.ResourceUtil; import org.forgerock.util.promise.Promise; import org.forgerock.util.query.QueryFilter; import org.forgerock.util.query.QueryFilterVisitor; import org.identityconnectors.common.security.GuardedString; import org.identityconnectors.framework.api.APIConfiguration; import org.identityconnectors.framework.api.ConnectorFacade; import org.identityconnectors.framework.api.ConnectorFacadeFactory; import org.identityconnectors.framework.api.ConnectorInfo; import org.identityconnectors.framework.api.operations.APIOperation; import org.identityconnectors.framework.api.operations.AuthenticationApiOp; import org.identityconnectors.framework.api.operations.CreateApiOp; import org.identityconnectors.framework.api.operations.DeleteApiOp; import org.identityconnectors.framework.api.operations.GetApiOp; import org.identityconnectors.framework.api.operations.ScriptOnConnectorApiOp; import org.identityconnectors.framework.api.operations.ScriptOnResourceApiOp; import org.identityconnectors.framework.api.operations.SearchApiOp; import org.identityconnectors.framework.api.operations.SyncApiOp; import org.identityconnectors.framework.api.operations.TestApiOp; import org.identityconnectors.framework.api.operations.UpdateApiOp; import org.identityconnectors.framework.common.FrameworkUtil; import org.identityconnectors.framework.common.exceptions.AlreadyExistsException; import org.identityconnectors.framework.common.exceptions.ConfigurationException; import org.identityconnectors.framework.common.exceptions.ConnectionBrokenException; import org.identityconnectors.framework.common.exceptions.ConnectionFailedException; import org.identityconnectors.framework.common.exceptions.ConnectorException; import org.identityconnectors.framework.common.exceptions.ConnectorIOException; import org.identityconnectors.framework.common.exceptions.ConnectorSecurityException; import org.identityconnectors.framework.common.exceptions.InvalidAttributeValueException; import org.identityconnectors.framework.common.exceptions.InvalidCredentialException; import org.identityconnectors.framework.common.exceptions.InvalidPasswordException; import org.identityconnectors.framework.common.exceptions.OperationTimeoutException; import org.identityconnectors.framework.common.exceptions.PasswordExpiredException; import org.identityconnectors.framework.common.exceptions.PermissionDeniedException; import org.identityconnectors.framework.common.exceptions.PreconditionFailedException; import org.identityconnectors.framework.common.exceptions.PreconditionRequiredException; import org.identityconnectors.framework.common.exceptions.RetryableException; import org.identityconnectors.framework.common.exceptions.UnknownUidException; import org.identityconnectors.framework.common.objects.Attribute; import org.identityconnectors.framework.common.objects.AttributeUtil; import org.identityconnectors.framework.common.objects.ConnectorObject; import org.identityconnectors.framework.common.objects.Name; import org.identityconnectors.framework.common.objects.ObjectClass; import org.identityconnectors.framework.common.objects.OperationOptions; import org.identityconnectors.framework.common.objects.OperationOptionsBuilder; import org.identityconnectors.framework.common.objects.ResultsHandler; import org.identityconnectors.framework.common.objects.ScriptContextBuilder; import org.identityconnectors.framework.common.objects.SearchResult; import org.identityconnectors.framework.common.objects.SortKey; import org.identityconnectors.framework.common.objects.SyncDelta; import org.identityconnectors.framework.common.objects.SyncResultsHandler; import org.identityconnectors.framework.common.objects.SyncToken; import org.identityconnectors.framework.common.objects.Uid; import org.identityconnectors.framework.common.objects.filter.Filter; import org.identityconnectors.framework.common.serializer.SerializerUtil; import org.identityconnectors.framework.impl.api.local.LocalConnectorFacadeImpl; import org.identityconnectors.framework.impl.api.remote.RemoteWrappedException; import org.osgi.framework.Constants; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.ComponentException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The OpenICFProvisionerService is the implementation of * {@link CollectionResourceProvider} interface with <a * href="http://openicf.forgerock.org">OpenICF</a>. * <p/> */ @Component(name = OpenICFProvisionerService.PID, policy = ConfigurationPolicy.REQUIRE, metatype = true, description = "OpenIDM OpenICF Provisioner Service", immediate = true) @Service(value = {ProvisionerService.class}) @Properties({ @Property(name = Constants.SERVICE_VENDOR, value = ServerConstants.SERVER_VENDOR_NAME), @Property(name = Constants.SERVICE_DESCRIPTION, value = "OpenIDM OpenICF Provisioner Service") }) public class OpenICFProvisionerService implements ProvisionerService, SingletonResourceProvider { // Public Constants public static final String PID = "org.forgerock.openidm.provisioner.openicf"; //Private Constants private static final String REAUTH_HEADER = "X-OpenIDM-Reauth-Password"; private static final String RUN_AS_USER = "runAsUser"; private static final String ACCOUNT_USERNAME_ATTRIBUTES = "accountUserNameAttributes"; private static final Logger logger = LoggerFactory.getLogger(OpenICFProvisionerService.class); // Monitoring event name prefix private static final String EVENT_PREFIX = "openidm/internal/system/"; private static final int UNAUTHORIZED_ERROR_CODE = 401; private SimpleSystemIdentifier systemIdentifier = null; private OperationHelperBuilder operationHelperBuilder = null; private Promise<ConnectorInfo, RuntimeException> connectorFacadeCallback = null; private boolean serviceAvailable = false; private JsonValue jsonConfiguration = null; private ConnectorReference connectorReference = null; private SyncFailureHandler syncFailureHandler = null; private String factoryPid = null; /** use null-object activity logger until/unless ConnectionFactory binder updates it */ private ActivityLogger activityLogger = NullActivityLogger.INSTANCE; /** * Cache the SystemActions from local and {@code provisioner.json} jsonConfiguration. */ private final ConcurrentMap<String, SystemAction> localSystemActionCache = new ConcurrentHashMap<String, SystemAction>(); private final ConcurrentMap<String, RequestHandler> objectClassHandlers = new ConcurrentHashMap<String, RequestHandler>(); /* Internal routing objects to register and remove the routes. */ private RouteEntry routeEntry; /* Object Types*/ private Map<String, ObjectClassInfoHelper> objectTypes; /** * Holder of non ObjectClass operations: * * <pre> * ValidateApiOp * TestApiOp * ScriptOnConnectorApiOp * ScriptOnResourceApiOp * SchemaApiOp * </pre> */ private Map<Class<? extends APIOperation>, OperationOptionInfoHelper> systemOperations = null; /** The Connection Factory */ @Reference(policy = ReferencePolicy.STATIC) protected IDMConnectionFactory connectionFactory; void bindConnectionFactory(final IDMConnectionFactory connectionFactory) { this.connectionFactory = connectionFactory; // update activityLogger to use the "real" activity logger on the router this.activityLogger = new RouterActivityLogger(connectionFactory); } void unbindConnectionFactory(final IDMConnectionFactory connectionFactory) { this.connectionFactory = null; // ConnectionFactory has gone away, use null activity logger this.activityLogger = NullActivityLogger.INSTANCE; } /** * ConnectorInfoProvider service. */ @Reference(policy = ReferencePolicy.DYNAMIC) protected ConnectorInfoProvider connectorInfoProvider = null; /** * RouterRegistryService service. */ @Reference(policy = ReferencePolicy.STATIC) protected RouterRegistry routerRegistry; /** * Cryptographic service. */ @Reference(policy = ReferencePolicy.DYNAMIC) protected CryptoService cryptoService = null; /** * SyncFailureHandlerFactory service. */ @Reference protected SyncFailureHandlerFactory syncFailureHandlerFactory = null; /** * Enhanced configuration service. */ @Reference(policy = ReferencePolicy.DYNAMIC) private EnhancedConfig enhancedConfig; /** * Reference to the ThreadSafe {@code ConnectorFacade} instance. */ private final AtomicReference<ConnectorFacade> connectorFacade = new AtomicReference<ConnectorFacade>(); @Activate protected void activate(ComponentContext context) { try { factoryPid = (String)context.getProperties().get("config.factory-pid"); jsonConfiguration = enhancedConfig.getConfigurationAsJson(context); systemIdentifier = new SimpleSystemIdentifier(jsonConfiguration); if (!jsonConfiguration.get("enabled").defaultTo(true).asBoolean()) { logger.info("OpenICF Provisioner Service {} is disabled, \"enabled\" set to false in configuration", systemIdentifier.getName()); return; } loadLocalSystemActions(jsonConfiguration); connectorReference = ConnectorUtil.getConnectorReference(jsonConfiguration); syncFailureHandler = syncFailureHandlerFactory.create(jsonConfiguration.get("syncFailureHandler")); connectorInfoProvider.findConnectorInfoAsync(connectorReference).thenOnResult( new org.forgerock.util.promise.ResultHandler<ConnectorInfo>() { public void handleResult(ConnectorInfo connectorInfo) { try { APIConfiguration config = connectorInfo.createDefaultAPIConfiguration(); operationHelperBuilder = new OperationHelperBuilder(systemIdentifier.getName(), jsonConfiguration, config, cryptoService); try { Map<String, Map<Class<? extends APIOperation>, OperationOptionInfoHelper>> objectOperations = ConnectorUtil.getOperationOptionConfiguration(jsonConfiguration); objectTypes = ConnectorUtil.getObjectTypes(jsonConfiguration); for (Map.Entry<String, ObjectClassInfoHelper> entry : objectTypes.entrySet()) { objectClassHandlers.put(entry.getKey(), new ObjectClassResourceProvider( entry.getKey(), entry.getValue(), objectOperations.get(entry.getKey()))); } // TODO Fix this Map // ValidateApiOp // TestApiOp // ScriptOnConnectorApiOp // ScriptOnResourceApiOp // SchemaApiOp systemOperations = Collections.emptyMap(); } catch (Exception e) { logger.error("OpenICF connector jsonConfiguration of {} has errors.", systemIdentifier.getName(), e); throw new ComponentException( "OpenICF connector jsonConfiguration has errors and the service can not be initiated.", e); } ConnectorUtil.configureDefaultAPIConfiguration(jsonConfiguration, config, cryptoService); final ConnectorFacade facade = connectorInfoProvider.createConnectorFacade(config); if (null == facade) { logger.warn("OpenICF ConnectorFacade of {} is not available", connectorReference); } else { facade.validate(); if (connectorFacade.compareAndSet(null, facade)) { if (facade.getSupportedOperations().contains(TestApiOp.class)) { try { facade.test(); logger.debug("OpenICF connector test of {} succeeded!", systemIdentifier); serviceAvailable = true; } catch (InvalidCredentialException e) { logger.error("Connection error for {} ", systemIdentifier, e); } catch (Exception e) { logger.error("OpenICF connector test of {} failed!", systemIdentifier, e); } } else { logger.debug("OpenICF connector of {} does not support test.", connectorReference); serviceAvailable = true; } } } logger.info("OpenICF Provisioner Service component {} is activated.", systemIdentifier.getName()); } catch (Throwable t) { logger.warn(t.getMessage()); } } }); routeEntry = routerRegistry.addRoute(RouteBuilder.newBuilder() .withTemplate(ProvisionerService.ROUTER_PREFIX + "/" + systemIdentifier.getName()) .withSingletonResourceProvider(this) .buildNext() .withModeStartsWith() .withTemplate(ProvisionerService.ROUTER_PREFIX + "/" + systemIdentifier.getName() + ObjectClassRequestHandler.OBJECTCLASS_TEMPLATE) .withRequestHandler(new ObjectClassRequestHandler()) .seal()); logger.info("OpenICF Provisioner Service component {} route enabled{}", systemIdentifier.getName(), (null != connectorFacade.get() ? "." : " although the service is not available yet.")); } catch (Exception e) { logger.error("OpenICF Provisioner Service configuration has errors", e); throw new ComponentException("OpenICF Provisioner Service configuration has errors", e); } } @Deactivate protected void deactivate(ComponentContext context) { if (null != connectorFacadeCallback) { connectorFacadeCallback.cancel(false); connectorFacadeCallback = null; } if (null != routeEntry) { routeEntry.removeRoute(); routeEntry = null; } if (connectorFacade.get() instanceof LocalConnectorFacadeImpl) { ((LocalConnectorFacadeImpl) connectorFacade.get()).dispose(); } connectorFacade.set(null); logger.info("OpenICF Provisioner Service component {} is deactivated.", systemIdentifier.getName()); systemIdentifier = null; } private void loadLocalSystemActions(JsonValue configuration) { // TODO delay initialization /config/system if (configuration.isDefined("systemActions")) { for (JsonValue actionValue : configuration.get("systemActions").expect(List.class)) { SystemAction action = new SystemAction(actionValue); localSystemActionCache.put(action.getName(), action); } } } ConnectorFacade getConnectorFacade() { return connectorFacade.get(); } /** * Handle ConnectorExceptions from ConnectorFacade invocations. Maps each ConnectorException subtype to the * appropriate {@link ResourceException} for passing to {@code handleError}. Optionally logs to activity log. * * @param context the Context from the original request * @param request the original request * @param exception the ConnectorException that was thrown by the facade * @param resourceId the resourceId being operated on * @param before the object value "before" the request * @param after the object value "after" the request * @param connectorExceptionActivityLogger the ActivityLogger to use to log the exception */ private ResourceException adaptConnectorException(Context context, Request request, ConnectorException exception, String resourceContainer, String resourceId, JsonValue before, JsonValue after, ActivityLogger connectorExceptionActivityLogger) { // default message String message = MessageFormat.format("Operation {0} failed with {1} on system object: {2}", request.getRequestType(), exception.getClass().getSimpleName(), resourceId); try { throw exception; } catch (AlreadyExistsException e) { message = MessageFormat.format("System object {0} already exists", resourceId); // TODO-crest3 return new org.forgerock.json.resource.PreconditionFailedException(message, exception); } catch (ConfigurationException e) { message = MessageFormat.format("Operation {0} failed with ConfigurationException on system object: {1}", request.getRequestType().toString(), resourceId); return new InternalServerErrorException(message, exception); } catch (ConnectionBrokenException e) { message = MessageFormat.format("Operation {0} failed with ConnectionBrokenException on system object: {1}", request.getRequestType().toString(), resourceId); return new ServiceUnavailableException(message, exception); } catch (ConnectionFailedException e) { message = MessageFormat.format("Connection failed during operation {0} on system object: {1}", request.getRequestType().toString(), resourceId); return new ServiceUnavailableException(message, exception); } catch (ConnectorIOException e) { message = MessageFormat.format("Operation {0} failed with ConnectorIOException on system object: {1}", request.getRequestType().toString(), resourceId); return new ServiceUnavailableException(message, exception); } catch (OperationTimeoutException e) { message = MessageFormat.format("Operation {0} Timeout on system object: {1}", request.getRequestType().toString(), resourceId); return new ServiceUnavailableException(message, exception); } catch (PasswordExpiredException e) { message = MessageFormat.format("Operation {0} failed with PasswordExpiredException on system object: {1}", request.getRequestType().toString(), resourceId); return new ForbiddenException(message, exception); } catch (InvalidPasswordException e) { message = MessageFormat.format("Invalid password has been provided to operation {0} for system object: {1}", request.getRequestType().toString(), resourceId); return ResourceException.getException(UNAUTHORIZED_ERROR_CODE, message, exception); } catch (UnknownUidException e) { message = MessageFormat.format("Operation {0} could not find resource {1} on system object: {2}", request.getRequestType().toString(), resourceId, resourceContainer); return new NotFoundException(message, exception).setDetail(new JsonValue(new HashMap<String, Object>())); } catch (InvalidCredentialException e) { message = MessageFormat.format("Invalid credential has been provided to operation {0} for system object: {1}", request.getRequestType().toString(), resourceId); return ResourceException.getException(UNAUTHORIZED_ERROR_CODE, message, exception); } catch (PermissionDeniedException e) { message = MessageFormat.format("Permission was denied on {0} operation for system object: {1}", request.getRequestType().toString(), resourceId); return new ForbiddenException(message, exception); } catch (ConnectorSecurityException e) { message = MessageFormat.format("Operation {0} failed with ConnectorSecurityException on system object: {1}", request.getRequestType().toString(), resourceId); return new InternalServerErrorException(message, exception); } catch (InvalidAttributeValueException e) { message = MessageFormat.format("Attribute value conflicts with the attribute''s schema definition on " + "operation {0} for system object: {1}", request.getRequestType().toString(), resourceId); return new BadRequestException(message, exception); } catch (PreconditionFailedException e) { message = MessageFormat.format("The resource version for {0} does not match the version provided on " + "operation {1} for system object: {2}", resourceId, request.getRequestType().toString(), resourceContainer); return new org.forgerock.json.resource.PreconditionFailedException(message, exception); } catch (PreconditionRequiredException e) { message = MessageFormat.format("No resource version for resource {0} has been provided on operation {1} for system object: {2}", resourceId , request.getRequestType().toString(), resourceContainer); return new org.forgerock.json.resource.PreconditionRequiredException(message, exception); } catch (RetryableException e) { message = MessageFormat.format("Request temporarily unavailable on operation {0} for system object: {1}", request.getRequestType().toString(), resourceId); return new ServiceUnavailableException(message, exception); } catch (UnsupportedOperationException e) { message = MessageFormat.format("Operation {0} is no supported for system object: {1}", request.getRequestType().toString(), resourceId); return new NotFoundException(message, exception); } catch (IllegalArgumentException e) { message = MessageFormat.format("Operation {0} failed with IllegalArgumentException on system object: {1}", request.getRequestType().toString(), resourceId); return new InternalServerErrorException(message, e); } catch (RemoteWrappedException e) { return adaptRemoteWrappedException(context, request, exception, resourceContainer, resourceId, before, after, connectorExceptionActivityLogger); } catch (ConnectorException e) { message = MessageFormat.format("Operation {0} failed with ConnectorException on system object: {1}", request.getRequestType().toString(), resourceId); return new InternalServerErrorException(message, exception); } finally { // log the ConnectorException logger.debug(message, exception); try { connectorExceptionActivityLogger.log(context, request, message, resourceId, before, after, Status.FAILURE); } catch (ResourceException e) { // this means the ActivityLogger couldn't log request; log to error log logger.warn("Failed to write activity log", e); } } } /** * .NET Exceptions that may be wrapped in a RemoteWrappedException */ private enum DotNetExceptionHelper { ArgumentException("System.ArgumentException") { Exception getMappedException(Exception e) { return new IllegalArgumentException(e.getMessage(), e.getCause()); } }, InvalidOperationException("System.InvalidOperationException") { Exception getMappedException(Exception e) { return new IllegalStateException(e.getMessage(), e.getCause()); } }, NullReferenceException("System.NullReferenceException") { Exception getMappedException(Exception e) { return new NullPointerException(e.getMessage()); } }, NotSupportedException("System.NotSupportedException") { Exception getMappedException(Exception e) { return new UnsupportedOperationException(e.getMessage(), e.getCause()); } }, UnknownDotNetException("") { Exception getMappedException(Exception e) { return new InternalServerErrorException(e.getMessage(), e.getCause()); } }; private final String exceptionName; private DotNetExceptionHelper(final String exceptionName) { this.exceptionName = exceptionName; } abstract Exception getMappedException(Exception e); ConnectorException getConnectorException(Exception e) { return new ConnectorException(e.getMessage(), getMappedException(e)); } static DotNetExceptionHelper fromExceptionClass(String name) { for (DotNetExceptionHelper helper : values()) { if (helper.exceptionName.equals(name)) { return helper; } } return UnknownDotNetException; } } /** * Checks the RemoteWrappedException to determine which Exception has been wrapped and returns * the appropriated corresponding exception. * * @param context the Context from the original request * @param request the original request * @param exception the ConnectorException that was thrown by the facade * @param resourceId the resourceId being operated on * @param before the object value "before" the request * @param after the object value "after" the request * @param connectorExceptionActivityLogger the ActivityLogger to use to log the exception */ private ResourceException adaptRemoteWrappedException(Context context, Request request, ConnectorException exception, String resourceContainer, String resourceId, JsonValue before, JsonValue after, ActivityLogger connectorExceptionActivityLogger) { RemoteWrappedException remoteWrappedException = (RemoteWrappedException) exception; final String message = exception.getMessage(); final Throwable cause = exception.getCause(); if (remoteWrappedException.is(AlreadyExistsException.class)) { return adaptConnectorException(context, request, new AlreadyExistsException(message, cause), resourceContainer, resourceId, before, after, connectorExceptionActivityLogger); } else if (remoteWrappedException.is(ConfigurationException.class)) { return adaptConnectorException(context, request, new ConfigurationException(message, cause), resourceContainer, resourceId, before, after, connectorExceptionActivityLogger); } else if (remoteWrappedException.is(ConnectionBrokenException.class)) { return adaptConnectorException(context, request, new ConnectionBrokenException(message, cause), resourceContainer, resourceId, before, after, connectorExceptionActivityLogger); } else if (remoteWrappedException.is(ConnectionFailedException.class)) { return adaptConnectorException(context, request, new ConnectionFailedException(message, cause), resourceContainer, resourceId, before, after, connectorExceptionActivityLogger); } else if (remoteWrappedException.is(ConnectorIOException.class)) { return adaptConnectorException(context, request, new ConnectorIOException(message, cause), resourceContainer, resourceId, before, after, connectorExceptionActivityLogger); } else if (remoteWrappedException.is(InvalidAttributeValueException.class)) { return adaptConnectorException(context, request, new InvalidAttributeValueException(message, cause), resourceContainer, resourceId, before, after, connectorExceptionActivityLogger); } else if (remoteWrappedException.is(InvalidCredentialException.class)) { return adaptConnectorException(context, request, new InvalidCredentialException(message, cause), resourceContainer, resourceId, before, after, connectorExceptionActivityLogger); } else if (remoteWrappedException.is(InvalidPasswordException.class)) { return adaptConnectorException(context, request, new InvalidPasswordException(message, cause), resourceContainer, resourceId, before, after, connectorExceptionActivityLogger); } else if (remoteWrappedException.is(OperationTimeoutException.class)) { return adaptConnectorException(context, request, new OperationTimeoutException(message, cause), resourceContainer, resourceId, before, after, connectorExceptionActivityLogger); } else if (remoteWrappedException.is(PasswordExpiredException.class)) { return adaptConnectorException(context, request, new PasswordExpiredException(message, cause), resourceContainer, resourceId, before, after, connectorExceptionActivityLogger); } else if (remoteWrappedException.is(PermissionDeniedException.class)) { return adaptConnectorException(context, request, new PermissionDeniedException(message, cause), resourceContainer, resourceId, before, after, connectorExceptionActivityLogger); } else if (remoteWrappedException.is(PreconditionFailedException.class)) { return adaptConnectorException(context, request, new PreconditionFailedException(message, cause), resourceContainer, resourceId, before, after, connectorExceptionActivityLogger); } else if (remoteWrappedException.is(PreconditionRequiredException.class)) { return adaptConnectorException(context, request, new PreconditionRequiredException(message, cause), resourceContainer, resourceId, before, after, connectorExceptionActivityLogger); } else if (remoteWrappedException.is(RetryableException.class)) { return adaptConnectorException(context, request, RetryableException.wrap(message, cause), resourceContainer, resourceId, before, after, connectorExceptionActivityLogger); } else if (remoteWrappedException.is(UnknownUidException.class)) { return adaptConnectorException(context, request, new UnknownUidException(message, cause), resourceContainer, resourceId, before, after, connectorExceptionActivityLogger); } else if (remoteWrappedException.is(ConnectorException.class)) { return adaptConnectorException(context, request, new ConnectorException(message, cause), resourceContainer, resourceId, before, after, connectorExceptionActivityLogger); } else { // handle .NET exceptions return adaptConnectorException(context, request, DotNetExceptionHelper.fromExceptionClass(remoteWrappedException.getExceptionClass()) .getConnectorException(remoteWrappedException), resourceContainer, resourceId, before, after, connectorExceptionActivityLogger); } } /** * @return the smartevent Name for a given query */ org.forgerock.openidm.smartevent.Name getQueryEventName(String objectClass, QueryRequest request) { String prefix = EVENT_PREFIX + getSystemIdentifierName() + "/" + objectClass + "/query/"; if (request.getQueryId() != null) { return org.forgerock.openidm.smartevent.Name.get(prefix + request.getQueryId()); } else if (request.getQueryExpression() != null) { return org.forgerock.openidm.smartevent.Name.get(prefix + "_query_expression"); } else if (request.getQueryFilter() != null) { return org.forgerock.openidm.smartevent.Name.get(prefix + "_queryFilter"); } else { // This should never happen... return org.forgerock.openidm.smartevent.Name.get(prefix + "_UNKNOWN"); } } private enum ConnectorAction { script, test, livesync } @Override public Promise<ResourceResponse, ResourceException> readInstance(Context context, ReadRequest request) { return new NotSupportedException("Read operations are not supported").asPromise(); } @Override public Promise<ActionResponse, ResourceException> actionInstance( final Context context, final ActionRequest request) { try { switch (request.getActionAsEnum(ConnectorAction.class)) { case script: return handleScriptAction(request); case test: return handleTestAction(context, request); case livesync: return handleLiveSyncAction(context, request); default: return new BadRequestException("Unsupported action: " + request.getAction()).asPromise(); } } catch (ConnectorException e) { return adaptConnectorException(context, request, e, null, request.getResourcePath(), null, null, activityLogger) .asPromise(); } catch (JsonValueException e) { return new BadRequestException(e.getMessage(), e).asPromise(); } catch (IllegalArgumentException e) { // from request.getActionAsEnum return new BadRequestException(e.getMessage(), e).asPromise(); } catch (Exception e) { return new InternalServerErrorException(e.getMessage(), e).asPromise(); } } @Override public Promise<ResourceResponse, ResourceException> patchInstance(Context context, PatchRequest request) { return new NotSupportedException("Patch operations are not supported").asPromise(); } @Override public Promise<ResourceResponse, ResourceException> updateInstance(Context context, UpdateRequest request) { return new NotSupportedException("Update operations are not supported").asPromise(); } private Promise<ActionResponse, ResourceException> handleScriptAction(final ActionRequest request) { try { final String scriptId = request.getAdditionalParameter(SystemAction.SCRIPT_ID); if (StringUtils.isBlank(scriptId)) { return new BadRequestException("Missing required parameter: " + SystemAction.SCRIPT_ID).asPromise(); } if (!localSystemActionCache.containsKey(scriptId)) { return new BadRequestException("Script ID: " + scriptId + " is not defined.").asPromise(); } SystemAction action = localSystemActionCache.get(scriptId); String systemType = connectorReference.getConnectorKey().getConnectorName(); final List<ScriptContextBuilder> scriptContextBuilderList = action.getScriptContextBuilders(systemType); if (scriptContextBuilderList.isEmpty()) { return new BadRequestException("Script ID: " + scriptId + " for systemType " + systemType + " is not defined.") .asPromise(); } JsonValue result = new JsonValue(new HashMap<String, Object>()); boolean onConnector = !"resource".equalsIgnoreCase( request.getAdditionalParameter(SystemAction.SCRIPT_EXECUTE_MODE)); final ConnectorFacade facade = getConnectorFacade0(onConnector ? ScriptOnConnectorApiOp.class : ScriptOnResourceApiOp.class); String variablePrefix = request.getAdditionalParameter(SystemAction.SCRIPT_VARIABLE_PREFIX); List<Map<String, Object>> resultList = new ArrayList<Map<String, Object>>(scriptContextBuilderList.size()); result.put("actions", resultList); for (ScriptContextBuilder contextBuilder : scriptContextBuilderList) { boolean isShell = contextBuilder.getScriptLanguage().equalsIgnoreCase("Shell"); for (Entry<String, String> entry : request.getAdditionalParameters().entrySet()) { final String key = entry.getKey(); if (SystemAction.SCRIPT_PARAMS.contains(key)) { continue; } Object value = entry.getValue(); Object newValue = value; if (isShell) { if ("password".equalsIgnoreCase(key)) { if (value instanceof String) { newValue = new GuardedString(((String) value).toCharArray()); } else { return new BadRequestException("Invalid type for password.").asPromise(); } } if ("username".equalsIgnoreCase(key)) { if (value instanceof String == false) { return new BadRequestException("Invalid type for username.").asPromise(); } } if ("workingdir".equalsIgnoreCase(key)) { if (value instanceof String == false) { return new BadRequestException("Invalid type for workingdir.").asPromise(); } } if ("timeout".equalsIgnoreCase(key)) { if (!(value instanceof String) && !(value instanceof Number)) { return new BadRequestException("Invalid type for timeout.").asPromise(); } } contextBuilder.addScriptArgument(key, newValue); continue; } if (null != value) { if (value instanceof Collection) { newValue = Array.newInstance(Object.class, ((Collection) value).size()); int i = 0; for (Object v : (Collection) value) { if (null == v || FrameworkUtil.isSupportedAttributeType(v.getClass())) { Array.set(newValue, i, v); } else { // Serializable may not be // acceptable Array.set(newValue, i, v instanceof Serializable ? v : v.toString()); } i++; } } else if (value.getClass().isArray()) { // TODO implement the array support later } else if (!FrameworkUtil.isSupportedAttributeType(value.getClass())) { // Serializable may not be acceptable newValue = value instanceof Serializable ? value : value.toString(); } } contextBuilder.addScriptArgument(key, newValue); } JsonValue content = request.getContent(); // if there is no content(content.isNull()), skip adding content to script arguments if (content.isMap()) { for (Entry<String, Object> entry : content.asMap().entrySet()) { contextBuilder.addScriptArgument(entry.getKey(), entry.getValue()); } } else if (!content.isNull()) { return new BadRequestException("Content is not of type Map").asPromise(); } // ScriptContext scriptContext = script.getScriptContextBuilder().build(); OperationOptionsBuilder operationOptionsBuilder = new OperationOptionsBuilder(); // It's necessary to keep the backward compatibility with Waveset IDM if (null != variablePrefix && isShell) { operationOptionsBuilder.setOption("variablePrefix", variablePrefix); } Map<String, Object> actionResult = new HashMap<String, Object>(2); try { Object scriptResult = null; if (onConnector) { scriptResult = facade.runScriptOnConnector( contextBuilder.build(), operationOptionsBuilder.build()); } else { scriptResult = facade.runScriptOnResource( contextBuilder.build(), operationOptionsBuilder.build()); } actionResult.put("result", ConnectorUtil.coercedTypeCasting(scriptResult, Object.class)); } catch (Throwable t) { logger.error("Script execution error.", t); actionResult.put("error", t.getMessage()); } resultList.add(actionResult); } return newActionResponse(result).asPromise(); } catch (ResourceException e) { return e.asPromise(); } catch (Exception e) { return new InternalServerErrorException(e.getMessage(), e).asPromise(); } } private Promise<ActionResponse, ResourceException> handleTestAction(Context context, ActionRequest request) { return newActionResponse(new JsonValue(getStatus(context))).asPromise(); } private Promise<ActionResponse, ResourceException> handleLiveSyncAction( final Context context, final ActionRequest request) { final String objectTypeName = getObjectTypeName(ObjectClass.ALL); if (objectTypeName == null) { return new BadRequestException("__ALL__ object class is not configured").asPromise(); } final ActionRequest forwardRequest = Requests.newActionRequest(getSource(objectTypeName), request.getAction()); // forward request to be handled in ObjectClassResourceProvider#actionCollection try { return connectionFactory.getConnection().action(context, forwardRequest).asPromise(); } catch (ResourceException e) { return e.asPromise(); } } /** * Checks the {@code operation} permission before execution. * * @param operation * @return if {@code denied} is true and the {@code onDeny} equals * {@link org.forgerock.openidm.provisioner.openicf.commons.OperationOptionInfoHelper.OnActionPolicy#ALLOW} * returns false else true * @throws ResourceException * if {@code denied} is true and the {@code onDeny} equals * {@link org.forgerock.openidm.provisioner.openicf.commons.OperationOptionInfoHelper.OnActionPolicy#THROW_EXCEPTION} */ private ConnectorFacade getConnectorFacade0(Class<? extends APIOperation> operation) throws ResourceException { final ConnectorFacade facade = getConnectorFacade(); if (null == facade) { throw new ServiceUnavailableException(); } OperationOptionInfoHelper operationOptionInfoHelper = null; if (null == facade.getOperation(operation)) { throw new NotSupportedException("Operation " + operation.getCanonicalName() + " is not supported by the Connector"); } else if (null != operationOptionInfoHelper && OperationOptionInfoHelper.OnActionPolicy.THROW_EXCEPTION .equals(operationOptionInfoHelper.getOnActionPolicy())) { throw new ForbiddenException("Operation " + operation.getCanonicalName() + " is configured to be denied"); } return facade; } private class ObjectClassRequestHandler implements RequestHandler { public static final String OBJECTCLASS = "objectclass"; public static final String OBJECTCLASS_TEMPLATE = "/{objectclass}"; protected String getObjectClass(Context context) throws ResourceException { Map<String, String> variables = ResourceUtil.getUriTemplateVariables(context); if (null != variables && variables.containsKey(OBJECTCLASS)) { return variables.get(OBJECTCLASS); } throw new ForbiddenException( "Direct access without Router to this service is forbidden."); } public Promise<ActionResponse, ResourceException> handleAction(Context context, ActionRequest request) { try { String objectClass = getObjectClass(context); RequestHandler delegate = objectClassHandlers.get(objectClass); if (null != delegate) { return delegate.handleAction(context, request); } else { throw new NotFoundException("Not found: " + objectClass); } } catch (ResourceException e) { return e.asPromise(); } catch (Exception e) { return new InternalServerErrorException(e.getMessage(), e).asPromise(); } } public Promise<ResourceResponse, ResourceException> handleCreate(Context context, CreateRequest request) { try { String objectClass = getObjectClass(context); RequestHandler delegate = objectClassHandlers.get(objectClass); if (null != delegate) { return delegate.handleCreate(context, request); } else { throw new NotFoundException("Not found: " + objectClass); } } catch (ResourceException e) { return e.asPromise(); } catch (Exception e) { return new InternalServerErrorException(e.getMessage(), e).asPromise(); } } public Promise<ResourceResponse, ResourceException> handleDelete(Context context, DeleteRequest request) { try { String objectClass = getObjectClass(context); RequestHandler delegate = objectClassHandlers.get(objectClass); if (null != delegate) { return delegate.handleDelete(context, request); } else { throw new NotFoundException("Not found: " + objectClass); } } catch (ResourceException e) { return e.asPromise(); } catch (Exception e) { return new InternalServerErrorException(e.getMessage(), e).asPromise(); } } public Promise<ResourceResponse, ResourceException> handlePatch(Context context, PatchRequest request) { try { String objectClass = getObjectClass(context); RequestHandler delegate = objectClassHandlers.get(objectClass); if (null != delegate) { return delegate.handlePatch(context, request); } else { throw new NotFoundException("Not found: " + objectClass); } } catch (ResourceException e) { return e.asPromise(); } catch (Exception e) { return new InternalServerErrorException(e.getMessage(), e).asPromise(); } } public Promise<QueryResponse, ResourceException> handleQuery(Context context, QueryRequest request, QueryResourceHandler handler) { try { String objectClass = getObjectClass(context); RequestHandler delegate = objectClassHandlers.get(objectClass); if (null != delegate) { return delegate.handleQuery(context, request, handler); } else { throw new NotFoundException("Not found: " + objectClass); } } catch (ResourceException e) { return e.asPromise(); } catch (Exception e) { return new InternalServerErrorException(e.getMessage(), e).asPromise(); } } public Promise<ResourceResponse, ResourceException> handleRead(Context context, ReadRequest request) { try { String objectClass = getObjectClass(context); RequestHandler delegate = objectClassHandlers.get(objectClass); if (null != delegate) { return delegate.handleRead(context, request); } else { throw new NotFoundException("Not found: " + objectClass); } } catch (ResourceException e) { return e.asPromise(); } catch (Exception e) { return new InternalServerErrorException(e.getMessage(), e).asPromise(); } } public Promise<ResourceResponse, ResourceException> handleUpdate(Context context, UpdateRequest request) { try { String objectClass = getObjectClass(context); RequestHandler delegate = objectClassHandlers.get(objectClass); if (null != delegate) { return delegate.handleUpdate(context, request); } else { throw new NotFoundException("Not found: " + objectClass); } } catch (ResourceException e) { return e.asPromise(); } catch (Exception e) { return new InternalServerErrorException(e.getMessage(), e).asPromise(); } } } /** * ActionRequest actions we support on /system/[systemName]/[objectClass/{id} */ private enum ObjectClassAction { authenticate, resolveUsername, liveSync } /** * Handle request on /system/[systemName]/[objectClass]/{id} * * @ThreadSafe */ private class ObjectClassResourceProvider implements RequestHandler { private final ObjectClassInfoHelper objectClassInfoHelper; private final Map<Class<? extends APIOperation>, OperationOptionInfoHelper> operations; private final String objectClass; private ObjectClassResourceProvider(String objectClass, ObjectClassInfoHelper objectClassInfoHelper, Map<Class<? extends APIOperation>, OperationOptionInfoHelper> operations) { this.objectClassInfoHelper = objectClassInfoHelper; this.operations = operations; this.objectClass = objectClass; } /** * Checks the {@code operation} permission before execution. * * @param operation * @return if {@code denied} is true and the {@code onDeny} equals * {@link org.forgerock.openidm.provisioner.openicf.commons.OperationOptionInfoHelper.OnActionPolicy#ALLOW} * returns false else true * @throws ResourceException * if {@code denied} is true and the {@code onDeny} equals * {@link org.forgerock.openidm.provisioner.openicf.commons.OperationOptionInfoHelper.OnActionPolicy#THROW_EXCEPTION} */ private ConnectorFacade getConnectorFacade0(Class<? extends APIOperation> operation) throws ResourceException { final ConnectorFacade facade = getConnectorFacade(); if (null == facade) { throw new ServiceUnavailableException(); } OperationOptionInfoHelper operationOptionInfoHelper = operations.get(operation); if (null == facade.getOperation(operation)) { throw new NotSupportedException( "Operation " + operation.getCanonicalName() + " is not supported by the Connector"); } else if (null != operationOptionInfoHelper && (null != operationOptionInfoHelper.getSupportedObjectTypes())) { if (!operationOptionInfoHelper.getSupportedObjectTypes().contains( objectClassInfoHelper.getObjectClass().getObjectClassValue())) { throw new NotSupportedException( "Actions are not supported for resource instances"); } else if (OperationOptionInfoHelper.OnActionPolicy.THROW_EXCEPTION.equals( operationOptionInfoHelper.getOnActionPolicy())) { throw new ForbiddenException( "Operation " + operation.getCanonicalName() + " is configured to be denied"); } } return facade; } private Promise<ActionResponse, ResourceException> handleAuthenticate(Context context, ActionRequest request) throws IOException { try { final ConnectorFacade facade = getConnectorFacade0(AuthenticationApiOp.class); final JsonValue params = new JsonValue(request.getAdditionalParameters()); final String username = params.get("username").required().asString(); final String password = params.get("password").required().asString(); OperationOptions operationOptions = operations.get(AuthenticationApiOp.class) .build(jsonConfiguration, objectClassInfoHelper) .build(); // Throw ConnectorException Uid uid = facade.authenticate(objectClassInfoHelper.getObjectClass(), username, new GuardedString(password.toCharArray()), operationOptions); JsonValue result = new JsonValue(new HashMap<String, Object>()); result.put(ResourceResponse.FIELD_CONTENT_ID, uid.getUidValue()); if (null != uid.getRevision()) { result.put(ResourceResponse.FIELD_CONTENT_REVISION, uid.getRevision()); } return newActionResponse(result).asPromise(); } catch (ConnectorException e) { // handle ConnectorException from facade.authenticate: // log to activity log only if this is an external request // (let internal requests do their own logging upon the handleError...) throw adaptConnectorException(context, request, e, null, null, null, null, ContextUtil.isExternal(context) ? activityLogger : NullActivityLogger.INSTANCE); } } private Promise<ActionResponse, ResourceException> handleLiveSync( Context context, ActionRequest request) throws ResourceException { final ActionRequest forwardRequest = Requests.newActionRequest(ProvisionerService.ROUTER_PREFIX, request.getAction()) .setAdditionalParameter("source", getSource(objectClass)); // forward request to be handled in SystemObjectSetService#actionInstance return connectionFactory.getConnection().action(context, forwardRequest).asPromise(); } public Promise<ActionResponse, ResourceException> handleAction( Context context, ActionRequest request) { try { switch (request.getActionAsEnum(ObjectClassAction.class)) { case authenticate: return handleAuthenticate(context, request); case liveSync: return handleLiveSync(context, request); default: throw new BadRequestException("Unsupported action: " + request.getAction()); } } catch (ResourceException e) { return e.asPromise(); } catch (JsonValueException e) { return new BadRequestException(e.getMessage(), e).asPromise(); } catch (IllegalArgumentException e) { // from request.getActionAsEnum return new BadRequestException(e.getMessage(), e).asPromise(); } catch (Exception e) { return new InternalServerErrorException(e.getMessage(), e).asPromise(); } } @Override public Promise<ResourceResponse, ResourceException> handleCreate(Context context, CreateRequest request) { try { final ConnectorFacade facade = getConnectorFacade0(CreateApiOp.class); final Set<Attribute> createAttributes = objectClassInfoHelper.getCreateAttributes(request, cryptoService); OperationOptions operationOptions = operations.get(CreateApiOp.class) .build(jsonConfiguration, objectClassInfoHelper) .build(); Uid uid = facade.create(objectClassInfoHelper.getObjectClass(), AttributeUtil.filterUid(createAttributes), operationOptions); ResourceResponse resource = getCurrentResource(facade, uid, null); activityLogger.log(context, request, "message", getSource(objectClass, uid.getUidValue()), null, resource.getContent(), Status.SUCCESS); return resource.asPromise(); } catch (ResourceException e) { return e.asPromise(); } catch (ConnectorException e) { return adaptConnectorException(context, request, e, getSource(objectClass), objectClassInfoHelper.getFullResourceId(request), request.getContent(), null, activityLogger) .asPromise(); } catch (JsonValueException e) { return new BadRequestException(e.getMessage(), e).asPromise(); } catch (Exception e) { return new InternalServerErrorException(e.getMessage(), e).asPromise(); } } @Override public Promise<ResourceResponse, ResourceException> handleDelete( Context context, DeleteRequest request) { String resourceId = objectClassInfoHelper.getFullResourceId(request); try { if (resourceId.isEmpty()) { throw new BadRequestException( "The resource collection " + request.getResourcePath() + " cannot be deleted"); } final ConnectorFacade facade = getConnectorFacade0(DeleteApiOp.class); final Uid uid = request.getRevision() != null ? new Uid(resourceId, request.getRevision()) : new Uid(resourceId); // do a read first (largely for logging) ResourceResponse before = getCurrentResource(facade, uid, null); OperationOptions operationOptions = operations.get(DeleteApiOp.class) .build(jsonConfiguration, objectClassInfoHelper) .build(); facade.delete(objectClassInfoHelper.getObjectClass(), uid, operationOptions); JsonValue result = before.getContent().copy(); result.put(ResourceResponse.FIELD_CONTENT_ID, uid.getUidValue()); if (null != uid.getRevision()) { result.put(ResourceResponse.FIELD_CONTENT_REVISION, uid.getRevision()); } activityLogger.log(context, request, "message", getSource(objectClass, uid.getUidValue()), before.getContent(), null, Status.SUCCESS); return newResourceResponse(uid.getUidValue(), uid.getRevision(), result).asPromise(); } catch (ResourceException e) { return e.asPromise(); } catch (ConnectorException e) { return adaptConnectorException(context, request, e, getSource(objectClass), resourceId, null, null, activityLogger) .asPromise(); } catch (JsonValueException e) { return new BadRequestException(e.getMessage(), e).asPromise(); } catch (Exception e) { return new InternalServerErrorException(e.getMessage(), e).asPromise(); } } @Override public Promise<ResourceResponse, ResourceException> handlePatch( Context context, PatchRequest request) { JsonValue beforeValue = null; String resourceId = objectClassInfoHelper.getFullResourceId(request); try { if (resourceId.isEmpty()) { throw new BadRequestException( "The resource collection " + request.getResourcePath() + " cannot be patched"); } final ConnectorFacade facade = getConnectorFacade0(UpdateApiOp.class); final Uid _uid = request.getRevision() != null ? new Uid(resourceId, request.getRevision()) : new Uid(resourceId); // read resource before update for logging ResourceResponse before = getCurrentResource(facade, _uid, null); beforeValue = before.getContent(); final Set<String> attributeNames = new HashSet<String>(); final Set<Attribute> addedAttributes = new HashSet<Attribute>(); final Set<Attribute> removedAttributes = new HashSet<Attribute>(); final Set<Attribute> updatedAttributes = new HashSet<Attribute>(); for (PatchOperation operation : request.getPatchOperations()) { Attribute attribute = objectClassInfoHelper.getPatchAttribute(operation, beforeValue, cryptoService); if (attribute != null) { if (operation.isAdd()) { addedAttributes.add(attribute); } else if (operation.isRemove()) { removedAttributes.add(attribute); } else { updatedAttributes.add(attribute); } attributeNames.add(attribute.getName()); } } OperationOptions operationOptions; OperationOptionsBuilder operationOptionsBuilder = operations.get(UpdateApiOp.class) .build(jsonConfiguration, objectClassInfoHelper); final String reauthPassword = getReauthPassword(context); // if reauth and updating attribute requiring user credentials if (runAsUser(attributeNames, reauthPassword)) { // get username attribute final List<String> usernameAttrs = jsonConfiguration.get(ConnectorUtil.OPENICF_CONFIGURATION_PROPERTIES) .get(ACCOUNT_USERNAME_ATTRIBUTES) .asList(String.class); final String username = beforeValue.get(usernameAttrs.get(0)).asString(); if (StringUtils.isNotBlank(username)) { operationOptionsBuilder.setRunAsUser(username) .setRunWithPassword(new GuardedString(reauthPassword.toCharArray())); } } operationOptions = operationOptionsBuilder.build(); Uid uid = _uid; if (addedAttributes.size() > 0) { // Perform any add operations uid = facade.addAttributeValues(objectClassInfoHelper.getObjectClass(), uid, AttributeUtil.filterUid(addedAttributes), operationOptions); } if (removedAttributes.size() > 0) { // Perform any remove operations try { uid = facade.removeAttributeValues(objectClassInfoHelper.getObjectClass(), uid, AttributeUtil.filterUid(removedAttributes), operationOptions); } catch (ConnectorException e) { logger.debug("Error removing attribute values for object {}", uid, e); } } if (updatedAttributes.size() > 0) { // Perform any increment or replace operations uid = facade.update(objectClassInfoHelper.getObjectClass(), uid, AttributeUtil.filterUid(updatedAttributes), operationOptions); } ResourceResponse resource = getCurrentResource(facade, uid, null); activityLogger.log(context, request, "message", getSource(objectClass, uid.getUidValue()), beforeValue, resource.getContent(), Status.SUCCESS); return resource.asPromise(); } catch (ResourceException e) { return e.asPromise(); } catch (ConnectorException e) { return adaptConnectorException(context, request, e, getSource(objectClass), resourceId, beforeValue, null, activityLogger) .asPromise(); } catch (JsonValueException e) { return new BadRequestException(e.getMessage(), e).asPromise(); } catch (Exception e) { return new InternalServerErrorException(e.getMessage(), e).asPromise(); } } @Override public Promise<QueryResponse, ResourceException> handleQuery( final Context context, final QueryRequest request, final QueryResourceHandler handler) { EventEntry measure = Publisher.start(getQueryEventName( objectClass, request), request, null); String resourceId = objectClassInfoHelper.getFullResourceId(request); try { if (!resourceId.isEmpty()) { throw new BadRequestException( "The resource instance " + request.getResourcePath() + " cannot be queried"); } final ConnectorFacade facade = getConnectorFacade0(SearchApiOp.class); OperationOptionsBuilder operationOptionsBuilder = operations.get(SearchApiOp.class) .build(jsonConfiguration, objectClassInfoHelper); Filter filter = null; if (request.getQueryId() != null) { if (ServerConstants.QUERY_ALL_IDS.equals(request.getQueryId())) { operationOptionsBuilder.setAttributesToGet(Uid.NAME); } else { throw new BadRequestException("Unsupported _queryId: " + request.getQueryId()); } } else if (request.getQueryExpression() != null) { filter = QueryFilters.parse(request.getQueryExpression()).accept( RESOURCE_FILTER, objectClassInfoHelper); } else if (request.getQueryFilter() != null) { // No filtering or query by filter. filter = request.getQueryFilter().accept(RESOURCE_FILTER, objectClassInfoHelper); } else { throw new BadRequestException("One of _queryId, _queryExpression, or _queryFilter is required."); } // If paged results are requested then decode the cookie in // order to determine // the index of the first result to be returned. final int pageSize = request.getPageSize(); final String pagedResultsCookie = request.getPagedResultsCookie(); final boolean pagedResultsRequested = request.getPageSize() > 0; if (pageSize > 0) { operationOptionsBuilder.setPageSize(pageSize); } if (null != pagedResultsCookie) { operationOptionsBuilder.setPagedResultsCookie(pagedResultsCookie); } operationOptionsBuilder.setPagedResultsOffset(request.getPagedResultsOffset()); if (null != request.getSortKeys()) { List<SortKey> sortKeys = new ArrayList<SortKey>(request.getSortKeys().size()); for (org.forgerock.json.resource.SortKey s: request.getSortKeys()){ sortKeys.add(new SortKey(s.getField().leaf(), s.isAscendingOrder())); } operationOptionsBuilder.setSortKeys(sortKeys); } // Override ATTRS_TO_GET if fields are specified within the Request if (!request.getFields().isEmpty()) { objectClassInfoHelper.setAttributesToGet(operationOptionsBuilder, request.getFields()); } final JsonValue logValue = json(array()); final Exception[] ex = new Exception[] { null }; SearchResult searchResult = facade.search(objectClassInfoHelper.getObjectClass(), filter, new ResultsHandler() { @Override public boolean handle(ConnectorObject obj) { try { ResourceResponse resource = objectClassInfoHelper.build(obj, cryptoService); logValue.add(resource.getContent().getObject()); return handler.handleResource(resource); } catch (Exception e) { ex[0] = e; // TODO ICF needs a way to handle exceptions through the facade return false; } } }, operationOptionsBuilder.build()); if (ex[0] != null) { throw new InternalServerErrorException(ex[0].getMessage(), ex[0]); } activityLogger.log(context, request, "query: " + request.getQueryId() + ", queryExpression: " + request.getQueryExpression() + ", queryFilter: " + (request.getQueryFilter() != null ? request.getQueryFilter().toString() : null) + ", parameters: " + request.getAdditionalParameters(), request.getQueryId(), null, logValue, Status.SUCCESS); // TODO-crest3- fix contract for remainingPagedResults return newResultPromise( newQueryResponse(searchResult != null ? searchResult.getPagedResultsCookie() : null)); } catch (ResourceException e) { return e.asPromise(); } catch (ConnectorException e) { return adaptConnectorException(context, request, e, null, null, null, null, activityLogger).asPromise(); } catch (JsonValueException e) { return new BadRequestException(e.getMessage(), e).asPromise(); } catch (AttributeMissingException e) { return new BadRequestException(e.getMessage(), e).asPromise(); } catch (IllegalArgumentException e) { return new BadRequestException(e.getMessage(), e).asPromise(); } catch (Exception e) { return new InternalServerErrorException(e.getMessage(), e).asPromise(); } finally { measure.end(); } } @Override public Promise<ResourceResponse, ResourceException> handleRead( Context context, ReadRequest request) { String resourceId = objectClassInfoHelper.getFullResourceId(request); try { if (resourceId.isEmpty()) { throw new BadRequestException( "The resource collection " + request.getResourcePath() + " cannot be read"); } final ConnectorFacade facade = getConnectorFacade0(GetApiOp.class); Uid uid = new Uid(resourceId); ConnectorObject connectorObject = getConnectorObject(facade, uid, request.getFields()); if (null != connectorObject) { ResourceResponse resource = objectClassInfoHelper.build(connectorObject, cryptoService); activityLogger.log(context, request, "message", getSource(objectClass, uid.getUidValue()), resource.getContent(), resource.getContent(), Status.SUCCESS); return resource.asPromise(); } else { final String matchedUri = context.containsContext(UriRouterContext.class) ? context.asContext(UriRouterContext.class).getMatchedUri() : "unknown path"; throw new NotFoundException("Object " + resourceId + " not found on " + matchedUri); } } catch (ResourceException e) { return e.asPromise(); } catch (ConnectorException e) { return adaptConnectorException(context, request, e, getSource(objectClass), resourceId, null, null, activityLogger) .asPromise(); } catch (JsonValueException e) { return new BadRequestException(e.getMessage(), e).asPromise(); } catch (Exception e) { return new InternalServerErrorException(e.getMessage(), e).asPromise(); } } @Override public Promise<ResourceResponse, ResourceException> handleUpdate( Context context, UpdateRequest request) { JsonValue content = request.getContent(); String resourceId = objectClassInfoHelper.getFullResourceId(request); try { if (resourceId.isEmpty()) { throw new BadRequestException( "The resource collection " + request.getResourcePath() + " cannot be updated"); } final ConnectorFacade facade = getConnectorFacade0(UpdateApiOp.class); final Uid _uid = request.getRevision() != null ? new Uid(resourceId, request.getRevision()) : new Uid(resourceId); // read resource before update for logging ResourceResponse before = getCurrentResource(facade, _uid, null); // TODO Fix for http://bugster.forgerock.org/jira/browse/CREST-29 final Name newName = null; final Set<Attribute> replaceAttributes = objectClassInfoHelper.getUpdateAttributes(request, newName, cryptoService); OperationOptions operationOptions; OperationOptionsBuilder operationOptionsBuilder = operations.get(UpdateApiOp.class) .build(jsonConfiguration, objectClassInfoHelper); final String reauthPassword = getReauthPassword(context); // if reauth and updating attributes requiring user credentials if (runAsUser(content.asMap().keySet(), reauthPassword)) { // get username attribute final List<String> usernameAttrs = jsonConfiguration.get(ConnectorUtil.OPENICF_CONFIGURATION_PROPERTIES) .get(ACCOUNT_USERNAME_ATTRIBUTES) .asList(String.class); final String username = content.get(usernameAttrs.get(0)).asString(); if (StringUtils.isNotBlank(username)) { operationOptionsBuilder.setRunAsUser(username) .setRunWithPassword(new GuardedString(reauthPassword.toCharArray())); } } operationOptions = operationOptionsBuilder.build(); Uid uid = facade.update(objectClassInfoHelper.getObjectClass(), _uid, AttributeUtil.filterUid(replaceAttributes), operationOptions); ResourceResponse resource = getCurrentResource(facade, uid, null); activityLogger.log(context, request, "message", getSource(objectClass, uid.getUidValue()), before.getContent(), resource.getContent(), Status.SUCCESS); return resource.asPromise(); } catch (ResourceException e) { return e.asPromise(); } catch (ConnectorException e) { return adaptConnectorException(context, request, e, getSource(objectClass), resourceId, content, null, activityLogger) .asPromise(); } catch (JsonValueException e) { return new BadRequestException(e.getMessage(), e).asPromise(); } catch (Exception e) { return new InternalServerErrorException(e.getMessage(), e).asPromise(); } } // see if there is a reauth password provided private String getReauthPassword(Context context) { try { // get reauth password return context.asContext(HttpContext.class).getHeaderAsString(REAUTH_HEADER); } catch (Exception e) { // there will not always be a HttpContext and this is acceptable so catch exception to // prevent the exception from stopping the remaining update return null; } } /** * Checks if any of the supplied attributes require re-authentication to update. * * @param attributes the attributes being updated * @param reauthPassword the re-authentication password * @return true if a password is re-authentication is required, false otherwise. */ private boolean runAsUser(Set<String> attributes, String reauthPassword) { final JsonValue properties = objectClassInfoHelper.getProperties(); final Predicate<String> attributesToRunAsUser = new Predicate<String>() { @Override public boolean apply(String attribute) { return !properties.get(attribute).isNull() && properties.get(attribute).get(RUN_AS_USER).defaultTo(false).asBoolean(); } }; return StringUtils.isNotEmpty(reauthPassword) && FluentIterable.from(attributes).filter(attributesToRunAsUser).iterator().hasNext(); } private ResourceResponse getCurrentResource(final ConnectorFacade facade, final Uid uid, final List<JsonPointer> fields) throws IOException, JsonCryptoException { final ConnectorObject co = getConnectorObject(facade, uid, fields); if (null != co) { return objectClassInfoHelper.build(co, cryptoService); } else { JsonValue result = new JsonValue(new HashMap<String, Object>()); result.put(ResourceResponse.FIELD_CONTENT_ID, uid.getUidValue()); if (null != uid.getRevision()) { result.put(ResourceResponse.FIELD_CONTENT_REVISION, uid.getRevision()); } return newResourceResponse(uid.getUidValue(), uid.getRevision(), result); } } private ConnectorObject getConnectorObject(final ConnectorFacade facade, final Uid uid, final List<JsonPointer> fields) throws IOException, JsonCryptoException { final OperationOptions operationOptions; if (fields == null || fields.isEmpty()) { operationOptions = operations.get(GetApiOp.class) .build(jsonConfiguration, objectClassInfoHelper) .build(); } else { OperationOptionsBuilder operationOptionsBuilder = new OperationOptionsBuilder(); objectClassInfoHelper.setAttributesToGet(operationOptionsBuilder, fields); operationOptions = operationOptionsBuilder.build(); } return facade.getObject(objectClassInfoHelper.getObjectClass(), uid, operationOptions); } } /** * Handle request on /system/[systemName]/schema * * @ThreadSafe */ private static class SchemaResourceProvider implements SingletonResourceProvider { private final ResourceResponse schema; private SchemaResourceProvider(ResourceResponse schema) { this.schema = schema; } @Override public Promise<ResourceResponse, ResourceException> readInstance(Context context, ReadRequest request) { return schema.asPromise(); } @Override public Promise<ActionResponse, ResourceException> actionInstance(Context context, ActionRequest request) { return new NotSupportedException("Actions are not supported for resource instances").asPromise(); } @Override public Promise<ResourceResponse, ResourceException> patchInstance(Context context, PatchRequest request) { return new NotSupportedException("Patch operations are not supported").asPromise(); } @Override public Promise<ResourceResponse, ResourceException> updateInstance(Context context, UpdateRequest request) { return new NotSupportedException("Update operations are not supported").asPromise(); } } /** * A container for information about a sync retry after a failure */ private class SyncRetry { /** * The retry value, true if the sync should be retried, false otherwise. */ boolean value; /** * The {@link Throwable} associated with the failure */ Throwable throwable; public SyncRetry() { value = false; throwable = null; } /** * Returns the retry value. * * @return true if the sync should be retried, false otherwise. */ public boolean getValue() { return value; } /** * Sets the retry value. * * @param value true if the sync should be retried, false otherwise. */ public void setValue(boolean value) { this.value = value; } /** * Returns the {@link Throwable} associated with the failure * * @return the {@link Throwable} associated with the failure */ public Throwable getThrowable() { return throwable; } /** * Sets the {@link Throwable} associated with the failure. * * @param throwable the {@link Throwable} associated with the failure. */ public void setThrowable(Throwable throwable) { this.throwable = throwable; } } private static final QueryFilterVisitor<Filter, ObjectClassInfoHelper, JsonPointer> RESOURCE_FILTER = new QueryFilterVisitor<Filter, ObjectClassInfoHelper, JsonPointer>() { @Override public Filter visitAndFilter(final ObjectClassInfoHelper helper, List<QueryFilter<JsonPointer>> subFilters) { final Iterator<QueryFilter<JsonPointer>> iterator = subFilters.iterator(); if (iterator.hasNext()) { return buildAnd(helper, iterator.next(), iterator); } else { throw new IllegalArgumentException("cannot parse 'and' QueryFilter with zero operands"); } } private Filter buildAnd(final ObjectClassInfoHelper helper, final QueryFilter<JsonPointer> left, final Iterator<QueryFilter<JsonPointer>> iterator) { if (iterator.hasNext()) { final QueryFilter<JsonPointer> right = iterator.next(); return and(left.accept(this, helper), buildAnd(helper, right, iterator)); } else { return left.accept(this, helper); } } @Override public Filter visitOrFilter(ObjectClassInfoHelper helper, List<QueryFilter<JsonPointer>> subFilters) { final Iterator<QueryFilter<JsonPointer>> iterator = subFilters.iterator(); if (iterator.hasNext()) { return buildOr(helper, iterator.next(), iterator); } else { throw new IllegalArgumentException("cannot parse 'or' QueryFilter with zero operands"); } } private Filter buildOr(final ObjectClassInfoHelper helper, final QueryFilter<JsonPointer> left, final Iterator<QueryFilter<JsonPointer>> iterator) { if (iterator.hasNext()) { final QueryFilter<JsonPointer> right = iterator.next(); return or(left.accept(this, helper), buildOr(helper, right, iterator)); } else { return left.accept(this, helper); } } @Override public Filter visitBooleanLiteralFilter(final ObjectClassInfoHelper helper, final boolean value) { return new Filter() { public boolean accept(ConnectorObject obj) { return value; } public <R extends Object, P extends Object> R accept( org.identityconnectors.framework.common.objects.filter.FilterVisitor<R,P> v, P p) { // OpenICF team explained that // return v.visitExtendedFilter(p, this); // would not yet (1.4) work with all connectors and/or remotely. // Instead the return null evaluates to always true. if (value) { return null; // OpenICF contract evaluates null return to always true } else { throw new UnsupportedOperationException( "visitBooleanLiteralFilter only supported for literal true, not false"); } } }; } @Override public Filter visitContainsFilter(ObjectClassInfoHelper helper, JsonPointer field, Object valueAssertion) { return contains(helper.filterAttribute(field, valueAssertion)); } @Override public Filter visitEqualsFilter(ObjectClassInfoHelper helper, JsonPointer field, Object valueAssertion) { return equalTo(helper.filterAttribute(field, valueAssertion)); } /** * EndsWith filter */ private static final String EW = "ew"; /** * ContainsAll filter */ private static final String CA = "ca"; @Override public Filter visitExtendedMatchFilter(ObjectClassInfoHelper helper, JsonPointer field, String matchingRuleId, Object valueAssertion) { if (EW.equals(matchingRuleId)) { return endsWith(helper.filterAttribute(field, valueAssertion)); } else if (CA.equals(matchingRuleId)) { return containsAllValues(helper.filterAttribute(field, valueAssertion)); } throw new IllegalArgumentException("ExtendedMatchFilter is not supported"); } @Override public Filter visitGreaterThanFilter(ObjectClassInfoHelper helper, JsonPointer field, Object valueAssertion) { return greaterThan(helper.filterAttribute(field, valueAssertion)); } @Override public Filter visitGreaterThanOrEqualToFilter(ObjectClassInfoHelper helper, JsonPointer field, Object valueAssertion) { return greaterThanOrEqualTo(helper.filterAttribute(field, valueAssertion)); } @Override public Filter visitLessThanFilter(ObjectClassInfoHelper helper, JsonPointer field, Object valueAssertion) { return lessThan(helper.filterAttribute(field, valueAssertion)); } @Override public Filter visitLessThanOrEqualToFilter(ObjectClassInfoHelper helper, JsonPointer field, Object valueAssertion) { return lessThanOrEqualTo(helper.filterAttribute(field, valueAssertion)); } @Override public Filter visitNotFilter(ObjectClassInfoHelper helper, QueryFilter<JsonPointer> subFilter) { return not(subFilter.accept(this, helper)); } @Override public Filter visitPresentFilter(ObjectClassInfoHelper helper, JsonPointer field) { throw new IllegalArgumentException("PresentFilter is not supported"); } @Override public Filter visitStartsWithFilter(ObjectClassInfoHelper helper, JsonPointer field, Object valueAssertion) { return startsWith(helper.filterAttribute(field, valueAssertion)); } }; /** * Gets the unique {@link org.forgerock.openidm.provisioner.SystemIdentifier} of this instance. * <p/> * The service which refers to this service instance can distinguish between multiple instances by this value. * * @return */ public SystemIdentifier getSystemIdentifier() { return systemIdentifier; } public String getSystemIdentifierName() { return systemIdentifier.getName(); } /** * Gets the fully qualified path name * @param objectClass the object class for the intended resource * @param optionalId ids to append to the fully qualified path * @return */ private String getSource(final String objectClass, final String... optionalId) { final StringBuilder sb = new StringBuilder("system") .append("/") .append(systemIdentifier.getName()) .append("/") .append(objectClass); for (String id : optionalId) { sb.append("/").append(id); } return sb.toString(); } /** * Gets a brief status report about the current status of this service instance. * <p> * An example response when the configuration is enabled * {@code { * "name" : "ldap", * "enabled" : true, * "config" : "config/provisioner.openicf/ldap" * "objectTypes": * [ * "group", * "account" * ], * "connectorRef" : * { * "connectorName": "org.identityconnectors.ldap.LdapConnector", * "bundleName": "org.forgerock.openicf.connectors.ldap-connector", * "bundleVersion": "[1.1.0.1,1.1.2.0)" * } , * "ok" : true * }} * * An example response when the configuration is disabled * {@code { * "name": "ldap", * "enabled": false, * "config": "config/provisioner.openicf/ldap", * "objectTypes": * [ * "group", * "account" * ], * "connectorRef": * { * "connectorName": "org.identityconnectors.ldap.LdapConnector", * "bundleName": "org.forgerock.openicf.connectors.ldap-connector", * "bundleVersion": "[1.4.0.0,2.0.0.0)" * }, * "error": "connector not available", * "ok": false * }} * * @param context the Context of the request requesting the status * @return a Map of the current status of a connector */ public Map<String, Object> getStatus(Context context) { Map<String, Object> result = new LinkedHashMap<String, Object>(); JsonValue jv = new JsonValue(result); boolean ok = false; jv.put("name", systemIdentifier.getName()); jv.put("enabled", jsonConfiguration.get("enabled").defaultTo(Boolean.TRUE).asBoolean()); jv.put("config", "config/provisioner.openicf/" + factoryPid); jv.put("objectTypes", ConnectorUtil.getObjectTypes(jsonConfiguration).keySet()); ConnectorReference connectorReference = ConnectorUtil.getConnectorReference(jsonConfiguration); if (connectorReference != null) { jv.put(ConnectorUtil.OPENICF_CONNECTOR_REF, ConnectorUtil.getConnectorKey( connectorReference.getConnectorKey())); ConnectorInfo connectorInfo = connectorInfoProvider.findConnectorInfo(connectorReference); if (connectorInfo != null) { jv.put("displayName", connectorInfo.getConnectorDisplayName()); } } try { ConnectorFacade connectorFacade = getConnectorFacade(); if (connectorFacade == null) { jv.put("error", "connector not available"); } else { connectorFacade.test(); ok = true; } } catch (UnsupportedOperationException e) { jv.put("error", "TEST UnsupportedOperation"); } catch (InvalidCredentialException e) { jv.put("error", "Connection Error"); } catch (Exception e) { jv.put("error", e.getMessage()); } jv.put("ok", ok); return result; } public Map<String, Object> testConfig(JsonValue config) { JsonValue jv = json(object()); jv.put("name", systemIdentifier.getName()); jv.put("ok", false); SimpleSystemIdentifier testIdentifier = null; ConnectorReference connectorReference = null; try { testIdentifier = new SimpleSystemIdentifier(config); connectorReference = ConnectorUtil.getConnectorReference(jsonConfiguration); } catch (JsonValueException e) { jv.put("error", "OpenICF Provisioner Service jsonConfiguration has errors: " + e.getMessage()); return jv.asMap(); } ConnectorInfo connectorInfo = connectorInfoProvider.findConnectorInfo(connectorReference); if (null != connectorInfo) { ConnectorFacade facade = null; try { OperationHelperBuilder ohb = new OperationHelperBuilder(testIdentifier.getName(), config, connectorInfo.createDefaultAPIConfiguration(), cryptoService); ConnectorFacadeFactory connectorFacadeFactory = ConnectorFacadeFactory.getInstance(); facade = connectorFacadeFactory.newInstance(ohb.getRuntimeAPIConfiguration()); } catch (Exception e) { jv.put("error", "OpenICF connector jsonConfiguration has errors: " + e.getMessage()); return jv.asMap(); } if (null != facade && facade.getSupportedOperations().contains(TestApiOp.class)) { try { facade.test(); } catch (UnsupportedOperationException e) { jv.put("reason", "TEST UnsupportedOperation"); } catch (InvalidCredentialException e) { jv.put("error", "Connection Error"); } catch (Throwable e) { jv.put("error", e.getMessage()); return jv.asMap(); } jv.put("ok", true); } else if (null == facade) { jv.put("error", "OpenICF ConnectorFacade of " + connectorReference + " is not available"); } else { jv.put("error", "OpenICF connector of " + connectorReference + " does not support test."); } } else if (connectorReference.getConnectorLocation().equals(ConnectorReference.ConnectorLocation.LOCAL)) { jv.put("error", "OpenICF ConnectorInfo can not be loaded for " + connectorReference + " from #LOCAL"); } else { jv.put("error", "OpenICF ConnectorInfo for " + connectorReference + " is not available yet."); } return jv.asMap(); } /** * Get the corresponding object type name from provisioner config * @param objectClass the objectClass to get the name of * @return the name of the objectClass or null if not found */ protected String getObjectTypeName(final ObjectClass objectClass) { if (objectClass == null) { return null; } final Predicate<Entry<String, ObjectClassInfoHelper>> objectClassFilter = new Predicate<Entry<String, ObjectClassInfoHelper>>() { public boolean apply(Entry<String, ObjectClassInfoHelper> entry) { return objectClass.equals(entry.getValue().getObjectClass()); } }; final Iterable<Entry<String, ObjectClassInfoHelper>> objectClasses = FluentIterable.from(objectTypes.entrySet()).filter(objectClassFilter); return objectClasses.iterator().hasNext() ? objectClasses.iterator().next().getKey() : null; } /** * This newBuilder and this method can not be scheduled. The call MUST go * through the {@code org.forgerock.openidm.provisioner} * <p/> * Invoked by the scheduler when the scheduler triggers. * <p/> * Synchronization object: {@code "connectorData" : "syncToken" : * "1305555929000", "nativeType" : "JAVA_TYPE_LONG" }, * "synchronizationStatus" : { "errorStatus" : null, "lastKnownServer" : * "localServer", "lastModDate" : "2011-05-16T14:47:58.587Z", "lastModNum" : * 668, "lastPollDate" : "2011-05-16T14:47:52.875Z", "lastStartTime" : * "2011-05-16T14:29:07.863Z", "progressMessage" : "SUCCEEDED" } }} * <p/> * {@inheritDoc} Synchronize the changes from the end system for the given * {@code objectType}. * <p/> * OpenIDM takes active role in the synchronization process by asking the * end system to get all changed object. Not all systems are capable to * fulfill this kind of request but if the end system is capable then the * implementation sends each change to a new request on the router and when * it is finished, it returns a new <b>stage</b> object. * <p/> * The {@code previousStage} object is the previously returned value of this * method. * * @param context the request context associated with the invocation * @param previousStage * The previously returned object. If null then it's the first * execution. * @return The new updated stage object. This will be the * {@code previousStage} at buildNext call. * @throws IllegalArgumentException * if the value of {@code connectorData} can not be converted to * {@link SyncToken}. * @throws UnsupportedOperationException * if the {@link SyncApiOp} operation is not implemented in * connector. * @throws org.forgerock.json.JsonValueException * if the {@code previousStage} is not Map. * @see {@link ConnectorUtil#convertToSyncToken(org.forgerock.json.JsonValue)} * or any exception happed inside the connector. */ public JsonValue liveSynchronize(final Context context, final String objectType, final JsonValue previousStage) throws ResourceException { if (!serviceAvailable) { return previousStage; } final JsonValue stage = previousStage != null ? previousStage.copy() : new JsonValue(new LinkedHashMap<String, Object>()); JsonValue connectorData = stage.get("connectorData"); SyncToken token = null; if (!connectorData.isNull()) { if (connectorData.isMap()) { token = ConnectorUtil.convertToSyncToken(connectorData); } else { throw new IllegalArgumentException("Illegal connectorData property. Value must be Map"); } } stage.remove("lastException"); try { final SyncRetry syncRetry = new SyncRetry(); final OperationHelper helper = operationHelperBuilder.build(objectType, stage, cryptoService); if (helper.isOperationPermitted(SyncApiOp.class)) { ConnectorFacade connector = getConnectorFacade(); SyncApiOp operation = (SyncApiOp) connector.getOperation(SyncApiOp.class); if (null == operation) { throw new UnsupportedOperationException(SyncApiOp.class.getCanonicalName()); } if (null == token) { token = operation.getLatestSyncToken(helper.getObjectClass()); logger.debug("New LatestSyncToken has been fetched. New token is: {}", token); } else { final SyncToken[] lastToken = new SyncToken[]{token}; final String[] failedRecord = new String[1]; OperationOptionsBuilder operationOptionsBuilder = helper.getOperationOptionsBuilder(SyncApiOp.class, null, previousStage); try { logger.debug("Execute sync(ObjectClass:{}, SyncToken:{})", new Object[] { helper.getObjectClass().getObjectClassValue(), token }); SyncToken syncToken = operation.sync(helper.getObjectClass(), token, new SyncResultsHandler() { /** * Called to handle a delta in the stream. The Connector framework will call * this method multiple times, once for each result. * Although this method is callback, the framework will invoke it synchronously. * Thus, the framework guarantees that once an application's call to * {@link org.identityconnectors.framework.api.operations.SyncApiOp#sync(org.identityconnectors.framework.common.objects.ObjectClass, org.identityconnectors.framework.common.objects.SyncToken, org.identityconnectors.framework.common.objects.SyncResultsHandler, org.identityconnectors.framework.common.objects.OperationOptions)} SyncApiOp#sync() returns, * the framework will no longer call this method * to handle results from that <code>sync()</code> operation. * * @param syncDelta The change * @return True iff the application wants to continue processing more results. * @throws RuntimeException If the application encounters an exception. This will * stop iteration and the exception will propagate to the application. */ @SuppressWarnings("fallthrough") public boolean handle(SyncDelta syncDelta) { try { // Q: are we going to encode ids? final String resourceId = syncDelta.getUid().getUidValue(); final String objectTypeName = getObjectTypeName(syncDelta.getObjectClass()); final String resourceContainer = getSource(objectTypeName == null ? objectType : objectTypeName); final JsonValue content = new JsonValue(new LinkedHashMap<String, Object>(2)); //rebuild the OperationHelper if the helper is for the __ALL__ object class final OperationHelper syncDeltaOperationHelper = helper.getObjectClass().equals(ObjectClass.ALL) ? operationHelperBuilder.build(objectTypeName, stage, cryptoService) : helper; switch (syncDelta.getDeltaType()) { case CREATE: { JsonValue deltaObject = syncDeltaOperationHelper.build(syncDelta.getObject()); content.put("oldValue", null); content.put("newValue", deltaObject.getObject()); // TODO import SynchronizationService.Action.notifyCreate and ACTION_PARAM_ constants ActionRequest onCreateRequest = Requests.newActionRequest("sync", "notifyCreate") .setAdditionalParameter("resourceContainer", resourceContainer) .setAdditionalParameter("resourceId", resourceId) .setContent(content); connectionFactory.getConnection().action(context, onCreateRequest); activityLogger.log(context, onCreateRequest, "sync-create", onCreateRequest.getResourcePath(), deltaObject, deltaObject, Status.SUCCESS); break; } case UPDATE: case CREATE_OR_UPDATE: { JsonValue deltaObject = syncDeltaOperationHelper.build(syncDelta.getObject()); content.put("oldValue", null); content.put("newValue", deltaObject.getObject()); if (null != syncDelta.getPreviousUid()) { deltaObject.put("_previous-id", syncDelta.getPreviousUid().getUidValue()); } // TODO import SynchronizationService.Action.notifyUpdate and ACTION_PARAM_ constants ActionRequest onUpdateRequest = Requests.newActionRequest("sync", "notifyUpdate") .setAdditionalParameter("resourceContainer", resourceContainer) .setAdditionalParameter("resourceId", resourceId) .setContent(content); connectionFactory.getConnection().action(context, onUpdateRequest); activityLogger.log(context, onUpdateRequest, "sync-update", onUpdateRequest.getResourcePath(), deltaObject, deltaObject, Status.SUCCESS); break; } case DELETE: // TODO Pass along the old deltaObject - do we have it? content.put("oldValue", null); // TODO import SynchronizationService.Action.notifyDelete and ACTION_PARAM_ constants ActionRequest onDeleteRequest = Requests.newActionRequest("sync", "notifyDelete") .setAdditionalParameter("resourceContainer", resourceContainer) .setAdditionalParameter("resourceId", resourceId) .setContent(content); connectionFactory.getConnection().action(context, onDeleteRequest); activityLogger.log(context, onDeleteRequest, "sync-delete", onDeleteRequest.getResourcePath(), null, null, Status.SUCCESS); break; } } catch (Exception e) { failedRecord[0] = SerializerUtil.serializeXmlObject(syncDelta, true); logger.debug("Failed to synchronize {} object, handle failure using {}", syncDelta.getUid(), syncFailureHandler, e); Map<String, Object> syncFailureMap = new HashMap<String, Object>(6); syncFailureMap.put("token", syncDelta.getToken().getValue()); syncFailureMap.put("systemIdentifier", systemIdentifier.getName()); syncFailureMap.put("objectType", objectType); syncFailureMap.put("uid", syncDelta.getUid().getUidValue()); syncFailureMap.put("failedRecord", failedRecord[0]); try { syncFailureHandler.invoke(context, syncFailureMap, e); } catch (SyncHandlerException syncHandlerException) { // Current contract of the failure handler is that throwing this exception indicates // that it should retry for this entry syncRetry.setValue(true); syncRetry.setThrowable(syncHandlerException); logger.debug("Sync failure handler indicated to stop current change set processing until retry handling: {}", syncHandlerException.getMessage(), syncHandlerException); } } if (syncRetry.getValue()) { // Stop the processing of this result set. Next retry will start again after last token. return false; } else { // success (either by original sync or by failure handler) // Continue the processing of the rest of the result set lastToken[0] = syncDelta.getToken(); return true; } } }, operationOptionsBuilder.build()); if (syncRetry.getValue()) { Throwable throwable = syncRetry.getThrowable(); Map<String, Object> lastException = new LinkedHashMap<String, Object>(2); lastException.put("throwable", throwable.getMessage()); if (null != failedRecord[0]) { lastException.put("syncDelta", failedRecord[0]); } stage.put("lastException", lastException); logger.debug("Live synchronization of {} failed on {}", new Object[] { objectType, systemIdentifier.getName() }, throwable); } else { if (syncToken != null) { lastToken[0] = syncToken; } } } finally { token = lastToken[0]; logger.debug("Synchronization is finished. New LatestSyncToken value: {}", token); } } if (null != token) { stage.put("connectorData", ConnectorUtil.convertFromSyncToken(token)); } } } catch (ResourceException e) { logger.debug("Failed to get OperationHelper", e); throw new RuntimeException(e); } catch (UnsupportedOperationException e) { logger.debug("Failed to get OperationOptionsBuilder", e); throw new NotFoundException("Failed to get latest sync token", e).setDetail(new JsonValue(e.getMessage())); } catch (Exception e) { logger.debug("Failed to get OperationOptionsBuilder", e); throw new InternalServerErrorException("Failed to get OperationOptionsBuilder: " + e.getMessage(), e); } return stage; } /** * Package level setter to allow unit tests to set the logger. * @param activityLogger */ void setActivityLogger(ActivityLogger activityLogger) { this.activityLogger = activityLogger; } }