diff --git a/gui/admin-gui/src/main/java/com/evolveum/midpoint/gui/impl/page/admin/connector/development/component/wizard/scimrest/connection/BaseUrlConnectorStepPanel.java b/gui/admin-gui/src/main/java/com/evolveum/midpoint/gui/impl/page/admin/connector/development/component/wizard/scimrest/connection/BaseUrlConnectorStepPanel.java index 5f013f09641..9fd1daa41c1 100644 --- a/gui/admin-gui/src/main/java/com/evolveum/midpoint/gui/impl/page/admin/connector/development/component/wizard/scimrest/connection/BaseUrlConnectorStepPanel.java +++ b/gui/admin-gui/src/main/java/com/evolveum/midpoint/gui/impl/page/admin/connector/development/component/wizard/scimrest/connection/BaseUrlConnectorStepPanel.java @@ -34,6 +34,7 @@ import com.evolveum.midpoint.xml.ns._public.common.common_3.*; import org.apache.commons.lang3.StringUtils; +import java.util.List; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.behavior.AttributeAppender; import org.apache.wicket.markup.html.WebMarkupContainer; @@ -51,7 +52,9 @@ public class BaseUrlConnectorStepPanel extends AbstractFormWizardStepPanel { private static final String PANEL_TYPE = "cdw-base-url"; - public static final ItemName PROPERTY_ITEM_NAME = ItemName.from("", "baseAddress"); + public static final ItemName BASE_ADDRESS_ITEM_NAME = ItemName.from("", "baseAddress"); + public static final ItemName DEVELOPMENT_MODE_ITEM_NAME = ItemName.from("", "developmentMode"); + public static final ItemName SCIM_BASE_URL_ITEM_NAME = ItemName.from("", "scimBaseUrl"); private static final String ID_AI_ALERT = "aiAlert"; @@ -83,12 +86,14 @@ protected IModel getContainerFormModel() { protected void onInitialize() { super.onInitialize(); try { - PrismPropertyValueWrapper suggestedValue = getDetailsModel().getObjectWrapper().findProperty( - ItemPath.create(ConnectorDevelopmentType.F_APPLICATION, ConnDevApplicationInfoType.F_BASE_API_ENDPOINT)).getValue(); - if (StringUtils.isNotEmpty((String) suggestedValue.getRealValue())) { - PrismPropertyValueWrapper configurationValue = (PrismPropertyValueWrapper) getContainerFormModel().getObject().findProperty(PROPERTY_ITEM_NAME).getValue(); - if (StringUtils.isEmpty(configurationValue.getRealValue())) { - configurationValue.setRealValue((String) suggestedValue.getRealValue()); + String suggestedUrl = (String) getDetailsModel().getObjectWrapper().findProperty( + ItemPath.create(ConnectorDevelopmentType.F_APPLICATION, ConnDevApplicationInfoType.F_BASE_API_ENDPOINT)) + .getValue().getRealValue(); + if (StringUtils.isNotEmpty(suggestedUrl)) { + ItemName urlField = isScim() ? SCIM_BASE_URL_ITEM_NAME : BASE_ADDRESS_ITEM_NAME; + PrismPropertyValueWrapper fieldValue = (PrismPropertyValueWrapper) getContainerFormModel().getObject().findProperty(urlField).getValue(); + if (StringUtils.isEmpty(fieldValue.getRealValue())) { + fieldValue.setRealValue(suggestedUrl); } } } catch (SchemaException e) { @@ -186,8 +191,14 @@ protected IModel getSubTextModel() { } protected boolean checkMandatory(ItemWrapper wrapper) { - if (QNameUtil.match(wrapper.getItemName(), PROPERTY_ITEM_NAME)) { - return true; + if (isScim()) { + if (QNameUtil.match(wrapper.getItemName(), SCIM_BASE_URL_ITEM_NAME)) { + return true; + } + } else { + if (QNameUtil.match(wrapper.getItemName(), BASE_ADDRESS_ITEM_NAME)) { + return true; + } } return wrapper.isMandatory(); } @@ -195,13 +206,33 @@ protected boolean checkMandatory(ItemWrapper wrapper) { @Override protected ItemVisibilityHandler getVisibilityHandler() { return wrapper -> { - if (QNameUtil.match(wrapper.getItemName(), PROPERTY_ITEM_NAME)) { - return ItemVisibility.AUTO; + if (isScim()) { + if (scimItemNames().stream().anyMatch(name -> QNameUtil.match(wrapper.getItemName(), name))) { + return ItemVisibility.AUTO; + } + } else { + if (QNameUtil.match(wrapper.getItemName(), BASE_ADDRESS_ITEM_NAME)) { + return ItemVisibility.AUTO; + } } return ItemVisibility.HIDDEN; }; } + private List scimItemNames() { + return List.of(SCIM_BASE_URL_ITEM_NAME, DEVELOPMENT_MODE_ITEM_NAME); + } + + private boolean isScim() { + try { + PrismPropertyWrapper integrationType = getDetailsModel().getObjectWrapper().findProperty( + ItemPath.create(ConnectorDevelopmentType.F_CONNECTOR, ConnDevConnectorType.F_INTEGRATION_TYPE)); + return ConnDevIntegrationType.SCIM.equals(integrationType.getValue().getRealValue()); + } catch (SchemaException e) { + return false; + } + } + @Override public String getStepId() { return PANEL_TYPE; @@ -236,7 +267,8 @@ public boolean onNextPerformed(AjaxRequestTarget target) { @Override public boolean isCompleted() { + ItemName fieldToCheck = isScim() ? SCIM_BASE_URL_ITEM_NAME : BASE_ADDRESS_ITEM_NAME; return ConnectorDevelopmentWizardUtil.existTestingResourcePropertyValue( - getDetailsModel(), getPanelType(), PROPERTY_ITEM_NAME); + getDetailsModel(), getPanelType(), fieldToCheck); } } diff --git a/gui/admin-gui/src/main/java/com/evolveum/midpoint/gui/impl/page/admin/connector/development/component/wizard/summary/ConnectorDevelopmentWizardSummaryPanel.java b/gui/admin-gui/src/main/java/com/evolveum/midpoint/gui/impl/page/admin/connector/development/component/wizard/summary/ConnectorDevelopmentWizardSummaryPanel.java index 21b0cc2ff35..0a747c29613 100644 --- a/gui/admin-gui/src/main/java/com/evolveum/midpoint/gui/impl/page/admin/connector/development/component/wizard/summary/ConnectorDevelopmentWizardSummaryPanel.java +++ b/gui/admin-gui/src/main/java/com/evolveum/midpoint/gui/impl/page/admin/connector/development/component/wizard/summary/ConnectorDevelopmentWizardSummaryPanel.java @@ -255,7 +255,7 @@ protected StringValuesWidgetDetailsDto load() { values.put( createStringResource("ConnectorDevelopmentWizardSummaryPanel.baseUrl"), defineValueModel((String) ConnectorDevelopmentWizardUtil.getTestingResourcePropertyValue( - detailsModel, null, BaseUrlConnectorStepPanel.PROPERTY_ITEM_NAME))); + detailsModel, null, BaseUrlConnectorStepPanel.BASE_ADDRESS_ITEM_NAME))); values.put( createStringResource("ConnectorDevelopmentWizardSummaryPanel.testEndpoint"), defineValueModel((String) ConnectorDevelopmentWizardUtil.getTestingResourcePropertyValue( diff --git a/model/smart-api/src/main/java/com/evolveum/midpoint/smart/api/conndev/ScimRestConfigurationProperties.java b/model/smart-api/src/main/java/com/evolveum/midpoint/smart/api/conndev/ScimRestConfigurationProperties.java index 139908b6d15..b8b113955b6 100644 --- a/model/smart-api/src/main/java/com/evolveum/midpoint/smart/api/conndev/ScimRestConfigurationProperties.java +++ b/model/smart-api/src/main/java/com/evolveum/midpoint/smart/api/conndev/ScimRestConfigurationProperties.java @@ -8,6 +8,7 @@ public class ScimRestConfigurationProperties { public static final ItemName REST_TOKEN_NAME = new ItemName("restTokenName"); public static final ItemName REST_TOKEN_VALUE = new ItemName("restTokenValue"); public static final ItemName BASE_ADDRESS = new ItemName("baseAddress"); + public static final ItemName DEVELOPMENT_MODE = new ItemName("developmentMode"); public static final ItemName TRUST_ALL_CERTIFICATES = new ItemName("trustAllCertificates"); public static final ItemName SCIM_BEARER_TOKEN = new ItemName("scimBearerToken"); public static final ItemName SCIM_BASE_URL = new ItemName("scimBaseUrl"); diff --git a/model/smart-impl/src/main/java/com/evolveum/midpoint/smart/impl/conndev/ConnectorDevelopmentBackend.java b/model/smart-impl/src/main/java/com/evolveum/midpoint/smart/impl/conndev/ConnectorDevelopmentBackend.java index 353935170ca..1a1a51b1e3d 100644 --- a/model/smart-impl/src/main/java/com/evolveum/midpoint/smart/impl/conndev/ConnectorDevelopmentBackend.java +++ b/model/smart-impl/src/main/java/com/evolveum/midpoint/smart/impl/conndev/ConnectorDevelopmentBackend.java @@ -77,7 +77,7 @@ public static ConnectorDevelopmentBackend backendFor(String connectorDevelopment private static ConnectorDevelopmentBackend backendFor(ConnDevIntegrationType integrationType, ConnectorDevelopmentType connDev, ConnDevBeans beans, Task task, OperationResult result) { return switch (integrationType) { case REST -> new RestBackend(beans, connDev, task, result); - case SCIM -> new JsonHalBackend(beans, connDev, task, result); + case SCIM -> new ScimBackend(beans, connDev, task, result); case DUMMY -> new OfflineBackend(beans, connDev, task, result); }; diff --git a/model/smart-impl/src/main/java/com/evolveum/midpoint/smart/impl/conndev/JsonHalBackend.java b/model/smart-impl/src/main/java/com/evolveum/midpoint/smart/impl/conndev/JsonHalBackend.java deleted file mode 100644 index e9fca7e0908..00000000000 --- a/model/smart-impl/src/main/java/com/evolveum/midpoint/smart/impl/conndev/JsonHalBackend.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.evolveum.midpoint.smart.impl.conndev; - -import com.evolveum.midpoint.prism.PrismContext; -import com.evolveum.midpoint.schema.result.OperationResult; -import com.evolveum.midpoint.smart.impl.conndev.activity.ConnDevBeans; -import com.evolveum.midpoint.task.api.Task; -import com.evolveum.midpoint.util.exception.*; -import com.evolveum.midpoint.xml.ns._public.common.common_3.ConnDevDocumentationSourceType; -import com.evolveum.midpoint.xml.ns._public.common.common_3.ConnectorDevelopmentType; - -import java.util.ArrayList; -import java.util.List; - -public class JsonHalBackend extends RestBackend { - - private static final ConnDevDocumentationSourceType OPENAPI = new ConnDevDocumentationSourceType() - .name("OpenProject OpenAPI specificaiton") - .description("OpenAPI specification") - .uri("file:///home/evolveum/vaia/vaia-foundry/eval-data/wp1/openproject/docs/spec.yml"); - - public JsonHalBackend(ConnDevBeans beans, ConnectorDevelopmentType connDev, Task task, OperationResult result) { - super(beans, connDev, task, result); - } - - @Override - public List discoverDocumentation() { - return super.discoverDocumentation(); - } - - @Override - public void processDocumentation() throws SchemaException, ExpressionEvaluationException, CommunicationException, SecurityViolationException, ConfigurationException, ObjectNotFoundException, PolicyViolationException, ObjectAlreadyExistsException { - var documentations = new ArrayList(); - documentations.add(downloadAndCache(OPENAPI)); - - var delta = PrismContext.get().deltaFor(ConnectorDevelopmentType.class) - .item(ConnectorDevelopmentType.F_PROCESSED_DOCUMENTATION) - .addRealValues(documentations.stream().map(ProcessedDocumentation::toBean).toList()) - .asObjectDelta(developmentObject().getOid()); - beans.modelService.executeChanges(List.of(delta), null, task, result); - } -} diff --git a/model/smart-impl/src/main/java/com/evolveum/midpoint/smart/impl/conndev/RestBackend.java b/model/smart-impl/src/main/java/com/evolveum/midpoint/smart/impl/conndev/RestBackend.java index c163576ff82..3bea3d8dbd4 100644 --- a/model/smart-impl/src/main/java/com/evolveum/midpoint/smart/impl/conndev/RestBackend.java +++ b/model/smart-impl/src/main/java/com/evolveum/midpoint/smart/impl/conndev/RestBackend.java @@ -28,7 +28,7 @@ public class RestBackend extends ConnectorDevelopmentBackend { private static final long SLEEP_TIME = 5 * 1000L; - private static final JsonNodeFactory JSON_FACTORY = JsonNodeFactory.instance; + protected static final JsonNodeFactory JSON_FACTORY = JsonNodeFactory.instance; private static final Trace LOGGER = TraceManager.getTrace(ConnectorDevelopmentBackend.class); @@ -249,7 +249,7 @@ private String sessionId() { return developmentObject().getOid(); } - private ServiceClient client() { + protected ServiceClient client() { return beans.client(sessionId(), this::restoreSession, this::synchronizeSession, result); } diff --git a/model/smart-impl/src/main/java/com/evolveum/midpoint/smart/impl/conndev/ScimBackend.java b/model/smart-impl/src/main/java/com/evolveum/midpoint/smart/impl/conndev/ScimBackend.java new file mode 100644 index 00000000000..5e8bad90464 --- /dev/null +++ b/model/smart-impl/src/main/java/com/evolveum/midpoint/smart/impl/conndev/ScimBackend.java @@ -0,0 +1,150 @@ +package com.evolveum.midpoint.smart.impl.conndev; + +import com.evolveum.midpoint.model.api.util.ResourceUtils; +import com.evolveum.midpoint.prism.PrismContext; +import com.evolveum.midpoint.prism.path.ItemPath; +import com.evolveum.midpoint.schema.constants.SchemaConstants; +import com.evolveum.midpoint.schema.result.OperationResult; +import com.evolveum.midpoint.smart.api.conndev.ScimRestConfigurationProperties; +import com.evolveum.midpoint.util.logging.Trace; +import com.evolveum.midpoint.util.logging.TraceManager; +import com.evolveum.midpoint.smart.impl.conndev.activity.ConnDevBeans; +import com.evolveum.midpoint.task.api.Task; +import com.evolveum.midpoint.util.exception.*; +import com.evolveum.midpoint.xml.ns._public.common.common_3.ConnDevDocumentationSourceType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.evolveum.midpoint.xml.ns._public.common.common_3.ConnectorDevelopmentType; +import com.evolveum.midpoint.xml.ns._public.common.common_3.ProcessedDocumentationType; +import com.evolveum.midpoint.xml.ns._public.common.common_3.ResourceType; +import com.evolveum.midpoint.xml.ns._public.common.common_3.ShadowType; + +import javax.xml.namespace.QName; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Stream; + +public class ScimBackend extends RestBackend { + + private static final Trace LOGGER = TraceManager.getTrace(ScimBackend.class); + + private static final List SCIM_OBJECT_CLASSES = List.of("conndev_ScimSchema", "conndev_ScimResource"); + + public ScimBackend(ConnDevBeans beans, ConnectorDevelopmentType connDev, Task task, OperationResult result) { + super(beans, connDev, task, result); + } + + @Override + public List discoverDocumentation() { + return super.discoverDocumentation(); + } + + @Override + public void ensureDocumentationIsProcessed() throws SchemaException, ExpressionEvaluationException, CommunicationException, SecurityViolationException, ConfigurationException, ObjectNotFoundException, PolicyViolationException, ObjectAlreadyExistsException { + super.ensureDocumentationIsProcessed(); + + refreshScimDocumentation(); + } + + private void refreshScimDocumentation() throws SchemaException, ExpressionEvaluationException, CommunicationException, SecurityViolationException, ConfigurationException, ObjectNotFoundException, PolicyViolationException, ObjectAlreadyExistsException { + var testingResourceOid = getTestingResourceOid(); + + if (!isScimDiscoveryConfigured(testingResourceOid)) { + LOGGER.info("Testing resource {} is not configured for SCIM discovery (missing scimBaseUrl or developmentMode), skipping", testingResourceOid); + return; + } + + LOGGER.info("Starting SCIM schema discovery from shadows on testing resource {}", testingResourceOid); + ResourceUtils.deleteSchema(testingResourceOid, beans.modelService, task, result); + beans.provisioningService.testResource(testingResourceOid, task, result); + + var newScimDocs = SCIM_OBJECT_CLASSES + .stream() + .flatMap(objectClass -> loadShadowsAsDocumentation(testingResourceOid, objectClass).stream()) + .map(ProcessedDocumentation::toBean) + .toList(); + + var mergedDocs = new ArrayList<>( + developmentObject() + .getProcessedDocumentation() + .stream() + .filter(d -> SCIM_OBJECT_CLASSES.stream().noneMatch(c -> d.getUri().startsWith(c))) + .map(ProcessedDocumentationType::clone) + .toList() + ); + mergedDocs.addAll(newScimDocs); + + var delta = PrismContext.get().deltaFor(ConnectorDevelopmentType.class) + .item(ConnectorDevelopmentType.F_PROCESSED_DOCUMENTATION) + .replaceRealValues(mergedDocs) + .asObjectDelta(developmentObject().getOid()); + beans.modelService.executeChanges(List.of(delta), null, task, result); + reload(); + ensureDocumentationIsUploaded(client().synchronizationClient()); + } + + private List loadShadowsAsDocumentation(String resourceOid, String objectClassLocalName) { + try { + var objectClass = new QName(SchemaConstants.NS_RI, objectClassLocalName); + var query = PrismContext.get().queryFor(ShadowType.class) + .item(ShadowType.F_RESOURCE_REF).ref(resourceOid) + .and().item(ShadowType.F_OBJECT_CLASS).eq(objectClass) + .build(); + + var shadows = beans.provisioningService.searchObjects(ShadowType.class, query, null, task, result); + if (shadows.isEmpty()) { + LOGGER.warn("No shadows found for object class {} on resource {}", objectClassLocalName, resourceOid); + return List.of(); + } + + var serializer = PrismContext.get().jsonSerializer(); + var mapper = new ObjectMapper(); + var docs = new ArrayList(); + for (var shadow : shadows) { + var attrs = shadow.findContainer(ShadowType.F_ATTRIBUTES); + if (attrs != null && !attrs.isEmpty()) { + var json = serializer.serialize(attrs.getValue()); + var parsedAttrs = mapper.readTree(json).get("attributes"); + var idNode = parsedAttrs.get("id"); + var name = idNode != null + ? idNode.asText() + : shadow.getOid(); + var doc = new ProcessedDocumentation(UUID.randomUUID().toString(), objectClassLocalName + "_" + name); + doc.write(parsedAttrs.toString()); + docs.add(doc); + } + } + return docs; + } catch (Exception e) { + throw new SystemException("Could not load shadow documentation for " + objectClassLocalName, e); + } + } + + private String getTestingResourceOid() { + var testing = developmentObject().getTesting(); + if (testing == null || testing.getTestingResource() == null) { + throw new SystemException("No testing resource configured"); + } + return testing.getTestingResource().getOid(); + } + + private boolean isScimDiscoveryConfigured(String testingResourceOid) { + try { + var resource = beans.modelService.getObject(ResourceType.class, testingResourceOid, null, task, result).asObjectable(); + var connectorConfig = ItemPath.create(ResourceType.F_CONNECTOR_CONFIGURATION, SchemaConstants.ICF_CONFIGURATION_PROPERTIES_LOCAL_NAME); + + var scimBaseUrl = resource.asPrismObject().findProperty(ItemPath.create(connectorConfig, ScimRestConfigurationProperties.SCIM_BASE_URL)); + var developmentMode = resource.asPrismObject().findProperty(ItemPath.create(connectorConfig, ScimRestConfigurationProperties.DEVELOPMENT_MODE)); + + var hasScimBaseUrl = scimBaseUrl != null && scimBaseUrl.getRealValue() != null + && !((String) scimBaseUrl.getRealValue()).isBlank(); + var hasDevelopmentMode = developmentMode != null && Boolean.TRUE.equals(developmentMode.getRealValue()); + + return hasScimBaseUrl && hasDevelopmentMode; + } catch (Exception e) { + LOGGER.warn("Could not check configuration on testing resource {}: {}", testingResourceOid, e.getMessage()); + return false; + } + } +}