This is an automated email from the ASF dual-hosted git repository.
lahirujayathilake pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-custos.git
The following commit(s) were added to refs/heads/master by this push:
new 43f2f89b5 automate CILogon IDP configuration with ciLogon.enabled flag
for super-tenant bootstrap and child tenant activation
43f2f89b5 is described below
commit 43f2f89b577eb37fde3c3dd374e1d93c2c98383a
Author: lahiruj <[email protected]>
AuthorDate: Fri Mar 13 15:03:27 2026 -0400
automate CILogon IDP configuration with ciLogon.enabled flag for
super-tenant bootstrap and child tenant activation
---
.../application/src/main/resources/application.yml | 1 +
.../federated/client/keycloak/KeycloakClient.java | 1 +
.../management/SuperTenantBootstrapper.java | 39 +++++++-
.../service/management/TenantActivationTask.java | 53 +++++-----
.../management/SuperTenantBootstrapperTest.java | 111 +++++++++++++++++++--
5 files changed, 166 insertions(+), 39 deletions(-)
diff --git a/identity/application/src/main/resources/application.yml
b/identity/application/src/main/resources/application.yml
index 5232bf63c..83417d98e 100644
--- a/identity/application/src/main/resources/application.yml
+++ b/identity/application/src/main/resources/application.yml
@@ -101,6 +101,7 @@ iam:
jwksUri: https://cilogon.org/oauth2/certs
ciLogon:
+ enabled: true
admin:
client:
id: abc
diff --git
a/identity/services/src/main/java/org/apache/custos/service/federated/client/keycloak/KeycloakClient.java
b/identity/services/src/main/java/org/apache/custos/service/federated/client/keycloak/KeycloakClient.java
index a4a04fefa..24225bbbe 100644
---
a/identity/services/src/main/java/org/apache/custos/service/federated/client/keycloak/KeycloakClient.java
+++
b/identity/services/src/main/java/org/apache/custos/service/federated/client/keycloak/KeycloakClient.java
@@ -854,6 +854,7 @@ public class KeycloakClient {
idp.getConfig().put("issuer", ciLogonIssuerUri);
idp.getConfig().put("jwksUri", jwksUri);
idp.getConfig().put("forwardParameters", "idphint");
+ idp.getConfig().put("clientAuthMethod", "client_secret_post");
realmResource.identityProviders().create(idp);
diff --git
a/identity/services/src/main/java/org/apache/custos/service/management/SuperTenantBootstrapper.java
b/identity/services/src/main/java/org/apache/custos/service/management/SuperTenantBootstrapper.java
index dc2f6cfb9..f749ecac7 100644
---
a/identity/services/src/main/java/org/apache/custos/service/management/SuperTenantBootstrapper.java
+++
b/identity/services/src/main/java/org/apache/custos/service/management/SuperTenantBootstrapper.java
@@ -29,9 +29,11 @@ import org.apache.custos.core.tenant.profile.api.TenantType;
import org.apache.custos.core.tenant.profile.api.UpdateStatusRequest;
import org.apache.custos.core.tenant.profile.api.UpdateStatusResponse;
import org.apache.custos.service.federated.client.keycloak.KeycloakClient;
+import
org.apache.custos.service.federated.client.keycloak.KeycloakClientSecret;
import org.apache.custos.service.profile.TenantProfileService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -45,7 +47,7 @@ import java.util.List;
* super-tenant at application startup when {@code
custos.bootstrap.enabled=true}.
*
* <p>If an ACTIVE super-tenant (parentId == 0) already exists,
- * the bootstrap is skipped and the application starts normally.</p>
+ * the bootstrap is skipped and the application starts normally.</p>
*
* <p>All admin credentials are sourced from Spring configuration properties,
which in production
* resolve from environment variables.</p>
@@ -64,6 +66,15 @@ public class SuperTenantBootstrapper implements
ApplicationRunner {
private final KeycloakClient keycloakClient;
private final SuperTenantProperties properties;
+ @Value("${ciLogon.enabled:false}")
+ private boolean ciLogonEnabled;
+
+ @Value("${ciLogon.admin.client.id:}")
+ private String ciLogonClientId;
+
+ @Value("${ciLogon.admin.client.secret:}")
+ private String ciLogonClientSecret;
+
public SuperTenantBootstrapper(TenantManagementService
tenantManagementService,
TenantProfileService tenantProfileService,
KeycloakClient keycloakClient,
@@ -122,6 +133,8 @@ public class SuperTenantBootstrapper implements
ApplicationRunner {
+ "Secret stored in Vault at /secret/{}/CUSTOS",
createResponse.getClientId(),
statusResponse.getTenantId());
+
+ configureCILogonIfEnabled(statusResponse.getTenantId());
}
private void waitForKeycloak() {
@@ -160,6 +173,30 @@ public class SuperTenantBootstrapper implements
ApplicationRunner {
return response != null && response.getTenantList() != null &&
!response.getTenantList().isEmpty();
}
+ private void configureCILogonIfEnabled(long tenantId) {
+ if (!ciLogonEnabled) {
+ LOGGER.info("SuperTenantBootstrapper: CILogon disabled, skipping
IDP configuration");
+ return;
+ }
+ if (ciLogonClientId.isBlank() || ciLogonClientSecret.isBlank()) {
+ LOGGER.warn("SuperTenantBootstrapper: CILogon enabled but admin
credentials not configured, skipping");
+ return;
+ }
+
+ LOGGER.info("SuperTenantBootstrapper: configuring CILogon IDP for
super-tenant realm {}", tenantId);
+
+ KeycloakClientSecret secret = new
KeycloakClientSecret(ciLogonClientId, ciLogonClientSecret);
+ keycloakClient.configureOIDCFederatedIDP(
+ String.valueOf(tenantId),
+ "CILogon",
+ "openid profile email org.cilogon.userinfo",
+ secret,
+ null
+ );
+
+ LOGGER.info("SuperTenantBootstrapper: CILogon IDP configured
successfully");
+ }
+
/**
* Parses a comma-separated redirect URI string into a trimmed list.
*/
diff --git
a/identity/services/src/main/java/org/apache/custos/service/management/TenantActivationTask.java
b/identity/services/src/main/java/org/apache/custos/service/management/TenantActivationTask.java
index 87b616eb2..e83b6ff32 100644
---
a/identity/services/src/main/java/org/apache/custos/service/management/TenantActivationTask.java
+++
b/identity/services/src/main/java/org/apache/custos/service/management/TenantActivationTask.java
@@ -63,8 +63,8 @@ public class TenantActivationTask<T, U> extends
ServiceTaskImpl<T, U> {
private final TenantProfileService tenantProfileService;
- @Value("${spring.profiles.active}")
- private String activeProfile;
+ @Value("${ciLogon.enabled:false}")
+ private boolean ciLogonEnabled;
public TenantActivationTask(IamAdminService iamAdminService,
FederatedAuthenticationService federatedAuthentication, CredentialStoreService
credentialStoreService, TenantProfileService tenantProfileService) {
@@ -212,32 +212,29 @@ public class TenantActivationTask<T, U> extends
ServiceTaskImpl<T, U> {
clientMetadataBuilder.setClientId(creMeta.getId());
- if (!update) {
- // skip CILOGON client creation for local development
- if (!activeProfile.equalsIgnoreCase("local")) {
- RegisterClientResponse registerClientResponse =
federatedAuthentication.addClient(clientMetadataBuilder.build());
-
- CredentialMetadata credentialMetadataCILogon =
CredentialMetadata
- .newBuilder()
- .setId(registerClientResponse.getClientId())
- .setSecret(registerClientResponse.getClientSecret())
- .setOwnerId(tenant.getTenantId())
- .setType(Type.CILOGON)
- .build();
-
-
credentialStoreService.putCredential(credentialMetadataCILogon);
-
- ConfigureFederateIDPRequest request =
ConfigureFederateIDPRequest
- .newBuilder()
- .setTenantId(tenant.getTenantId())
- .setClientID(registerClientResponse.getClientId())
- .setClientSec(registerClientResponse.getClientSecret())
- .setScope(tenant.getScope())
- .setRequesterEmail(tenant.getRequesterEmail())
- .setType(FederatedIDPs.CILOGON)
- .build();
- iamAdminService.configureFederatedIDP(request);
- }
+ if (!update && ciLogonEnabled && tenant.getParentTenantId() != 0) {
+ RegisterClientResponse registerClientResponse =
federatedAuthentication.addClient(clientMetadataBuilder.build());
+
+ CredentialMetadata credentialMetadataCILogon = CredentialMetadata
+ .newBuilder()
+ .setId(registerClientResponse.getClientId())
+ .setSecret(registerClientResponse.getClientSecret())
+ .setOwnerId(tenant.getTenantId())
+ .setType(Type.CILOGON)
+ .build();
+
+ credentialStoreService.putCredential(credentialMetadataCILogon);
+
+ ConfigureFederateIDPRequest request = ConfigureFederateIDPRequest
+ .newBuilder()
+ .setTenantId(tenant.getTenantId())
+ .setClientID(registerClientResponse.getClientId())
+ .setClientSec(registerClientResponse.getClientSecret())
+ .setScope(tenant.getScope())
+ .setRequesterEmail(tenant.getRequesterEmail())
+ .setType(FederatedIDPs.CILOGON)
+ .build();
+ iamAdminService.configureFederatedIDP(request);
}
org.apache.custos.core.tenant.profile.api.UpdateStatusRequest
updateTenantRequest =
org.apache.custos.core.tenant.profile.api.UpdateStatusRequest.newBuilder()
diff --git
a/identity/services/src/test/java/org/apache/custos/service/management/SuperTenantBootstrapperTest.java
b/identity/services/src/test/java/org/apache/custos/service/management/SuperTenantBootstrapperTest.java
index 19780c2d2..6f47a2cca 100644
---
a/identity/services/src/test/java/org/apache/custos/service/management/SuperTenantBootstrapperTest.java
+++
b/identity/services/src/test/java/org/apache/custos/service/management/SuperTenantBootstrapperTest.java
@@ -29,6 +29,7 @@ import org.apache.custos.core.tenant.profile.api.TenantType;
import org.apache.custos.core.tenant.profile.api.UpdateStatusRequest;
import org.apache.custos.core.tenant.profile.api.UpdateStatusResponse;
import org.apache.custos.service.federated.client.keycloak.KeycloakClient;
+import
org.apache.custos.service.federated.client.keycloak.KeycloakClientSecret;
import org.apache.custos.service.profile.TenantProfileService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
@@ -38,10 +39,14 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.ApplicationArguments;
+import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -51,27 +56,26 @@ import static org.mockito.Mockito.when;
@Tag("unit")
class SuperTenantBootstrapperTest {
+ private static final String TEST_USERNAME = "custosadmin";
+ private static final String TEST_PASSWORD = "testpass123";
+ private static final String TEST_EMAIL = "[email protected]";
+ private static final String TEST_CLIENT_ID = "custos-abc123";
+ private static final long TEST_TENANT_ID = 10000001L;
+
+ private static final String CILOGON_CLIENT_ID =
"cilogon:/client_id/test123";
+ private static final String CILOGON_CLIENT_SECRET = "test-cilogon-secret";
+
@Mock
private TenantManagementService tenantManagementService;
-
@Mock
private TenantProfileService tenantProfileService;
-
@Mock
private KeycloakClient keycloakClient;
-
@Mock
private ApplicationArguments applicationArguments;
-
private SuperTenantProperties properties;
private SuperTenantBootstrapper bootstrapper;
- private static final String TEST_USERNAME = "custosadmin";
- private static final String TEST_PASSWORD = "testpass123";
- private static final String TEST_EMAIL = "[email protected]";
- private static final String TEST_CLIENT_ID = "custos-abc123";
- private static final long TEST_TENANT_ID = 10000001L;
-
@BeforeEach
void setUp() {
properties = new SuperTenantProperties(
@@ -316,4 +320,91 @@ class SuperTenantBootstrapperTest {
assertThat(tenantCaptor.getValue().getRedirectUrisList()).isEmpty();
}
+
+ private void stubFullBootstrap() {
+ GetAllTenantsResponse emptyResponse =
GetAllTenantsResponse.newBuilder().build();
+
when(tenantProfileService.getAllTenants(any(GetTenantsRequest.class))).thenReturn(emptyResponse);
+
+ CreateTenantResponse createResponse = CreateTenantResponse.newBuilder()
+ .setClientId(TEST_CLIENT_ID)
+ .build();
+
when(tenantManagementService.createTenant(any(Tenant.class))).thenReturn(createResponse);
+
+ UpdateStatusResponse statusResponse = UpdateStatusResponse.newBuilder()
+ .setTenantId(TEST_TENANT_ID)
+ .setStatus(TenantStatus.ACTIVE)
+ .build();
+
when(tenantManagementService.updateTenantStatus(any(UpdateStatusRequest.class))).thenReturn(statusResponse);
+ }
+
+ @Test
+ void configureCILogonIDP_whenEnabled_callsKeycloakClient() {
+ stubFullBootstrap();
+ when(keycloakClient.configureOIDCFederatedIDP(anyString(),
anyString(), anyString(), any(KeycloakClientSecret.class), isNull()))
+ .thenReturn(true);
+
+ ReflectionTestUtils.setField(bootstrapper, "ciLogonEnabled", true);
+ ReflectionTestUtils.setField(bootstrapper, "ciLogonClientId",
CILOGON_CLIENT_ID);
+ ReflectionTestUtils.setField(bootstrapper, "ciLogonClientSecret",
CILOGON_CLIENT_SECRET);
+
+ bootstrapper.run(applicationArguments);
+
+ ArgumentCaptor<KeycloakClientSecret> secretCaptor =
ArgumentCaptor.forClass(KeycloakClientSecret.class);
+ verify(keycloakClient).configureOIDCFederatedIDP(
+ eq(String.valueOf(TEST_TENANT_ID)),
+ eq("CILogon"),
+ eq("openid profile email org.cilogon.userinfo"),
+ secretCaptor.capture(),
+ isNull()
+ );
+
+ KeycloakClientSecret capturedSecret = secretCaptor.getValue();
+ assertThat(capturedSecret.clientId()).isEqualTo(CILOGON_CLIENT_ID);
+
assertThat(capturedSecret.clientSecret()).isEqualTo(CILOGON_CLIENT_SECRET);
+ }
+
+ @Test
+ void configureCILogonIDP_whenDisabled_skipsConfiguration() throws
Exception {
+ stubFullBootstrap();
+
+ ReflectionTestUtils.setField(bootstrapper, "ciLogonEnabled", false);
+ ReflectionTestUtils.setField(bootstrapper, "ciLogonClientId",
CILOGON_CLIENT_ID);
+ ReflectionTestUtils.setField(bootstrapper, "ciLogonClientSecret",
CILOGON_CLIENT_SECRET);
+
+ bootstrapper.run(applicationArguments);
+
+ verify(keycloakClient, never()).configureOIDCFederatedIDP(anyString(),
anyString(), anyString(), any(KeycloakClientSecret.class), any());
+ }
+
+ @Test
+ void
configureCILogonIDP_whenEnabledButCredentialsMissing_skipsWithWarning() {
+ stubFullBootstrap();
+
+ ReflectionTestUtils.setField(bootstrapper, "ciLogonEnabled", true);
+ ReflectionTestUtils.setField(bootstrapper, "ciLogonClientId", "");
+ ReflectionTestUtils.setField(bootstrapper, "ciLogonClientSecret", "");
+
+ bootstrapper.run(applicationArguments);
+
+ verify(keycloakClient, never()).configureOIDCFederatedIDP(anyString(),
anyString(), anyString(), any(KeycloakClientSecret.class), any());
+ }
+
+ @Test
+ void fullBootstrap_whenCILogonEnabled_configuresIDPAfterTenantCreation() {
+ stubFullBootstrap();
+ when(keycloakClient.configureOIDCFederatedIDP(anyString(),
anyString(), anyString(), any(KeycloakClientSecret.class), isNull()))
+ .thenReturn(true);
+
+ ReflectionTestUtils.setField(bootstrapper, "ciLogonEnabled", true);
+ ReflectionTestUtils.setField(bootstrapper, "ciLogonClientId",
CILOGON_CLIENT_ID);
+ ReflectionTestUtils.setField(bootstrapper, "ciLogonClientSecret",
CILOGON_CLIENT_SECRET);
+
+ bootstrapper.run(applicationArguments);
+
+ // Verify full sequence: create tenant -> activate -> configure
CILogon IDP
+ var inOrder = org.mockito.Mockito.inOrder(tenantManagementService,
keycloakClient);
+
inOrder.verify(tenantManagementService).createTenant(any(Tenant.class));
+
inOrder.verify(tenantManagementService).updateTenantStatus(any(UpdateStatusRequest.class));
+ inOrder.verify(keycloakClient).configureOIDCFederatedIDP(anyString(),
anyString(), anyString(), any(KeycloakClientSecret.class), isNull());
+ }
}