diff --git a/components/org.wso2.carbon.identity.api.server.api.resource/org.wso2.carbon.identity.api.server.api.resource.v1/pom.xml b/components/org.wso2.carbon.identity.api.server.api.resource/org.wso2.carbon.identity.api.server.api.resource.v1/pom.xml
index 027d122052..883f27573d 100644
--- a/components/org.wso2.carbon.identity.api.server.api.resource/org.wso2.carbon.identity.api.server.api.resource.v1/pom.xml
+++ b/components/org.wso2.carbon.identity.api.server.api.resource/org.wso2.carbon.identity.api.server.api.resource.v1/pom.xml
@@ -125,6 +125,22 @@
org.wso2.carbon.identity.oauth.rar
provided
+
+ org.testng
+ testng
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ org.jacoco
+ org.jacoco.agent
+ runtime
+ test
+
diff --git a/components/org.wso2.carbon.identity.api.server.api.resource/org.wso2.carbon.identity.api.server.api.resource.v1/src/gen/java/org/wso2/carbon/identity/api/server/api/resource/v1/APIResourceMap.java b/components/org.wso2.carbon.identity.api.server.api.resource/org.wso2.carbon.identity.api.server.api.resource.v1/src/gen/java/org/wso2/carbon/identity/api/server/api/resource/v1/APIResourceMap.java
index 3562b7d33a..eeeff8f25c 100644
--- a/components/org.wso2.carbon.identity.api.server.api.resource/org.wso2.carbon.identity.api.server.api.resource.v1/src/gen/java/org/wso2/carbon/identity/api/server/api/resource/v1/APIResourceMap.java
+++ b/components/org.wso2.carbon.identity.api.server.api.resource/org.wso2.carbon.identity.api.server.api.resource.v1/src/gen/java/org/wso2/carbon/identity/api/server/api/resource/v1/APIResourceMap.java
@@ -39,6 +39,12 @@ public class APIResourceMap {
private List write = null;
+ private List create = null;
+
+ private List update = null;
+
+ private List delete = null;
+
/**
**/
@@ -92,7 +98,85 @@ public APIResourceMap addWriteItem(APIResourceCollectionItem writeItem) {
return this;
}
-
+ /**
+ **/
+ public APIResourceMap create(List create) {
+
+ this.create = create;
+ return this;
+ }
+
+ @ApiModelProperty(value = "")
+ @JsonProperty("create")
+ @Valid
+ public List getCreate() {
+ return create;
+ }
+ public void setCreate(List create) {
+ this.create = create;
+ }
+
+ public APIResourceMap addCreateItem(APIResourceCollectionItem createItem) {
+ if (this.create == null) {
+ this.create = new ArrayList();
+ }
+ this.create.add(createItem);
+ return this;
+ }
+
+ /**
+ **/
+ public APIResourceMap update(List update) {
+
+ this.update = update;
+ return this;
+ }
+
+ @ApiModelProperty(value = "")
+ @JsonProperty("update")
+ @Valid
+ public List getUpdate() {
+ return update;
+ }
+ public void setUpdate(List update) {
+ this.update = update;
+ }
+
+ public APIResourceMap addUpdateItem(APIResourceCollectionItem updateItem) {
+ if (this.update == null) {
+ this.update = new ArrayList();
+ }
+ this.update.add(updateItem);
+ return this;
+ }
+
+ /**
+ **/
+ public APIResourceMap delete(List delete) {
+
+ this.delete = delete;
+ return this;
+ }
+
+ @ApiModelProperty(value = "")
+ @JsonProperty("delete")
+ @Valid
+ public List getDelete() {
+ return delete;
+ }
+ public void setDelete(List delete) {
+ this.delete = delete;
+ }
+
+ public APIResourceMap addDeleteItem(APIResourceCollectionItem deleteItem) {
+ if (this.delete == null) {
+ this.delete = new ArrayList();
+ }
+ this.delete.add(deleteItem);
+ return this;
+ }
+
+
@Override
public boolean equals(java.lang.Object o) {
@@ -105,12 +189,15 @@ public boolean equals(java.lang.Object o) {
}
APIResourceMap apIResourceMap = (APIResourceMap) o;
return Objects.equals(this.read, apIResourceMap.read) &&
- Objects.equals(this.write, apIResourceMap.write);
+ Objects.equals(this.write, apIResourceMap.write) &&
+ Objects.equals(this.create, apIResourceMap.create) &&
+ Objects.equals(this.update, apIResourceMap.update) &&
+ Objects.equals(this.delete, apIResourceMap.delete);
}
@Override
public int hashCode() {
- return Objects.hash(read, write);
+ return Objects.hash(read, write, create, update, delete);
}
@Override
@@ -121,6 +208,9 @@ public String toString() {
sb.append(" read: ").append(toIndentedString(read)).append("\n");
sb.append(" write: ").append(toIndentedString(write)).append("\n");
+ sb.append(" create: ").append(toIndentedString(create)).append("\n");
+ sb.append(" update: ").append(toIndentedString(update)).append("\n");
+ sb.append(" delete: ").append(toIndentedString(delete)).append("\n");
sb.append("}");
return sb.toString();
}
diff --git a/components/org.wso2.carbon.identity.api.server.api.resource/org.wso2.carbon.identity.api.server.api.resource.v1/src/main/java/org/wso2/carbon/identity/api/server/api/resource/v1/core/ServerAPIResourceCollectionManagementService.java b/components/org.wso2.carbon.identity.api.server.api.resource/org.wso2.carbon.identity.api.server.api.resource.v1/src/main/java/org/wso2/carbon/identity/api/server/api/resource/v1/core/ServerAPIResourceCollectionManagementService.java
index f1358c7145..96c86b7ea8 100644
--- a/components/org.wso2.carbon.identity.api.server.api.resource/org.wso2.carbon.identity.api.server.api.resource.v1/src/main/java/org/wso2/carbon/identity/api/server/api/resource/v1/core/ServerAPIResourceCollectionManagementService.java
+++ b/components/org.wso2.carbon.identity.api.server.api.resource/org.wso2.carbon.identity.api.server.api.resource.v1/src/main/java/org/wso2/carbon/identity/api/server/api/resource/v1/core/ServerAPIResourceCollectionManagementService.java
@@ -26,6 +26,7 @@
import org.wso2.carbon.identity.api.resource.collection.mgt.exception.APIResourceCollectionMgtException;
import org.wso2.carbon.identity.api.resource.collection.mgt.model.APIResourceCollection;
import org.wso2.carbon.identity.api.resource.collection.mgt.model.APIResourceCollectionSearchResult;
+import org.wso2.carbon.identity.api.resource.collection.mgt.util.APIResourceCollectionManagementUtil;
import org.wso2.carbon.identity.api.server.api.resource.v1.APIResourceCollectionItem;
import org.wso2.carbon.identity.api.server.api.resource.v1.APIResourceCollectionListItem;
import org.wso2.carbon.identity.api.server.api.resource.v1.APIResourceCollectionListResponse;
@@ -244,6 +245,17 @@ private APIResourceMap buildAPIResourceMap(APIResourceCollection apiResourceColl
new APIResourceMap();
apiResourceCollectionResponseApiResources.setRead(readAPIResourceCollectionItems);
apiResourceCollectionResponseApiResources.setWrite(writeAPIResourceCollectionItems);
+ // Expose the granular create/update/delete scopes only when granular console permissions are enabled
+ // (ConsoleSettings.UseGranularConsolePermissions). Defaults to off so existing deployments keep the
+ // legacy view/edit (read/write) behaviour until they explicitly opt in.
+ if (APIResourceCollectionManagementUtil.isGranularConsolePermissionsEnabled()) {
+ apiResourceCollectionResponseApiResources.setCreate(getAPIResourceCollectionItems(
+ apiResourceCollection, APIResourceCollectionManagementConstants.CREATE));
+ apiResourceCollectionResponseApiResources.setUpdate(getAPIResourceCollectionItems(
+ apiResourceCollection, APIResourceCollectionManagementConstants.UPDATE));
+ apiResourceCollectionResponseApiResources.setDelete(getAPIResourceCollectionItems(
+ apiResourceCollection, APIResourceCollectionManagementConstants.DELETE));
+ }
return apiResourceCollectionResponseApiResources;
}
}
diff --git a/components/org.wso2.carbon.identity.api.server.api.resource/org.wso2.carbon.identity.api.server.api.resource.v1/src/main/resources/APIResources.yaml b/components/org.wso2.carbon.identity.api.server.api.resource/org.wso2.carbon.identity.api.server.api.resource.v1/src/main/resources/APIResources.yaml
index c0142ed231..262c818004 100644
--- a/components/org.wso2.carbon.identity.api.server.api.resource/org.wso2.carbon.identity.api.server.api.resource.v1/src/main/resources/APIResources.yaml
+++ b/components/org.wso2.carbon.identity.api.server.api.resource/org.wso2.carbon.identity.api.server.api.resource.v1/src/main/resources/APIResources.yaml
@@ -1193,6 +1193,18 @@ components:
type: array
items:
$ref: '#/components/schemas/APIResourceCollectionItem'
+ create:
+ type: array
+ items:
+ $ref: '#/components/schemas/APIResourceCollectionItem'
+ update:
+ type: array
+ items:
+ $ref: '#/components/schemas/APIResourceCollectionItem'
+ delete:
+ type: array
+ items:
+ $ref: '#/components/schemas/APIResourceCollectionItem'
APIResourceCollectionListItem:
type: object
diff --git a/components/org.wso2.carbon.identity.api.server.api.resource/org.wso2.carbon.identity.api.server.api.resource.v1/src/test/java/org/wso2/carbon/identity/api/server/api/resource/v1/core/ServerAPIResourceCollectionManagementServiceTest.java b/components/org.wso2.carbon.identity.api.server.api.resource/org.wso2.carbon.identity.api.server.api.resource.v1/src/test/java/org/wso2/carbon/identity/api/server/api/resource/v1/core/ServerAPIResourceCollectionManagementServiceTest.java
new file mode 100644
index 0000000000..6c023f1801
--- /dev/null
+++ b/components/org.wso2.carbon.identity.api.server.api.resource/org.wso2.carbon.identity.api.server.api.resource.v1/src/test/java/org/wso2/carbon/identity/api/server/api/resource/v1/core/ServerAPIResourceCollectionManagementServiceTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com).
+ *
+ * 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
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.wso2.carbon.identity.api.server.api.resource.v1.core;
+
+import org.mockito.MockedStatic;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.wso2.carbon.identity.api.resource.collection.mgt.APIResourceCollectionManager;
+import org.wso2.carbon.identity.api.resource.collection.mgt.constant.APIResourceCollectionManagementConstants;
+import org.wso2.carbon.identity.api.resource.collection.mgt.model.APIResourceCollection;
+import org.wso2.carbon.identity.api.resource.collection.mgt.util.APIResourceCollectionManagementUtil;
+import org.wso2.carbon.identity.api.server.api.resource.v1.APIResourceMap;
+import org.wso2.carbon.identity.api.server.common.ContextLoader;
+import org.wso2.carbon.identity.application.common.model.APIResource;
+
+import java.lang.reflect.Method;
+import java.net.URI;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+/**
+ * Test for ServerAPIResourceCollectionManagementService, focusing on how the
+ * {@code ConsoleSettings.UseGranularConsolePermissions} configuration gates the create/update/delete
+ * buckets in the API resource map.
+ */
+public class ServerAPIResourceCollectionManagementServiceTest {
+
+ private static final String COLLECTION_ID = "Y29sbGVjdGlvbi1pZA";
+
+ private ServerAPIResourceCollectionManagementService service;
+
+ @BeforeMethod
+ public void setUp() {
+
+ APIResourceCollectionManager apiResourceCollectionManager = mock(APIResourceCollectionManager.class);
+ service = new ServerAPIResourceCollectionManagementService(apiResourceCollectionManager);
+ }
+
+ @Test
+ public void testGranularConsolePermissionsEnabledExposesCreateUpdateDelete() throws Exception {
+
+ APIResourceMap apiResources = buildApiResourceMapForCollection(true);
+
+ assertNotNull(apiResources.getRead(), "Read bucket should always be populated.");
+ assertNotNull(apiResources.getWrite(), "Write bucket should always be populated.");
+ assertEquals(apiResources.getRead().size(), 1);
+ assertEquals(apiResources.getWrite().size(), 1);
+
+ // Granular buckets must be exposed when the config is enabled.
+ assertNotNull(apiResources.getCreate(), "Create bucket should be populated when granular " +
+ "console permissions are enabled.");
+ assertNotNull(apiResources.getUpdate(), "Update bucket should be populated when granular " +
+ "console permissions are enabled.");
+ assertNotNull(apiResources.getDelete(), "Delete bucket should be populated when granular " +
+ "console permissions are enabled.");
+ assertEquals(apiResources.getCreate().size(), 1);
+ assertEquals(apiResources.getUpdate().size(), 1);
+ assertEquals(apiResources.getDelete().size(), 1);
+ }
+
+ @Test
+ public void testGranularConsolePermissionsDisabledOmitsCreateUpdateDelete() throws Exception {
+
+ APIResourceMap apiResources = buildApiResourceMapForCollection(false);
+
+ // Legacy view/edit (read/write) behaviour is always preserved.
+ assertNotNull(apiResources.getRead(), "Read bucket should always be populated.");
+ assertNotNull(apiResources.getWrite(), "Write bucket should always be populated.");
+ assertEquals(apiResources.getRead().size(), 1);
+ assertEquals(apiResources.getWrite().size(), 1);
+
+ // Granular buckets must stay null (and thus be omitted from the JSON response) when the config is off.
+ assertNull(apiResources.getCreate(), "Create bucket should be omitted when granular console " +
+ "permissions are disabled.");
+ assertNull(apiResources.getUpdate(), "Update bucket should be omitted when granular console " +
+ "permissions are disabled.");
+ assertNull(apiResources.getDelete(), "Delete bucket should be omitted when granular console " +
+ "permissions are disabled.");
+ }
+
+ /**
+ * Builds the API resource map for a collection that carries all five scope types, with the granular console
+ * permissions config stubbed to the given state. Invokes the private {@code buildAPIResourceMap} directly so the
+ * gating logic can be exercised without standing up the Carbon runtime context required by the public endpoints.
+ *
+ * @param granularEnabled Value returned by {@code isGranularConsolePermissionsEnabled()}.
+ * @return The API resource map built for the collection.
+ */
+ private APIResourceMap buildApiResourceMapForCollection(boolean granularEnabled) throws Exception {
+
+ APIResourceCollection collection = collectionWithAllScopeTypes();
+
+ try (MockedStatic utilMock =
+ mockStatic(APIResourceCollectionManagementUtil.class);
+ MockedStatic contextLoaderMock = mockStatic(ContextLoader.class)) {
+
+ utilMock.when(APIResourceCollectionManagementUtil::isGranularConsolePermissionsEnabled)
+ .thenReturn(granularEnabled);
+ contextLoaderMock.when(() -> ContextLoader.buildURIForBody(anyString()))
+ .thenReturn(URI.create("/api/resources"));
+
+ Method buildAPIResourceMap = ServerAPIResourceCollectionManagementService.class
+ .getDeclaredMethod("buildAPIResourceMap", APIResourceCollection.class);
+ buildAPIResourceMap.setAccessible(true);
+ APIResourceMap apiResources = (APIResourceMap) buildAPIResourceMap.invoke(service, collection);
+ assertNotNull(apiResources, "API resource map should never be null.");
+ return apiResources;
+ }
+ }
+
+ private APIResourceCollection collectionWithAllScopeTypes() {
+
+ Map> apiResourcesByScopeType = new HashMap<>();
+ apiResourcesByScopeType.put(APIResourceCollectionManagementConstants.READ,
+ Collections.singletonList(buildApiResource("read-api")));
+ apiResourcesByScopeType.put(APIResourceCollectionManagementConstants.WRITE,
+ Collections.singletonList(buildApiResource("write-api")));
+ apiResourcesByScopeType.put(APIResourceCollectionManagementConstants.CREATE,
+ Collections.singletonList(buildApiResource("create-api")));
+ apiResourcesByScopeType.put(APIResourceCollectionManagementConstants.UPDATE,
+ Collections.singletonList(buildApiResource("update-api")));
+ apiResourcesByScopeType.put(APIResourceCollectionManagementConstants.DELETE,
+ Collections.singletonList(buildApiResource("delete-api")));
+
+ return new APIResourceCollection.APIResourceCollectionBuilder()
+ .id(COLLECTION_ID)
+ .name("Applications")
+ .displayName("Applications")
+ .type("tenant")
+ .apiResources(apiResourcesByScopeType)
+ .build();
+ }
+
+ private APIResource buildApiResource(String name) {
+
+ return new APIResource.APIResourceBuilder()
+ .id(name + "-id")
+ .name(name)
+ .description(name + " description")
+ .type("BUSINESS")
+ .build();
+ }
+}
diff --git a/components/org.wso2.carbon.identity.api.server.api.resource/org.wso2.carbon.identity.api.server.api.resource.v1/src/test/resources/testng.xml b/components/org.wso2.carbon.identity.api.server.api.resource/org.wso2.carbon.identity.api.server.api.resource.v1/src/test/resources/testng.xml
new file mode 100644
index 0000000000..72c3389482
--- /dev/null
+++ b/components/org.wso2.carbon.identity.api.server.api.resource/org.wso2.carbon.identity.api.server.api.resource.v1/src/test/resources/testng.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/org.wso2.carbon.identity.api.server.organization.selfservice/org.wso2.carbon.identity.api.server.organization.selfservice.v1/src/test/java/org/wso2/carbon/identity/api/server/organization/selfservice/v1/core/SelfServiceMgtServiceTest.java b/components/org.wso2.carbon.identity.api.server.organization.selfservice/org.wso2.carbon.identity.api.server.organization.selfservice.v1/src/test/java/org/wso2/carbon/identity/api/server/organization/selfservice/v1/core/SelfServiceMgtServiceTest.java
index a89c3edd00..7ee77c7f3f 100644
--- a/components/org.wso2.carbon.identity.api.server.organization.selfservice/org.wso2.carbon.identity.api.server.organization.selfservice.v1/src/test/java/org/wso2/carbon/identity/api/server/organization/selfservice/v1/core/SelfServiceMgtServiceTest.java
+++ b/components/org.wso2.carbon.identity.api.server.organization.selfservice/org.wso2.carbon.identity.api.server.organization.selfservice.v1/src/test/java/org/wso2/carbon/identity/api/server/organization/selfservice/v1/core/SelfServiceMgtServiceTest.java
@@ -23,15 +23,12 @@
import org.testng.annotations.Test;
import org.wso2.carbon.base.CarbonBaseConstants;
import org.wso2.carbon.context.PrivilegedCarbonContext;
-import org.wso2.carbon.identity.api.resource.mgt.APIResourceManager;
import org.wso2.carbon.identity.api.server.organization.selfservice.common.SelfServiceMgtServiceHolder;
import org.wso2.carbon.identity.api.server.organization.selfservice.v1.model.PropertyPatchReq;
import org.wso2.carbon.identity.api.server.organization.selfservice.v1.model.PropertyReq;
import org.wso2.carbon.identity.api.server.organization.selfservice.v1.model.PropertyRes;
import org.wso2.carbon.identity.api.server.organization.selfservice.v1.util.SelfServiceMgtConstants;
import org.wso2.carbon.identity.application.common.model.Property;
-import org.wso2.carbon.identity.application.mgt.ApplicationManagementService;
-import org.wso2.carbon.identity.application.mgt.AuthorizedAPIManagementService;
import org.wso2.carbon.identity.governance.IdentityGovernanceException;
import org.wso2.carbon.identity.governance.IdentityGovernanceService;
import org.wso2.carbon.identity.governance.bean.ConnectorConfig;
@@ -68,16 +65,13 @@ public class SelfServiceMgtServiceTest {
public void setUp() {
identityGovernanceService = mock(IdentityGovernanceService.class);
- ApplicationManagementService applicationManagementService = mock(ApplicationManagementService.class);
- APIResourceManager apiResourceManager = mock(APIResourceManager.class);
- AuthorizedAPIManagementService authorizedAPIManagementService = mock(AuthorizedAPIManagementService.class);
// Create SelfServiceMgtService with mock dependencies.
selfServiceMgtService = new SelfServiceMgtService(
identityGovernanceService,
- applicationManagementService,
- apiResourceManager,
- authorizedAPIManagementService
+ null,
+ null,
+ null
);
mockCarbonContext();
@@ -131,7 +125,7 @@ public void testGetOrganizationGovernanceConfigs() throws IdentityGovernanceExce
@Test
public void testUpdateOrganizationGovernanceConfigs() throws IdentityGovernanceException {
- PropertyPatchReq mockGovernanceConnector = mock(PropertyPatchReq.class);
+ PropertyPatchReq mockGovernanceConnector = new PropertyPatchReq();
List propertyReqs = new ArrayList<>();
// Update onboardAdminToSubOrg as false.
@@ -144,7 +138,7 @@ public void testUpdateOrganizationGovernanceConfigs() throws IdentityGovernanceE
property1.setValue(property1.getValue());
propertyReqs.add(property1);
- when(mockGovernanceConnector.getProperties()).thenReturn(propertyReqs);
+ mockGovernanceConnector.setProperties(propertyReqs);
Map configurationDetails = new HashMap<>();
configurationDetails.put(property1.getName(), property1.getValue());
diff --git a/pom.xml b/pom.xml
index 046285289f..5dcb387850 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1109,7 +1109,7 @@
1.4
1.2.4
1.11.103
- 7.11.159
+ 7.11.160
3.0.5
4.9.8.2
4.9.8