diff --git a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/AdminApi.java b/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/AdminApi.java index 77c0cee8124..86e8478a3e1 100644 --- a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/AdminApi.java +++ b/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/AdminApi.java @@ -1,11 +1,17 @@ package org.keycloak.admin.api; import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; -import org.keycloak.admin.api.realm.RealmsApi; +import org.keycloak.admin.api.client.ClientsApi; public interface AdminApi { - @Path("realms") - RealmsApi realms(); + String CONTENT_TYPE_MERGE_PATCH = "application/merge-patch+json"; + + /** + * Retrieve the Clients API group by version + */ + @Path("clients/{version:v\\d+}") + ClientsApi clients(@PathParam("version") String version); } diff --git a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/AdminRootV2.java b/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/AdminRootV2.java index c1a43c1123f..2543bb7665b 100644 --- a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/AdminRootV2.java +++ b/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/AdminRootV2.java @@ -3,7 +3,9 @@ package org.keycloak.admin.api; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.OPTIONS; import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.Provider; import org.keycloak.common.Profile; @@ -19,18 +21,19 @@ public class AdminRootV2 { @Context protected KeycloakSession session; - @Path("v2") - public AdminApi adminApi() { + @Path("{realmName}") + public AdminApi adminApi(@PathParam("realmName") String realmName) { checkApiEnabled(); - return new DefaultAdminApi(session); + return new DefaultAdminApi(session, realmName); } - @Path("{any:.*}") + // TODO Fix preflights + @Path("{realmName}/{any:.*}") @OPTIONS @Operation(hidden = true) - public Object preFlight() { + public Response preFlight() { checkApiEnabled(); - return new AdminCorsPreflightService(); + return new AdminCorsPreflightService().preflight(); } private void checkApiEnabled() { diff --git a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/DefaultAdminApi.java b/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/DefaultAdminApi.java index 5b0ef6a106b..d08ffeed215 100644 --- a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/DefaultAdminApi.java +++ b/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/DefaultAdminApi.java @@ -1,24 +1,28 @@ package org.keycloak.admin.api; import jakarta.ws.rs.NotAuthorizedException; +import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; import org.keycloak.Config; -import org.keycloak.admin.api.realm.DefaultRealmsApi; -import org.keycloak.admin.api.realm.RealmsApi; +import org.keycloak.admin.api.client.ClientsApi; +import org.keycloak.admin.api.client.DefaultClientsApi; import org.keycloak.models.AdminRoles; import org.keycloak.models.KeycloakSession; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.services.resources.admin.AdminAuth; import org.keycloak.services.resources.admin.AdminRoot; +import org.keycloak.services.resources.admin.RealmAdminResource; import org.keycloak.services.resources.admin.RealmsAdminResource; public class DefaultAdminApi implements AdminApi { private final KeycloakSession session; private final RealmsAdminResource realmsAdminResource; + private final RealmAdminResource realmAdminResource; private final AdminAuth auth; - public DefaultAdminApi(KeycloakSession session) { + public DefaultAdminApi(KeycloakSession session, String realmName) { this.session = session; this.auth = AdminRoot.authenticateRealmAdminRequest(session); @@ -27,12 +31,15 @@ public class DefaultAdminApi implements AdminApi { throw new NotAuthorizedException("Wrong permissions"); } this.realmsAdminResource = new RealmsAdminResource(session, auth, new TokenManager()); + this.realmAdminResource = realmsAdminResource.getRealmAdmin(realmName); } - @Path("realms") + @Path("clients/{version:v\\d+}") @Override - public RealmsApi realms() { - return new DefaultRealmsApi(session, realmsAdminResource); + public ClientsApi clients(@PathParam("version") String version) { + return switch (version) { + case "v2" -> new DefaultClientsApi(session, realmAdminResource); + default -> throw new NotFoundException(); + }; } - } diff --git a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/ClientApi.java b/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/ClientApi.java index 01bc82f9f3b..fb020de7a14 100644 --- a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/ClientApi.java +++ b/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/ClientApi.java @@ -14,10 +14,9 @@ import org.keycloak.representations.admin.v2.BaseClientRepresentation; import com.fasterxml.jackson.databind.JsonNode; -public interface ClientApi { +import static org.keycloak.admin.api.AdminApi.CONTENT_TYPE_MERGE_PATCH; - // TODO move these - String CONTENT_TYPE_MERGE_PATCH = "application/merge-patch+json"; +public interface ClientApi { @GET @Produces(MediaType.APPLICATION_JSON) diff --git a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/DefaultClientApi.java b/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/DefaultClientApi.java index 860d5c0b6bc..8c2c6245c36 100644 --- a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/DefaultClientApi.java +++ b/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/DefaultClientApi.java @@ -3,13 +3,17 @@ package org.keycloak.admin.api.client; import java.io.IOException; import java.util.Objects; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.PATCH; +import jakarta.ws.rs.PUT; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import org.keycloak.models.ClientModel; +import org.keycloak.admin.api.AdminApi; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.representations.admin.v2.BaseClientRepresentation; @@ -30,7 +34,6 @@ public class DefaultClientApi implements ClientApi { private final KeycloakSession session; private final RealmModel realm; - private final ClientModel client; private final ClientService clientService; private final ClientResource clientResource; @@ -45,18 +48,19 @@ public class DefaultClientApi implements ClientApi { this.clientId = clientId; this.realm = Objects.requireNonNull(session.getContext().getRealm()); - this.client = Objects.requireNonNull(session.getContext().getClient()); this.clientService = new DefaultClientService(session, realmAdminResource, clientResource); this.objectMapper = MAPPER; } + @GET @Override public BaseClientRepresentation getClient() { - return clientService.getClient(realm, client.getClientId(), null) + return clientService.getClient(realm, clientId, null) .orElseThrow(() -> new NotFoundException("Cannot find the specified client")); } + @PUT @Override public Response createOrUpdateClient(BaseClientRepresentation client) { try { @@ -71,13 +75,14 @@ public class DefaultClientApi implements ClientApi { } } + @PATCH @Override public BaseClientRepresentation patchClient(JsonNode patch) { BaseClientRepresentation client = getClient(); try { String contentType = session.getContext().getHttpRequest().getHttpHeaders().getHeaderString(HttpHeaders.CONTENT_TYPE); MediaType mediaType = contentType == null ? null : MediaType.valueOf(contentType); - MediaType mergePatch = MediaType.valueOf(ClientApi.CONTENT_TYPE_MERGE_PATCH); + MediaType mergePatch = MediaType.valueOf(AdminApi.CONTENT_TYPE_MERGE_PATCH); if (mediaType == null || !mediaType.isCompatible(mergePatch)) { throw new WebApplicationException("Unsupported media type", Response.Status.UNSUPPORTED_MEDIA_TYPE); } @@ -96,6 +101,7 @@ public class DefaultClientApi implements ClientApi { } } + @DELETE @Override public void deleteClient() { if (clientResource == null) { diff --git a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/DefaultClientsApi.java b/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/DefaultClientsApi.java index c4f4da214bd..d1a09db9ac4 100644 --- a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/DefaultClientsApi.java +++ b/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/DefaultClientsApi.java @@ -5,6 +5,9 @@ import java.util.Optional; import java.util.stream.Stream; import jakarta.validation.Valid; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.Response; @@ -39,11 +42,13 @@ public class DefaultClientsApi implements ClientsApi { this.clientsResource = realmAdminResource.getClients(); } + @GET @Override public Stream getClients() { return clientService.getClients(realm, null, null, null); } + @POST @Override public Response createClient(@Valid BaseClientRepresentation client) { try { @@ -57,6 +62,7 @@ public class DefaultClientsApi implements ClientsApi { } } + @Path("{id}") @Override public ClientApi client(@PathParam("id") String clientId) { var client = Optional.ofNullable(session.clients().getClientByClientId(realm, clientId)); diff --git a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/realm/DefaultRealmApi.java b/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/realm/DefaultRealmApi.java deleted file mode 100644 index 9384de9fdf8..00000000000 --- a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/realm/DefaultRealmApi.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.keycloak.admin.api.realm; - -import jakarta.ws.rs.Path; - -import org.keycloak.admin.api.client.ClientsApi; -import org.keycloak.admin.api.client.DefaultClientsApi; -import org.keycloak.models.KeycloakSession; -import org.keycloak.services.resources.admin.RealmAdminResource; - -public class DefaultRealmApi implements RealmApi { - private final KeycloakSession session; - private final RealmAdminResource realmAdminResource; - - public DefaultRealmApi(KeycloakSession session, RealmAdminResource realmAdmin) { - this.session = session; - this.realmAdminResource = realmAdmin; - } - - @Path("clients") - @Override - public ClientsApi clients() { - return new DefaultClientsApi(session, realmAdminResource); - } - -} diff --git a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/realm/DefaultRealmsApi.java b/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/realm/DefaultRealmsApi.java deleted file mode 100644 index b217f08f023..00000000000 --- a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/realm/DefaultRealmsApi.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.keycloak.admin.api.realm; - -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; - -import org.keycloak.models.KeycloakSession; -import org.keycloak.services.resources.admin.RealmsAdminResource; - -public class DefaultRealmsApi implements RealmsApi { - private final KeycloakSession session; - private final RealmsAdminResource realmsAdminResource; - - public DefaultRealmsApi(KeycloakSession session, RealmsAdminResource realmsAdminResource) { - this.session = session; - this.realmsAdminResource = realmsAdminResource; - } - - @Path("{name}") - @Override - public RealmApi realm(@PathParam("name") String name) { - var realmAdmin = realmsAdminResource.getRealmAdmin(name); - return new DefaultRealmApi(session, realmAdmin); - } - -} diff --git a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/realm/RealmApi.java b/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/realm/RealmApi.java deleted file mode 100644 index 17ea5d56f01..00000000000 --- a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/realm/RealmApi.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.keycloak.admin.api.realm; - -import jakarta.ws.rs.Path; - -import org.keycloak.admin.api.client.ClientsApi; - -public interface RealmApi { - - @Path("clients") - ClientsApi clients(); -} diff --git a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/realm/RealmsApi.java b/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/realm/RealmsApi.java deleted file mode 100644 index f3df56c23c7..00000000000 --- a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/realm/RealmsApi.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.keycloak.admin.api.realm; - -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; - -public interface RealmsApi { - - @Path("{name}") - RealmApi realm(@PathParam("name") String name); -} diff --git a/rest/admin-v2/tests/src/test/java/org/keycloak/tests/admin/client/v2/ClientApiV2Test.java b/rest/admin-v2/tests/src/test/java/org/keycloak/tests/admin/client/v2/ClientApiV2Test.java index 4ec6a6807fb..bc449448549 100644 --- a/rest/admin-v2/tests/src/test/java/org/keycloak/tests/admin/client/v2/ClientApiV2Test.java +++ b/rest/admin-v2/tests/src/test/java/org/keycloak/tests/admin/client/v2/ClientApiV2Test.java @@ -23,7 +23,7 @@ import java.util.Set; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; -import org.keycloak.admin.api.client.ClientApi; +import org.keycloak.admin.api.AdminApi; import org.keycloak.admin.client.Keycloak; import org.keycloak.common.Profile; import org.keycloak.representations.admin.v2.BaseClientRepresentation; @@ -45,6 +45,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.http.HttpMessage; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpOptions; import org.apache.http.client.methods.HttpPatch; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; @@ -54,6 +55,9 @@ import org.apache.http.util.EntityUtils; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import static org.keycloak.services.cors.Cors.ACCESS_CONTROL_ALLOW_METHODS; +import static org.keycloak.services.cors.Cors.ORIGIN_HEADER; + import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -63,7 +67,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; @KeycloakIntegrationTest(config = ClientApiV2Test.AdminV2Config.class) public class ClientApiV2Test { - public static final String HOSTNAME_LOCAL_ADMIN = "http://localhost:8080/admin/api/v2"; + public static final String HOSTNAME_LOCAL_ADMIN = "http://localhost:8080/admin/api/master/clients/v2"; private static ObjectMapper mapper; @InjectHttpClient @@ -85,7 +89,7 @@ public class ClientApiV2Test { @Test public void getClient() throws Exception { - HttpGet request = new HttpGet(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients/account"); + HttpGet request = new HttpGet(HOSTNAME_LOCAL_ADMIN + "/account"); setAuthHeader(request); try (var response = client.execute(request)) { assertEquals(200, response.getStatusLine().getStatusCode()); @@ -96,7 +100,7 @@ public class ClientApiV2Test { @Test public void jsonPatchClient() throws Exception { - HttpPatch request = new HttpPatch(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients/account"); + HttpPatch request = new HttpPatch(HOSTNAME_LOCAL_ADMIN + "/account"); setAuthHeader(request); request.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_PATCH_JSON); try (var response = client.execute(request)) { @@ -107,9 +111,9 @@ public class ClientApiV2Test { @Test public void jsonMergePatchClient() throws Exception { - HttpPatch request = new HttpPatch(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients/account"); + HttpPatch request = new HttpPatch(HOSTNAME_LOCAL_ADMIN + "/account"); setAuthHeader(request); - request.setHeader(HttpHeaders.CONTENT_TYPE, ClientApi.CONTENT_TYPE_MERGE_PATCH); + request.setHeader(HttpHeaders.CONTENT_TYPE, AdminApi.CONTENT_TYPE_MERGE_PATCH); OIDCClientRepresentation patch = new OIDCClientRepresentation(); patch.setDescription("I'm also a description"); @@ -126,7 +130,7 @@ public class ClientApiV2Test { @Test public void putFailsWithDifferentClientId() throws Exception { - HttpPut request = new HttpPut(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients/account"); + HttpPut request = new HttpPut(HOSTNAME_LOCAL_ADMIN + "/account"); setAuthHeader(request); request.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); @@ -142,7 +146,7 @@ public class ClientApiV2Test { @Test public void putCreateOrUpdates() throws Exception { - HttpPut request = new HttpPut(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients/other"); + HttpPut request = new HttpPut(HOSTNAME_LOCAL_ADMIN + "/other"); setAuthHeader(request); request.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); @@ -171,7 +175,7 @@ public class ClientApiV2Test { @Test public void createClient() throws Exception { - HttpPost request = new HttpPost(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients"); + HttpPost request = new HttpPost(HOSTNAME_LOCAL_ADMIN); setAuthHeader(request); request.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); @@ -197,7 +201,7 @@ public class ClientApiV2Test { @Test public void deleteClient() throws Exception { - HttpPut createRequest = new HttpPut(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients/to-delete"); + HttpPut createRequest = new HttpPut(HOSTNAME_LOCAL_ADMIN + "/to-delete"); setAuthHeader(createRequest); createRequest.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); @@ -211,13 +215,13 @@ public class ClientApiV2Test { assertEquals(201, response.getStatusLine().getStatusCode()); } - HttpGet getRequest = new HttpGet(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients/to-delete"); + HttpGet getRequest = new HttpGet(HOSTNAME_LOCAL_ADMIN + "/to-delete"); setAuthHeader(getRequest); try (var response = client.execute(getRequest)) { assertEquals(200, response.getStatusLine().getStatusCode()); } - HttpDelete deleteRequest = new HttpDelete(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients/to-delete"); + HttpDelete deleteRequest = new HttpDelete(HOSTNAME_LOCAL_ADMIN + "/to-delete"); setAuthHeader(deleteRequest); try (var response = client.execute(deleteRequest)) { assertEquals(204, response.getStatusLine().getStatusCode()); @@ -231,7 +235,7 @@ public class ClientApiV2Test { @Test public void getClientsMixedProtocols() throws Exception { // Create an OIDC client with OIDC-specific fields - HttpPost oidcRequest = new HttpPost(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients"); + HttpPost oidcRequest = new HttpPost(HOSTNAME_LOCAL_ADMIN); setAuthHeader(oidcRequest); oidcRequest.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); @@ -250,7 +254,7 @@ public class ClientApiV2Test { } // Create a SAML client with SAML-specific fields - HttpPost samlRequest = new HttpPost(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients"); + HttpPost samlRequest = new HttpPost(HOSTNAME_LOCAL_ADMIN); setAuthHeader(samlRequest); samlRequest.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); @@ -272,7 +276,7 @@ public class ClientApiV2Test { } // Get all clients - this should work with mixed protocols - HttpGet getRequest = new HttpGet(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients"); + HttpGet getRequest = new HttpGet(HOSTNAME_LOCAL_ADMIN); setAuthHeader(getRequest); try (var response = client.execute(getRequest)) { @@ -308,7 +312,7 @@ public class ClientApiV2Test { } // Get individual OIDC client and verify OIDC-specific fields - HttpGet getOidcRequest = new HttpGet(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients/mixed-test-oidc"); + HttpGet getOidcRequest = new HttpGet(HOSTNAME_LOCAL_ADMIN + "/mixed-test-oidc"); setAuthHeader(getOidcRequest); try (var response = client.execute(getOidcRequest)) { @@ -321,7 +325,7 @@ public class ClientApiV2Test { } // Get individual SAML client and verify SAML-specific fields - HttpGet getSamlRequest = new HttpGet(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients/mixed-test-saml"); + HttpGet getSamlRequest = new HttpGet(HOSTNAME_LOCAL_ADMIN + "/mixed-test-saml"); setAuthHeader(getSamlRequest); try (var response = client.execute(getSamlRequest)) { @@ -338,13 +342,13 @@ public class ClientApiV2Test { } // Cleanup - HttpDelete deleteOidc = new HttpDelete(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients/mixed-test-oidc"); + HttpDelete deleteOidc = new HttpDelete(HOSTNAME_LOCAL_ADMIN + "/mixed-test-oidc"); setAuthHeader(deleteOidc); try (var response = client.execute(deleteOidc)) { assertEquals(204, response.getStatusLine().getStatusCode()); } - HttpDelete deleteSaml = new HttpDelete(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients/mixed-test-saml"); + HttpDelete deleteSaml = new HttpDelete(HOSTNAME_LOCAL_ADMIN + "/mixed-test-saml"); setAuthHeader(deleteSaml); try (var response = client.execute(deleteSaml)) { assertEquals(204, response.getStatusLine().getStatusCode()); @@ -353,7 +357,7 @@ public class ClientApiV2Test { @Test public void OIDCClientRepresentationValidation() throws Exception { - HttpPost request = new HttpPost(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients"); + HttpPost request = new HttpPost(HOSTNAME_LOCAL_ADMIN); setAuthHeader(request); request.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); @@ -401,7 +405,7 @@ public class ClientApiV2Test { @Test public void authenticationRequired() throws Exception { - HttpGet request = new HttpGet(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients/account"); + HttpGet request = new HttpGet(HOSTNAME_LOCAL_ADMIN + "/account"); setAuthHeader(request, noAccessAdminClient); try (var response = client.execute(request)) { assertEquals(401, response.getStatusLine().getStatusCode()); @@ -410,7 +414,7 @@ public class ClientApiV2Test { @Test public void createFullClient() throws Exception { - HttpPost request = new HttpPost(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients"); + HttpPost request = new HttpPost(HOSTNAME_LOCAL_ADMIN); setAuthHeader(request); request.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); @@ -426,7 +430,7 @@ public class ClientApiV2Test { @Test public void createFullClientWrongServiceAccountRoles() throws Exception { - HttpPost request = new HttpPost(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients"); + HttpPost request = new HttpPost(HOSTNAME_LOCAL_ADMIN); setAuthHeader(request); request.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); @@ -443,7 +447,7 @@ public class ClientApiV2Test { @Test public void declarativeRoleManagement() throws Exception { // 1. Create a client with initial roles - HttpPut createRequest = new HttpPut(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients/declarative-role-test"); + HttpPut createRequest = new HttpPut(HOSTNAME_LOCAL_ADMIN + "/declarative-role-test"); setAuthHeader(createRequest); createRequest.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); @@ -461,7 +465,7 @@ public class ClientApiV2Test { } // 2. Update with completely new roles - should remove old ones and add new ones - HttpPut updateRequest = new HttpPut(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients/declarative-role-test"); + HttpPut updateRequest = new HttpPut(HOSTNAME_LOCAL_ADMIN + "/declarative-role-test"); setAuthHeader(updateRequest); updateRequest.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); @@ -508,7 +512,7 @@ public class ClientApiV2Test { @Test public void declarativeServiceAccountRoleManagement() throws Exception { // 1. Create a client with service account and initial realm roles - HttpPut createRequest = new HttpPut(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients/sa-declarative-test"); + HttpPut createRequest = new HttpPut(HOSTNAME_LOCAL_ADMIN + "/sa-declarative-test"); setAuthHeader(createRequest); createRequest.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); @@ -528,7 +532,7 @@ public class ClientApiV2Test { } // 2. Update with completely new roles - should remove old ones and add new ones - HttpPut updateRequest = new HttpPut(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients/sa-declarative-test"); + HttpPut updateRequest = new HttpPut(HOSTNAME_LOCAL_ADMIN + "/sa-declarative-test"); setAuthHeader(updateRequest); updateRequest.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); @@ -572,6 +576,54 @@ public class ClientApiV2Test { } } + @Test + public void versionedClientsApi() throws Exception { + final var ADMIN_API_URL = "http://localhost:8080/admin/api/master"; + + // no version specified - default + HttpGet request = new HttpGet(ADMIN_API_URL + "/clients"); + setAuthHeader(request); + try (var response = client.execute(request)) { + assertThat(response.getStatusLine().getStatusCode(), is(405)); // 405 for now due to the preflight check (needs to be fixed) + } + + // v2 specified + request = new HttpGet(ADMIN_API_URL + "/clients/v2"); + setAuthHeader(request); + try (var response = client.execute(request)) { + assertThat(response.getStatusLine().getStatusCode(), is(200)); + EntityUtils.consumeQuietly(response.getEntity()); + } + + // unknown version + request = new HttpGet(ADMIN_API_URL + "/clients/v3"); + setAuthHeader(request); + try (var response = client.execute(request)) { + assertThat(response.getStatusLine().getStatusCode(), is(404)); + } + + // invalid version + request = new HttpGet(ADMIN_API_URL + "/clients/4"); + setAuthHeader(request); + try (var response = client.execute(request)) { + assertThat(response.getStatusLine().getStatusCode(), is(405)); // 405 for now due to the preflight check (needs to be fixed) + } + } + + @Test + public void preflight() throws Exception { + HttpOptions request = new HttpOptions(HOSTNAME_LOCAL_ADMIN); + request.setHeader(ORIGIN_HEADER, "http://localhost:8080"); + + // we can improve preflight logic in follow-up issues + try (var response = client.execute(request)) { + assertThat(response.getStatusLine().getStatusCode(), is(200)); + var header = response.getFirstHeader(ACCESS_CONTROL_ALLOW_METHODS); + assertThat(header, notNullValue()); + assertThat(header.getValue(), is("DELETE, POST, GET, PUT")); + } + } + private OIDCClientRepresentation getTestingFullClientRep() { var rep = new OIDCClientRepresentation(); rep.setClientId("my-client");