diff --git a/provisioning/provisioning-impl/src/main/java/com/evolveum/midpoint/provisioning/impl/resources/ConnectorManager.java b/provisioning/provisioning-impl/src/main/java/com/evolveum/midpoint/provisioning/impl/resources/ConnectorManager.java index cd62f37e972..7a15b0f081b 100644 --- a/provisioning/provisioning-impl/src/main/java/com/evolveum/midpoint/provisioning/impl/resources/ConnectorManager.java +++ b/provisioning/provisioning-impl/src/main/java/com/evolveum/midpoint/provisioning/impl/resources/ConnectorManager.java @@ -44,6 +44,7 @@ import com.evolveum.midpoint.prism.PrismContext; import com.evolveum.midpoint.prism.PrismObject; import com.evolveum.midpoint.prism.delta.ItemDelta; +import com.evolveum.midpoint.prism.query.ObjectFilter; import com.evolveum.midpoint.prism.query.ObjectQuery; import com.evolveum.midpoint.repo.api.RepositoryService; import com.evolveum.midpoint.repo.api.CacheRegistry; @@ -261,10 +262,11 @@ private static boolean doesNeedReconfiguration( ConfiguredConnectorInstanceEntry getOrCreateConnectorInstanceCacheEntry(ConnectorSpec connectorSpec, OperationResult result) throws ObjectNotFoundException, SchemaException, ConfigurationException { ConfiguredConnectorCacheKey cacheKey = connectorSpec.getCacheKey(); + String connectorOid = resolveConnectorOidRequired(connectorSpec, result); ConfiguredConnectorInstanceEntry existingCacheEntry = connectorInstanceCache.get(cacheKey); if (existingCacheEntry != null) { - if (existingCacheEntry.matchesConnectorOid(connectorSpec.getConnectorOidRequired())) { + if (existingCacheEntry.matchesConnectorOid(connectorOid)) { LOGGER.trace("HIT in connector cache: returning configured connector {} from cache; it may or may not be fresh", connectorSpec); return existingCacheEntry; @@ -282,7 +284,7 @@ ConfiguredConnectorInstanceEntry getOrCreateConnectorInstanceCacheEntry(Connecto // No usable connector in cache. Let's create it - unconfigured. return new ConfiguredConnectorInstanceEntry( - connectorSpec.getConnectorOidRequired(), + connectorOid, createConnectorInstance(connectorSpec, result)); } @@ -383,12 +385,51 @@ void configureAndInitializeConnectorInstance( // Currently, we need to throw a ConfigurationException here for the test to pass. // E.g., IllegalStateException won't work. return getConnectorWithSchema( - MiscUtil.configNonNull( - connectorSpec.getConnectorOid(), - "Connector OID missing in %s", connectorSpec), + resolveConnectorOidRequired(connectorSpec, result), result); } + /** + * Returns the connector OID for the given specification, resolving a filter-based {@code connectorRef} if needed. + * + * If the reference is resolved from a filter, the resolved OID is stored in the {@link ConnectorSpec} + * as transient state. This is intentional internal normalization: later completion/cache code reads + * the OID from the same specification, but the resource {@code connectorRef} is not modified and no + * repository delta is created for this change. + */ + private @NotNull String resolveConnectorOidRequired(ConnectorSpec connectorSpec, OperationResult result) + throws SchemaException, ConfigurationException { + String oid = connectorSpec.getConnectorOid(); + if (oid != null) { + return oid; + } + + ObjectReferenceType connectorRef = connectorSpec.getConnectorRef(); + if (connectorRef == null || connectorRef.getFilter() == null) { + return MiscUtil.configNonNull(oid, "Connector OID missing in %s", connectorSpec); + } + + ObjectFilter filter = prismContext.getQueryConverter().parseFilter(connectorRef.getFilter(), ConnectorType.class); + ObjectQuery query = prismContext.queryFactory().createQuery(filter); + SearchResultList> connectors = + repositoryService.searchObjects(ConnectorType.class, query, readOnly(), result); + + if (connectors.isEmpty()) { + throw new ConfigurationException( + "Connector reference in " + connectorSpec + " cannot be resolved: filter matches no object"); + } + if (connectors.size() > 1) { + throw new ConfigurationException( + "Connector reference in " + connectorSpec + " cannot be resolved: filter matches " + + connectors.size() + " objects"); + } + + String resolvedOid = connectors.get(0).getOid(); + connectorSpec.setResolvedConnectorOid(resolvedOid); + LOGGER.trace("Resolved connector reference in {} to OID {}", connectorSpec, resolvedOid); + return resolvedOid; + } + private @NotNull ConnectorWithSchema getConnectorWithSchema(String connOid, OperationResult result) throws ObjectNotFoundException, SchemaException { diff --git a/provisioning/provisioning-impl/src/main/java/com/evolveum/midpoint/provisioning/impl/resources/ConnectorSpec.java b/provisioning/provisioning-impl/src/main/java/com/evolveum/midpoint/provisioning/impl/resources/ConnectorSpec.java index 4bff9df1655..c18f595ec6b 100644 --- a/provisioning/provisioning-impl/src/main/java/com/evolveum/midpoint/provisioning/impl/resources/ConnectorSpec.java +++ b/provisioning/provisioning-impl/src/main/java/com/evolveum/midpoint/provisioning/impl/resources/ConnectorSpec.java @@ -44,6 +44,7 @@ public abstract class ConnectorSpec { @NotNull protected final ResourceType resource; + @Nullable private String resolvedConnectorOid; private ConnectorSpec(@NotNull ResourceType resource) { this.resource = resource; @@ -134,7 +135,18 @@ private ConnectorSpec(@NotNull ResourceType resource) { * Note that connector OID is not required here, as the resource may be not resolved yet, or it may be * an abstract resource with missing connectorRef. */ - public abstract @Nullable String getConnectorOid(); + public final @Nullable String getConnectorOid() { + return resolvedConnectorOid != null ? resolvedConnectorOid : getConnectorOidFromDefinition(); + } + + protected abstract @Nullable String getConnectorOidFromDefinition(); + + void setResolvedConnectorOid(@NotNull String resolvedConnectorOid) { + this.resolvedConnectorOid = resolvedConnectorOid; + } + + /** Returns the backing connector reference, if present. */ + abstract @Nullable ObjectReferenceType getConnectorRef(); /** * To be used when we are sure to deal with fully expanded, non-abstract resources. @@ -202,10 +214,15 @@ public ItemPath getBasePath() { } @Override - public @Nullable String getConnectorOid() { + protected @Nullable String getConnectorOidFromDefinition() { return ResourceTypeUtil.getConnectorOid(resource); } + @Override + @Nullable ObjectReferenceType getConnectorRef() { + return resource.getConnectorRef(); + } + @Override public @Nullable PrismContainer getConnectorConfigurationContainer() { return resource.asPrismObject().findContainer(ResourceType.F_CONNECTOR_CONFIGURATION); @@ -272,10 +289,15 @@ public ItemPath getBasePath() { } @Override - public @Nullable String getConnectorOid() { + protected @Nullable String getConnectorOidFromDefinition() { return getOid(definitionBean.getConnectorRef()); } + @Override + @Nullable ObjectReferenceType getConnectorRef() { + return definitionBean.getConnectorRef(); + } + @Override public @Nullable PrismContainer getConnectorConfigurationContainer() { //noinspection unchecked diff --git a/provisioning/provisioning-impl/src/test/java/com/evolveum/midpoint/provisioning/impl/csv/AbstractCsvTest.java b/provisioning/provisioning-impl/src/test/java/com/evolveum/midpoint/provisioning/impl/csv/AbstractCsvTest.java index 5d49ce6762d..07e72591df4 100644 --- a/provisioning/provisioning-impl/src/test/java/com/evolveum/midpoint/provisioning/impl/csv/AbstractCsvTest.java +++ b/provisioning/provisioning-impl/src/test/java/com/evolveum/midpoint/provisioning/impl/csv/AbstractCsvTest.java @@ -29,6 +29,7 @@ import com.evolveum.midpoint.prism.*; import com.evolveum.midpoint.prism.delta.PropertyDelta; +import com.evolveum.midpoint.prism.query.ObjectFilter; import com.evolveum.midpoint.prism.util.PrismAsserts; import com.evolveum.midpoint.prism.util.PrismTestUtil; import com.evolveum.midpoint.provisioning.impl.AbstractProvisioningIntegrationTest; @@ -41,6 +42,7 @@ import com.evolveum.midpoint.task.api.Task; import com.evolveum.midpoint.test.IntegrationTestTools; import com.evolveum.midpoint.test.util.TestUtil; +import com.evolveum.midpoint.util.exception.ConfigurationException; import com.evolveum.midpoint.xml.ns._public.common.common_3.*; import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.ActivationCapabilityType; import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.ScriptCapabilityHostType; @@ -253,6 +255,200 @@ public void test006Capabilities() throws Exception { dumpResourceCapabilities(resource); } + /** + * Verifies that filter-based connector references work consistently for both the primary connector and + * an additional connector, while direct-OID additional connectors continue to work. + */ + @Test + public void test007PrimaryAndAdditionalConnectorRefsByFilter() throws Exception { + Task task = getTestTask(); + OperationResult result = task.getResult(); + + PrismObject sourceResource = + repositoryService.getObject(ResourceType.class, getResourceOid(), null, result); + String connectorOid = sourceResource.asObjectable().getConnectorRef().getOid(); + ObjectReferenceType connectorByFilter = createCsvConnectorRefByFilter(); + String resourceOid = getConnectorRefTestResourceOid("cf0"); + + PrismObject resourceToAdd = cloneResourceForConnectorRefTest( + sourceResource, + resourceOid, + "Test CSV: filtered connectors"); + ResourceType resourceBean = resourceToAdd.asObjectable(); + resourceBean.setConnectorRef(connectorByFilter.clone()); + resourceBean.getAdditionalConnector().clear(); + resourceBean.getAdditionalConnector().add( + new ConnectorInstanceSpecificationType() + .name("csv-additional-filtered") + .connectorRef(connectorByFilter.clone())); + resourceBean.getAdditionalConnector().add( + new ConnectorInstanceSpecificationType() + .name("csv-additional-direct") + .connectorRef(new ObjectReferenceType() + .oid(connectorOid) + .type(ConnectorType.COMPLEX_TYPE))); + + repositoryService.addObject(resourceToAdd, null, result); + + when("the resource with connector refs by filter is retrieved through provisioning"); + PrismObject resourceAfter = provisioningService.getObject( + ResourceType.class, resourceOid, null, task, result); + + then("all connector refs are usable"); + result.computeStatus(); + TestUtil.assertSuccess(result); + ResourceType resourceAfterBean = resourceAfter.asObjectable(); + assertNull("Primary connector OID resolution should not be visible in the resource object", + resourceAfterBean.getConnectorRef().getOid()); + assertEquals("Wrong filtered additional connector name", "csv-additional-filtered", + resourceAfterBean.getAdditionalConnector().get(0).getName()); + assertNull("Filtered additional connector OID resolution should not be visible in the resource object", + resourceAfterBean.getAdditionalConnector().get(0).getConnectorRef().getOid()); + assertEquals("Wrong direct additional connector name", "csv-additional-direct", + resourceAfterBean.getAdditionalConnector().get(1).getName()); + assertEquals("Wrong direct additional connector OID", connectorOid, + resourceAfterBean.getAdditionalConnector().get(1).getConnectorRef().getOid()); + + PrismObject resourceRepoAfter = repositoryService.getObject(ResourceType.class, resourceOid, null, result); + ResourceType resourceRepoAfterBean = resourceRepoAfter.asObjectable(); + assertNull("Primary connector OID resolution should not be persisted", + resourceRepoAfterBean.getConnectorRef().getOid()); + assertNull("Additional connector OID resolution should not be persisted", + resourceRepoAfterBean.getAdditionalConnector().get(0).getConnectorRef().getOid()); + } + + /** + * Verifies that a filter-based additional connector reference that matches no connector reports a clear + * resolution error, instead of falling through to the old "Connector OID missing" failure. + */ + @Test + public void test008AdditionalConnectorRefByFilterMatchesNone() throws Exception { + Task task = getTestTask(); + OperationResult result = task.getResult(); + String resourceOid = getConnectorRefTestResourceOid("cf1"); + + PrismObject resourceToAdd = cloneResourceForConnectorRefTest( + repositoryService.getObject(ResourceType.class, getResourceOid(), null, result), + resourceOid, + "Test CSV: no connector match"); + ResourceType resourceBean = resourceToAdd.asObjectable(); + resourceBean.getAdditionalConnector().clear(); + resourceBean.getAdditionalConnector().add( + new ConnectorInstanceSpecificationType() + .name("csv-additional") + .connectorRef(createConnectorRefByConnectorType("no.such.Connector"))); + repositoryService.addObject(resourceToAdd, null, result); + + when("the resource with an unresolvable additional connector filter is retrieved through provisioning"); + String failureText = getResourceExpectingConnectorResolutionFailure(resourceOid, task, result); + + then("the failure reason is clear"); + assertTextContains(failureText, "Connector reference in ConnectorSpec.Additional"); + assertTextContains(failureText, "cannot be resolved: filter matches no object"); + assertTextDoesNotContain(failureText, "Connector OID missing in ConnectorSpec.Additional"); + } + + /** + * Verifies that an ambiguous filter-based additional connector reference reports a clear resolution + * error, instead of falling through to the old "Connector OID missing" failure. + */ + @Test + public void test009AdditionalConnectorRefByFilterMatchesMultiple() throws Exception { + Task task = getTestTask(); + OperationResult result = task.getResult(); + String resourceOid = getConnectorRefTestResourceOid("cf2"); + + assertAllConnectorsFilterIsAmbiguous(result); + + PrismObject resourceToAdd = cloneResourceForConnectorRefTest( + repositoryService.getObject(ResourceType.class, getResourceOid(), null, result), + resourceOid, + "Test CSV: multiple connector matches"); + ResourceType resourceBean = resourceToAdd.asObjectable(); + resourceBean.getAdditionalConnector().clear(); + resourceBean.getAdditionalConnector().add( + new ConnectorInstanceSpecificationType() + .name("csv-additional") + .connectorRef(createAllConnectorsRef())); + repositoryService.addObject(resourceToAdd, null, result); + + when("the resource with an ambiguous additional connector filter is retrieved through provisioning"); + String failureText = getResourceExpectingConnectorResolutionFailure(resourceOid, task, result); + + then("the failure reason is clear"); + assertTextContains(failureText, "Connector reference in ConnectorSpec.Additional"); + assertTextContains(failureText, "cannot be resolved: filter matches"); + assertTextContains(failureText, "objects"); + assertTextDoesNotContain(failureText, "Connector OID missing in ConnectorSpec.Additional"); + } + + private String getConnectorRefTestResourceOid(String suffix) { + String resourceOid = getResourceOid(); + return resourceOid.substring(0, resourceOid.length() - suffix.length()) + suffix; + } + + private PrismObject cloneResourceForConnectorRefTest( + PrismObject sourceResource, String oid, String name) { + PrismObject resourceToAdd = sourceResource.clone(); + ResourceType resourceBean = resourceToAdd.asObjectable(); + resourceBean.setOid(oid); + resourceBean.setName(createPolyStringType(name)); + return resourceToAdd; + } + + private ObjectReferenceType createCsvConnectorRefByFilter() throws Exception { + return createConnectorRefByConnectorType(CSV_CONNECTOR_TYPE); + } + + private ObjectReferenceType createConnectorRefByConnectorType(String connectorType) throws Exception { + ObjectFilter filter = prismContext.queryFor(ConnectorType.class) + .item(ConnectorType.F_CONNECTOR_TYPE) + .eq(connectorType) + .buildFilter(); + return new ObjectReferenceType() + .type(ConnectorType.COMPLEX_TYPE) + .filter(prismContext.getQueryConverter().createSearchFilterType(filter)); + } + + private ObjectReferenceType createAllConnectorsRef() throws Exception { + ObjectFilter filter = prismContext.queryFactory().createAll(); + return new ObjectReferenceType() + .type(ConnectorType.COMPLEX_TYPE) + .filter(prismContext.getQueryConverter().createSearchFilterType(filter)); + } + + private String getResourceExpectingConnectorResolutionFailure(String oid, Task task, OperationResult result) + throws Exception { + try { + provisioningService.getObject(ResourceType.class, oid, null, task, result); + result.computeStatus(); + assertPartialError(result); + return result.debugDump(); + } catch (ConfigurationException e) { + return e.getMessage(); + } + } + + private void assertAllConnectorsFilterIsAmbiguous(OperationResult result) throws Exception { + int connectorCount = repositoryService + .searchObjects( + ConnectorType.class, + prismContext.queryFactory().createQuery(prismContext.queryFactory().createAll()), + null, + result) + .size(); + assertTrue("Expected more than one connector for the ambiguous connectorRef filter, got " + connectorCount, + connectorCount > 1); + } + + private void assertTextContains(String text, String expectedText) { + assertTrue("Text does not contain '" + expectedText + "':\n" + text, text.contains(expectedText)); + } + + private void assertTextDoesNotContain(String text, String unexpectedText) { + assertFalse("Text contains '" + unexpectedText + "':\n" + text, text.contains(unexpectedText)); + } + private void assertScriptHost(ScriptCapabilityType capScript, ProvisioningScriptHostType expectedHostType) { for (ScriptCapabilityHostType host : capScript.getHost()) { if (host.getType() == expectedHostType) { diff --git a/release-notes.adoc b/release-notes.adoc index c606d71c93d..d069101f61b 100644 --- a/release-notes.adoc +++ b/release-notes.adoc @@ -102,6 +102,7 @@ Overall, midPoint 4.10 opens up the world of identity management and governance * Delineation suggestions: filter parsing broken after recent GUI change. See bug:MID-11175[] * Fixed work item search by name causing repository mapping error. See bug:MID-8834[] * Fixed translation of archetype display labels in assignment picker and summary panel. See bug:MID-11177[] +* Fixed additional connector failure when connectorRef is specified using a filter. See bug:MID-9537[] === Releases Of Other Components