Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@
<groupId>org.wso2.carbon.identity.framework</groupId>
<artifactId>org.wso2.carbon.identity.application.authentication.framework</artifactId>
</dependency>
<dependency>
<groupId>org.wso2.carbon.identity.framework</groupId>
<artifactId>org.wso2.carbon.identity.central.log.mgt</artifactId>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
Expand Down Expand Up @@ -173,6 +177,8 @@
version="${carbon.identity.package.import.version.range}",
org.wso2.carbon.identity.application.authentication.framework.util;
version="${carbon.identity.package.import.version.range}",
org.wso2.carbon.identity.central.log.mgt.utils;
version="${carbon.identity.package.import.version.range}",
</Import-Package>
<Export-Package>
!org.wso2.carbon.identity.provisioning.internal,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,24 +204,17 @@ private Map<String, RuntimeProvisioningConfig> getOutboundProvisioningConnectors
String connectorType = fIdP.getDefaultProvisioningConnectorConfig()
.getName();

boolean enableJitProvisioning = false;
boolean enableJitProvisioning = fIdP.getJustInTimeProvisioningConfig() != null
&& fIdP.getJustInTimeProvisioningConfig().isProvisioningEnabled();

if (fIdP.getJustInTimeProvisioningConfig() != null
&& fIdP.getJustInTimeProvisioningConfig().isProvisioningEnabled()) {
enableJitProvisioning = true;
}

connector = getOutboundProvisioningConnector(fIdP,
registeredConnectorFactories, tenantDomain,
enableJitProvisioning);
connector = getOutboundProvisioningConnector(fIdP, registeredConnectorFactories, tenantDomain);
// add to the provisioning connectors list. there will be one item for each
// provisioning identity provider found in the out-bound provisioning
// configuration of the local service provider.
if (connector != null) {
RuntimeProvisioningConfig proConfig = new RuntimeProvisioningConfig();
proConfig
.setProvisioningConnectorEntry(new SimpleEntry<>(
connectorType, connector));
proConfig.setJitProvisioningEnabled(enableJitProvisioning);
proConfig.setProvisioningConnectorEntry(new SimpleEntry<>(connectorType, connector));
Comment on lines +207 to +217
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

JIT enablement is derived from a potentially stale IdP object.

enableJitProvisioning is computed from fIdP before the enabled IdP is re-resolved in getOutboundProvisioningConnector(...). This can propagate outdated JIT state into RuntimeProvisioningConfig and cause incorrect per-IdP behavior.

Suggested direction
- boolean enableJitProvisioning = fIdP.getJustInTimeProvisioningConfig() != null
-         && fIdP.getJustInTimeProvisioningConfig().isProvisioningEnabled();
- connector = getOutboundProvisioningConnector(fIdP, registeredConnectorFactories, tenantDomain);
+ connector = getOutboundProvisioningConnector(fIdP, registeredConnectorFactories, tenantDomain);
+ IdentityProvider effectiveIdP = IdentityProviderManager.getInstance()
+         .getEnabledIdPByName(fIdP.getIdentityProviderName(), tenantDomain);
+ boolean enableJitProvisioning = effectiveIdP != null
+         && effectiveIdP.getJustInTimeProvisioningConfig() != null
+         && effectiveIdP.getJustInTimeProvisioningConfig().isProvisioningEnabled();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@components/provisioning/org.wso2.carbon.identity.provisioning/src/main/java/org/wso2/carbon/identity/provisioning/OutboundProvisioningManager.java`
around lines 207 - 217, The JIT flag is read from the potentially stale fIdP
before re-resolving the IdP in getOutboundProvisioningConnector, which can
propagate outdated state into RuntimeProvisioningConfig; fix by computing
enableJitProvisioning after obtaining the resolved IdP (i.e., move or
re-evaluate fIdP.getJustInTimeProvisioningConfig().isProvisioningEnabled() once
getOutboundProvisioningConnector(...) returns the up-to-date connector/IdP) and
then set proConfig.setJitProvisioningEnabled(...) using that fresh value so
RuntimeProvisioningConfig receives the correct per-IdP JIT state (refer to fIdP,
getOutboundProvisioningConnector, RuntimeProvisioningConfig, and proConfig).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional. At this point, the fIdP is created using the application's configured outbound JIT provisioning settings. getOutboundProvisioningConnector fetches an IdP-level JIT enablement check, which is not relevant in the context of outbound provisioning.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PasinduYeshan, thank you for the clarification! That makes perfect sense—the application's outbound provisioning configuration should take precedence for JIT settings in the outbound flow, rather than the IdP's general JIT configuration. I appreciate you explaining the architectural intent here.

proConfig.setBlocking(defaultConnector.isBlocking());
proConfig.setPolicyEnabled(defaultConnector.isRulesEnabled());
connectors.put(fIdP.getIdentityProviderName(), proConfig);
Expand Down Expand Up @@ -250,15 +243,14 @@ private Map<String, RuntimeProvisioningConfig> getOutboundProvisioningConnectors
* @param fIdP
* @param registeredConnectorFactories
* @param tenantDomainName
* @param enableJitProvisioning
* @return
* @throws IdentityProviderManagementException
* @throws UserStoreException
*/
private AbstractOutboundProvisioningConnector getOutboundProvisioningConnector(
IdentityProvider fIdP,
Map<String, AbstractProvisioningConnectorFactory> registeredConnectorFactories,
String tenantDomainName, boolean enableJitProvisioning)
String tenantDomainName)
throws IdentityProviderManagementException, IdentityProvisioningException {

String idpName = fIdP.getIdentityProviderName();
Expand Down Expand Up @@ -303,13 +295,17 @@ private AbstractOutboundProvisioningConnector getOutboundProvisioningConnector(
Property[] provisioningProperties = defaultProvisioningConfig
.getProvisioningProperties();

if (enableJitProvisioning) {
Property jitEnabled = new Property();
jitEnabled.setName(IdentityProvisioningConstants.JIT_PROVISIONING_ENABLED);
jitEnabled.setValue("1");
provisioningProperties = IdentityApplicationManagementUtil.concatArrays(
provisioningProperties, new Property[]{jitEnabled});
}
/*
* Always set JIT provisioning as enabled at the connector level. The actual JIT enablement check is
* performed in ProvisioningThread before executing outbound provisioning for JIT provisioned entity.
* Therefore, validating JIT conditions at the connector level is unnecessary and may cause issues
* when the same connector is used by applications with different JIT configurations.
*/
Property jitEnabled = new Property();
jitEnabled.setName(IdentityProvisioningConstants.JIT_PROVISIONING_ENABLED);
jitEnabled.setValue("1");
provisioningProperties = IdentityApplicationManagementUtil.concatArrays(
provisioningProperties, new Property[]{jitEnabled});

Property userIdClaimURL = new Property();
userIdClaimURL.setName("userIdClaimUri");
Expand Down Expand Up @@ -357,9 +353,7 @@ public void provision(ProvisioningEntity provisioningEntity, String serviceProvi
}

// Any provisioning request coming via Console, considered as coming from the resident SP.
// If the application based outbound provisioning is disabled, resident SP configuration will be used.
if (StringUtils.equals(CONSOLE_APPLICATION_NAME, serviceProviderIdentifier) ||
!isApplicationBasedOutboundProvisioningEnabled()) {
if (StringUtils.equals(CONSOLE_APPLICATION_NAME, serviceProviderIdentifier)) {
serviceProviderIdentifier = LOCAL_SP;
inboundClaimDialect = IdentityProvisioningConstants.WSO2_CARBON_DIALECT;
}
Expand All @@ -375,11 +369,6 @@ public void provision(ProvisioningEntity provisioningEntity, String serviceProvi
+ serviceProviderIdentifier);
}

String provisioningEntityTenantDomainName = spTenantDomainName;
if (serviceProvider.isSaasApp() && isUserTenantBasedOutboundProvisioningEnabled()) {
provisioningEntityTenantDomainName = CarbonContext.getThreadLocalCarbonContext().getTenantDomain();
}

ClaimMapping[] spClaimMappings = null;

// if we know the serviceProviderClaimDialect - we do not need to find it again.
Expand All @@ -393,6 +382,32 @@ public void provision(ProvisioningEntity provisioningEntity, String serviceProvi
Map<String, RuntimeProvisioningConfig> connectors =
getOutboundProvisioningConnectors(serviceProvider, spTenantDomainName);

// When application-based outbound provisioning is disabled and the application has no
// outbound provisioning connectors configured, fall back to LOCAL_SP (resident app) connectors.
if (!isApplicationBasedOutboundProvisioningEnabled() && MapUtils.isEmpty(connectors)
&& !LOCAL_SP.equals(serviceProviderIdentifier)) {
Comment on lines +385 to +388
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new LOCAL_SP fallback path (when application-based outbound provisioning is disabled and an app has no connectors) introduces important behavior changes, but there is no corresponding test coverage in the provisioning module tests. Please add a unit/integration test that exercises this fallback (connectors empty for app, LOCAL_SP connectors present) and asserts the effective connectors/dialect/tenant flow used.

Copilot uses AI. Check for mistakes.
ServiceProvider localSP = ApplicationManagementService.getInstance()
Comment on lines +385 to +389
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When falling back to LOCAL_SP you replace serviceProvider (and related dialect/claim-mapping decisions), but provisioningEntityTenantDomainName was computed earlier based on the original service provider’s isSaasApp() value. This can cause the provisioning thread to pick the wrong tenant flow after the fallback. Consider recomputing provisioningEntityTenantDomainName (and any other SP-derived variables) after selecting the final serviceProvider to use.

Copilot uses AI. Check for mistakes.
.getServiceProvider(LOCAL_SP, spTenantDomainName);
if (localSP != null) {
if (log.isDebugEnabled()) {
log.debug(String.format(
"No outbound provisioning connectors are configured for the application: %s. " +
"Falling back to resident application outbound provisioning connectors.",
serviceProviderIdentifier));
}
serviceProvider = localSP;
connectors = getOutboundProvisioningConnectors(localSP, spTenantDomainName);
inboundClaimDialect = IdentityProvisioningConstants.WSO2_CARBON_DIALECT;
// LOCAL_SP uses WSO2_CARBON_DIALECT; spClaimMappings is not needed.
spClaimMappings = null;
}
}

String provisioningEntityTenantDomainName = spTenantDomainName;
if (serviceProvider.isSaasApp() && isUserTenantBasedOutboundProvisioningEnabled()) {
provisioningEntityTenantDomainName = CarbonContext.getThreadLocalCarbonContext().getTenantDomain();
}

ProvisioningEntity outboundProEntity;

ExecutorService executors = null;
Expand All @@ -412,6 +427,7 @@ public void provision(ProvisioningEntity provisioningEntity, String serviceProvi
AbstractOutboundProvisioningConnector connector = connectorEntry.getValue();
String connectorType = connectorEntry.getKey();
String idPName = entry.getKey();
boolean jitProvisioningEnabledForIdP = entry.getValue().isJitProvisioningEnabled();

IdentityProvider provisioningIdp =
IdentityProviderManager.getInstance().getIdPByName(idPName, spTenantDomainName);
Expand Down Expand Up @@ -535,7 +551,8 @@ public void provision(ProvisioningEntity provisioningEntity, String serviceProvi
outboundProEntity = new ProvisioningEntity(ProvisioningEntityType.USER,
user, ProvisioningOperation.POST, mappedUserClaims);
Callable<Boolean> proThread = new ProvisioningThread(outboundProEntity, spTenantDomainName,
provisioningEntityTenantDomainName, connector, connectorType, idPName, dao);
provisioningEntityTenantDomainName, connector, connectorType, idPName, dao,
jitProvisioningEnabledForIdP);
outboundProEntity.setIdentifier(provisionedIdentifier);
outboundProEntity.setJitProvisioning(jitProvisioning);
boolean isBlocking = entry.getValue().isBlocking();
Expand All @@ -560,7 +577,8 @@ public void provision(ProvisioningEntity provisioningEntity, String serviceProvi
outboundProEntity = new ProvisioningEntity(ProvisioningEntityType.USER,
user, ProvisioningOperation.DELETE, mappedUserClaims);
Callable<Boolean> proThread = new ProvisioningThread(outboundProEntity, spTenantDomainName,
provisioningEntityTenantDomainName, connector, connectorType, idPName, dao);
provisioningEntityTenantDomainName, connector, connectorType, idPName, dao,
jitProvisioningEnabledForIdP);
outboundProEntity.setIdentifier(provisionedUserIdentifier);
outboundProEntity.setJitProvisioning(jitProvisioning);
boolean isBlocking = entry.getValue().isBlocking();
Expand All @@ -586,7 +604,8 @@ public void provision(ProvisioningEntity provisioningEntity, String serviceProvi
provisioningEntity.getEntityName(), provisioningOp, mapppedClaims);

Callable<Boolean> proThread = new ProvisioningThread(outboundProEntity, spTenantDomainName,
provisioningEntityTenantDomainName, connector, connectorType, idPName, dao);
provisioningEntityTenantDomainName, connector, connectorType, idPName, dao,
jitProvisioningEnabledForIdP);
outboundProEntity.setIdentifier(provisionedIdentifier);
outboundProEntity.setJitProvisioning(jitProvisioning);
boolean isAllowed = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

package org.wso2.carbon.identity.provisioning;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.context.PrivilegedCarbonContext;
Expand All @@ -40,6 +39,7 @@ public class ProvisioningThread implements Callable<Boolean> {
private String connectorType;
private String idPName;
private CacheBackedProvisioningMgtDAO dao;
private boolean jitProvisioningEnabledForIdP;
private static final Log log = LogFactory.getLog(ProvisioningThread.class);

public ProvisioningThread(ProvisioningEntity provisioningEntity, String tenantDomainName,
Expand All @@ -63,6 +63,28 @@ public ProvisioningThread(ProvisioningEntity provisioningEntity, String spTenant
this.provisioningEntityTenantDomainName = provisioningEntityTenantDomainName;
}

/**
* Parameterized constructor to propagate JIT provisioning state to the connector.
*
* @param provisioningEntity Provisioning entity containing details of the provisioning operation.
* @param spTenantDomainName Service provider tenant domain name.
* @param provisioningEntityTenantDomainName Tenant domain name of the provisioning entity.
* @param connector Outbound provisioning connector to perform the provisioning operation.
* @param connectorType Type of the outbound provisioning connector.
* @param idPName Name of the identity provider through which provisioning is triggered.
* @param dao Data access object to store and manage provisioning entity identifiers.
* @param jitProvisioningEnabledForIdP Flag indicating whether JIT provisioning is enabled or not.
*/
public ProvisioningThread(ProvisioningEntity provisioningEntity, String spTenantDomainName,
String provisioningEntityTenantDomainName,
AbstractOutboundProvisioningConnector connector, String connectorType, String idPName,
CacheBackedProvisioningMgtDAO dao, boolean jitProvisioningEnabledForIdP) {

this(provisioningEntity, spTenantDomainName, provisioningEntityTenantDomainName, connector, connectorType,
idPName, dao);
this.jitProvisioningEnabledForIdP = jitProvisioningEnabledForIdP;
}

@Override
public Boolean call() throws IdentityProvisioningException {

Expand All @@ -83,7 +105,13 @@ public Boolean call() throws IdentityProvisioningException {

/* Skip outbound provisioning triggered for JIT provisioning flow, where the JIT outbound is disabled for
the configured connector. */
if (provisioningEntity.isJitProvisioning() && !connector.isJitProvisioningEnabled()) {
if (provisioningEntity.isJitProvisioning() && !jitProvisioningEnabledForIdP) {
if (log.isDebugEnabled()) {
log.debug(String.format("Skipping outbound provisioning for entity: %s via IDP: %s, connector: " +
"%s. Reason: JIT provisioning is not enabled for this provisioning connector.",
ProvisioningUtil.maskIfRequired(provisioningEntity.getEntityName()), idPName,
connectorType));
}
return true;
}
ProvisionedIdentifier provisionedIdentifier = null;
Expand Down Expand Up @@ -118,8 +146,10 @@ public Boolean call() throws IdentityProvisioningException {
}
success = true;
} catch (Exception e) {
String errMsg = "Fail the Provisioning for Entity " + provisioningEntity.getEntityName() +
" For operation = " + provisioningEntity.getOperation();
String maskedEntityName = ProvisioningUtil.maskIfRequired(provisioningEntity.getEntityName());
String errMsg = "Outbound provisioning failed for connection: " + idPName + ", connector: " + connectorType
+ ", entity: " + maskedEntityName + ", entity type: " + provisioningEntity.getEntityType()
+ ", operation: " + provisioningEntity.getOperation();
log.warn(errMsg);
throw new IdentityProvisioningException(errMsg, e);
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.wso2.carbon.identity.application.common.util.IdentityApplicationConstants;
import org.wso2.carbon.identity.application.mgt.ApplicationConstants;
import org.wso2.carbon.identity.application.mgt.ApplicationManagementService;
import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils;
import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataHandler;
import org.wso2.carbon.identity.core.util.IdentityUtil;

Expand Down Expand Up @@ -618,4 +619,15 @@ public static boolean isApplicationBasedOutboundProvisioningEnabled() {
}
return applicationBasedOutboundProvisioningEnabled;
}

/**
* Mask the given value if it is required.
*
* @param value Value to be masked.
* @return Masked/unmasked value.
*/
public static String maskIfRequired(String value) {

return LoggerUtils.isLogMaskingEnable ? LoggerUtils.getMaskedContent(value) : value;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/*
* Copyright (c) 2014, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
* Copyright (c) 2014-2026, WSO2 LLC. (http://www.wso2.com).
*
* WSO2 Inc. licenses this file to you under the Apache License,
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
Expand All @@ -27,6 +27,7 @@ public class RuntimeProvisioningConfig implements Serializable {

private boolean blocking;
private boolean policyEnabled;
private boolean jitProvisioningEnabled;
private Entry<String, AbstractOutboundProvisioningConnector> provisioningConnectorEntry;

/**
Expand All @@ -43,6 +44,24 @@ public void setBlocking(boolean blocking) {
this.blocking = blocking;
}

/**
* Get JIT provisioning enabled or not.
* @return JIT provisioning enabled or not.
*/
public boolean isJitProvisioningEnabled() {

return jitProvisioningEnabled;
}

/**
* Set JIT provisioning enabled or not.
* @param jitProvisioningEnabled JIT provisioning enabled or not.
*/
public void setJitProvisioningEnabled(boolean jitProvisioningEnabled) {

this.jitProvisioningEnabled = jitProvisioningEnabled;
}

/**
* @return
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ private ServiceProviderProvisioningConnectorCache() {
*/
public static ServiceProviderProvisioningConnectorCache getInstance() {
if (instance == null) {
synchronized (ProvisioningConnectorCache.class) {
synchronized (ServiceProviderProvisioningConnectorCache.class) {
if (instance == null) {
instance = new ServiceProviderProvisioningConnectorCache();
}
Expand Down
Loading
Loading