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());
+    }
 }

Reply via email to