Skip to content

Commit 35b1ad9

Browse files
authored
Merge pull request #588 from Evolveum/vaia/wp1/3673
Smart integration: SCIM connector development support
2 parents 7aae441 + f612c31 commit 35b1ad9

File tree

7 files changed

+199
-57
lines changed

7 files changed

+199
-57
lines changed

gui/admin-gui/src/main/java/com/evolveum/midpoint/gui/impl/page/admin/connector/development/component/wizard/scimrest/connection/BaseUrlConnectorStepPanel.java

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import com.evolveum.midpoint.xml.ns._public.common.common_3.*;
3535

3636
import org.apache.commons.lang3.StringUtils;
37+
import java.util.List;
3738
import org.apache.wicket.ajax.AjaxRequestTarget;
3839
import org.apache.wicket.behavior.AttributeAppender;
3940
import org.apache.wicket.markup.html.WebMarkupContainer;
@@ -51,7 +52,9 @@
5152
public class BaseUrlConnectorStepPanel extends AbstractFormWizardStepPanel<ConnectorDevelopmentDetailsModel> {
5253

5354
private static final String PANEL_TYPE = "cdw-base-url";
54-
public static final ItemName PROPERTY_ITEM_NAME = ItemName.from("", "baseAddress");
55+
public static final ItemName BASE_ADDRESS_ITEM_NAME = ItemName.from("", "baseAddress");
56+
public static final ItemName DEVELOPMENT_MODE_ITEM_NAME = ItemName.from("", "developmentMode");
57+
public static final ItemName SCIM_BASE_URL_ITEM_NAME = ItemName.from("", "scimBaseUrl");
5558

5659
private static final String ID_AI_ALERT = "aiAlert";
5760

@@ -83,12 +86,14 @@ protected IModel<? extends PrismContainerWrapper> getContainerFormModel() {
8386
protected void onInitialize() {
8487
super.onInitialize();
8588
try {
86-
PrismPropertyValueWrapper<Object> suggestedValue = getDetailsModel().getObjectWrapper().findProperty(
87-
ItemPath.create(ConnectorDevelopmentType.F_APPLICATION, ConnDevApplicationInfoType.F_BASE_API_ENDPOINT)).getValue();
88-
if (StringUtils.isNotEmpty((String) suggestedValue.getRealValue())) {
89-
PrismPropertyValueWrapper<String> configurationValue = (PrismPropertyValueWrapper<String>) getContainerFormModel().getObject().findProperty(PROPERTY_ITEM_NAME).getValue();
90-
if (StringUtils.isEmpty(configurationValue.getRealValue())) {
91-
configurationValue.setRealValue((String) suggestedValue.getRealValue());
89+
String suggestedUrl = (String) getDetailsModel().getObjectWrapper().findProperty(
90+
ItemPath.create(ConnectorDevelopmentType.F_APPLICATION, ConnDevApplicationInfoType.F_BASE_API_ENDPOINT))
91+
.getValue().getRealValue();
92+
if (StringUtils.isNotEmpty(suggestedUrl)) {
93+
ItemName urlField = isScim() ? SCIM_BASE_URL_ITEM_NAME : BASE_ADDRESS_ITEM_NAME;
94+
PrismPropertyValueWrapper<String> fieldValue = (PrismPropertyValueWrapper<String>) getContainerFormModel().getObject().findProperty(urlField).getValue();
95+
if (StringUtils.isEmpty(fieldValue.getRealValue())) {
96+
fieldValue.setRealValue(suggestedUrl);
9297
}
9398
}
9499
} catch (SchemaException e) {
@@ -186,22 +191,48 @@ protected IModel<?> getSubTextModel() {
186191
}
187192

188193
protected boolean checkMandatory(ItemWrapper wrapper) {
189-
if (QNameUtil.match(wrapper.getItemName(), PROPERTY_ITEM_NAME)) {
190-
return true;
194+
if (isScim()) {
195+
if (QNameUtil.match(wrapper.getItemName(), SCIM_BASE_URL_ITEM_NAME)) {
196+
return true;
197+
}
198+
} else {
199+
if (QNameUtil.match(wrapper.getItemName(), BASE_ADDRESS_ITEM_NAME)) {
200+
return true;
201+
}
191202
}
192203
return wrapper.isMandatory();
193204
}
194205

195206
@Override
196207
protected ItemVisibilityHandler getVisibilityHandler() {
197208
return wrapper -> {
198-
if (QNameUtil.match(wrapper.getItemName(), PROPERTY_ITEM_NAME)) {
199-
return ItemVisibility.AUTO;
209+
if (isScim()) {
210+
if (scimItemNames().stream().anyMatch(name -> QNameUtil.match(wrapper.getItemName(), name))) {
211+
return ItemVisibility.AUTO;
212+
}
213+
} else {
214+
if (QNameUtil.match(wrapper.getItemName(), BASE_ADDRESS_ITEM_NAME)) {
215+
return ItemVisibility.AUTO;
216+
}
200217
}
201218
return ItemVisibility.HIDDEN;
202219
};
203220
}
204221

222+
private List<ItemName> scimItemNames() {
223+
return List.of(SCIM_BASE_URL_ITEM_NAME, DEVELOPMENT_MODE_ITEM_NAME);
224+
}
225+
226+
private boolean isScim() {
227+
try {
228+
PrismPropertyWrapper<ConnDevIntegrationType> integrationType = getDetailsModel().getObjectWrapper().findProperty(
229+
ItemPath.create(ConnectorDevelopmentType.F_CONNECTOR, ConnDevConnectorType.F_INTEGRATION_TYPE));
230+
return ConnDevIntegrationType.SCIM.equals(integrationType.getValue().getRealValue());
231+
} catch (SchemaException e) {
232+
return false;
233+
}
234+
}
235+
205236
@Override
206237
public String getStepId() {
207238
return PANEL_TYPE;
@@ -236,7 +267,8 @@ public boolean onNextPerformed(AjaxRequestTarget target) {
236267

237268
@Override
238269
public boolean isCompleted() {
270+
ItemName fieldToCheck = isScim() ? SCIM_BASE_URL_ITEM_NAME : BASE_ADDRESS_ITEM_NAME;
239271
return ConnectorDevelopmentWizardUtil.existTestingResourcePropertyValue(
240-
getDetailsModel(), getPanelType(), PROPERTY_ITEM_NAME);
272+
getDetailsModel(), getPanelType(), fieldToCheck);
241273
}
242274
}

gui/admin-gui/src/main/java/com/evolveum/midpoint/gui/impl/page/admin/connector/development/component/wizard/summary/ConnectorDevelopmentWizardSummaryPanel.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ protected StringValuesWidgetDetailsDto load() {
255255
values.put(
256256
createStringResource("ConnectorDevelopmentWizardSummaryPanel.baseUrl"),
257257
defineValueModel((String) ConnectorDevelopmentWizardUtil.getTestingResourcePropertyValue(
258-
detailsModel, null, BaseUrlConnectorStepPanel.PROPERTY_ITEM_NAME)));
258+
detailsModel, null, BaseUrlConnectorStepPanel.BASE_ADDRESS_ITEM_NAME)));
259259
values.put(
260260
createStringResource("ConnectorDevelopmentWizardSummaryPanel.testEndpoint"),
261261
defineValueModel((String) ConnectorDevelopmentWizardUtil.getTestingResourcePropertyValue(

model/smart-api/src/main/java/com/evolveum/midpoint/smart/api/conndev/ScimRestConfigurationProperties.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public class ScimRestConfigurationProperties {
88
public static final ItemName REST_TOKEN_NAME = new ItemName("restTokenName");
99
public static final ItemName REST_TOKEN_VALUE = new ItemName("restTokenValue");
1010
public static final ItemName BASE_ADDRESS = new ItemName("baseAddress");
11+
public static final ItemName DEVELOPMENT_MODE = new ItemName("developmentMode");
1112
public static final ItemName TRUST_ALL_CERTIFICATES = new ItemName("trustAllCertificates");
1213
public static final ItemName SCIM_BEARER_TOKEN = new ItemName("scimBearerToken");
1314
public static final ItemName SCIM_BASE_URL = new ItemName("scimBaseUrl");

model/smart-impl/src/main/java/com/evolveum/midpoint/smart/impl/conndev/ConnectorDevelopmentBackend.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public static ConnectorDevelopmentBackend backendFor(String connectorDevelopment
7777
private static ConnectorDevelopmentBackend backendFor(ConnDevIntegrationType integrationType, ConnectorDevelopmentType connDev, ConnDevBeans beans, Task task, OperationResult result) {
7878
return switch (integrationType) {
7979
case REST -> new RestBackend(beans, connDev, task, result);
80-
case SCIM -> new JsonHalBackend(beans, connDev, task, result);
80+
case SCIM -> new ScimBackend(beans, connDev, task, result);
8181
case DUMMY -> new OfflineBackend(beans, connDev, task, result);
8282
};
8383

model/smart-impl/src/main/java/com/evolveum/midpoint/smart/impl/conndev/JsonHalBackend.java

Lines changed: 0 additions & 41 deletions
This file was deleted.

model/smart-impl/src/main/java/com/evolveum/midpoint/smart/impl/conndev/RestBackend.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
public class RestBackend extends ConnectorDevelopmentBackend {
2929

3030
private static final long SLEEP_TIME = 5 * 1000L;
31-
private static final JsonNodeFactory JSON_FACTORY = JsonNodeFactory.instance;
31+
protected static final JsonNodeFactory JSON_FACTORY = JsonNodeFactory.instance;
3232

3333

3434
private static final Trace LOGGER = TraceManager.getTrace(ConnectorDevelopmentBackend.class);
@@ -249,7 +249,7 @@ private String sessionId() {
249249
return developmentObject().getOid();
250250
}
251251

252-
private ServiceClient client() {
252+
protected ServiceClient client() {
253253
return beans.client(sessionId(), this::restoreSession, this::synchronizeSession, result);
254254
}
255255

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package com.evolveum.midpoint.smart.impl.conndev;
2+
3+
import com.evolveum.midpoint.model.api.util.ResourceUtils;
4+
import com.evolveum.midpoint.prism.PrismContext;
5+
import com.evolveum.midpoint.prism.path.ItemPath;
6+
import com.evolveum.midpoint.schema.constants.SchemaConstants;
7+
import com.evolveum.midpoint.schema.result.OperationResult;
8+
import com.evolveum.midpoint.smart.api.conndev.ScimRestConfigurationProperties;
9+
import com.evolveum.midpoint.util.logging.Trace;
10+
import com.evolveum.midpoint.util.logging.TraceManager;
11+
import com.evolveum.midpoint.smart.impl.conndev.activity.ConnDevBeans;
12+
import com.evolveum.midpoint.task.api.Task;
13+
import com.evolveum.midpoint.util.exception.*;
14+
import com.evolveum.midpoint.xml.ns._public.common.common_3.ConnDevDocumentationSourceType;
15+
import com.fasterxml.jackson.databind.ObjectMapper;
16+
import com.evolveum.midpoint.xml.ns._public.common.common_3.ConnectorDevelopmentType;
17+
import com.evolveum.midpoint.xml.ns._public.common.common_3.ProcessedDocumentationType;
18+
import com.evolveum.midpoint.xml.ns._public.common.common_3.ResourceType;
19+
import com.evolveum.midpoint.xml.ns._public.common.common_3.ShadowType;
20+
21+
import javax.xml.namespace.QName;
22+
import java.util.ArrayList;
23+
import java.util.List;
24+
import java.util.Objects;
25+
import java.util.UUID;
26+
import java.util.stream.Stream;
27+
28+
public class ScimBackend extends RestBackend {
29+
30+
private static final Trace LOGGER = TraceManager.getTrace(ScimBackend.class);
31+
32+
private static final List<String> SCIM_OBJECT_CLASSES = List.of("conndev_ScimSchema", "conndev_ScimResource");
33+
34+
public ScimBackend(ConnDevBeans beans, ConnectorDevelopmentType connDev, Task task, OperationResult result) {
35+
super(beans, connDev, task, result);
36+
}
37+
38+
@Override
39+
public List<ConnDevDocumentationSourceType> discoverDocumentation() {
40+
return super.discoverDocumentation();
41+
}
42+
43+
@Override
44+
public void ensureDocumentationIsProcessed() throws SchemaException, ExpressionEvaluationException, CommunicationException, SecurityViolationException, ConfigurationException, ObjectNotFoundException, PolicyViolationException, ObjectAlreadyExistsException {
45+
super.ensureDocumentationIsProcessed();
46+
47+
refreshScimDocumentation();
48+
}
49+
50+
private void refreshScimDocumentation() throws SchemaException, ExpressionEvaluationException, CommunicationException, SecurityViolationException, ConfigurationException, ObjectNotFoundException, PolicyViolationException, ObjectAlreadyExistsException {
51+
var testingResourceOid = getTestingResourceOid();
52+
53+
if (!isScimDiscoveryConfigured(testingResourceOid)) {
54+
LOGGER.info("Testing resource {} is not configured for SCIM discovery (missing scimBaseUrl or developmentMode), skipping", testingResourceOid);
55+
return;
56+
}
57+
58+
LOGGER.info("Starting SCIM schema discovery from shadows on testing resource {}", testingResourceOid);
59+
ResourceUtils.deleteSchema(testingResourceOid, beans.modelService, task, result);
60+
beans.provisioningService.testResource(testingResourceOid, task, result);
61+
62+
var newScimDocs = SCIM_OBJECT_CLASSES
63+
.stream()
64+
.flatMap(objectClass -> loadShadowsAsDocumentation(testingResourceOid, objectClass).stream())
65+
.map(ProcessedDocumentation::toBean)
66+
.toList();
67+
68+
var mergedDocs = new ArrayList<>(
69+
developmentObject()
70+
.getProcessedDocumentation()
71+
.stream()
72+
.filter(d -> SCIM_OBJECT_CLASSES.stream().noneMatch(c -> d.getUri().startsWith(c)))
73+
.map(ProcessedDocumentationType::clone)
74+
.toList()
75+
);
76+
mergedDocs.addAll(newScimDocs);
77+
78+
var delta = PrismContext.get().deltaFor(ConnectorDevelopmentType.class)
79+
.item(ConnectorDevelopmentType.F_PROCESSED_DOCUMENTATION)
80+
.replaceRealValues(mergedDocs)
81+
.<ConnectorDevelopmentType>asObjectDelta(developmentObject().getOid());
82+
beans.modelService.executeChanges(List.of(delta), null, task, result);
83+
reload();
84+
ensureDocumentationIsUploaded(client().synchronizationClient());
85+
}
86+
87+
private List<ProcessedDocumentation> loadShadowsAsDocumentation(String resourceOid, String objectClassLocalName) {
88+
try {
89+
var objectClass = new QName(SchemaConstants.NS_RI, objectClassLocalName);
90+
var query = PrismContext.get().queryFor(ShadowType.class)
91+
.item(ShadowType.F_RESOURCE_REF).ref(resourceOid)
92+
.and().item(ShadowType.F_OBJECT_CLASS).eq(objectClass)
93+
.build();
94+
95+
var shadows = beans.provisioningService.searchObjects(ShadowType.class, query, null, task, result);
96+
if (shadows.isEmpty()) {
97+
LOGGER.warn("No shadows found for object class {} on resource {}", objectClassLocalName, resourceOid);
98+
return List.of();
99+
}
100+
101+
var serializer = PrismContext.get().jsonSerializer();
102+
var mapper = new ObjectMapper();
103+
var docs = new ArrayList<ProcessedDocumentation>();
104+
for (var shadow : shadows) {
105+
var attrs = shadow.findContainer(ShadowType.F_ATTRIBUTES);
106+
if (attrs != null && !attrs.isEmpty()) {
107+
var json = serializer.serialize(attrs.getValue());
108+
var parsedAttrs = mapper.readTree(json).get("attributes");
109+
var idNode = parsedAttrs.get("id");
110+
var name = idNode != null
111+
? idNode.asText()
112+
: shadow.getOid();
113+
var doc = new ProcessedDocumentation(UUID.randomUUID().toString(), objectClassLocalName + "_" + name);
114+
doc.write(parsedAttrs.toString());
115+
docs.add(doc);
116+
}
117+
}
118+
return docs;
119+
} catch (Exception e) {
120+
throw new SystemException("Could not load shadow documentation for " + objectClassLocalName, e);
121+
}
122+
}
123+
124+
private String getTestingResourceOid() {
125+
var testing = developmentObject().getTesting();
126+
if (testing == null || testing.getTestingResource() == null) {
127+
throw new SystemException("No testing resource configured");
128+
}
129+
return testing.getTestingResource().getOid();
130+
}
131+
132+
private boolean isScimDiscoveryConfigured(String testingResourceOid) {
133+
try {
134+
var resource = beans.modelService.getObject(ResourceType.class, testingResourceOid, null, task, result).asObjectable();
135+
var connectorConfig = ItemPath.create(ResourceType.F_CONNECTOR_CONFIGURATION, SchemaConstants.ICF_CONFIGURATION_PROPERTIES_LOCAL_NAME);
136+
137+
var scimBaseUrl = resource.asPrismObject().findProperty(ItemPath.create(connectorConfig, ScimRestConfigurationProperties.SCIM_BASE_URL));
138+
var developmentMode = resource.asPrismObject().findProperty(ItemPath.create(connectorConfig, ScimRestConfigurationProperties.DEVELOPMENT_MODE));
139+
140+
var hasScimBaseUrl = scimBaseUrl != null && scimBaseUrl.getRealValue() != null
141+
&& !((String) scimBaseUrl.getRealValue()).isBlank();
142+
var hasDevelopmentMode = developmentMode != null && Boolean.TRUE.equals(developmentMode.getRealValue());
143+
144+
return hasScimBaseUrl && hasDevelopmentMode;
145+
} catch (Exception e) {
146+
LOGGER.warn("Could not check configuration on testing resource {}: {}", testingResourceOid, e.getMessage());
147+
return false;
148+
}
149+
}
150+
}

0 commit comments

Comments
 (0)