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 @@ -125,6 +125,22 @@
<artifactId>org.wso2.carbon.identity.oauth.rar</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>org.jacoco.agent</artifactId>
<classifier>runtime</classifier>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ public class APIResourceMap {

private List<APIResourceCollectionItem> write = null;

private List<APIResourceCollectionItem> create = null;

private List<APIResourceCollectionItem> update = null;

private List<APIResourceCollectionItem> delete = null;


/**
**/
Expand Down Expand Up @@ -92,7 +98,85 @@ public APIResourceMap addWriteItem(APIResourceCollectionItem writeItem) {
return this;
}


/**
**/
public APIResourceMap create(List<APIResourceCollectionItem> create) {

this.create = create;
return this;
}

@ApiModelProperty(value = "")
@JsonProperty("create")
@Valid
public List<APIResourceCollectionItem> getCreate() {
return create;
}
public void setCreate(List<APIResourceCollectionItem> create) {
this.create = create;
}

public APIResourceMap addCreateItem(APIResourceCollectionItem createItem) {
if (this.create == null) {
this.create = new ArrayList<APIResourceCollectionItem>();
}
this.create.add(createItem);
return this;
}

/**
**/
public APIResourceMap update(List<APIResourceCollectionItem> update) {

this.update = update;
return this;
}

@ApiModelProperty(value = "")
@JsonProperty("update")
@Valid
public List<APIResourceCollectionItem> getUpdate() {
return update;
}
public void setUpdate(List<APIResourceCollectionItem> update) {
this.update = update;
}

public APIResourceMap addUpdateItem(APIResourceCollectionItem updateItem) {
if (this.update == null) {
this.update = new ArrayList<APIResourceCollectionItem>();
}
this.update.add(updateItem);
return this;
}

/**
**/
public APIResourceMap delete(List<APIResourceCollectionItem> delete) {

this.delete = delete;
return this;
}

@ApiModelProperty(value = "")
@JsonProperty("delete")
@Valid
public List<APIResourceCollectionItem> getDelete() {
return delete;
}
public void setDelete(List<APIResourceCollectionItem> delete) {
this.delete = delete;
}

public APIResourceMap addDeleteItem(APIResourceCollectionItem deleteItem) {
if (this.delete == null) {
this.delete = new ArrayList<APIResourceCollectionItem>();
}
this.delete.add(deleteItem);
return this;
}



@Override
public boolean equals(java.lang.Object o) {
Expand All @@ -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
Expand All @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<APIResourceCollectionManagementUtil> utilMock =
mockStatic(APIResourceCollectionManagementUtil.class);
MockedStatic<ContextLoader> 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<String, List<APIResource>> 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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >

<suite name="api-resource-rest-api">

<test name="api-resource-collection-management-tests" preserve-order="false" parallel="false">
<classes>
<class name="org.wso2.carbon.identity.api.server.api.resource.v1.core.ServerAPIResourceCollectionManagementServiceTest"/>
</classes>
</test>
</suite>
Loading
Loading