/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2011-2015 ForgeRock AS. All Rights Reserved * * 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 * http://forgerock.org/license/CDDLv1.0.html * See the License for the specific language governing * permission and limitations under the License. * * When distributing Covered Code, include this CDDL * Header Notice in each file and include the License file * at http://forgerock.org/license/CDDLv1.0.html * If applicable, add the following below the CDDL Header, * with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" */ package org.forgerock.openidm.provisioner.impl; import static org.forgerock.json.JsonValue.array; import static org.forgerock.json.JsonValue.field; 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.openidm.provisioner.ConnectorConfigurationHelper.CONNECTOR_NAME; import static org.forgerock.openidm.provisioner.ConnectorConfigurationHelper.CONNECTOR_REF; import static org.forgerock.openidm.provisioner.ConnectorConfigurationHelper.CONFIGURATION_PROPERTIES; import static org.forgerock.util.promise.Promises.newResultPromise; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.ConfigurationPolicy; 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.ReferenceCardinality; import org.apache.felix.scr.annotations.ReferencePolicy; import org.apache.felix.scr.annotations.ReferenceStrategy; import org.apache.felix.scr.annotations.Service; import org.forgerock.audit.events.AuditEvent; import org.forgerock.services.context.Context; 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.CreateRequest; import org.forgerock.json.resource.InternalServerErrorException; import org.forgerock.json.resource.NotFoundException; import org.forgerock.json.resource.NotSupportedException; import org.forgerock.json.resource.PatchRequest; import org.forgerock.json.resource.ReadRequest; import org.forgerock.json.resource.Requests; import org.forgerock.json.resource.ResourceException; import org.forgerock.json.resource.ResourceResponse; import org.forgerock.json.resource.ServiceUnavailableException; import org.forgerock.json.resource.SingletonResourceProvider; import org.forgerock.json.resource.UpdateRequest; import org.forgerock.openidm.core.ServerConstants; import org.forgerock.openidm.provisioner.ConnectorConfigurationHelper; import org.forgerock.openidm.provisioner.Id; import org.forgerock.openidm.provisioner.ProvisionerService; import org.forgerock.openidm.provisioner.SystemIdentifier; import org.forgerock.openidm.quartz.impl.ExecutionException; import org.forgerock.openidm.quartz.impl.ScheduledService; import org.forgerock.openidm.router.IDMConnectionFactory; import org.forgerock.util.promise.Promise; import org.osgi.framework.Constants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * SystemObjectSetService is a {@link SingletonResourceProvider} to manage provisioner/connector configuration * and to dispatch liveSync to the correct provisioner implementation. */ @Component(name = "org.forgerock.openidm.provisioner", policy = ConfigurationPolicy.IGNORE, metatype = true, description = "OpenIDM System Object Set Service", immediate = true) @Service(value = {ScheduledService.class, SingletonResourceProvider.class}) @Properties({ @Property(name = Constants.SERVICE_VENDOR, value = ServerConstants.SERVER_VENDOR_NAME), @Property(name = Constants.SERVICE_DESCRIPTION, value = "OpenIDM System Object Set Service"), @Property(name = ServerConstants.ROUTER_PREFIX, value = ProvisionerService.ROUTER_PREFIX) }) public class SystemObjectSetService implements ScheduledService, SingletonResourceProvider { private final static Logger logger = LoggerFactory.getLogger(SystemObjectSetService.class); /** the system (provisioner) type (within connectorRef) */ private static final String SYSTEM_TYPE = "systemType"; /** systemType prefix prepended per connectorRef list */ private static final String PROVISIONER_PREFIX = "provisioner"; /** * Actions supported on this resource provider */ public enum SystemAction { /** Captures the changes on a remote system, the pushes those changes to OpenIDM */ activeSync, /** Captures the changes on a remote system, the pushes those changes to OpenIDM */ liveSync, /** Test a connector to see if the connection is available */ test, /** Test an existing connector configuration */ testConfig { @Override boolean requiresConnectorConfigurationHelper(JsonValue requestContent) { return true; } }, /** Multi phase configuration event calls this to generate the response */ createConfiguration { /** * ConnectorConfigurationHelper is required if there is request content */ @Override boolean requiresConnectorConfigurationHelper(JsonValue requestContent) { return requestContent != null && requestContent.size() > 0; } }, /** List the connector [types] available in the system */ availableConnectors, /** Generates the core configuration for a connector */ createCoreConfig { /** * ConnectorConfigurationHelper is required always */ @Override boolean requiresConnectorConfigurationHelper(JsonValue requestContent) { return true; } }, /** Generates the full configuration for a connector */ createFullConfig { /** * ConnectorConfigurationHelper is required always */ @Override boolean requiresConnectorConfigurationHelper(JsonValue requestContent) { return true; } }; /** * Checks to see that ConnectorConfigurationHelper is needed - default to false */ boolean requiresConnectorConfigurationHelper(JsonValue requestContent) { return false; } private static Set<SystemAction> liveSyncActions = EnumSet.of(activeSync, liveSync); /** Checks to see if action is live sync */ boolean isLiveSync() { return liveSyncActions.contains(this); } } @Reference(referenceInterface = ProvisionerService.class, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE, bind = "bindProvisionerService", unbind = "unbindProvisionerService", policy = ReferencePolicy.DYNAMIC, strategy = ReferenceStrategy.EVENT) private Map<SystemIdentifier, ProvisionerService> provisionerServices = new HashMap<SystemIdentifier, ProvisionerService>(); protected void bindProvisionerService(ProvisionerService service, Map properties) { provisionerServices.put(service.getSystemIdentifier(), service); } protected void unbindProvisionerService(ProvisionerService service, Map properties) { for (Map.Entry<SystemIdentifier, ProvisionerService> entry : provisionerServices.entrySet()) { if (service.equals(entry.getValue())) { provisionerServices.remove(entry.getKey()); break; } } } /** The Connection Factory */ @Reference(policy = ReferencePolicy.STATIC) protected IDMConnectionFactory connectionFactory; protected void bindConnectionFactory(IDMConnectionFactory connectionFactory) { this.connectionFactory = connectionFactory; } @Reference(referenceInterface = ConnectorConfigurationHelper.class, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE, bind = "bindConnectorConfigurationHelper", unbind = "unbindConnectorConfigurationHelper", policy = ReferencePolicy.DYNAMIC) private Map<String, ConnectorConfigurationHelper> connectorConfigurationHelpers = new HashMap<String, ConnectorConfigurationHelper>(); protected void bindConnectorConfigurationHelper(ConnectorConfigurationHelper helper, Map properties) throws ResourceException { connectorConfigurationHelpers.put(helper.getProvisionerType(), helper); } protected void unbindConnectorConfigurationHelper(ConnectorConfigurationHelper helper, Map properties) { connectorConfigurationHelpers.remove(helper.getProvisionerType()); } @Override public Promise<ActionResponse, ResourceException> actionInstance(Context context, ActionRequest request) { try { final ProvisionerService ps; final JsonValue content = request.getContent(); final JsonValue id = content.get("id"); final JsonValue name = content.get("name"); final SystemAction action = request.getActionAsEnum(SystemAction.class); String provisionerType = null; if (action.requiresConnectorConfigurationHelper(content)) { final String connectorName = content.get(CONNECTOR_REF).get(CONNECTOR_NAME).asString(); if (connectorName == null) { return new NotFoundException("No connector name provided").asPromise(); } provisionerType = getProvisionerType(connectorName); if (provisionerType == null || !connectorConfigurationHelpers.containsKey(provisionerType)) { return new ServiceUnavailableException("The required service is not available").asPromise(); } } final ConnectorConfigurationHelper helper = connectorConfigurationHelpers.get(provisionerType); switch (action) { case createConfiguration: // Multi phase configuration event calls this to generate the response for the next phase. if (content.size() == 0) { // Stage 1 : list available connectors return newActionResponse(getAvailableConnectors()).asPromise(); } else if (isGenerateConnectorCoreConfig(content)) { // Stage 2: generate basic configuration return newActionResponse(helper.generateConnectorCoreConfig(content)).asPromise(); } else if (isGenerateFullConfig(content)) { // Stage 3: generate/validate full configuration return newActionResponse(helper.generateConnectorFullConfig(content)).asPromise(); } else { // illegal request ?? return newActionResponse(json(object())).asPromise(); } case testConfig: JsonValue config = content; if (!id.isNull()) { return new BadRequestException("A system ID must not be specified in the request").asPromise(); } if (name.isNull()) { return new BadRequestException("Invalid configuration to test: no 'name' specified").asPromise(); } ps = locateServiceForTest(name); if (ps != null) { return newActionResponse(new JsonValue(ps.testConfig(config))).asPromise(); } else { // service for config-name doesn't exist; test it using the ConnectorConfigurationHelper return newActionResponse(new JsonValue(helper.test(config))).asPromise(); } case test: if (id.isNull()) { List<Object> list = new ArrayList<Object>(); for (Map.Entry<SystemIdentifier, ProvisionerService> entry : provisionerServices.entrySet()) { list.add(entry.getValue().getStatus(context)); } return newActionResponse(new JsonValue(list)).asPromise(); } else { ps = locateServiceForTest(id); if (ps == null) { return new NotFoundException("System: " + id.asString() + " is not available.").asPromise(); } else { return newActionResponse(new JsonValue(ps.getStatus(context))).asPromise(); } } case activeSync: case liveSync: JsonValue params = new JsonValue(request.getAdditionalParameters()); String source = params.get("source").asString(); if (source == null) { logger.debug("liveSync requires an explicit source parameter, source is : {}", source); return new BadRequestException("liveSync action requires either an explicit source parameter, " + "or needs to be called on a specific provisioner URI") .asPromise(); } else { logger.debug("liveSync called with explicit source parameter {}", source); } return newResultPromise(newActionResponse( liveSync(context, source, Boolean.valueOf(params.get("detailedFailure").asString())))); case availableConnectors: // stage 1 - direct action to get available connectors return newActionResponse(getAvailableConnectors()).asPromise(); case createCoreConfig: // stage 2 - direct action to create core configuration return newActionResponse(helper.generateConnectorCoreConfig(content)).asPromise(); case createFullConfig: // stage 3 - direct action to create full configuration return newActionResponse(helper.generateConnectorFullConfig(content)).asPromise(); default: return new BadRequestException("Unsupported actionId: " + request.getAction()).asPromise(); } } catch (IllegalArgumentException e) { // from getActionAsEnum return new BadRequestException(e.getMessage(), e).asPromise(); } catch (Exception e) { return new InternalServerErrorException(e).asPromise(); } } private String getProvisionerType(String connectorName) throws ResourceException { for (Map.Entry<String, ConnectorConfigurationHelper> entry : connectorConfigurationHelpers.entrySet()) { for (JsonValue connectorRef : entry.getValue().getAvailableConnectors().get(CONNECTOR_REF)) { if (connectorRef.get(CONNECTOR_NAME).asString().equals(connectorName)) { return entry.getKey(); } } } return null; } /** * Validates that the connectorRef is defined in the connector configuration * * @param requestConfig connector configuration * @return true if connectorRef is not null and configurationProperties is null; false otherwise */ private boolean isGenerateConnectorCoreConfig(JsonValue requestConfig) { return !requestConfig.get(CONNECTOR_REF).isNull() && !requestConfig.get(CONNECTOR_REF).get(CONNECTOR_NAME).isNull() && requestConfig.get(CONFIGURATION_PROPERTIES).isNull(); } /** * Validates that connectorRef and configurationProperties inside the connector configuration are both not null * * @param requestConfig connector configuration * @return true if both connectorRef and configurationProperties are not null; false otherwise */ private boolean isGenerateFullConfig(JsonValue requestConfig) { return !requestConfig.get(CONNECTOR_REF).isNull() && !requestConfig.get(CONNECTOR_REF).get(CONNECTOR_NAME).isNull() && !requestConfig.get(CONFIGURATION_PROPERTIES).isNull(); } private JsonValue getAvailableConnectors() throws ResourceException { JsonValue availableConnectors = json(array()); for (Map.Entry<String, ConnectorConfigurationHelper> helperEntry : connectorConfigurationHelpers.entrySet()) { for (JsonValue connectorRef : helperEntry.getValue().getAvailableConnectors().get(CONNECTOR_REF)) { connectorRef.put(SYSTEM_TYPE, getSystemType(helperEntry.getKey())); availableConnectors.add(connectorRef.getObject()); } } return json(object(field(CONNECTOR_REF, availableConnectors.getObject()))); } /** * The system type is comprised by the prefix "provisioner." and the provionser's type; e.g. openicf * * @param provisionerType the provisioner type * @return the system type */ private String getSystemType(String provisionerType) { return new StringBuilder(PROVISIONER_PREFIX).append(".").append(provisionerType).toString(); } @Override public Promise<ResourceResponse, ResourceException> readInstance(Context context, ReadRequest request) { return new NotSupportedException("Read are not supported for resource instances").asPromise(); } @Override public Promise<ResourceResponse, ResourceException> patchInstance(Context context, PatchRequest request) { return new NotSupportedException("Patch are not supported for resource instances").asPromise(); } @Override public Promise<ResourceResponse, ResourceException> updateInstance(Context context, UpdateRequest request) { return new NotSupportedException("Update are not supported for resource instances").asPromise(); } /** * Invoked by the scheduler when the scheduler triggers. * * @param schedulerContext Context information passed by the scheduler service * @throws org.forgerock.openidm.quartz.impl.ExecutionException * if execution of the scheduled work failed. * Implementations can also throw RuntimeExceptions which will get logged. */ public void execute(Context context, Map<String, Object> schedulerContext) throws ExecutionException { try { JsonValue params = new JsonValue(schedulerContext).get(CONFIGURED_INVOKE_CONTEXT); if (params.get("action").asEnum(SystemAction.class).isLiveSync()) { String source = params.get("source").required().asString(); liveSync(context, source, true); } } catch (JsonValueException jve) { throw new ExecutionException(jve); } catch (ResourceException e) { throw new ExecutionException(e); } catch (IllegalArgumentException e) { // not a liveSync action, so no-op } catch (RuntimeException e) { throw new ExecutionException(e); } } @Override public void auditScheduledService(final Context context, final AuditEvent auditEvent) throws ExecutionException { try { connectionFactory.getConnection().create( context, Requests.newCreateRequest("audit/access", auditEvent.getValue())); } catch (ResourceException e) { logger.error("Unable to audit scheduled service {}", auditEvent.toString()); throw new ExecutionException("Unable to audit scheduled service", e); } } /** * Live sync the specified provisioner resource. * * @param context the request context associated with the invocation * @param source the URI of the provisioner instance to live sync * @param detailedFailure whether in the case of failures additional details such as the */ private JsonValue liveSync(Context context, String source, boolean detailedFailure) throws ResourceException { JsonValue response; Id id = new Id(source); String previousStageResourceContainer = "repo/synchronisation/pooledSyncStage"; String previousStageId = id.toString().replace("/", "").toUpperCase(); ResourceResponse previousStage = null; try { ReadRequest readRequest = Requests.newReadRequest(previousStageResourceContainer, previousStageId); previousStage = connectionFactory.getConnection().read(context, readRequest); response = locateService(id).liveSynchronize(context, id.getObjectType(), previousStage != null && previousStage.getContent() != null ? previousStage.getContent() : null); UpdateRequest updateRequest = Requests.newUpdateRequest(previousStageResourceContainer, previousStageId, response); updateRequest.setRevision(previousStage.getRevision()); connectionFactory.getConnection().update(context, updateRequest); } catch (ResourceException e) { // NotFoundException? if (previousStage != null) { throw e; } response = locateService(id).liveSynchronize(context, id.getObjectType(), null); if (response != null) { CreateRequest createRequest = Requests.newCreateRequest(previousStageResourceContainer, previousStageId, response); connectionFactory.getConnection().create(context, createRequest); } } if (response != null && !detailedFailure) { // The detailedFailure option handling ideally should move into provisioners response.get("lastException").remove("syncDelta"); } return response; } private ProvisionerService locateService(Id identifier) throws ResourceException { for (Map.Entry<SystemIdentifier, ProvisionerService> entry : provisionerServices.entrySet()) { if (entry.getKey().is(identifier)) { return entry.getValue(); } } throw new ServiceUnavailableException("System: " + identifier + " is not available."); } private ProvisionerService locateServiceForTest(JsonValue requestId) throws ResourceException { if (requestId.isNull()) { return null; } Id id = new Id(requestId.asString() + "/test"); for (Map.Entry<SystemIdentifier, ProvisionerService> entry : provisionerServices.entrySet()) { if (entry.getKey().is(id)) { return entry.getValue(); } } return null; } }