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