diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/common/RESTTestBase.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/common/RESTTestBase.java index 9c35ada9f77..9f84b830c66 100644 --- a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/common/RESTTestBase.java +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/common/RESTTestBase.java @@ -338,6 +338,24 @@ protected Response getResponseOfGetWithOAuth2(String endpointURL, String accessT .get(endpointURL); } + /** + * Invoke given endpointURL with params for GET with OAuth2 authentication, using the provided token. + * + * @param endpointURL Endpoint to be invoked. + * @param accessToken OAuth2 access token. + * @param queryParams request query parameters + * @return Response. + */ + protected Response getResponseOfGetWithOAuth2(String endpointURL, String accessToken, + Map queryParams) { + + return given().auth().preemptive().oauth2(accessToken) + .header(HttpHeaders.ACCEPT, ContentType.JSON) + .queryParams(queryParams) + .when() + .get(endpointURL); + } + /** * Invoke given endpointUri for GET without authentication. * diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/ApplicationManagementBaseTest.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/ApplicationManagementBaseTest.java index 82672472b9e..65444e7fe14 100644 --- a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/ApplicationManagementBaseTest.java +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/ApplicationManagementBaseTest.java @@ -1,25 +1,34 @@ /* - * Copyright (c) 2019, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2019-2025, WSO2 LLC. (https://www.wso2.com). * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * 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 + * 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. + * 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.identity.integration.test.rest.api.server.application.management.v1; import io.restassured.RestAssured; import io.restassured.response.Response; + +import java.io.IOException; +import java.util.Set; + import org.apache.commons.lang.StringUtils; import org.apache.http.HttpStatus; +import org.json.JSONArray; import org.json.JSONException; +import org.json.JSONObject; import org.testng.Assert; import org.testng.annotations.AfterClass; import org.testng.annotations.AfterMethod; @@ -28,9 +37,8 @@ import org.testng.annotations.DataProvider; import org.wso2.carbon.automation.engine.context.TestUserMode; import org.wso2.identity.integration.test.rest.api.server.common.RESTAPIServerTestBase; - -import java.io.IOException; -import java.util.Set; +import org.wso2.identity.integration.test.rest.api.user.common.model.GroupRequestObject; +import org.wso2.identity.integration.test.restclients.SCIM2RestClient; /** * Base test class for Application Management REST APIs. @@ -45,9 +53,11 @@ public class ApplicationManagementBaseTest extends RESTAPIServerTestBase { static final String RESIDENT_APP_API_BASE_PATH = APPLICATION_MANAGEMENT_API_BASE_PATH + "/resident"; static final String APPLICATION_TEMPLATE_MANAGEMENT_API_BASE_PATH = APPLICATION_MANAGEMENT_API_BASE_PATH + "/templates"; + static final String GROUPS_METADATA_PATH = METADATA_API_BASE_PATH + "/groups"; static final String PATH_SEPARATOR = "/"; protected static String swaggerDefinition; + private SCIM2RestClient scim2RestClient; static { String API_PACKAGE_NAME = "org.wso2.carbon.identity.api.server.application.management.v1"; @@ -72,12 +82,14 @@ public ApplicationManagementBaseTest(TestUserMode userMode) throws Exception { public void init() throws IOException { super.testInit(API_VERSION, swaggerDefinition, tenant); + scim2RestClient = new SCIM2RestClient(serverURL, tenantInfo); } @AfterClass(alwaysRun = true) public void testConclude() throws Exception { super.conclude(); + scim2RestClient.closeHttpClient(); } @BeforeMethod(alwaysRun = true) @@ -116,4 +128,85 @@ protected void cleanUpApplications(Set appsToCleanUp) { getResponseOfGet(applicationPath).then().assertThat().statusCode(HttpStatus.SC_NOT_FOUND); }); } + + /** + * Create a set of groups and return the group IDs. + * + * @param groupCount The number of groups to be created. + * @param groupNamePrefix The prefix of the group name. + * @return The group IDs. + * @throws Exception If an error occurs while creating the groups. + */ + protected String[] createGroups(int groupCount, String groupNamePrefix) throws Exception { + + String[] groupIDs = new String[groupCount]; + for (int i = 0; i < groupCount; i++) { + String groupName = groupNamePrefix + i; + groupIDs[i] = scim2RestClient.createGroup(new GroupRequestObject().displayName(groupName)); + } + return groupIDs; + } + + /** + * Delete the groups with the given group IDs. + * + * @param groupIDs The group IDs. + * @throws Exception If an error occurs while deleting the groups. + */ + protected void deleteGroups(String[] groupIDs) throws Exception { + + for (String groupID : groupIDs) { + scim2RestClient.deleteGroup(groupID); + } + } + + /** + * Add discoverable groups to the application payload. + * + * @param payload Application payload. + * @param userStore User store. + * @param groupIDs Group IDs. + * @throws JSONException If an error occurs while adding discoverable groups to the payload. + */ + protected JSONObject addDiscoverableGroupsToApplicationPayload(JSONObject payload, String userStore, + String[] groupIDs) + throws JSONException { + + JSONObject discoverableGroup = new JSONObject(); + discoverableGroup.put("userStore", userStore); + JSONArray groups = new JSONArray(); + for (String groupID : groupIDs) { + JSONObject group = new JSONObject(); + group.put("id", groupID); + groups.put(group); + } + discoverableGroup.put("groups", groups); + JSONArray discoverableGroups = new JSONArray(); + discoverableGroups.put(discoverableGroup); + JSONObject advancedConfigs = payload.getJSONObject("advancedConfigurations"); + advancedConfigs.put("discoverableGroups", discoverableGroups); + return payload; + } + + /** + * Verify the discoverable groups in the response. + * + * @param response The response. + * @param userStore User store. + * @param groupIDs Group IDs. + * @throws JSONException If an error occurs while verifying the discoverable groups. + */ + protected void verifyDiscoverableGroups(JSONObject response, String userStore, String[] groupIDs) + throws JSONException { + + JSONArray discoverableGroups = response.getJSONArray("discoverableGroups"); + Assert.assertEquals(discoverableGroups.length(), 1, "Discoverable groups count mismatched."); + JSONObject discoverableGroup = discoverableGroups.getJSONObject(0); + Assert.assertEquals(discoverableGroup.getString("userStore"), userStore, "User store mismatched."); + JSONArray groups = discoverableGroup.getJSONArray("groups"); + Assert.assertEquals(groups.length(), groupIDs.length, "Group count mismatched."); + for (String groupID : groupIDs) { + Assert.assertTrue(groups.toString().contains(groupID), "Group ID not found in the response."); + } + } } diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/ApplicationManagementFailureTest.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/ApplicationManagementFailureTest.java index 01ba22a680a..fabf9f5b38a 100644 --- a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/ApplicationManagementFailureTest.java +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/ApplicationManagementFailureTest.java @@ -1,34 +1,42 @@ /* - * Copyright (c) 2019, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2019-2025, WSO2 LLC. (https://www.wso2.com). * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * 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 + * 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. + * 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.identity.integration.test.rest.api.server.application.management.v1; import io.restassured.response.Response; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + import org.apache.http.HttpHeaders; import org.apache.http.HttpStatus; +import org.hamcrest.Matchers; import org.json.JSONException; import org.json.JSONObject; import org.testng.annotations.AfterMethod; +import org.testng.annotations.DataProvider; import org.testng.annotations.Factory; import org.testng.annotations.Test; import org.wso2.carbon.automation.engine.context.TestUserMode; import org.wso2.carbon.identity.application.mgt.ApplicationConstants; -import java.util.HashSet; -import java.util.Set; - import static org.hamcrest.core.IsNull.notNullValue; import static org.wso2.identity.integration.test.rest.api.server.application.management.v1.Utils.assertNotBlank; import static org.wso2.identity.integration.test.rest.api.server.application.management.v1.Utils.extractApplicationIdFromLocationHeader; @@ -203,6 +211,32 @@ public void testCreateDiscoverableAppWithoutAccessUrl() throws Exception { validateErrorResponse(response, HttpStatus.SC_BAD_REQUEST, "APP-60001"); } + @Test(description = "Test configuring invalid discoverable groups for an application.") + public void testAddInvalidDiscoverableGroups() throws Exception { + + String payload = readResource("invalid-discoverable-groups.json"); + Response response = getResponseOfPost(APPLICATION_MANAGEMENT_API_BASE_PATH, payload); + validateErrorResponse(response, HttpStatus.SC_BAD_REQUEST, "APP-60001"); + response.then() + .body("description", Matchers.equalTo(String.format( + "Invalid application configuration for application: 'Test Invalid App' of tenantDomain: %s." + + " No group found for the given group ID: 'invalid-id-1'. No group found for the" + + " given group ID: 'invalid-id-2'. The provided user store: 'INVALID_USER_STORE_1'" + + " is not found.", + tenant))); + JSONObject applicationPayload = new JSONObject(payload); + JSONObject advancedConfigs = applicationPayload.getJSONObject("advancedConfigurations"); + advancedConfigs.put("discoverableByEndUsers", false); + payload = applicationPayload.toString(); + response = getResponseOfPost(APPLICATION_MANAGEMENT_API_BASE_PATH, payload); + validateErrorResponse(response, HttpStatus.SC_BAD_REQUEST, "APP-60001"); + response.then() + .body("description", Matchers.equalTo(String.format( + "Invalid application configuration for application: 'Test Invalid App' of tenantDomain: %s." + + " Discoverable groups are defined for a non-discoverable application.", + tenant))); + } + @Test(description = "Tests whether inbound unique key is validated during application creation.") public void testCreateApplicationsWithConflictingInboundKeys() throws Exception { @@ -233,6 +267,36 @@ public void testCreateApplicationsWithInvalidAllowedOrigins() throws Exception { validateErrorResponse(createAppResponse, HttpStatus.SC_BAD_REQUEST, "APP-60001"); } + @Test(description = "Test groups metadata endpoint with an invalid domain.") + public void testGetGroupsMetadataWithInvalidDomain() { + + Map queryParam = new HashMap<>(); + queryParam.put("domain", "invalid"); + Response response = getResponseOfGetWithQueryParams(GROUPS_METADATA_PATH, queryParam); + validateErrorResponse(response, HttpStatus.SC_BAD_REQUEST, "APP-60001"); + } + + @DataProvider(name = "testGetGroupsMetadataWithInvalidFilters") + public Object[][] testGetGroupsMetadataWithInvalidFilters() { + + return new Object[][]{ + { "name eq Group_1_1" }, + { "name sw Gro" }, + { "name ew _1" }, + { "id co 12" } + }; + } + + @Test(description = "Test groups metadata endpoint with invalid filters.", + dataProvider = "testGetGroupsMetadataWithInvalidFilters") + public void testGetGroupsMetadataWithInvalidFilters(String filter) { + + Map queryParam = new HashMap<>(); + queryParam.put("filter", filter); + Response response = getResponseOfGetWithQueryParams(GROUPS_METADATA_PATH, queryParam); + validateErrorResponse(response, HttpStatus.SC_BAD_REQUEST, "APP-60004"); + } + private String getApplicationId(Response createFirstAppResponse) { String location = createFirstAppResponse.getHeader(HttpHeaders.LOCATION); diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/ApplicationManagementSuccessTest.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/ApplicationManagementSuccessTest.java index fef332c2ce5..8fb1d992e86 100644 --- a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/ApplicationManagementSuccessTest.java +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/ApplicationManagementSuccessTest.java @@ -1,31 +1,42 @@ /* - * Copyright (c) 2019-2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2019-2025, WSO2 LLC. (https://www.wso2.com). * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * 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 + * 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. + * 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.identity.integration.test.rest.api.server.application.management.v1; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.databind.ObjectMapper; -import com.nimbusds.oauth2.sdk.util.URLUtils; + import io.restassured.response.Response; -import java.util.Arrays; -import java.util.List; + +import java.util.HashMap; +import java.io.IOException; +import java.util.Map; import java.util.Optional; + import org.apache.commons.lang.StringUtils; import org.apache.http.HttpHeaders; import org.apache.http.HttpStatus; +import org.hamcrest.Matchers; +import org.json.JSONObject; import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; import org.testng.annotations.Factory; import org.testng.annotations.Test; import org.wso2.carbon.automation.engine.context.TestUserMode; @@ -33,16 +44,13 @@ import org.wso2.carbon.utils.multitenancy.MultitenantConstants; import org.wso2.identity.integration.test.rest.api.server.application.management.v1.model.ApplicationListItem; import org.wso2.identity.integration.test.rest.api.server.application.management.v1.model.ApplicationListResponse; - -import java.io.IOException; -import java.net.URLEncoder; -import java.util.HashMap; -import java.util.Map; +import org.wso2.identity.integration.test.rest.api.server.application.management.v1.model.GroupBasicInfo; import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsNull.notNullValue; -import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; import static org.wso2.identity.integration.test.rest.api.server.application.management.v1.Utils.assertNotBlank; import static org.wso2.identity.integration.test.rest.api.server.application.management.v1.Utils.extractApplicationIdFromLocationHeader; @@ -56,7 +64,10 @@ public class ApplicationManagementSuccessTest extends ApplicationManagementBaseT private static final String CREATED_APP_NAME = "My SAMPLE APP"; private static final String CREATED_APP_TEMPLATE_ID = "Test_template_1"; private static final String CREATED_APP_TEMPLATE_VERSION = "v1.0.0"; + private static final int GROUPS_COUNT = 2; + private static final String GROUP_NAME_PREFIX = "Group_1_"; private String createdAppId; + private String[] groupIDs; @Factory(dataProvider = "restAPIUserConfigProvider") public ApplicationManagementSuccessTest(TestUserMode userMode) throws Exception { @@ -64,6 +75,20 @@ public ApplicationManagementSuccessTest(TestUserMode userMode) throws Exception super(userMode); } + @BeforeClass(alwaysRun = true) + public void testStart() throws Exception { + + super.init(); + groupIDs = super.createGroups(GROUPS_COUNT, GROUP_NAME_PREFIX); + } + + @AfterClass(alwaysRun = true) + public void testEnd() throws Exception { + + super.deleteGroups(groupIDs); + super.testConclude(); + } + @Test public void testGetAllApplications() throws IOException { @@ -128,6 +153,7 @@ public void testGetResidentApplication() throws IOException { public void createApplication() throws Exception { String body = readResource("create-basic-application.json"); + body = super.addDiscoverableGroupsToApplicationPayload(new JSONObject(body), "PRIMARY", groupIDs).toString(); Response responseOfPost = getResponseOfPost(APPLICATION_MANAGEMENT_API_BASE_PATH, body); responseOfPost.then() .log().ifValidationFails() @@ -144,7 +170,7 @@ public void createApplication() throws Exception { public void testGetAllApplicationsWithParams() throws IOException { Map params = new HashMap<>(); - params.put("attributes", "templateId,templateVersion"); + params.put("attributes", "templateId,templateVersion,advancedConfigurations"); Response response = getResponseOfGet(APPLICATION_MANAGEMENT_API_BASE_PATH, params); response.then() .log().ifValidationFails() @@ -162,6 +188,19 @@ public void testGetAllApplicationsWithParams() throws IOException { "Newly Created application '" + CREATED_APP_NAME + "' is not listed by the API."); Assert.assertEquals(newlyCreatedAppData.get().getTemplateId(), CREATED_APP_TEMPLATE_ID); Assert.assertEquals(newlyCreatedAppData.get().getTemplateVersion(), CREATED_APP_TEMPLATE_VERSION); + Assert.assertEquals(newlyCreatedAppData.get().getAdvancedConfigurations().getDiscoverableGroups().size(), 1); + Assert.assertEquals( + newlyCreatedAppData.get().getAdvancedConfigurations().getDiscoverableGroups().get(0).getUserStore(), + "PRIMARY"); + Assert.assertEquals( + newlyCreatedAppData.get().getAdvancedConfigurations().getDiscoverableGroups().get(0).getGroups() + .size(), + GROUPS_COUNT); + Assert.assertEqualsNoOrder( + newlyCreatedAppData.get().getAdvancedConfigurations().getDiscoverableGroups().get(0).getGroups() + .stream() + .map(GroupBasicInfo::getId).toArray(), + groupIDs); } @Test(dependsOnMethods = {"createApplication"}) @@ -174,7 +213,11 @@ public void testGetApplicationById() throws Exception { .statusCode(HttpStatus.SC_OK) .body("name", equalTo(CREATED_APP_NAME)) .body("templateId", equalTo(CREATED_APP_TEMPLATE_ID)) - .body("templateVersion", equalTo(CREATED_APP_TEMPLATE_VERSION)); + .body("templateVersion", equalTo(CREATED_APP_TEMPLATE_VERSION)) + .body("advancedConfigurations.discoverableGroups", hasSize(1)) + .body("advancedConfigurations.discoverableGroups[0].userStore", Matchers.equalTo("PRIMARY")) + .body("advancedConfigurations.discoverableGroups[0].groups[0].id", Matchers.oneOf(groupIDs)) + .body("advancedConfigurations.discoverableGroups[0].groups[1].id", Matchers.oneOf(groupIDs)); } @Test(dependsOnMethods = {"createApplication"}) @@ -342,4 +385,37 @@ public void testDeleteApplicationById() throws Exception { .assertThat() .statusCode(HttpStatus.SC_NOT_FOUND); } + + @DataProvider(name = "testGetGroupsMetadataFromApplicationEndpoint") + public Object[][] testGetGroupsMetadataFromApplicationEndpoint() { + + return new Object[][]{ + { null, null, GROUPS_COUNT }, + { null, "name co Gro", GROUPS_COUNT }, + { null,"name co rou", GROUPS_COUNT }, + { null, "name co adm", 1 }, + { null, "name co Group_1_1", 1 }, + { "PRIMARY", "name co Gro", GROUPS_COUNT } + }; + } + + @Test(description = "Test to get groups metadata from application endpoint.", + dataProvider = "testGetGroupsMetadataFromApplicationEndpoint") + public void testGetGroupsMetadataFromApplicationEndpoint(String domain, String filter, int expectedGroupCount) { + + Map queryParams = new HashMap<>(); + if (filter != null) { + queryParams.put("filter", filter); + } + if (domain != null) { + queryParams.put("domain", domain); + } + Response response = getResponseOfGetWithQueryParams(GROUPS_METADATA_PATH, queryParams); + response.then() + .log().ifValidationFails() + .assertThat() + .statusCode(HttpStatus.SC_OK) + .body("size()", + domain == null && filter == null ? greaterThan(expectedGroupCount) : is(expectedGroupCount)); + } } diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/ApplicationPatchTest.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/ApplicationPatchTest.java index fa7384ed486..39b98a4e45f 100644 --- a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/ApplicationPatchTest.java +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/ApplicationPatchTest.java @@ -1,30 +1,37 @@ /* - * Copyright (c) 2019-2024, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2019-2025, WSO2 LLC. (https://www.wso2.com). * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * 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 + * 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. + * 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.identity.integration.test.rest.api.server.application.management.v1; import io.restassured.response.Response; + import org.apache.http.HttpHeaders; import org.apache.http.HttpStatus; import org.hamcrest.Matchers; import org.json.JSONObject; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Factory; import org.testng.annotations.Test; import org.wso2.carbon.automation.engine.context.TestUserMode; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.core.IsNull.notNullValue; import static org.hamcrest.core.IsNull.nullValue; import static org.wso2.identity.integration.test.rest.api.server.application.management.v1.Utils.assertNotBlank; @@ -40,7 +47,10 @@ public class ApplicationPatchTest extends ApplicationManagementBaseTest { private static final String APP_TEMPLATE_ID = "Test_template_1"; private static final String APP_TEMPLATE_VERSION = "v1.0.0"; public static final String SUBJECT_CLAIM_URI = "http://wso2.org/claims/username"; + private static final int GROUPS_COUNT = 2; + private static final String GROUP_NAME_PREFIX = "Group_2_"; private String appId; + private String[] groupIDs; @Factory(dataProvider = "restAPIUserConfigProvider") public ApplicationPatchTest(TestUserMode userMode) throws Exception { @@ -48,6 +58,20 @@ public ApplicationPatchTest(TestUserMode userMode) throws Exception { super(userMode); } + @BeforeClass(alwaysRun = true) + public void testStart() throws Exception { + + super.init(); + groupIDs = super.createGroups(GROUPS_COUNT, GROUP_NAME_PREFIX); + } + + @AfterClass(alwaysRun = true) + public void testEnd() throws Exception { + + super.deleteGroups(groupIDs); + super.testConclude(); + } + @Test public void testCreateApplication() throws Exception { @@ -137,6 +161,9 @@ public void testUpdateAdvancedConfiguration() throws Exception { // Do the PATCH update request. String patchRequest = readResource("patch-application-advanced-configuration.json"); + patchRequest = + super.addDiscoverableGroupsToApplicationPayload(new JSONObject(patchRequest), "PRIMARY", groupIDs) + .toString(); String path = APPLICATION_MANAGEMENT_API_BASE_PATH + "/" + appId; getResponseOfPatch(path, patchRequest.toString()).then() .assertThat() @@ -159,7 +186,11 @@ public void testUpdateAdvancedConfiguration() throws Exception { .body("advancedConfigurations.trustedAppConfiguration.find{ it.key == 'androidThumbprints' }.value", Matchers.hasItem("sampleThumbprint")) .body("advancedConfigurations.trustedAppConfiguration.find{ it.key == 'appleAppId' }.value", - equalTo("sample.app.id")); + equalTo("sample.app.id")) + .body("advancedConfigurations.discoverableGroups", hasSize(1)) + .body("advancedConfigurations.discoverableGroups[0].userStore", equalTo("PRIMARY")) + .body("advancedConfigurations.discoverableGroups[0].groups[0].id", Matchers.oneOf(groupIDs)) + .body("advancedConfigurations.discoverableGroups[0].groups[1].id", Matchers.oneOf(groupIDs)); } @Test(description = "Test updating the claim configuration of an application", diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/model/AccessTokenConfiguration.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/model/AccessTokenConfiguration.java index 62c416e78e5..7e843375d55 100644 --- a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/model/AccessTokenConfiguration.java +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/model/AccessTokenConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, WSO2 LLC. (http://www.wso2.com). + * Copyright (c) 2019-2025, 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 @@ -27,22 +27,24 @@ import javax.validation.Valid; public class AccessTokenConfiguration { - + private String type; private Long userAccessTokenExpiryInSeconds; private Long applicationAccessTokenExpiryInSeconds; + private String bindingType = "None"; private Boolean revokeTokensWhenIDPSessionTerminated; private Boolean validateTokenBinding; private List accessTokenAttributes = null; + /** - **/ + **/ public AccessTokenConfiguration type(String type) { this.type = type; return this; } - + @ApiModelProperty(example = "JWT", value = "") @JsonProperty("type") @Valid @@ -54,13 +56,13 @@ public void setType(String type) { } /** - **/ + **/ public AccessTokenConfiguration userAccessTokenExpiryInSeconds(Long userAccessTokenExpiryInSeconds) { this.userAccessTokenExpiryInSeconds = userAccessTokenExpiryInSeconds; return this; } - + @ApiModelProperty(example = "3600", value = "") @JsonProperty("userAccessTokenExpiryInSeconds") @Valid @@ -72,13 +74,13 @@ public void setUserAccessTokenExpiryInSeconds(Long userAccessTokenExpiryInSecond } /** - **/ + **/ public AccessTokenConfiguration applicationAccessTokenExpiryInSeconds(Long applicationAccessTokenExpiryInSeconds) { this.applicationAccessTokenExpiryInSeconds = applicationAccessTokenExpiryInSeconds; return this; } - + @ApiModelProperty(example = "3600", value = "") @JsonProperty("applicationAccessTokenExpiryInSeconds") @Valid @@ -90,24 +92,26 @@ public void setApplicationAccessTokenExpiryInSeconds(Long applicationAccessToken } /** + * OAuth2 access token and refresh token can be bound to an external attribute during the token generation so that it can be optionally validated during the API invocation. **/ - public AccessTokenConfiguration validateTokenBinding(Boolean validateTokenBinding) { + public AccessTokenConfiguration bindingType(String bindingType) { - this.validateTokenBinding = validateTokenBinding; + this.bindingType = bindingType; return this; } - @ApiModelProperty(example = "false", value = "") - @JsonProperty("validateTokenBinding") + @ApiModelProperty(example = "cookie", value = "OAuth2 access token and refresh token can be bound to an external attribute during the token generation so that it can be optionally validated during the API invocation.") + @JsonProperty("bindingType") @Valid - public boolean getValidateTokenBinding() { - return validateTokenBinding; + public String getBindingType() { + return bindingType; } - public void setValidateTokenBinding(Boolean validateTokenBinding) { - this.validateTokenBinding = validateTokenBinding; + public void setBindingType(String bindingType) { + this.bindingType = bindingType; } /** + * If enabled, when the IDP session is terminated, all the access tokens bound to the session will get revoked. **/ public AccessTokenConfiguration revokeTokensWhenIDPSessionTerminated(Boolean revokeTokensWhenIDPSessionTerminated) { @@ -115,16 +119,35 @@ public AccessTokenConfiguration revokeTokensWhenIDPSessionTerminated(Boolean rev return this; } - @ApiModelProperty(example = "false", value = "") + @ApiModelProperty(value = "If enabled, when the IDP session is terminated, all the access tokens bound to the session will get revoked.") @JsonProperty("revokeTokensWhenIDPSessionTerminated") @Valid - public boolean getRevokeTokensWhenIDPSessionTerminated() { + public Boolean getRevokeTokensWhenIDPSessionTerminated() { return revokeTokensWhenIDPSessionTerminated; } public void setRevokeTokensWhenIDPSessionTerminated(Boolean revokeTokensWhenIDPSessionTerminated) { this.revokeTokensWhenIDPSessionTerminated = revokeTokensWhenIDPSessionTerminated; } + /** + * If enabled, both access token and the token binding needs to be present for a successful API invocation. + **/ + public AccessTokenConfiguration validateTokenBinding(Boolean validateTokenBinding) { + + this.validateTokenBinding = validateTokenBinding; + return this; + } + + @ApiModelProperty(value = "If enabled, both access token and the token binding needs to be present for a successful API invocation.") + @JsonProperty("validateTokenBinding") + @Valid + public Boolean getValidateTokenBinding() { + return validateTokenBinding; + } + public void setValidateTokenBinding(Boolean validateTokenBinding) { + this.validateTokenBinding = validateTokenBinding; + } + /** **/ public AccessTokenConfiguration accessTokenAttributes(List accessTokenAttributes) { @@ -151,8 +174,10 @@ public AccessTokenConfiguration addAccessTokenAttributesItem(String accessTokenA return this; } + + @Override - public boolean equals(Object o) { + public boolean equals(java.lang.Object o) { if (this == o) { return true; @@ -162,15 +187,17 @@ public boolean equals(Object o) { } AccessTokenConfiguration accessTokenConfiguration = (AccessTokenConfiguration) o; return Objects.equals(this.type, accessTokenConfiguration.type) && - Objects.equals(this.userAccessTokenExpiryInSeconds, accessTokenConfiguration.userAccessTokenExpiryInSeconds) && - Objects.equals(this.applicationAccessTokenExpiryInSeconds, accessTokenConfiguration.applicationAccessTokenExpiryInSeconds) && + Objects.equals(this.userAccessTokenExpiryInSeconds, accessTokenConfiguration.userAccessTokenExpiryInSeconds) && + Objects.equals(this.applicationAccessTokenExpiryInSeconds, accessTokenConfiguration.applicationAccessTokenExpiryInSeconds) && + Objects.equals(this.bindingType, accessTokenConfiguration.bindingType) && + Objects.equals(this.revokeTokensWhenIDPSessionTerminated, accessTokenConfiguration.revokeTokensWhenIDPSessionTerminated) && + Objects.equals(this.validateTokenBinding, accessTokenConfiguration.validateTokenBinding) && Objects.equals(this.accessTokenAttributes, accessTokenConfiguration.accessTokenAttributes); } @Override public int hashCode() { - return Objects.hash(type, userAccessTokenExpiryInSeconds, applicationAccessTokenExpiryInSeconds, - accessTokenAttributes); + return Objects.hash(type, userAccessTokenExpiryInSeconds, applicationAccessTokenExpiryInSeconds, bindingType, revokeTokensWhenIDPSessionTerminated, validateTokenBinding, accessTokenAttributes); } @Override @@ -182,16 +209,19 @@ public String toString() { sb.append(" type: ").append(toIndentedString(type)).append("\n"); sb.append(" userAccessTokenExpiryInSeconds: ").append(toIndentedString(userAccessTokenExpiryInSeconds)).append("\n"); sb.append(" applicationAccessTokenExpiryInSeconds: ").append(toIndentedString(applicationAccessTokenExpiryInSeconds)).append("\n"); + sb.append(" bindingType: ").append(toIndentedString(bindingType)).append("\n"); + sb.append(" revokeTokensWhenIDPSessionTerminated: ").append(toIndentedString(revokeTokensWhenIDPSessionTerminated)).append("\n"); + sb.append(" validateTokenBinding: ").append(toIndentedString(validateTokenBinding)).append("\n"); sb.append(" accessTokenAttributes: ").append(toIndentedString(accessTokenAttributes)).append("\n"); sb.append("}"); return sb.toString(); } /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). - */ - private String toIndentedString(Object o) { + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(java.lang.Object o) { if (o == null) { return "null"; diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/model/AdvancedApplicationConfiguration.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/model/AdvancedApplicationConfiguration.java index 6d5514fec93..32b3b23692b 100644 --- a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/model/AdvancedApplicationConfiguration.java +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/model/AdvancedApplicationConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, WSO2 LLC. (http://www.wso2.com). + * Copyright (c) 2019-2025, 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 @@ -22,6 +22,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.annotations.ApiModelProperty; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import javax.validation.Valid; @@ -31,6 +32,7 @@ public class AdvancedApplicationConfiguration { private Boolean saas; private Boolean discoverableByEndUsers; + private List discoverableGroups = null; private Certificate certificate; private Boolean skipLoginConsent; private Boolean skipLogoutConsent; @@ -83,6 +85,37 @@ public void setDiscoverableByEndUsers(Boolean discoverableByEndUsers) { this.discoverableByEndUsers = discoverableByEndUsers; } + /** + * List of groups from user stores where users in those groups can discover the application. + **/ + public AdvancedApplicationConfiguration discoverableGroups(List discoverableGroups) { + + this.discoverableGroups = discoverableGroups; + return this; + } + + @ApiModelProperty(value = "List of groups from user stores where users in those groups can discover the application.") + @JsonProperty("discoverableGroups") + @Valid + public List getDiscoverableGroups() { + + return discoverableGroups; + } + + public void setDiscoverableGroups(List discoverableGroups) { + + this.discoverableGroups = discoverableGroups; + } + + public AdvancedApplicationConfiguration addDiscoverableGroupsItem(DiscoverableGroup discoverableGroupsItem) { + + if (this.discoverableGroups == null) { + this.discoverableGroups = new ArrayList<>(); + } + this.discoverableGroups.add(discoverableGroupsItem); + return this; + } + /** **/ public AdvancedApplicationConfiguration certificate(Certificate certificate) { diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/model/ApplicationModel.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/model/ApplicationModel.java index ae6cd802ba8..cdd091cac12 100644 --- a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/model/ApplicationModel.java +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/model/ApplicationModel.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, WSO2 LLC. (http://www.wso2.com). + * Copyright (c) 2019-2025, 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 @@ -122,7 +122,7 @@ public ApplicationModel accessUrl(String accessUrl) { return this; } - @ApiModelProperty(example = "https://example.com/access", value = "") + @ApiModelProperty(example = "https://example.com/home", value = "") @JsonProperty("accessUrl") @Valid public String getAccessUrl() { diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/model/DiscoverableGroup.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/model/DiscoverableGroup.java new file mode 100644 index 00000000000..ac18d8a0ec7 --- /dev/null +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/model/DiscoverableGroup.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2025, 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.identity.integration.test.rest.api.server.application.management.v1.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import java.util.ArrayList; +import java.util.List; +import org.wso2.identity.integration.test.rest.api.server.application.management.v1.model.GroupBasicInfo; +import javax.validation.constraints.*; + + +import io.swagger.annotations.*; +import java.util.Objects; +import javax.validation.Valid; +import javax.xml.bind.annotation.*; + +public class DiscoverableGroup { + + private String userStore; + private List groups = new ArrayList<>(); + + + /** + * The user store domain to which the groups belong. + **/ + public DiscoverableGroup userStore(String userStore) { + + this.userStore = userStore; + return this; + } + + @ApiModelProperty(example = "PRIMARY", required = true, value = "The user store domain to which the groups belong.") + @JsonProperty("userStore") + @Valid + @NotNull(message = "Property userStore cannot be null.") + public String getUserStore() { + + return userStore; + } + + public void setUserStore(String userStore) { + + this.userStore = userStore; + } + + /** + * List of groups configured for discoverability. + **/ + public DiscoverableGroup groups(List groups) { + + this.groups = groups; + return this; + } + + @ApiModelProperty(required = true, value = "List of groups configured for discoverability.") + @JsonProperty("groups") + @Valid + @NotNull(message = "Property groups cannot be null.") + @Size(min=1) + public List getGroups() { + + return groups; + } + + public void setGroups(List groups) { + + this.groups = groups; + } + + public DiscoverableGroup addGroupsItem(GroupBasicInfo groupsItem) { + + this.groups.add(groupsItem); + return this; + } + + + + @Override + public boolean equals(java.lang.Object o) { + + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DiscoverableGroup discoverableGroup = (DiscoverableGroup) o; + return Objects.equals(this.userStore, discoverableGroup.userStore) && + Objects.equals(this.groups, discoverableGroup.groups); + } + + @Override + public int hashCode() { + + return Objects.hash(userStore, groups); + } + + @Override + public String toString() { + + StringBuilder sb = new StringBuilder(); + sb.append("class DiscoverableGroup {\n"); + sb.append(" userStore: ").append(toIndentedString(userStore)).append("\n"); + sb.append(" groups: ").append(toIndentedString(groups)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(java.lang.Object o) { + + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n"); + } +} diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/model/GroupBasicInfo.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/model/GroupBasicInfo.java new file mode 100644 index 00000000000..ee0353a71aa --- /dev/null +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/server/application/management/v1/model/GroupBasicInfo.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2025, 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.identity.integration.test.rest.api.server.application.management.v1.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import javax.validation.constraints.*; + + +import io.swagger.annotations.*; +import java.util.Objects; +import javax.validation.Valid; +import javax.xml.bind.annotation.*; + +public class GroupBasicInfo { + + private String id; + private String name; + + /** + * The unique identifier of the group. + **/ + public GroupBasicInfo id(String id) { + + this.id = id; + return this; + } + + @ApiModelProperty(example = "bf5abd05-3667-4a2a-a6c2-2fb9f4d26e47", required = true, value = "The unique identifier of the group.") + @JsonProperty("id") + @Valid + @NotNull(message = "Property id cannot be null.") + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + /** + * The name of the group. + **/ + public GroupBasicInfo name(String name) { + + this.name = name; + return this; + } + + @ApiModelProperty(example = "GroupA", value = "The name of the group.") + @JsonProperty("name") + @Valid + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + + + + @Override + public boolean equals(java.lang.Object o) { + + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GroupBasicInfo groupBasicInfo = (GroupBasicInfo) o; + return Objects.equals(this.id, groupBasicInfo.id) && + Objects.equals(this.name, groupBasicInfo.name); + } + + @Override + public int hashCode() { + return Objects.hash(id, name); + } + + @Override + public String toString() { + + StringBuilder sb = new StringBuilder(); + sb.append("class GroupBasicInfo {\n"); + + sb.append(" id: ").append(toIndentedString(id)).append("\n"); + sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(java.lang.Object o) { + + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n"); + } +} diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/user/application/v1/UserDiscoverableApplicationFailureTest.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/user/application/v1/UserDiscoverableApplicationFailureTest.java index 12ea7efaafc..3e76616f287 100644 --- a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/user/application/v1/UserDiscoverableApplicationFailureTest.java +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/user/application/v1/UserDiscoverableApplicationFailureTest.java @@ -1,7 +1,7 @@ /* - * Copyright (c) 2019, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2019-2025, WSO2 LLC. (http://www.wso2.com). * - * WSO2 Inc. licenses this file to you under the Apache License, + * 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 @@ -20,6 +20,7 @@ import io.restassured.response.Response; import org.apache.http.HttpStatus; +import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.DataProvider; import org.testng.annotations.Factory; @@ -55,7 +56,12 @@ public static Object[][] restAPIUserConfigProvider() { public void testStart() throws Exception { super.testInit(API_VERSION, swaggerDefinition, tenant); - super.testStart(); + } + + @AfterClass(alwaysRun = true) + public void testEnd() { + + super.conclude(); } @Test diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/user/application/v1/UserDiscoverableApplicationServiceTestBase.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/user/application/v1/UserDiscoverableApplicationServiceTestBase.java index 5a4b63a578a..9bffb664bfc 100644 --- a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/user/application/v1/UserDiscoverableApplicationServiceTestBase.java +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/user/application/v1/UserDiscoverableApplicationServiceTestBase.java @@ -1,7 +1,7 @@ /* - * Copyright (c) 2019, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2019-2025, WSO2 LLC. (http://www.wso2.com). * - * WSO2 Inc. licenses this file to you under the Apache License, + * 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 @@ -19,22 +19,33 @@ package org.wso2.identity.integration.test.rest.api.user.application.v1; import io.restassured.RestAssured; + +import java.io.IOException; + import org.apache.commons.lang.StringUtils; +import org.json.JSONException; +import org.json.JSONObject; import org.testng.Assert; import org.testng.annotations.AfterClass; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; +import org.wso2.carbon.base.MultitenantConstants; +import org.wso2.identity.integration.test.rest.api.server.application.management.v1.model.AccessTokenConfiguration; import org.wso2.identity.integration.test.rest.api.server.application.management.v1.model.AdvancedApplicationConfiguration; import org.wso2.identity.integration.test.rest.api.server.application.management.v1.model.ApplicationModel; -import org.wso2.identity.integration.test.rest.api.server.application.management.v1.model.ApplicationResponseModel; +import org.wso2.identity.integration.test.rest.api.server.application.management.v1.model.ApplicationPatchModel; +import org.wso2.identity.integration.test.rest.api.server.application.management.v1.model.ApplicationSharePOSTRequest; +import org.wso2.identity.integration.test.rest.api.server.application.management.v1.model.DiscoverableGroup; +import org.wso2.identity.integration.test.rest.api.server.application.management.v1.model.GroupBasicInfo; +import org.wso2.identity.integration.test.rest.api.server.application.management.v1.model.OAuth2PKCEConfiguration; +import org.wso2.identity.integration.test.rest.api.server.application.management.v1.model.OpenIDConnectConfiguration; import org.wso2.identity.integration.test.rest.api.user.common.RESTAPIUserTestBase; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import org.wso2.identity.integration.test.rest.api.user.common.model.GroupRequestObject; +import org.wso2.identity.integration.test.rest.api.user.common.model.UserObject; import org.wso2.identity.integration.test.restclients.OAuth2RestClient; +import org.wso2.identity.integration.test.restclients.OrgMgtRestClient; +import org.wso2.identity.integration.test.restclients.SCIM2RestClient; public class UserDiscoverableApplicationServiceTestBase extends RESTAPIUserTestBase { @@ -43,11 +54,44 @@ public class UserDiscoverableApplicationServiceTestBase extends RESTAPIUserTestB public static final String USER_APPLICATION_ENDPOINT_URI = "/me/applications"; protected static String swaggerDefinition; protected static String API_PACKAGE_NAME = "org.wso2.carbon.identity.rest.api.user.application.v1"; - protected static int TOTAL_DISCOVERABLE_APP_COUNT = 13; - private static final String APP_NAME_PREFIX = "APP_"; - private static final String APP_DESC_PREFIX = "This is APP_"; - private static final String APP_IMAGE_URL = "https://dummy-image-url.com"; - private static final String APP_ACCESS_URL = "https://dummy-access-url.com"; + private static final int TOTAL_DISCOVERABLE_APP_COUNT = 19; + private static final int TOTAL_NON_DISCOVERABLE_APP_COUNT = 2; + private static final int TOTAL_GROUP_COUNT = 3; + private static final String[] GROUP_IDS = new String[TOTAL_GROUP_COUNT]; + private static final int TOTAL_USER_COUNT = 3; + private static final String[] USER_IDS = new String[TOTAL_USER_COUNT]; + protected static final String[] USER_TOKENS = new String[TOTAL_USER_COUNT]; + private static final int TOTAL_SUB_ORG_GROUP_COUNT = 3; + private static final int TOTAL_SUB_ORG_USER_COUNT = 3; + private static final String[] SUB_ORG_USER_IDS = new String[TOTAL_SUB_ORG_USER_COUNT]; + private static final String[] SUB_ORG_GROUP_IDS = new String[TOTAL_SUB_ORG_GROUP_COUNT]; + protected static final String[] SUB_ORG_USER_TOKENS = new String[TOTAL_SUB_ORG_USER_COUNT]; + private static final String USER_NAME_PREFIX = "user-"; + private static final String USER_PASSWORD = "Wso2@test"; + private static final String GROUP_NAME_PREFIX = "GROUP_"; + protected static final String APP_NAME_PREFIX = "APP_"; + protected static final String APP_DESC_PREFIX = "This is APP_"; + protected static final String APP_IMAGE_URL = "https://dummy-image-url.com"; + protected static final String APP_ACCESS_URL = "https://dummy-access-url.com"; + private static final String SUB_ORG_NAME = "sub-org"; + private static final String PRIMARY_USER_STORE = "PRIMARY"; + private static final String MY_ACCOUNT_APP_NAME = "My Account"; + private static final String MY_ACCOUNT_APP_PATH = "myaccount"; + protected static final String[][] USER_DISCOVERABLE_APPS = new String[][]{ + { "19", "18", "17", "16", "15", "14", "13", "8", "7", "6", "5", "4", "3", "2", "1" }, + { "19", "18", "17", "16", "15", "14", "13", "12", "11", "10", "9", "8", "7", "6", "5", "4", "3"}, + { "19", "18", "17", "16", "15", "14", "13" } + }; + protected static final String[] USER_NON_DISCOVERABLE_APPS = new String[]{ "21", "20" }; + protected static final String APP_NAME_WITH_SPACES = "APP_SPACES IN NAME "; + protected static final int APP_NAME_WITH_SPACES_APP_NUM = 2; + protected static final int APP_NAME_WITH_SPACES_APP_NUM_WITHOUT_GROUPS = 18; + protected static final String[] DISCOVERABLE_APP_IDS = new String[TOTAL_DISCOVERABLE_APP_COUNT]; + protected static final String[] SUB_ORG_DISCOVERABLE_APP_IDS = new String[TOTAL_DISCOVERABLE_APP_COUNT]; + + private String subOrgID; + private String subOrgToken; + private String rootMyAccountAppId; static { try { @@ -58,21 +102,44 @@ public class UserDiscoverableApplicationServiceTestBase extends RESTAPIUserTestB } } - protected List applications = new ArrayList<>(); protected OAuth2RestClient oAuth2RestClient; + protected SCIM2RestClient scim2RestClient; + protected OrgMgtRestClient orgMgtRestClient; @BeforeClass(alwaysRun = true) public void testStart() throws Exception { - oAuth2RestClient = new OAuth2RestClient(serverURL, tenantInfo); - createServiceProviders(); + oAuth2RestClient = new OAuth2RestClient(serverURL, context.getContextTenant()); + scim2RestClient = new SCIM2RestClient(serverURL, context.getContextTenant()); + orgMgtRestClient = + new OrgMgtRestClient(context, context.getContextTenant(), serverURL, getAuthorizedAPIList()); + subOrgID = orgMgtRestClient.addOrganization(SUB_ORG_NAME); + subOrgToken = orgMgtRestClient.switchM2MToken(subOrgID); + + createUsers(); + createGroup(); + createSubOrgUsers(); + createSubOrgGroups(); + createApplications(); + changeMyAccountConfiguration(); + getTokenForUsers(); } @AfterClass(alwaysRun = true) public void testEnd() throws Exception { super.conclude(); - deleteServiceProviders(); + deleteUsers(); + deleteGroups(); + deleteSubOrgUsers(); + deleteSubOrgGroups(); + deleteApplications(); + revertMyAccountConfiguration(); + orgMgtRestClient.deleteOrganization(subOrgID); + oAuth2RestClient.deleteApplication(oAuth2RestClient.getAppIdUsingAppName("b2b-app")); + oAuth2RestClient.closeHttpClient(); + scim2RestClient.closeHttpClient(); + orgMgtRestClient.closeHttpClient(); } @BeforeMethod(alwaysRun = true) @@ -87,40 +154,344 @@ public void testMethodEnd() { RestAssured.basePath = StringUtils.EMPTY; } - private void createServiceProviders() throws Exception { + /** + * Create root tenant users for the test. + * + * @throws Exception If an error occurred while creating users. + */ + private void createUsers() throws Exception { - for (int i = 1; i <= TOTAL_DISCOVERABLE_APP_COUNT; i++) { - ApplicationResponseModel application = createServiceProvider(APP_NAME_PREFIX + i, APP_DESC_PREFIX + i); - if (application != null) { - applications.add(application); - } + for (int i = 1; i <= TOTAL_USER_COUNT; i++) { + UserObject userObject = new UserObject(); + userObject.setUserName(USER_NAME_PREFIX + i); + userObject.setPassword(USER_PASSWORD); + USER_IDS[i - 1] = scim2RestClient.createUser(userObject); + } + } + + /** + * Delete root tenant users created for the test. + * + * @throws Exception If an error occurred while deleting users. + */ + private void deleteUsers() throws Exception { + + for (int i = 1; i <= TOTAL_USER_COUNT; i++) { + scim2RestClient.deleteUser(USER_IDS[i - 1]); + } + } + + /** + * Create root tenant groups for the test. + * + * @throws Exception If an error occurred while creating groups. + */ + private void createGroup() throws Exception { + + for (int i = 1; i <= TOTAL_GROUP_COUNT; i++) { + GroupRequestObject groupRequestObject = new GroupRequestObject(); + groupRequestObject.displayName(GROUP_NAME_PREFIX + i); + assignMembersToGroup(groupRequestObject, i, USER_IDS); + GROUP_IDS[i - 1] = scim2RestClient.createGroup(groupRequestObject); + } + } + + /** + * Delete root tenant groups created for the test. + * + * @throws Exception If an error occurred while deleting groups. + */ + private void deleteGroups() throws Exception { + + for (int i = 1; i <= TOTAL_GROUP_COUNT; i++) { + scim2RestClient.deleteGroup(GROUP_IDS[i - 1]); + } + } + + /** + * Create sub organization users for the test. + * + * @throws Exception If an error occurred while creating users. + */ + private void createSubOrgUsers() throws Exception { + + for (int i = 1; i <= TOTAL_SUB_ORG_USER_COUNT; i++) { + UserObject userObject = new UserObject(); + userObject.setUserName(USER_NAME_PREFIX + i); + userObject.setPassword(USER_PASSWORD); + SUB_ORG_USER_IDS[i - 1] = scim2RestClient.createSubOrgUser(userObject, subOrgToken); + } + } + + /** + * Delete sub organization users created for the test. + * + * @throws Exception If an error occurred while deleting users. + */ + private void deleteSubOrgUsers() throws Exception { + + for (int i = 1; i <= TOTAL_SUB_ORG_USER_COUNT; i++) { + scim2RestClient.deleteSubOrgUser(SUB_ORG_USER_IDS[i - 1], subOrgToken); + } + } + + /** + * Create sub organization groups for the test. + * + * @throws Exception If an error occurred while creating groups. + */ + private void createSubOrgGroups() throws Exception { + + for (int i = 1; i <= TOTAL_SUB_ORG_GROUP_COUNT; i++) { + GroupRequestObject groupRequestObject = new GroupRequestObject(); + groupRequestObject.displayName(GROUP_NAME_PREFIX + i); + assignMembersToGroup(groupRequestObject, i, SUB_ORG_USER_IDS); + SUB_ORG_GROUP_IDS[i - 1] = scim2RestClient.createSubOrgGroup(groupRequestObject, subOrgToken); + } + } + + /** + * Delete sub organization groups created for the test. + * + * @throws Exception If an error occurred while deleting groups. + */ + private void deleteSubOrgGroups() throws Exception { + + for (int i = 1; i <= TOTAL_SUB_ORG_GROUP_COUNT; i++) { + scim2RestClient.deleteSubOrgGroup(SUB_ORG_GROUP_IDS[i - 1], subOrgToken); } + } + + /** + * Assign members to the group. + * The first user will be assigned to the first group. + * The second user will be assigned to the second and third groups. + * The third user will not be assigned to any group. + * + * @param groupRequestObject Group request object. + * @param groupNum Group number. + * @param userIDs User IDs. + */ + private void assignMembersToGroup(GroupRequestObject groupRequestObject, int groupNum, String[] userIDs) { + + GroupRequestObject.MemberItem member = new GroupRequestObject.MemberItem(); + if (groupNum == 1) { + member.value(userIDs[0]); + groupRequestObject.addMember(member); + } else if (groupNum == 2) { + member.value(userIDs[1]); + groupRequestObject.addMember(member); + } else if (groupNum == 3) { + member.value(userIDs[1]); + groupRequestObject.addMember(member); + } + } + + /** + * Get the list of sub APIs that need to be authorized for the B2B application. + * + * @return A JSON object containing the API and scopes list. + * @throws JSONException If an error occurs while creating the JSON object. + */ + private JSONObject getAuthorizedAPIList() throws JSONException { + + JSONObject jsonObject = new JSONObject(); + // SCIM2 Users. + jsonObject.put("/o/scim2/Users", + new String[] {"internal_org_user_mgt_create", "internal_org_user_mgt_delete"}); + // SCIM2 Groups. + jsonObject.put("/o/scim2/Groups", + new String[] {"internal_org_group_mgt_create", "internal_org_group_mgt_delete"}); + // Application management. + jsonObject.put("/o/api/server/v1/applications", + new String[] {"internal_org_application_mgt_view", "internal_org_application_mgt_create", + "internal_org_application_mgt_update"}); + jsonObject.put("/api/server/v1/applications", + new String[] {"internal_application_mgt_view", "internal_application_mgt_delete"}); + // Organization management. + jsonObject.put("/api/server/v1/organizations", + new String[] {"internal_organization_create", "internal_organization_delete"}); + + return jsonObject; + } - // Reverse the SP list as they are ordered by created timestamp. - Collections.reverse(applications); + /** + * Create applications for the test. + * + * @throws Exception If an error occurred while creating applications. + */ + private void createApplications() throws Exception { + + for (int i = 1; i <= TOTAL_DISCOVERABLE_APP_COUNT; i++) { + ApplicationModel application = new ApplicationModel(); + application.setName(getApplicationName(String.valueOf(i))); + application.setDescription(APP_DESC_PREFIX + i); + application.setImageUrl(APP_IMAGE_URL); + AdvancedApplicationConfiguration advancedApplicationConfiguration = new AdvancedApplicationConfiguration(); + advancedApplicationConfiguration.setDiscoverableByEndUsers(true); + assignDiscoverableGroups(advancedApplicationConfiguration, i, GROUP_IDS); + application.setAdvancedConfigurations(advancedApplicationConfiguration); + application.setAccessUrl(APP_ACCESS_URL); + String appId = oAuth2RestClient.createApplication(application); + DISCOVERABLE_APP_IDS[i - 1] = appId; + oAuth2RestClient.shareApplication(appId, new ApplicationSharePOSTRequest().shareWithAllChildren(true)); + String sharedAppId = null; + do { + if (sharedAppId != null) { + Thread.sleep(1000); + } + sharedAppId = oAuth2RestClient.getAppIdUsingAppNameInOrganization(getApplicationName( + String.valueOf(i)), subOrgToken); + } while (StringUtils.isEmpty(sharedAppId)); + SUB_ORG_DISCOVERABLE_APP_IDS[i - 1] = sharedAppId; + ApplicationPatchModel sharedAppPatch = new ApplicationPatchModel(); + AdvancedApplicationConfiguration sharedAppAdvancedConfig = new AdvancedApplicationConfiguration(); + assignDiscoverableGroups(sharedAppAdvancedConfig, i, SUB_ORG_GROUP_IDS); + sharedAppPatch.advancedConfigurations(sharedAppAdvancedConfig); + oAuth2RestClient.updateSubOrgApplication(sharedAppId, sharedAppPatch, subOrgToken); + } + for (int i = 1; i <= TOTAL_NON_DISCOVERABLE_APP_COUNT; i++) { + ApplicationModel application = new ApplicationModel(); + application.setName(getApplicationName(String.valueOf(i + TOTAL_DISCOVERABLE_APP_COUNT))); + application.setDescription(APP_DESC_PREFIX + (i + TOTAL_DISCOVERABLE_APP_COUNT)); + application.setImageUrl(APP_IMAGE_URL); + oAuth2RestClient.createApplication(application); + } } - private void deleteServiceProviders() throws Exception { + /** + * Delete applications created for the test. + * + * @throws Exception If an error occurred while deleting applications. + */ + private void deleteApplications() throws Exception { for (int i = 1; i <= TOTAL_DISCOVERABLE_APP_COUNT; i++) { - oAuth2RestClient.deleteApplication(applications.get(i - 1).getId()); - log.info("############## " + "Deleted app: " + applications.get(i - 1).getName()); + oAuth2RestClient.deleteApplication(DISCOVERABLE_APP_IDS[i - 1]); + } + for (int i = 1; i <= TOTAL_NON_DISCOVERABLE_APP_COUNT; i++) { + oAuth2RestClient.deleteApplication(oAuth2RestClient.getAppIdUsingAppName(getApplicationName( + String.valueOf(i + TOTAL_DISCOVERABLE_APP_COUNT)))); + } + } + + /** + * Assign discoverable groups to the application. + * + * @param advancedApplicationConfiguration Advanced application configuration. + * @param applicationNum Application number. + * @param groupIDs Group IDs. + */ + private void assignDiscoverableGroups(AdvancedApplicationConfiguration advancedApplicationConfiguration, + int applicationNum, String[] groupIDs) { + + DiscoverableGroup discoverableGroup = new DiscoverableGroup(); + discoverableGroup.setUserStore(PRIMARY_USER_STORE); + if (applicationNum >= 1 && applicationNum <= 8) { + GroupBasicInfo group = new GroupBasicInfo(); + group.setId(groupIDs[0]); + discoverableGroup.addGroupsItem(group); } + if (applicationNum >= 3 && applicationNum <= 10) { + GroupBasicInfo group = new GroupBasicInfo(); + group.setId(groupIDs[1]); + discoverableGroup.addGroupsItem(group); + } + if (applicationNum >= 5 && applicationNum <= 12) { + GroupBasicInfo group = new GroupBasicInfo(); + group.setId(groupIDs[2]); + discoverableGroup.addGroupsItem(group); + } + if (applicationNum < 13) { + advancedApplicationConfiguration.addDiscoverableGroupsItem(discoverableGroup); + } + } + + /** + * Make My Account application a confidential client. + * + * @throws Exception If an error occurred while making the application a confidential client. + */ + private void changeMyAccountConfiguration() throws Exception { + + rootMyAccountAppId = oAuth2RestClient.getAppIdUsingAppName(MY_ACCOUNT_APP_NAME); + OpenIDConnectConfiguration rootMyAccountAppOIDC = oAuth2RestClient.getOIDCInboundDetails(rootMyAccountAppId); + OAuth2PKCEConfiguration oAuth2PKCEConfiguration = rootMyAccountAppOIDC.getPkce(); + oAuth2PKCEConfiguration.setMandatory(false); + rootMyAccountAppOIDC.setPublicClient(false); + AccessTokenConfiguration accessTokenConfiguration = rootMyAccountAppOIDC.getAccessToken(); + accessTokenConfiguration.setBindingType(null); + accessTokenConfiguration.setValidateTokenBinding(false); + oAuth2RestClient.updateInboundDetailsOfApplication(rootMyAccountAppId, rootMyAccountAppOIDC, "oidc"); + oAuth2RestClient.shareApplication( + rootMyAccountAppId, new ApplicationSharePOSTRequest().shareWithAllChildren(true)); + } + + /** + * Revert the configuration of My Account application. + * + * @throws Exception If an error occurred while reverting the configuration. + */ + private void revertMyAccountConfiguration() throws Exception { + + OpenIDConnectConfiguration rootMyAccountAppOIDC = oAuth2RestClient.getOIDCInboundDetails(rootMyAccountAppId); + OAuth2PKCEConfiguration oAuth2PKCEConfiguration = rootMyAccountAppOIDC.getPkce(); + oAuth2PKCEConfiguration.setMandatory(true); + rootMyAccountAppOIDC.setPublicClient(true); + AccessTokenConfiguration accessTokenConfiguration = rootMyAccountAppOIDC.getAccessToken(); + accessTokenConfiguration.setBindingType("cookie"); + accessTokenConfiguration.setValidateTokenBinding(true); + oAuth2RestClient.updateInboundDetailsOfApplication(rootMyAccountAppId, rootMyAccountAppOIDC, "oidc"); + oAuth2RestClient.unshareApplication(rootMyAccountAppId); + } + + /** + * Get the access tokens for the users. + * + * @throws Exception If an error occurred while getting the access tokens. + */ + private void getTokenForUsers() throws Exception { - applications.clear(); + for (int i = 1; i <= TOTAL_USER_COUNT; i++) { + USER_TOKENS[i - 1] = + oAuth2RestClient.getAccessTokenUsingCodeGrantForRootUser(rootMyAccountAppId, USER_NAME_PREFIX + i, + USER_PASSWORD, "SYSTEM", getMyAccountRedirectUrl()); + } + for (int i = 1; i <= TOTAL_SUB_ORG_USER_COUNT; i++) { + SUB_ORG_USER_TOKENS[i - 1] = + oAuth2RestClient.getAccessTokenUsingCodeGrantForSubOrgUser(rootMyAccountAppId, SUB_ORG_NAME, + subOrgID, USER_NAME_PREFIX + i, USER_PASSWORD, "SYSTEM", getMyAccountRedirectUrl()); + } } - protected ApplicationResponseModel createServiceProvider(String appName, String appDescription) throws Exception { + /** + * Get my account redirect URL. + * + * @return My account redirect URL. + */ + private String getMyAccountRedirectUrl() { - ApplicationModel application = new ApplicationModel(); - application.setName(appName); - application.setDescription(appDescription); - application.setImageUrl(APP_IMAGE_URL); - AdvancedApplicationConfiguration advancedApplicationConfiguration = new AdvancedApplicationConfiguration(); - advancedApplicationConfiguration.setDiscoverableByEndUsers(true); - application.setAdvancedConfigurations(advancedApplicationConfiguration); - application.setAccessUrl(APP_ACCESS_URL); - String appId = oAuth2RestClient.createApplication(application); - return oAuth2RestClient.getApplication(appId); + if (StringUtils.equals(tenantInfo.getDomain(), MultitenantConstants.SUPER_TENANT_DOMAIN_NAME)) { + return serverURL + MY_ACCOUNT_APP_PATH; + } else { + return serverURL + TENANTED_URL_PATH_SPECIFIER.replace(URL_SEPARATOR, StringUtils.EMPTY) + URL_SEPARATOR + + tenantInfo.getDomain() + URL_SEPARATOR + MY_ACCOUNT_APP_PATH; + } + } + + /** + * Get the expected application name based on name prefix and application number. + * + * @param applicationNum Application number. + * @return Expected application name. + */ + protected String getApplicationName(String applicationNum) { + + if (Integer.parseInt(applicationNum) == APP_NAME_WITH_SPACES_APP_NUM || + Integer.parseInt(applicationNum) == APP_NAME_WITH_SPACES_APP_NUM_WITHOUT_GROUPS) { + return APP_NAME_WITH_SPACES + applicationNum; + } else { + return APP_NAME_PREFIX + applicationNum; + } } } diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/user/application/v1/UserDiscoverableApplicationSuccessTest.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/user/application/v1/UserDiscoverableApplicationSuccessTest.java index f66252f5cfe..704aab09d4a 100644 --- a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/user/application/v1/UserDiscoverableApplicationSuccessTest.java +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/rest/api/user/application/v1/UserDiscoverableApplicationSuccessTest.java @@ -1,7 +1,7 @@ /* - * Copyright (c) 2019, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2019-2025, WSO2 LLC. (http://www.wso2.com). * - * WSO2 Inc. licenses this file to you under the Apache License, + * 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 @@ -19,31 +19,29 @@ package org.wso2.identity.integration.test.rest.api.user.application.v1; import io.restassured.response.Response; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.IntStream; + +import org.apache.commons.lang.StringUtils; import org.apache.http.HttpStatus; -import org.testng.Assert; import org.testng.annotations.BeforeClass; import org.testng.annotations.DataProvider; import org.testng.annotations.Factory; import org.testng.annotations.Test; import org.wso2.carbon.automation.engine.context.TestUserMode; -import org.wso2.identity.integration.test.rest.api.server.application.management.v1.model.ApplicationResponseModel; +import org.wso2.carbon.base.MultitenantConstants; -import java.util.HashMap; -import java.util.Map; -import java.util.stream.IntStream; - -import javax.xml.xpath.XPathExpressionException; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasSize; public class UserDiscoverableApplicationSuccessTest extends UserDiscoverableApplicationServiceTestBase { private static final String PAGINATION_LINK_QUERY_PARAM_STRING = "?offset=%d&limit=%d"; - private static final String APP_NAME_WITH_SPACES = "APP_SPACES IN NAME"; - // Store application with spaces in the name. - private ApplicationResponseModel application; @Factory(dataProvider = "restAPIUserConfigProvider") public UserDiscoverableApplicationSuccessTest(TestUserMode userMode) throws Exception { @@ -64,18 +62,6 @@ public static Object[][] restAPIUserConfigProvider() { }; } - @DataProvider(name = "offsetLimitProvider") - public static Object[][] paginationLimitOffsetProvider() { - - return new Object[][]{ - {0, 5}, - {4, 5}, - {5, 5}, - {10, 5}, - {15, 5}, - }; - } - @BeforeClass(alwaysRun = true) public void testStart() throws Exception { @@ -83,172 +69,390 @@ public void testStart() throws Exception { super.testStart(); } - @Test(description = "Test listing all applications.") - public void testListAllApplications() { + @DataProvider(name = "testListAllApplications") + public Object[][] testListAllApplications() { + return new Object[][]{ + {1, false}, + {2, false}, + {3, false}, + {1, true}, + {2, true}, + {3, true} + }; + } - Response response = getResponseOfGet(USER_APPLICATION_ENDPOINT_URI); + @Test(description = "Test listing all applications.", dataProvider = "testListAllApplications") + public void testListAllApplications(int userNum, boolean isSubOrg) { + + Response response; + if (isSubOrg) { + String oldBasePath = this.basePath; + this.basePath = convertToOrgBasePath(oldBasePath); + response = getResponseOfGetWithOAuth2(USER_APPLICATION_ENDPOINT_URI, SUB_ORG_USER_TOKENS[userNum - 1]); + this.basePath = oldBasePath; + } else { + response = getResponseOfGetWithOAuth2(USER_APPLICATION_ENDPOINT_URI, USER_TOKENS[userNum - 1]); + } response.then().log().ifValidationFails().assertThat().statusCode(HttpStatus.SC_OK); response.then().log().ifValidationFails().assertThat().body("totalResults", equalTo - (TOTAL_DISCOVERABLE_APP_COUNT)); + (USER_DISCOVERABLE_APPS[userNum - 1].length)); response.then().log().ifValidationFails().assertThat().body("startIndex", equalTo(1)); - response.then().log().ifValidationFails().assertThat().body("count", equalTo(TOTAL_DISCOVERABLE_APP_COUNT)); + response.then().log().ifValidationFails().assertThat() + .body("count", equalTo(USER_DISCOVERABLE_APPS[userNum - 1].length)); response.then().log().ifValidationFails().body("links", hasSize(0)); response.then().log().ifValidationFails() - .body("applications", hasSize(TOTAL_DISCOVERABLE_APP_COUNT)); + .body("applications", hasSize(USER_DISCOVERABLE_APPS[userNum - 1].length)); + + assertForAllApplications(response, userNum, isSubOrg); + } - assertForAllApplications(response); + @DataProvider(name = "offsetLimitProvider") + public static Object[][] paginationLimitOffsetProvider() { + return new Object[][]{ + {1, false, 0, 5}, + {1, false, 4, 5}, + {1, false, 5, 5}, + {1, false, 10, 5}, + {1, false, 15, 5}, + {1, false, 17, 4}, + {2, false, 0, 5}, + {2, false, 4, 10}, + {2, false, 5, 5}, + {2, false, 10, 7}, + {2, false, 15, 4}, + {2, false, 17, 5}, + {2, false, 25, 3}, + {3, false, 0, 5}, + {3, false, 4, 5}, + {3, false, 5, 4}, + {3, false, 8, 2}, + {1, true, 0, 5}, + {1, true, 4, 5}, + {1, true, 5, 5}, + {1, true, 10, 5}, + {1, true, 15, 5}, + {1, true, 17, 4}, + {2, true, 0, 5}, + {2, true, 4, 10}, + {2, true, 5, 5}, + {2, true, 10, 7}, + {2, true, 15, 4}, + {2, true, 17, 5}, + {2, true, 25, 3}, + {3, true, 0, 5}, + {3, true, 4, 5}, + {3, true, 5, 4}, + {3, true, 8, 2} + }; } - @Test(description = "Test listing applications with offset and limit.", dataProvider = "offsetLimitProvider") - public void testListApplicationsWithOffsetLimit(int offset, int limit) throws Exception { + @Test(description = "Test listing applications with offset and limit.", dataProvider = "offsetLimitProvider", + dependsOnMethods = "testListAllApplications") + public void testListApplicationsWithOffsetLimit(int userNum, boolean isSubOrg, int offset, int limit) { Map params = new HashMap() {{ put("offset", offset); put("limit", limit); }}; - Response response = getResponseOfGet(USER_APPLICATION_ENDPOINT_URI, params); + Response response; + if (isSubOrg) { + String oldBasePath = this.basePath; + this.basePath = convertToOrgBasePath(oldBasePath); + response = getResponseOfGetWithOAuth2(USER_APPLICATION_ENDPOINT_URI, SUB_ORG_USER_TOKENS[userNum - 1], + params); + this.basePath = oldBasePath; + } else { + response = getResponseOfGetWithOAuth2(USER_APPLICATION_ENDPOINT_URI, USER_TOKENS[userNum - 1], params); + } response.then().log().ifValidationFails().assertThat().body("totalResults", equalTo - (TOTAL_DISCOVERABLE_APP_COUNT)); + (USER_DISCOVERABLE_APPS[userNum - 1].length)); response.then().log().ifValidationFails().assertThat().body("startIndex", equalTo(offset + 1)); // Assert for applications and application count. - assertApplicationsAndApplicationCountInPaginatedResponse(offset, limit, response); - assertNextLink(offset, limit, response); - assertPreviousLink(offset, limit, response); + assertApplicationsAndApplicationCountInPaginatedResponse(offset, limit, response, userNum, isSubOrg); + assertNextLink(offset, limit, response, userNum, isSubOrg); + assertPreviousLink(offset, limit, response, userNum, isSubOrg); + } + + @DataProvider(name = "testFilterApplicationsByNameWithAppNum") + public Object[][] filterApplicationsByNameWithAppNum() { + return new Object[][]{ + {1, false, 19}, + {1, false, 1}, + {2, false, 16}, + {2, false, 10}, + {3, false, 17}, + {1, true, 19}, + {1, true, 1}, + {2, true, 16}, + {2, true, 10}, + {3, true, 17} + }; } - @Test(description = "Test filtering applications by name with eq operator.") - public void testFilterApplicationsByNameForEQ() { + @Test(description = "Test filtering applications by name with eq operator.", + dataProvider = "testFilterApplicationsByNameWithAppNum", + dependsOnMethods = "testListApplicationsWithOffsetLimit") + public void testFilterApplicationsByNameForEQ(int userNum, boolean isSubOrg, int appNum) { Map params = new HashMap() {{ - put("filter", "name eq " + applications.get(0).getName()); + put("filter", "name eq " + APP_NAME_PREFIX + appNum); }}; - Response response = getResponseOfGet(USER_APPLICATION_ENDPOINT_URI, params); + Response response; + if (isSubOrg) { + String oldBasePath = this.basePath; + this.basePath = convertToOrgBasePath(oldBasePath); + response = getResponseOfGetWithOAuth2(USER_APPLICATION_ENDPOINT_URI, SUB_ORG_USER_TOKENS[userNum - 1], + params); + this.basePath = oldBasePath; + } else { + response = getResponseOfGetWithOAuth2(USER_APPLICATION_ENDPOINT_URI, USER_TOKENS[userNum - 1], params); + } response.then().log().ifValidationFails().assertThat().body("totalResults", equalTo(1)); response.then().log().ifValidationFails().assertThat().body("startIndex", equalTo(1)); response.then().log().ifValidationFails().assertThat().body("count", equalTo(1)); - assertForApplication(applications.get(0), response); + assertForApplication(appNum, isSubOrg, response); + } + + @DataProvider(name = "testFilterApplicationsByNameWithoutAppNum") + public Object[][] filterApplicationsByNameWithoutAppNum() { + + return new Object[][]{ + {1, false}, + {2, false}, + {3, false}, + {1, true}, + {2, true}, + {3, true} + }; } - @Test(description = "Test filtering applications by name with co operator.") - public void testFilterApplicationsByNameForCO() { + @Test(description = "Test filtering applications by name with co operator.", + dataProvider = "testFilterApplicationsByNameWithoutAppNum", + dependsOnMethods = "testFilterApplicationsByNameForEQ") + public void testFilterApplicationsByNameForCO(int userNum, boolean isSubOrg) { Map params = new HashMap() {{ put("filter", "name co APP"); }}; - Response response = getResponseOfGet(USER_APPLICATION_ENDPOINT_URI, params); + Response response; + if (isSubOrg) { + String oldBasePath = this.basePath; + this.basePath = convertToOrgBasePath(oldBasePath); + response = getResponseOfGetWithOAuth2(USER_APPLICATION_ENDPOINT_URI, SUB_ORG_USER_TOKENS[userNum - 1], + params); + this.basePath = oldBasePath; + } else { + response = getResponseOfGetWithOAuth2(USER_APPLICATION_ENDPOINT_URI, USER_TOKENS[userNum - 1], params); + } response.then().log().ifValidationFails().assertThat().body("totalResults", equalTo - (TOTAL_DISCOVERABLE_APP_COUNT)); + (USER_DISCOVERABLE_APPS[userNum - 1].length)); response.then().log().ifValidationFails().assertThat().body("startIndex", equalTo(1)); - response.then().log().ifValidationFails().assertThat().body("count", equalTo(applications.size())); - response.then().log().ifValidationFails().body("applications", hasSize(applications.size())); + response.then().log().ifValidationFails().assertThat().body("count", equalTo( + USER_DISCOVERABLE_APPS[userNum - 1].length)); + response.then().log().ifValidationFails() + .body("applications", hasSize(USER_DISCOVERABLE_APPS[userNum - 1].length)); //All application created matches the given filter - assertForAllApplications(response); - + assertForAllApplications(response, userNum, isSubOrg); } - @Test(description = "Test filtering applications by name with sw operator.") - public void testFilterApplicationsByNameForSW() { + @Test(description = "Test filtering applications by name with sw operator.", + dependsOnMethods = "testFilterApplicationsByNameForCO", + dataProvider = "testFilterApplicationsByNameWithoutAppNum") + public void testFilterApplicationsByNameForSW(int userNum, boolean isSubOrg) { Map params = new HashMap() {{ put("filter", "name sw APP"); - }}; - Response response = getResponseOfGet(USER_APPLICATION_ENDPOINT_URI, params); + Response response; + if (isSubOrg) { + String oldBasePath = this.basePath; + this.basePath = convertToOrgBasePath(oldBasePath); + response = getResponseOfGetWithOAuth2(USER_APPLICATION_ENDPOINT_URI, SUB_ORG_USER_TOKENS[userNum - 1], + params); + this.basePath = oldBasePath; + } else { + response = getResponseOfGetWithOAuth2(USER_APPLICATION_ENDPOINT_URI, USER_TOKENS[userNum - 1], params); + } response.then().log().ifValidationFails().assertThat().body("totalResults", equalTo - (TOTAL_DISCOVERABLE_APP_COUNT)); + (USER_DISCOVERABLE_APPS[userNum - 1].length)); response.then().log().ifValidationFails().assertThat().body("startIndex", equalTo(1)); - response.then().log().ifValidationFails().assertThat().body("count", equalTo(applications.size())); - response.then().log().ifValidationFails().body("applications", hasSize(applications.size())); + response.then().log().ifValidationFails().assertThat().body("count", equalTo( + USER_DISCOVERABLE_APPS[userNum - 1].length)); + response.then().log().ifValidationFails() + .body("applications", hasSize(USER_DISCOVERABLE_APPS[userNum - 1].length)); //All application created matches the given filter - assertForAllApplications(response); + assertForAllApplications(response, userNum, isSubOrg); } - @Test(description = "Test filtering applications by name with ew operator.") - public void testFilteringApplicationsByNameForEW() { + @Test(description = "Test filtering applications by name with ew operator.", + dependsOnMethods = "testFilterApplicationsByNameForSW", + dataProvider = "testFilterApplicationsByNameWithAppNum") + public void testFilteringApplicationsByNameForEW(int userNum, boolean isSubOrg, int appNum) { Map params = new HashMap() {{ - put("filter", "name ew 13"); + put("filter", "name ew " + appNum); }}; - Response response = getResponseOfGet(USER_APPLICATION_ENDPOINT_URI, params); + Response response; + if (isSubOrg) { + String oldBasePath = this.basePath; + this.basePath = convertToOrgBasePath(oldBasePath); + response = getResponseOfGetWithOAuth2(USER_APPLICATION_ENDPOINT_URI, SUB_ORG_USER_TOKENS[userNum - 1], + params); + this.basePath = oldBasePath; + } else { + response = getResponseOfGetWithOAuth2(USER_APPLICATION_ENDPOINT_URI, USER_TOKENS[userNum - 1], params); + } response.then().log().ifValidationFails().assertThat().body("totalResults", equalTo(1)); response.then().log().ifValidationFails().assertThat().body("startIndex", equalTo(1)); response.then().log().ifValidationFails().assertThat().body("count", equalTo(1)); // Only 0th index application matches the given filter - assertForApplication(applications.get(0), response); + assertForApplication(appNum, isSubOrg, response); } - @Test(description = "Test filtering applications by name with eq operator when name contains spaces.") - public void testFilterApplicationsByNameWithSpacesForEQ() throws Exception { + @DataProvider(name = "testFilterApplicationsByNameWithSpacesWithAppNum") + public Object[][] filterApplicationsByNameWithSpacesWithAppNum() { - // Create a discoverable SP with spaces in the name. - application = createServiceProvider(APP_NAME_WITH_SPACES, "This is " + APP_NAME_WITH_SPACES); - Assert.assertNotNull(application, "Failed to create service provider with spaces in name."); + return new Object[][]{ + {1, false, APP_NAME_WITH_SPACES_APP_NUM}, + {1, false, APP_NAME_WITH_SPACES_APP_NUM_WITHOUT_GROUPS}, + {1, true, APP_NAME_WITH_SPACES_APP_NUM}, + {1, true, APP_NAME_WITH_SPACES_APP_NUM_WITHOUT_GROUPS} + }; + } + + @Test(description = "Test filtering applications by name with eq operator when name contains spaces.", + dataProvider = "testFilterApplicationsByNameWithSpacesWithAppNum", + dependsOnMethods = "testFilteringApplicationsByNameForEW") + public void testFilterApplicationsByNameWithSpacesForEQ(int userNum, boolean isSubOrg, int appNum) { Map params = new HashMap() {{ - put("filter", "name eq " + APP_NAME_WITH_SPACES); + put("filter", "name eq " + APP_NAME_WITH_SPACES + appNum); }}; - Response response = getResponseOfGet(USER_APPLICATION_ENDPOINT_URI, params); + Response response; + if (isSubOrg) { + String oldBasePath = this.basePath; + this.basePath = convertToOrgBasePath(oldBasePath); + response = getResponseOfGetWithOAuth2(USER_APPLICATION_ENDPOINT_URI, SUB_ORG_USER_TOKENS[userNum - 1], + params); + this.basePath = oldBasePath; + } else { + response = getResponseOfGetWithOAuth2(USER_APPLICATION_ENDPOINT_URI, USER_TOKENS[userNum - 1], params); + } response.then().log().ifValidationFails().assertThat().body("totalResults", equalTo(1)); response.then().log().ifValidationFails().assertThat().body("startIndex", equalTo(1)); response.then().log().ifValidationFails().assertThat().body("count", equalTo(1)); - assertForApplication(application, response); + assertForApplication(appNum, isSubOrg, response); + } + + @DataProvider(name = "testFilterApplicationsByNameWithSpaces") + public Object[][] filterApplicationsByNameWithSpaces() { + + return new Object[][]{ + {1, false}, + {1, true}, + }; } @Test(description = "Test filtering applications by name with co operator when name contains spaces.", + dataProvider = "testFilterApplicationsByNameWithSpaces", dependsOnMethods = "testFilterApplicationsByNameWithSpacesForEQ") - public void testFilterApplicationsByNameWithSpacesForCO() throws Exception { + public void testFilterApplicationsByNameWithSpacesForCO(int userNum, boolean isSubOrg) { Map params = new HashMap() {{ put("filter", "name co " + APP_NAME_WITH_SPACES); }}; - Response response = getResponseOfGet(USER_APPLICATION_ENDPOINT_URI, params); - response.then().log().ifValidationFails().assertThat().body("totalResults", equalTo(1)); + Response response; + if (isSubOrg) { + String oldBasePath = this.basePath; + this.basePath = convertToOrgBasePath(oldBasePath); + response = getResponseOfGetWithOAuth2(USER_APPLICATION_ENDPOINT_URI, SUB_ORG_USER_TOKENS[userNum - 1], + params); + this.basePath = oldBasePath; + } else { + response = getResponseOfGetWithOAuth2(USER_APPLICATION_ENDPOINT_URI, USER_TOKENS[userNum - 1], params); + } + response.then().log().ifValidationFails().assertThat().body("totalResults", equalTo(2)); response.then().log().ifValidationFails().assertThat().body("startIndex", equalTo(1)); - response.then().log().ifValidationFails().assertThat().body("count", equalTo(1)); + response.then().log().ifValidationFails().assertThat().body("count", equalTo(2)); - assertForApplication(application, response); - - // Remove the discoverable SP with spaces in the name. - oAuth2RestClient.deleteApplication(application.getId()); - log.info("############## " + "Deleted app: " + application.getName()); + assertForApplication(APP_NAME_WITH_SPACES_APP_NUM_WITHOUT_GROUPS, isSubOrg, response, 0); + assertForApplication(APP_NAME_WITH_SPACES_APP_NUM, isSubOrg, response, 1); } - @Test(description = "Test get application.") - public void testGetApplication() { + @DataProvider(name = "testGetApplicationByResourceId") + public Object[][] testGetApplicationByResourceId() { - Response response = getResponseOfGet(USER_APPLICATION_ENDPOINT_URI + "/" + applications.get(0).getId()); + return new Object[][] { + {1, false, 19}, + {2, false, 11}, + {3, false, 15}, + {1, true, 3}, + {2, true, 9}, + {3, true, 13} + }; + } - response.then().log().ifValidationFails().assertThat().body("id", equalTo(applications.get(0).getId())); - response.then().log().ifValidationFails().assertThat().body("name", equalTo(applications.get(0).getName())); - response.then().log().ifValidationFails().assertThat().body("description", equalTo(applications.get(0) - .getDescription())); + @Test(description = "Test get application.", dataProvider = "testGetApplicationByResourceId", + dependsOnMethods = "testFilterApplicationsByNameWithSpacesForCO") + public void testGetApplication(int userNum, boolean isSubOrg, int appNum) { + + Response response; + if (isSubOrg) { + String oldBasePath = this.basePath; + this.basePath = convertToOrgBasePath(oldBasePath); + response = getResponseOfGetWithOAuth2( + USER_APPLICATION_ENDPOINT_URI + URL_SEPARATOR + SUB_ORG_DISCOVERABLE_APP_IDS[appNum - 1], + SUB_ORG_USER_TOKENS[userNum - 1]); + this.basePath = oldBasePath; + } else { + response = getResponseOfGetWithOAuth2( + USER_APPLICATION_ENDPOINT_URI + URL_SEPARATOR + DISCOVERABLE_APP_IDS[appNum - 1], + USER_TOKENS[userNum - 1]); + } + + response.then().log().ifValidationFails().assertThat() + .body("id", equalTo(isSubOrg ? + SUB_ORG_DISCOVERABLE_APP_IDS[appNum - 1] : DISCOVERABLE_APP_IDS[appNum - 1])) + .body("name", equalTo(getApplicationName(String.valueOf(appNum)))) + .body("image", isSubOrg ? nullValue() : equalTo(APP_IMAGE_URL)) + .body("accessUrl", equalTo(APP_ACCESS_URL)) + .body("description", equalTo(APP_DESC_PREFIX + appNum)); } - private void assertApplicationsAndApplicationCountInPaginatedResponse(int offset, int limit, Response response) { + /** + * Assert for applications and application count in paginated response. + * + * @param offset Offset. + * @param limit Limit. + * @param response Response. + */ + private void assertApplicationsAndApplicationCountInPaginatedResponse(int offset, int limit, Response response, + int userNum, boolean isSubOrg) { int applicationCount; - if ((offset + limit) < TOTAL_DISCOVERABLE_APP_COUNT) { - assertForApplicationsInRange(offset, (offset + limit), response); + int appCount = USER_DISCOVERABLE_APPS[userNum - 1].length; + if ((offset + limit) <= appCount) { + assertForApplicationsInRange(offset, (offset + limit), response, userNum, isSubOrg); applicationCount = limit; - } else if (offset < TOTAL_DISCOVERABLE_APP_COUNT && (offset + limit) > TOTAL_DISCOVERABLE_APP_COUNT) { - assertForApplicationsInRange(offset, TOTAL_DISCOVERABLE_APP_COUNT, response); - applicationCount = TOTAL_DISCOVERABLE_APP_COUNT - offset; - } else if (offset > TOTAL_DISCOVERABLE_APP_COUNT) { + } else if (offset < appCount && (offset + limit) > appCount) { + assertForApplicationsInRange(offset, appCount, response, userNum, isSubOrg); + applicationCount = appCount - offset; + } else if (offset >= appCount) { applicationCount = 0; } else { throw new RuntimeException("Offset: " + offset + " and limit: " + limit + " is not acceptable for this " + @@ -259,67 +463,109 @@ private void assertApplicationsAndApplicationCountInPaginatedResponse(int offset response.then().log().ifValidationFails().body("applications", hasSize(applicationCount)); } - private void assertForApplication(ApplicationResponseModel serviceProvider, Response response) { + /** + * Assert for application. + * + * @param appNum Application number. + * @param response Response. + */ + private void assertForApplication(int appNum, boolean isSubOrg, Response response) { - response.then().log().ifValidationFails().assertThat().body("applications.id", hasItem(serviceProvider - .getId())); - response.then().log().ifValidationFails().assertThat().body("applications.name", hasItem(serviceProvider - .getName())); - response.then().log().ifValidationFails().assertThat().body("applications.description", hasItem(serviceProvider - .getDescription())); + assertForApplication(appNum, isSubOrg, response, 0); } - private void assertForApplicationsInRange(int startIndex, int endIndex, Response response) { + /** + * Assert for application in the given index. + * + * @param appNum Application number. + * @param response Response. + */ + private void assertForApplication(int appNum, boolean isSubOrg, Response response, int appIndex) { + response.then().log().ifValidationFails() + .body("applications[" + appIndex + "].name", equalTo(getApplicationName(String.valueOf(appNum)))) + .body("applications[" + appIndex + "].image", isSubOrg ? nullValue() : equalTo(APP_IMAGE_URL)) + .body("applications[" + appIndex + "].accessUrl", equalTo(APP_ACCESS_URL)) + .body("applications[" + appIndex + "].description", equalTo(APP_DESC_PREFIX + appNum)); + } + + /** + * Assert for applications in the given range. + * + * @param startIndex Start index. + * @param endIndex End index. + * @param response Response. + */ + private void assertForApplicationsInRange(int startIndex, int endIndex, Response response, int userNum, + boolean isSubOrg) { + + String[] discoverableApps = USER_DISCOVERABLE_APPS[userNum - 1]; IntStream.range(startIndex, endIndex).forEach(i -> { response.then().log().ifValidationFails() - .body("applications.find{ it.id == '" + applications.get(i).getId() + - "'}.name", - equalTo(applications.get(i).getName())) - .body("applications.find{ it.id == '" + applications.get(i).getId() + - "'}.description", - equalTo(applications.get(i).getDescription())); + .body("applications[" + (i - startIndex) + "].name", + equalTo(getApplicationName(discoverableApps[i]))) + .body("applications[" + (i - startIndex) + "].image", + isSubOrg ? nullValue() : equalTo(APP_IMAGE_URL)) + .body("applications[" + (i - startIndex) + "].accessUrl", equalTo(APP_ACCESS_URL)) + .body("applications[" + (i - startIndex) + "].description", + equalTo(APP_DESC_PREFIX + discoverableApps[i])); }); } - private void assertForAllApplications(Response response) { + /** + * Assert for all applications. + * + * @param response Response. + * @param isSubOrg Whether the user is from a sub organization. + */ + private void assertForAllApplications(Response response, int userNum, boolean isSubOrg) { - applications.forEach(serviceProvider -> { + String[] discoverableApps = USER_DISCOVERABLE_APPS[userNum - 1]; + for (int i = 0; i < discoverableApps.length; i++) { response.then().log().ifValidationFails() - .body("applications.find{ it.id == '" + serviceProvider.getId() + "'}.name", - equalTo(serviceProvider.getName())) - .body("applications.find{ it.id == '" + serviceProvider.getId() + "'}.image", - equalTo(serviceProvider.getImageUrl())) - .body("applications.find{ it.id == '" + serviceProvider.getId() + "'}" + - ".accessUrl", equalTo(serviceProvider.getAccessUrl())) - .body("applications.find{ it.id == '" + serviceProvider.getId() + "'}" + - ".description", - equalTo(serviceProvider.getDescription())); - }); + .body("applications[" + i + "].name", equalTo(getApplicationName(discoverableApps[i]))) + .body("applications[" + i + "].image", isSubOrg ? nullValue() : equalTo(APP_IMAGE_URL)) + .body("applications[" + i + "].accessUrl", equalTo(APP_ACCESS_URL)) + .body("applications[" + i + "].description", equalTo(APP_DESC_PREFIX + discoverableApps[i])); + } } - private void assertNextLink(int offset, int limit, Response response) throws XPathExpressionException { - - if ((offset + limit) < TOTAL_DISCOVERABLE_APP_COUNT) { + /** + * Assert for next link. + * + * @param offset Offset. + * @param limit Limit. + * @param response Response. + */ + private void assertNextLink(int offset, int limit, Response response, int userNum, boolean isSubOrg) { + if ((offset + limit) < USER_DISCOVERABLE_APPS[userNum - 1].length) { response.then().log().ifValidationFails().body("links.rel", hasItem("next")); - response.then().log().ifValidationFails().body("links.find { it.rel == 'next'}.href", equalTo - (String.format(getTenantedRelativePath("/api/users/v1" + - USER_APPLICATION_ENDPOINT_URI, context.getContextTenant().getDomain()) + - PAGINATION_LINK_QUERY_PARAM_STRING, (offset + limit), limit))); + response.then().log().ifValidationFails().body("links.find { it.rel == 'next'}.href", equalTo( + String.format((isSubOrg ? convertToOrgBasePath(this.basePath) : this.basePath) + + USER_APPLICATION_ENDPOINT_URI + PAGINATION_LINK_QUERY_PARAM_STRING, (offset + limit), + limit))); } else { response.then().log().ifValidationFails().body("links", not(hasItem("next"))); } } - private void assertPreviousLink(int offset, int limit, Response response) throws XPathExpressionException { + /** + * Assert for previous link. + * + * @param offset Offset. + * @param limit Limit. + * @param response Response. + */ + private void assertPreviousLink(int offset, int limit, Response response, int userNum, boolean isSubOrg) { if (offset > 0) { // Previous link exists only if offset is greater than 0. int expectedOffsetQueryParam; int expectedLimitQueryParam; response.then().log().ifValidationFails().body("links.rel", hasItem("previous")); if ((offset - limit) >= 0) { // A previous page of size 'limit' exists - expectedOffsetQueryParam = calculateOffsetForPreviousLink(offset, limit, TOTAL_DISCOVERABLE_APP_COUNT); + expectedOffsetQueryParam = + calculateOffsetForPreviousLink(offset, limit, USER_DISCOVERABLE_APPS[userNum - 1].length); expectedLimitQueryParam = limit; } else { // A previous page exists but it's size is less than the specified limit expectedOffsetQueryParam = 0; @@ -327,10 +573,9 @@ private void assertPreviousLink(int offset, int limit, Response response) throws } response.then().log().ifValidationFails().body("links.find { it.rel == 'previous'}.href", equalTo - (String.format(getTenantedRelativePath("/api/users/v1" + - USER_APPLICATION_ENDPOINT_URI, context.getContextTenant().getDomain()) + + (String.format((isSubOrg ? convertToOrgBasePath(this.basePath) : this.basePath) + + USER_APPLICATION_ENDPOINT_URI + PAGINATION_LINK_QUERY_PARAM_STRING, expectedOffsetQueryParam, expectedLimitQueryParam))); - } else if (offset == 0) { response.then().log().ifValidationFails().body("links", not(hasItem("previous"))); } else { @@ -339,6 +584,14 @@ private void assertPreviousLink(int offset, int limit, Response response) throws } } + /** + * Calculate the offset for the previous link. + * + * @param offset Offset. + * @param limit Limit. + * @param total Total. + * @return Offset for the previous link. + */ private int calculateOffsetForPreviousLink(int offset, int limit, int total) { int newOffset = (offset - limit); @@ -348,4 +601,19 @@ private int calculateOffsetForPreviousLink(int offset, int limit, int total) { return calculateOffsetForPreviousLink(newOffset, limit, total); } + + /** + * Get the organization base path. + * + * @param basePath Tenant base path. + * @return Organization base path. + */ + private String convertToOrgBasePath(String basePath) { + + if (StringUtils.equals(MultitenantConstants.SUPER_TENANT_DOMAIN_NAME, tenant)) { + return ORGANIZATION_PATH_SPECIFIER + basePath; + } else { + return basePath.replace(tenant, tenant + ORGANIZATION_PATH_SPECIFIER); + } + } } diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/restclients/OAuth2RestClient.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/restclients/OAuth2RestClient.java index 6b7c4be2bd0..673b26d1729 100644 --- a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/restclients/OAuth2RestClient.java +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/restclients/OAuth2RestClient.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023-2024, WSO2 LLC. (https://www.wso2.com). + * Copyright (c) 2023-2025, WSO2 LLC. (https://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 @@ -15,20 +15,49 @@ * specific language governing permissions and limitations * under the License. */ + package org.wso2.identity.integration.test.restclients; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; + import io.restassured.http.ContentType; + +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang.StringUtils; import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.NameValuePair; import org.apache.http.StatusLine; +import org.apache.http.client.CookieStore; +import org.apache.http.client.config.CookieSpecs; +import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.http.config.Lookup; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.cookie.CookieSpecProvider; +import org.apache.http.impl.client.BasicCookieStore; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.cookie.RFC6265CookieSpecProvider; import org.apache.http.message.BasicHeader; import org.apache.http.util.EntityUtils; import org.json.JSONException; +import org.json.JSONObject; import org.testng.Assert; import org.wso2.carbon.automation.engine.context.beans.Tenant; import org.wso2.carbon.utils.multitenancy.MultitenantConstants; @@ -48,21 +77,27 @@ import org.wso2.identity.integration.test.rest.api.server.application.management.v1.model.ApplicationResponseModel; import org.wso2.identity.integration.test.rest.api.server.application.management.v1.model.ApplicationSharePOSTRequest; import org.wso2.identity.integration.test.rest.api.server.application.management.v1.model.AuthorizedAPICreationModel; -import org.wso2.identity.integration.test.rest.api.server.application.management.v1.model.AuthorizedDomainAPIResponse; import org.wso2.identity.integration.test.rest.api.server.application.management.v1.model.OpenIDConnectConfiguration; import org.wso2.identity.integration.test.rest.api.server.application.management.v1.model.SAML2ServiceProvider; import org.wso2.identity.integration.test.rest.api.server.roles.v2.model.RoleV2; import org.wso2.identity.integration.test.utils.OAuth2Constant; -import javax.servlet.http.HttpServletResponse; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - import static org.wso2.identity.integration.test.utils.CarbonUtils.isLegacyAuthzRuntimeEnabled; +import static org.wso2.identity.integration.test.utils.OAuth2Constant.ACCESS_TOKEN; +import static org.wso2.identity.integration.test.utils.OAuth2Constant.ACCESS_TOKEN_ENDPOINT; +import static org.wso2.identity.integration.test.utils.OAuth2Constant.AUTHORIZATION_CODE_NAME; +import static org.wso2.identity.integration.test.utils.OAuth2Constant.AUTHORIZE_ENDPOINT_URL; +import static org.wso2.identity.integration.test.utils.OAuth2Constant.COMMON_AUTH_URL; +import static org.wso2.identity.integration.test.utils.OAuth2Constant.FIDP_PARAM; +import static org.wso2.identity.integration.test.utils.OAuth2Constant.HTTP_RESPONSE_HEADER_LOCATION; +import static org.wso2.identity.integration.test.utils.OAuth2Constant.OAUTH2_CLIENT_ID; +import static org.wso2.identity.integration.test.utils.OAuth2Constant.OAUTH2_CLIENT_SECRET; +import static org.wso2.identity.integration.test.utils.OAuth2Constant.OAUTH2_GRANT_TYPE_CODE; +import static org.wso2.identity.integration.test.utils.OAuth2Constant.OAUTH2_RESPONSE_TYPE; +import static org.wso2.identity.integration.test.utils.OAuth2Constant.REDIRECT_URI_NAME; +import static org.wso2.identity.integration.test.utils.OAuth2Constant.SCOPE_PLAYGROUND_NAME; +import static org.wso2.identity.integration.test.utils.OAuth2Constant.SESSION_DATA_KEY; +import static org.wso2.identity.integration.test.utils.OAuth2Constant.USER_AGENT; public class OAuth2RestClient extends RestBaseClient { @@ -75,14 +110,22 @@ public class OAuth2RestClient extends RestBaseClient { private static final String SCIM_BASE_PATH = "scim2"; private static final String ROLE_V2_BASE_PATH = "/v2/Roles"; private static final String AUTHORIZATION_DETAILS_TYPES_PATH = "/authorization-details-types"; + private static final String USERNAME = "username"; + private static final String PASSWORD = "password"; + private static final String ORG_ID_PLACEHOLDER = "{orgId}"; + private final CookieStore cookieStore = new BasicCookieStore(); private final String applicationManagementApiBasePath; private final String subOrgApplicationManagementApiBasePath; private final String apiResourceManagementApiBasePath; private final String roleV2ApiBasePath; private final String username; private final String password; + private final String authorizeEndpoint; + private final String commonAuthURL; + private final String subOrgCommonAuthURL; + private final String tokenEndpoint; - public OAuth2RestClient(String backendUrl, Tenant tenantInfo) { + public OAuth2RestClient(String backendUrl, Tenant tenantInfo) throws IOException { this.username = tenantInfo.getContextUser().getUserName(); this.password = tenantInfo.getContextUser().getPassword(); @@ -92,6 +135,10 @@ public OAuth2RestClient(String backendUrl, Tenant tenantInfo) { subOrgApplicationManagementApiBasePath = getSubOrgApplicationsPath(backendUrl, tenantDomain); apiResourceManagementApiBasePath = getAPIResourcesPath(backendUrl, tenantDomain); roleV2ApiBasePath = getSCIM2RoleV2Path(backendUrl, tenantDomain); + authorizeEndpoint = getAuthorizeEndpoint(backendUrl, tenantDomain); + commonAuthURL = getCommonAuthURL(backendUrl, tenantDomain); + subOrgCommonAuthURL = getSubOrgCommonAuthURL(backendUrl); + tokenEndpoint = getTokenEndpoint(backendUrl, tenantDomain); } /** @@ -206,6 +253,18 @@ public List getApplicationsByClientId(String clientId) thro } } + /** + * Get application id using application name in a root organization. + * + * @param appName Application name. + * @return Application id. + * @throws IOException If an error occurred while retrieving the application. + */ + public String getAppIdUsingAppName(String appName) throws IOException { + + return getAppIdUsingAppNameInOrganization(appName, null); + } + /** * Get application id using application name in an organization. * @@ -216,11 +275,13 @@ public List getApplicationsByClientId(String clientId) thro */ public String getAppIdUsingAppNameInOrganization(String appName, String switchedM2MToken) throws IOException { - String endPointUrl = subOrgApplicationManagementApiBasePath + "?filter=name eq " + appName; + String endPointUrl = (switchedM2MToken != null + ? subOrgApplicationManagementApiBasePath : applicationManagementApiBasePath) + + "?filter=name eq " + appName; endPointUrl = endPointUrl.replace(" ", "%20"); try (CloseableHttpResponse response = getResponseOfHttpGet(endPointUrl, - getHeadersWithBearerToken(switchedM2MToken))) { + switchedM2MToken != null ? getHeadersWithBearerToken(switchedM2MToken) : getHeaders())) { String responseBody = EntityUtils.toString(response.getEntity()); ObjectMapper jsonWriter = new ObjectMapper(new JsonFactory()); @@ -274,6 +335,46 @@ public void updateApplication(String appId, ApplicationPatchModel application) } } + /** + * Update an existing sub organization application. + * + * @param appId Application id. + * @param application Updated application patch object. + * @param switchedM2MToken Switched m2m token generated for the given organization. + */ + public void updateSubOrgApplication(String appId, ApplicationPatchModel application, String switchedM2MToken) { + + String jsonRequest = toJSONString(application); + String endPointUrl = subOrgApplicationManagementApiBasePath + PATH_SEPARATOR + appId; + + try { + if (isLegacyAuthzRuntimeEnabled()) { + try (CloseableHttpResponse response = getResponseOfHttpPatch(endPointUrl, jsonRequest, + getHeadersWithBearerToken(switchedM2MToken))) { + Assert.assertEquals(response.getStatusLine().getStatusCode(), HttpServletResponse.SC_OK, + "Application update failed"); + } + } + if (!isLegacyAuthzRuntimeEnabled()) { + if ((application.getAssociatedRoles() != null) && application.getAssociatedRoles().getRoles() != null) { + try (CloseableHttpResponse response = getResponseOfHttpPatch(endPointUrl, jsonRequest, + getHeadersWithBearerToken(switchedM2MToken))) { + Assert.assertEquals(response.getStatusLine().getStatusCode(), HttpServletResponse.SC_FORBIDDEN, + "Application update failed"); + } + } else { + try (CloseableHttpResponse response = getResponseOfHttpPatch(endPointUrl, jsonRequest, + getHeadersWithBearerToken(switchedM2MToken))) { + Assert.assertEquals(response.getStatusLine().getStatusCode(), HttpServletResponse.SC_OK, + "Application update failed"); + } + } + } + } catch (Exception e) { + throw new Error("Unable to update the Application"); + } + } + /** * Get all applications. * @@ -383,6 +484,278 @@ public Boolean deleteInboundConfiguration(String appId, String inboundType) thro } } + /** + * Get the access token using the authorization code grant type for a root org user. + * + * @param appId Application id. + * @param username Username. + * @param password Password. + * @param scopes Scopes. + * @param redirectUrl Redirect URL. + * @return Access token. + * @throws Exception If an error occurred while getting the access token. + */ + public String getAccessTokenUsingCodeGrantForRootUser(String appId, String username, String password, String scopes, + String redirectUrl) throws Exception { + + initializeClientWithCookieStore(); + OpenIDConnectConfiguration oidcConfig = getOIDCInboundDetails(appId); + String sessionDataKey = initiateAuthRequest(oidcConfig.getClientId(), scopes, redirectUrl, false); + String authorizationCode = getAuthorizationCode(sessionDataKey, username, password, false, null); + String accessToken = + getAccessToken(authorizationCode, oidcConfig.getClientId(), oidcConfig.getClientSecret(), redirectUrl); + client = HttpClients.createDefault(); + return accessToken; + } + + /** + * Get the access token using the authorization code grant type for a sub org user. + * + * @param rootAppId Root Application id. + * @param organizationName Organization name. + * @param username Username. + * @param password Password. + * @param scopes Scopes. + * @param redirectUrl Redirect URL. + * @return Access token. + * @throws Exception If an error occurred while getting the access token. + */ + public String getAccessTokenUsingCodeGrantForSubOrgUser(String rootAppId, String organizationName, String ordId, + String username, String password, String scopes, + String redirectUrl) throws Exception { + + initializeClientWithCookieStore(); + OpenIDConnectConfiguration oidcConfig = getOIDCInboundDetails(rootAppId); + String sessionDataKey = initiateAuthRequest(oidcConfig.getClientId(), scopes, redirectUrl, true); + String subOrgSessionDataKey = getSubOrgSessionDataKey(sessionDataKey, organizationName); + String authorizationCode = getAuthorizationCode(subOrgSessionDataKey, username, password, true, ordId); + String accessToken = + getAccessToken(authorizationCode, oidcConfig.getClientId(), oidcConfig.getClientSecret(), redirectUrl); + client = HttpClients.createDefault(); + return accessToken; + } + + /** + * Initiate the authentication request. + * + * @param clientId Client id. + * @param scopes Scopes. + * @param redirectUrl Redirect URL. + * @param isOrganizationSSO Is organization login request. + * @return Session data key. + * @throws Exception If an error occurred while initiating the authentication request. + */ + private String initiateAuthRequest(String clientId, String scopes, String redirectUrl, boolean isOrganizationSSO) throws Exception { + + Map queryParams = new HashMap<>(); + queryParams.put(OAUTH2_RESPONSE_TYPE, OAUTH2_GRANT_TYPE_CODE); + queryParams.put(OAUTH2_CLIENT_ID, clientId); + queryParams.put(SCOPE_PLAYGROUND_NAME, scopes); + queryParams.put(REDIRECT_URI_NAME, redirectUrl); + if (isOrganizationSSO) { + queryParams.put(FIDP_PARAM, "OrganizationSSO"); + } + + HttpResponse response = getResponseOfHttpPostWithParameters(authorizeEndpoint, + new Header[] {new BasicHeader(USER_AGENT_ATTRIBUTE, USER_AGENT)}, queryParams); + if (response == null) { + throw new Error("Authorized response is null"); + } + Header locationHeader = getLocationHeader(response); + Map redirectURLQueryParams = extractQueryParams(locationHeader.getValue()); + return redirectURLQueryParams.get(SESSION_DATA_KEY); + } + + /** + * Get the authorization code. + * + * @param sessionDataKey Session data key. + * @param username Username. + * @param password Password. + * @param isOrganizationSSO Is organization SSO. + * @param orgId Organization id. + * @return Authorization code. + * @throws Exception If an error occurred while getting the authorization code. + */ + private String getAuthorizationCode(String sessionDataKey, String username, String password, + boolean isOrganizationSSO, String orgId) throws Exception { + + Map params = new HashMap<>(); + params.put(SESSION_DATA_KEY, sessionDataKey); + params.put(USERNAME, username); + params.put(PASSWORD, password); + + String commonAuthEndpoint = + isOrganizationSSO ? subOrgCommonAuthURL.replace(ORG_ID_PLACEHOLDER, orgId) : commonAuthURL; + + HttpResponse response = getResponseOfHttpPostWithParameters(commonAuthEndpoint, + new Header[] {new BasicHeader(USER_AGENT_ATTRIBUTE, USER_AGENT)}, params); + if (response == null) { + if (isOrganizationSSO) { + throw new Error("Sub organization commonauth response is null"); + } + throw new Error("Commonauth response is null"); + } + Header locationHeader = getLocationHeader(response); + + if (isOrganizationSSO) { + response = getResponseOfHttpGet(locationHeader.getValue(), + new Header[] {new BasicHeader(USER_AGENT_ATTRIBUTE, USER_AGENT)}); + if (response == null) { + throw new Error("Sub organization authorized response is null"); + } + locationHeader = getLocationHeader(response); + + response = getResponseOfHttpGet(locationHeader.getValue(), + new Header[] {new BasicHeader(USER_AGENT_ATTRIBUTE, USER_AGENT)}); + if (response == null) { + throw new Error("Commonauth response is null"); + } + locationHeader = getLocationHeader(response); + } + + response = getResponseOfHttpGet(locationHeader.getValue(), + new Header[] {new BasicHeader(USER_AGENT_ATTRIBUTE, USER_AGENT)}); + if (response == null) { + throw new Error("Authorized response is null"); + } + locationHeader = getLocationHeader(response); + Map queryParams = extractQueryParams(locationHeader.getValue()); + return queryParams.get("code"); + } + + /** + * Get session data key for a sub organization login. + * + * @param parentSessionDataKey Parent session data key. + * @param subOrgName Sub organization name. + * @return Session data key for the sub organization. + * @throws Exception If an error occurred while getting the session data key. + */ + private String getSubOrgSessionDataKey(String parentSessionDataKey, String subOrgName) throws Exception { + + Map urlParameters = new HashMap<>(); + urlParameters.put(SESSION_DATA_KEY, parentSessionDataKey); + urlParameters.put("org", subOrgName); + urlParameters.put("idp", "SSO"); + urlParameters.put("authenticator", "OrganizationAuthenticator"); + + HttpResponse response = getResponseOfHttpPostWithParameters(commonAuthURL, + new Header[] {new BasicHeader(USER_AGENT_ATTRIBUTE, USER_AGENT)}, urlParameters); + if (response == null) { + throw new Error("Commonauth response is null"); + } + Header locationHeader = getLocationHeader(response); + + response = getResponseOfHttpGet(locationHeader.getValue(), new Header[] {new BasicHeader(USER_AGENT_ATTRIBUTE, + USER_AGENT)}); + if (response == null) { + throw new Error("Authorized user response is null."); + } + locationHeader = getLocationHeader(response); + Map redirectURLQueryParams = extractQueryParams(locationHeader.getValue()); + return redirectURLQueryParams.get(SESSION_DATA_KEY); + } + + /** + * Get the access token using the authorization code. + * + * @param authCode Authorization code. + * @param clientId Client id. + * @param clientSecret Client secret. + * @param redirectUrl Redirect URL. + * @return Access token. + * @throws Exception If an error occurred while getting the access token. + */ + private String getAccessToken(String authCode, String clientId, String clientSecret, String redirectUrl) + throws Exception { + + Map params = new HashMap<>(); + params.put(OAuth2Constant.GRANT_TYPE_NAME, OAuth2Constant.OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE); + params.put(AUTHORIZATION_CODE_NAME, authCode); + params.put(REDIRECT_URI_NAME, redirectUrl); + params.put(OAUTH2_CLIENT_ID, clientId); + params.put(OAUTH2_CLIENT_SECRET, clientSecret); + + HttpResponse response = getResponseOfHttpPostWithParameters(tokenEndpoint, + new Header[] {new BasicHeader(USER_AGENT_ATTRIBUTE, USER_AGENT)}, params); + if (response == null) { + throw new Error("Access token response is null"); + } + if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { + throw new Error("Unexpected response status code."); + } + + JSONObject responseData = new JSONObject(EntityUtils.toString(response.getEntity())); + EntityUtils.consume(response.getEntity()); + return responseData.getString(ACCESS_TOKEN); + } + + /** + * Initialize the client with the cookie store. + * + * @throws IOException If an error occurred while initializing the client. + */ + private void initializeClientWithCookieStore() throws IOException { + if (client != null) { + client.close(); + } + Lookup cookieSpecRegistry = RegistryBuilder.create() + .register(CookieSpecs.DEFAULT, new RFC6265CookieSpecProvider()) + .build(); + RequestConfig requestConfig = RequestConfig.custom() + .setCookieSpec(CookieSpecs.DEFAULT) + .build(); + cookieStore.clear(); + client = HttpClientBuilder.create().disableRedirectHandling() + .setDefaultRequestConfig(requestConfig) + .setDefaultCookieStore(cookieStore) + .setDefaultCookieSpecRegistry(cookieSpecRegistry) + .build(); + } + + /** + * Get the location header from the response. + * + * @param response HttpResponse object. + * @return Location header. + * @throws IOException If an error occurred while getting the location header. + */ + private static Header getLocationHeader(HttpResponse response) throws IOException { + + if (response.getStatusLine().getStatusCode() != HttpStatus.SC_MOVED_TEMPORARILY) { + throw new Error("Unexpected status code: " + response.getStatusLine().getStatusCode()); + } + Header locationHeader = response.getFirstHeader(HTTP_RESPONSE_HEADER_LOCATION); + if (locationHeader == null) { + throw new Error("Location header not found in the response"); + } + EntityUtils.consume(response.getEntity()); + return locationHeader; + } + + /** + * Extract query parameters from the URL. + * + * @param url URL with query parameters. + * @return Map of query parameters. + * @throws Exception If an error occurred while extracting query parameters. + */ + private Map extractQueryParams(String url) throws Exception { + + Map queryParams = new HashMap<>(); + List params = URLEncodedUtils.parse(new URI(url), StandardCharsets.UTF_8); + if (params.isEmpty()) { + return queryParams; + } + + for (NameValuePair param : params) { + queryParams.put(param.getName(), param.getValue()); + } + + return queryParams; + } + private String getApplicationsPath(String serverUrl, String tenantDomain) { if (tenantDomain.equals(MultitenantConstants.SUPER_TENANT_DOMAIN_NAME)) { @@ -421,6 +794,65 @@ private String getSCIM2RoleV2Path(String serverUrl, String tenantDomain) { } } + /** + * Get the authorize endpoint. + * + * @param serverUrl Server URL. + * @param tenantDomain Tenant domain. + * @return Authorize endpoint. + */ + private String getAuthorizeEndpoint(String serverUrl, String tenantDomain) { + + if (tenantDomain.equals(MultitenantConstants.SUPER_TENANT_DOMAIN_NAME)) { + return AUTHORIZE_ENDPOINT_URL; + } else { + return AUTHORIZE_ENDPOINT_URL.replace(serverUrl, serverUrl + TENANT_PATH + tenantDomain + PATH_SEPARATOR); + } + } + + /** + * Get the common auth URL. + * + * @param serverUrl Server URL. + * @param tenantDomain Tenant domain. + * @return Common auth URL. + */ + private String getCommonAuthURL(String serverUrl, String tenantDomain) { + + if (tenantDomain.equals(MultitenantConstants.SUPER_TENANT_DOMAIN_NAME)) { + return COMMON_AUTH_URL; + } else { + return COMMON_AUTH_URL.replace(serverUrl, serverUrl + TENANT_PATH + tenantDomain + PATH_SEPARATOR); + } + } + + /** + * Get the sub organization common auth URL. + * + * @param serverUrl Server URL. + * @return Sub organization common auth URL. + */ + private String getSubOrgCommonAuthURL(String serverUrl) { + + return COMMON_AUTH_URL.replace(serverUrl, serverUrl + ORGANIZATION_PATH + ORG_ID_PLACEHOLDER + PATH_SEPARATOR); + } + + /** + * Get the token endpoint. + * + * @param serverUrl Server URL. + * @param tenantDomain Tenant domain. + * @return Token endpoint. + */ + private String getTokenEndpoint(String serverUrl, String tenantDomain) { + + if (tenantDomain.equals(MultitenantConstants.SUPER_TENANT_DOMAIN_NAME)) { + return ACCESS_TOKEN_ENDPOINT; + } else { + return ACCESS_TOKEN_ENDPOINT.replace(serverUrl, serverUrl + TENANT_PATH + tenantDomain + PATH_SEPARATOR); + } + } + private Header[] getHeaders() { Header[] headerList = new Header[3]; @@ -605,6 +1037,20 @@ public void shareApplication(String appId, ApplicationSharePOSTRequest applicati } } + /** + * Unshare the application with all organizations. + * + * @param appId The application ID. + * @throws IOException Error when unsharing the application. + */ + public void unshareApplication(String appId) throws IOException { + + try (CloseableHttpResponse response = getResponseOfHttpDelete(applicationManagementApiBasePath + + PATH_SEPARATOR + appId + PATH_SEPARATOR + "shared-apps", getHeaders())) { + Assert.assertEquals(response.getStatusLine().getStatusCode(), HttpServletResponse.SC_NO_CONTENT, + "Application unsharing failed"); + } + } /** * To create API Resources. diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/restclients/RestBaseClient.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/restclients/RestBaseClient.java index a322dfa9987..675f0f603db 100644 --- a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/restclients/RestBaseClient.java +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/restclients/RestBaseClient.java @@ -19,7 +19,16 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; + +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + import org.apache.http.Header; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; @@ -30,14 +39,11 @@ import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicNameValuePair; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; -import java.io.IOException; -import java.net.URI; -import java.util.Map; - public class RestBaseClient { public static final String LOCATION_HEADER = "Location"; @@ -52,7 +58,7 @@ public class RestBaseClient { public static final String PATH_SEPARATOR = "/"; public static final String OIDC = "oidc"; public static final String SAML = "saml"; - public final CloseableHttpClient client; + public CloseableHttpClient client; public RestBaseClient() { client = HttpClients.createDefault(); @@ -125,6 +131,30 @@ public CloseableHttpResponse getResponseOfHttpPost(String endPointUrl, String js return client.execute(request); } + /** + * Execute and get the response of HTTP POST with encoded parameters. + * + * @param endPointUrl REST endpoint. + * @param headers Header list of the request. + * @param urlParams URL parameters. + * @return Response of the Http request. + * @throws IOException If an error occurred while executing http POST request. + */ + public CloseableHttpResponse getResponseOfHttpPostWithParameters(String endPointUrl, Header[] headers, + Map urlParams) + throws IOException { + + HttpPost request = new HttpPost(endPointUrl); + request.setHeaders(headers); + List urlParameters = new ArrayList<>(); + for (Map.Entry entry : urlParams.entrySet()) { + urlParameters.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); + } + request.setEntity(new UrlEncodedFormEntity(urlParameters)); + + return client.execute(request); + } + /** * Execute and get the response of HTTP GET. * diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/utils/OAuth2Constant.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/utils/OAuth2Constant.java index 9fe04eef018..9244f7a6a70 100644 --- a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/utils/OAuth2Constant.java +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/utils/OAuth2Constant.java @@ -1,12 +1,12 @@ /* - * Copyright (c) WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2019-2025, WSO2 LLC. (http://www.wso2.com). * - * WSO2 Inc. 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 + * 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 + * 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 @@ -100,6 +100,7 @@ public final class OAuth2Constant { // OAuth 2 request parameters. public static final String OAUTH2_RESPONSE_TYPE = "response_type"; public static final String OAUTH2_CLIENT_ID = "client_id"; + public static final String OAUTH2_CLIENT_SECRET = "client_secret"; public static final String OAUTH2_REDIRECT_URI = "redirect_uri"; public static final String OAUTH2_SCOPE = "scope"; public static final String OAUTH2_NONCE = "nonce"; @@ -129,6 +130,8 @@ public final class OAuth2Constant { public static final String TENANT_INTROSPECT_ENDPOINT = "https://localhost:9853/t//oauth2/introspect"; + public static final String FIDP_PARAM = "fidp"; + public static final String SUBJECT_TOKEN = "subject_token"; public static final class PlaygroundAppPaths { diff --git a/modules/integration/tests-integration/tests-backend/src/test/resources/org/wso2/identity/integration/test/rest/api/server/application/management/v1/create-basic-application.json b/modules/integration/tests-integration/tests-backend/src/test/resources/org/wso2/identity/integration/test/rest/api/server/application/management/v1/create-basic-application.json index 276fb9533f3..37c3a6fa099 100644 --- a/modules/integration/tests-integration/tests-backend/src/test/resources/org/wso2/identity/integration/test/rest/api/server/application/management/v1/create-basic-application.json +++ b/modules/integration/tests-integration/tests-backend/src/test/resources/org/wso2/identity/integration/test/rest/api/server/application/management/v1/create-basic-application.json @@ -4,5 +4,8 @@ "imageUrl": "https://localhost/image", "accessUrl": "https://localhost/accessUrl", "templateId": "Test_template_1", - "templateVersion": "v1.0.0" + "templateVersion": "v1.0.0", + "advancedConfigurations": { + "discoverableByEndUsers": true + } } diff --git a/modules/integration/tests-integration/tests-backend/src/test/resources/org/wso2/identity/integration/test/rest/api/server/application/management/v1/invalid-discoverable-groups.json b/modules/integration/tests-integration/tests-backend/src/test/resources/org/wso2/identity/integration/test/rest/api/server/application/management/v1/invalid-discoverable-groups.json new file mode 100644 index 00000000000..fa289e89ac7 --- /dev/null +++ b/modules/integration/tests-integration/tests-backend/src/test/resources/org/wso2/identity/integration/test/rest/api/server/application/management/v1/invalid-discoverable-groups.json @@ -0,0 +1,31 @@ +{ + "name": "Test Invalid App", + "accessUrl": "https://example.com/access", + "advancedConfigurations": { + "discoverableByEndUsers": true, + "discoverableGroups": [ + { + "userStore": "PRIMARY", + "groups": [ + { + "id": "invalid-id-1" + }, + { + "id": "invalid-id-2" + } + ] + }, + { + "userStore": "INVALID_USER_STORE_1", + "groups": [ + { + "id": "invalid-id-1" + }, + { + "id": "invalid-id-2" + } + ] + } + ] + } +} diff --git a/pom.xml b/pom.xml index fecd40c484d..ca13cb5a031 100755 --- a/pom.xml +++ b/pom.xml @@ -1,19 +1,22 @@ +