diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java index aa62abd2f8ad..4a41953fe26d 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java @@ -17,16 +17,17 @@ package org.keycloak.services.resources.admin; import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.ws.rs.ForbiddenException; import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.NoCache; -import org.keycloak.http.HttpRequest; -import org.keycloak.http.HttpResponse; -import jakarta.ws.rs.NotFoundException; import org.keycloak.Config; import org.keycloak.common.ClientConnection; +import org.keycloak.common.Profile; import org.keycloak.common.Version; import org.keycloak.common.util.UriUtils; import org.keycloak.headers.SecurityHeadersProvider; +import org.keycloak.http.HttpRequest; +import org.keycloak.http.HttpResponse; import org.keycloak.models.AdminRoles; import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; @@ -48,6 +49,8 @@ import org.keycloak.utils.MediaType; import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotAuthorizedException; +import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.OPTIONS; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; @@ -57,7 +60,6 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; -import java.util.HashSet; import java.util.Locale; import java.util.Map; import java.util.Properties; @@ -191,7 +193,7 @@ public Response whoAmIPreFlight() { /** * Permission information * - * @param headers + * @param currentRealm * @return */ @Path("whoami") @@ -199,6 +201,10 @@ public Response whoAmIPreFlight() { @Produces(MediaType.APPLICATION_JSON) @NoCache public Response whoAmI(@QueryParam("currentRealm") String currentRealm) { + if (!Profile.isFeatureEnabled(Profile.Feature.ADMIN_API)) { + throw new NotFoundException(); + } + RealmManager realmManager = new RealmManager(session); AuthenticationManager.AuthResult authResult = new AppAuthManager.BearerTokenAuthenticator(session) .setRealm(realm) @@ -207,8 +213,13 @@ public Response whoAmI(@QueryParam("currentRealm") String currentRealm) { .authenticate(); if (authResult == null) { - return Response.status(401).build(); + throw new NotAuthorizedException("Bearer"); + } + + if (!Constants.ADMIN_CONSOLE_CLIENT_ID.equals(authResult.getToken().getIssuedFor())) { + throw new ForbiddenException("Token not valid for admin console"); } + UserModel user= authResult.getUser(); String displayName; if ((user.getFirstName() != null && !user.getFirstName().trim().equals("")) || (user.getLastName() != null && !user.getLastName().trim().equals(""))) { @@ -237,6 +248,11 @@ public Response whoAmI(@QueryParam("currentRealm") String currentRealm) { addRealmAccess(realm, user, realmAccess); } + if (realmAccess.isEmpty() || realmAccess.values().iterator().next().isEmpty()) { + // if the user has no access in the realm just return forbidden/403 + throw new ForbiddenException("No realm access"); + } + Locale locale = session.getContext().resolveLocale(user); Cors.add(request).allowedOrigins(authResult.getToken()).allowedMethods("GET").auth() @@ -256,29 +272,13 @@ private void addMasterRealmAccess(UserModel user, String currentRealm, Map HashSet union(Set set1, Set set2) { - if (set1 == null && set2 == null) { - return null; - } - HashSet res; - if (set1 instanceof HashSet) { - res = (HashSet ) set1; - } else { - res = set1 == null ? new HashSet<>() : new HashSet<>(set1); - } - if (set2 != null) { - res.addAll(set2); - } - return res; - } - private void getRealmAdminAccess(RealmModel realm, ClientModel client, UserModel user, Map> realmAdminAccess) { Set realmRoles = client.getRolesStream() .filter(user::hasRole) .map(RoleModel::getName) .collect(Collectors.toSet()); - realmAdminAccess.merge(realm.getName(), realmRoles, AdminConsole::union); + realmAdminAccess.put(realm.getName(), realmRoles); } /** diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AdminConsolePermissionsCalculatedTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AdminConsolePermissionsCalculatedTest.java deleted file mode 100644 index 8573bff7b64d..000000000000 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AdminConsolePermissionsCalculatedTest.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2020 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed 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.keycloak.testsuite.admin; - -import com.fasterxml.jackson.databind.JsonNode; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.keycloak.Config; -import org.keycloak.admin.client.Keycloak; -import org.keycloak.broker.provider.util.SimpleHttp; -import org.keycloak.representations.AccessTokenResponse; -import org.keycloak.representations.idm.RealmRepresentation; -import org.keycloak.testsuite.AbstractKeycloakTest; -import org.keycloak.testsuite.broker.util.SimpleHttpDefault; -import org.keycloak.testsuite.updaters.Creator; -import org.keycloak.testsuite.util.AdminClientUtil; -import org.keycloak.testsuite.util.RealmBuilder; - -import java.io.IOException; -import java.util.List; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -public class AdminConsolePermissionsCalculatedTest extends AbstractKeycloakTest { - - private static final String REALM_NAME = "realm-name"; - - private CloseableHttpClient client; - - @Before - public void before() { - client = HttpClientBuilder.create().build(); - } - - @After - public void after() { - try { - client.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public void addTestRealms(List testRealms) { - } - - @Test - public void changeRealmTokenAlgorithm() throws Exception { - try (Keycloak adminClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(), suiteContext.getAuthServerInfo().getContextRoot().toString()); - Creator c = Creator.create(adminClient, RealmBuilder.create().name(REALM_NAME).build())) { - AccessTokenResponse accessToken = adminClient.tokenManager().getAccessToken(); - assertNotNull(adminClient.realms().findAll()); - - String whoAmiUrl = suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/admin/master/console/whoami?currentRealm=master"; - - JsonNode jsonNode = SimpleHttpDefault.doGet(whoAmiUrl, client).auth(accessToken.getToken()).asJson(); - - assertTrue("Permissions for " + Config.getAdminRealm() + " realm.", jsonNode.at("/realm_access/" + Config.getAdminRealm()).isArray()); - } - } - -} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AdminConsoleWhoAmILocaleTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AdminConsoleWhoAmILocaleTest.java index 11597f578471..b4bfc24b5e91 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AdminConsoleWhoAmILocaleTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AdminConsoleWhoAmILocaleTest.java @@ -1,38 +1,42 @@ package org.keycloak.testsuite.admin; import com.fasterxml.jackson.databind.JsonNode; +import jakarta.ws.rs.core.Response; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; +import org.jboss.arquillian.graphene.page.Page; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.keycloak.OAuth2Constants; import org.keycloak.admin.client.Keycloak; import org.keycloak.broker.provider.util.SimpleHttp; +import org.keycloak.models.AdminRoles; +import org.keycloak.models.Constants; +import org.keycloak.protocol.oidc.utils.PkceUtils; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.auth.page.AuthRealm; import org.keycloak.testsuite.broker.util.SimpleHttpDefault; +import org.keycloak.testsuite.console.page.AdminConsole; +import org.keycloak.testsuite.util.AdminClientUtil; +import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.UserBuilder; -import java.io.IOException; -import java.util.HashSet; -import java.util.List; - -import static java.util.Arrays.asList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.keycloak.models.AdminRoles.REALM_ADMIN; -import static org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID; -import static org.keycloak.models.Constants.REALM_MANAGEMENT_CLIENT_ID; -import static org.keycloak.testsuite.util.AdminClientUtil.createAdminClient; - public class AdminConsoleWhoAmILocaleTest extends AbstractKeycloakTest { private static final String REALM_I18N_OFF = "realm-i18n-off"; private static final String REALM_I18N_ON = "realm-i18n-on"; private static final String USER_WITHOUT_LOCALE = "user-without-locale"; private static final String USER_WITH_LOCALE = "user-with-locale"; + private static final String USER_NO_ACCESS = "user-no-access"; private static final String PASSWORD = "password"; private static final String DEFAULT_LOCALE = "en"; private static final String REALM_LOCALE = "no"; @@ -41,6 +45,9 @@ public class AdminConsoleWhoAmILocaleTest extends AbstractKeycloakTest { private CloseableHttpClient client; + @Page + private AdminConsole adminConsole; + @Before public void createHttpClient() throws Exception { client = HttpClientBuilder.create().build(); @@ -63,108 +70,223 @@ public void addTestRealms(List testRealms) { realm.user(UserBuilder.create() .username(USER_WITHOUT_LOCALE) .password(PASSWORD) - .role(REALM_MANAGEMENT_CLIENT_ID, REALM_ADMIN)); + .role(Constants.REALM_MANAGEMENT_CLIENT_ID, AdminRoles.REALM_ADMIN)); realm.user(UserBuilder.create() .username(USER_WITH_LOCALE) .password(PASSWORD) .addAttribute("locale", USER_LOCALE) - .role(REALM_MANAGEMENT_CLIENT_ID, REALM_ADMIN)); + .role(Constants.REALM_MANAGEMENT_CLIENT_ID, AdminRoles.REALM_ADMIN)); testRealms.add(realm.build()); realm = RealmBuilder.create() .name(REALM_I18N_ON) .internationalizationEnabled(true) - .supportedLocales(new HashSet<>(asList(REALM_LOCALE, USER_LOCALE, EXTRA_LOCALE))) + .supportedLocales(new HashSet<>(Arrays.asList(REALM_LOCALE, USER_LOCALE, EXTRA_LOCALE))) .defaultLocale(REALM_LOCALE); realm.user(UserBuilder.create() .username(USER_WITHOUT_LOCALE) .password(PASSWORD) - .role(REALM_MANAGEMENT_CLIENT_ID, REALM_ADMIN)); + .role(Constants.REALM_MANAGEMENT_CLIENT_ID, AdminRoles.REALM_ADMIN)); realm.user(UserBuilder.create() .username(USER_WITH_LOCALE) .password(PASSWORD) .addAttribute("locale", USER_LOCALE) - .role(REALM_MANAGEMENT_CLIENT_ID, REALM_ADMIN)); + .role(Constants.REALM_MANAGEMENT_CLIENT_ID, AdminRoles.REALM_ADMIN)); + realm.user(UserBuilder.create() + .username(USER_NO_ACCESS) + .password(PASSWORD) + .addAttribute("locale", USER_LOCALE)); testRealms.add(realm.build()); } - private String accessToken(String realmName, String username) throws Exception { - try (Keycloak adminClient = createAdminClient(true, realmName, username, PASSWORD, ADMIN_CLI_CLIENT_ID, null)) { - AccessTokenResponse accessToken = adminClient.tokenManager().getAccessToken(); - assertNotNull(accessToken); - return accessToken.getToken(); - } + private OAuthClient.AccessTokenResponse accessToken(String realmName, String username, String password) throws Exception { + String codeVerifier = PkceUtils.generateCodeVerifier(); + oauth.realm(realmName) + .codeVerifier(codeVerifier) + .codeChallenge(PkceUtils.generateS256CodeChallenge(codeVerifier)) + .codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256) + .clientId(Constants.ADMIN_CONSOLE_CLIENT_ID) + .redirectUri(adminConsole.createUriBuilder().build(realmName).toASCIIString()); + oauth.doLogin(username, password); + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, null); + return response; } private String whoAmiUrl(String realmName) { - return suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/admin/" + realmName + "/console/whoami"; + return whoAmiUrl(realmName, null); + } + + private String whoAmiUrl(String realmName, String currentRealm) { + StringBuilder sb = new StringBuilder() + .append(suiteContext.getAuthServerInfo().getContextRoot().toString()) + .append("/auth/admin/") + .append(realmName) + .append("/console/whoami"); + if (currentRealm != null) { + sb.append("?currentRealm=").append(currentRealm); + } + return sb.toString(); + } + + private void checkRealmAccess(String realm, JsonNode whoAmI) { + Assert.assertNotNull(whoAmI.get("realm_access")); + Assert.assertNotNull(whoAmI.get("realm_access").get(realm)); + Assert.assertTrue(whoAmI.get("realm_access").get(realm).isArray()); + Assert.assertTrue(whoAmI.get("realm_access").get(realm).size() > 0); } @Test public void testLocaleRealmI18nDisabledUserWithoutLocale() throws Exception { + OAuthClient.AccessTokenResponse response = accessToken(REALM_I18N_OFF, USER_WITHOUT_LOCALE, PASSWORD); JsonNode whoAmI = SimpleHttpDefault .doGet(whoAmiUrl(REALM_I18N_OFF), client) .header("Accept", "application/json") - .auth(accessToken(REALM_I18N_OFF, USER_WITHOUT_LOCALE)) + .auth(response.getAccessToken()) .asJson(); - assertEquals(REALM_I18N_OFF, whoAmI.get("realm").asText()); - assertEquals(DEFAULT_LOCALE, whoAmI.get("locale").asText()); + Assert.assertEquals(REALM_I18N_OFF, whoAmI.get("realm").asText()); + Assert.assertEquals(DEFAULT_LOCALE, whoAmI.get("locale").asText()); + checkRealmAccess(REALM_I18N_OFF, whoAmI); + oauth.doLogout(response.getRefreshToken(), null); } @Test public void testLocaleRealmI18nDisabledUserWithLocale() throws Exception { + OAuthClient.AccessTokenResponse response = accessToken(REALM_I18N_OFF, USER_WITH_LOCALE, PASSWORD); JsonNode whoAmI = SimpleHttpDefault .doGet(whoAmiUrl(REALM_I18N_OFF), client) .header("Accept", "application/json") - .auth(accessToken(REALM_I18N_OFF, USER_WITH_LOCALE)) + .auth(response.getAccessToken()) .asJson(); - assertEquals(REALM_I18N_OFF, whoAmI.get("realm").asText()); - assertEquals(DEFAULT_LOCALE, whoAmI.get("locale").asText()); + Assert.assertEquals(REALM_I18N_OFF, whoAmI.get("realm").asText()); + Assert.assertEquals(DEFAULT_LOCALE, whoAmI.get("locale").asText()); + checkRealmAccess(REALM_I18N_OFF, whoAmI); + oauth.doLogout(response.getRefreshToken(), null); } @Test public void testLocaleRealmI18nEnabledUserWithoutLocale() throws Exception { + OAuthClient.AccessTokenResponse response = accessToken(REALM_I18N_ON, USER_WITHOUT_LOCALE, PASSWORD); JsonNode whoAmI = SimpleHttpDefault .doGet(whoAmiUrl(REALM_I18N_ON), client) .header("Accept", "application/json") - .auth(accessToken(REALM_I18N_ON, USER_WITHOUT_LOCALE)) + .auth(response.getAccessToken()) .asJson(); - assertEquals(REALM_I18N_ON, whoAmI.get("realm").asText()); - assertEquals(REALM_LOCALE, whoAmI.get("locale").asText()); + Assert.assertEquals(REALM_I18N_ON, whoAmI.get("realm").asText()); + Assert.assertEquals(REALM_LOCALE, whoAmI.get("locale").asText()); + checkRealmAccess(REALM_I18N_ON, whoAmI); + oauth.doLogout(response.getRefreshToken(), null); } @Test public void testLocaleRealmI18nEnabledUserWithLocale() throws Exception { + OAuthClient.AccessTokenResponse response = accessToken(REALM_I18N_ON, USER_WITH_LOCALE, PASSWORD); JsonNode whoAmI = SimpleHttpDefault .doGet(whoAmiUrl(REALM_I18N_ON), client) .header("Accept", "application/json") - .auth(accessToken(REALM_I18N_ON, USER_WITH_LOCALE)) + .auth(response.getAccessToken()) .asJson(); - assertEquals(REALM_I18N_ON, whoAmI.get("realm").asText()); - assertEquals(USER_LOCALE, whoAmI.get("locale").asText()); + Assert.assertEquals(REALM_I18N_ON, whoAmI.get("realm").asText()); + Assert.assertEquals(USER_LOCALE, whoAmI.get("locale").asText()); + checkRealmAccess(REALM_I18N_ON, whoAmI); + oauth.doLogout(response.getRefreshToken(), null); } @Test public void testLocaleRealmI18nEnabledAcceptLanguageHeader() throws Exception { + OAuthClient.AccessTokenResponse response = accessToken(REALM_I18N_ON, USER_WITHOUT_LOCALE, PASSWORD); JsonNode whoAmI = SimpleHttpDefault .doGet(whoAmiUrl(REALM_I18N_ON), client) .header("Accept", "application/json") - .auth(accessToken(REALM_I18N_ON, USER_WITHOUT_LOCALE)) + .auth(response.getAccessToken()) .header("Accept-Language", EXTRA_LOCALE) .asJson(); - assertEquals(REALM_I18N_ON, whoAmI.get("realm").asText()); - assertEquals(EXTRA_LOCALE, whoAmI.get("locale").asText()); + Assert.assertEquals(REALM_I18N_ON, whoAmI.get("realm").asText()); + Assert.assertEquals(EXTRA_LOCALE, whoAmI.get("locale").asText()); + checkRealmAccess(REALM_I18N_ON, whoAmI); + oauth.doLogout(response.getRefreshToken(), null); } @Test public void testLocaleRealmI18nEnabledKeycloakLocaleCookie() throws Exception { + OAuthClient.AccessTokenResponse response = accessToken(REALM_I18N_ON, USER_WITHOUT_LOCALE, PASSWORD); JsonNode whoAmI = SimpleHttpDefault .doGet(whoAmiUrl(REALM_I18N_ON), client) .header("Accept", "application/json") - .auth(accessToken(REALM_I18N_ON, USER_WITHOUT_LOCALE)) + .auth(response.getAccessToken()) .header("Cookie", "KEYCLOAK_LOCALE=" + EXTRA_LOCALE) .asJson(); - assertEquals(REALM_I18N_ON, whoAmI.get("realm").asText()); - assertEquals(EXTRA_LOCALE, whoAmI.get("locale").asText()); + Assert.assertEquals(REALM_I18N_ON, whoAmI.get("realm").asText()); + Assert.assertEquals(EXTRA_LOCALE, whoAmI.get("locale").asText()); + checkRealmAccess(REALM_I18N_ON, whoAmI); + oauth.doLogout(response.getRefreshToken(), null); + } + + @Test + public void testMasterRealm() throws Exception { + OAuthClient.AccessTokenResponse response = accessToken(AuthRealm.MASTER, AuthRealm.ADMIN, AuthRealm.ADMIN); + JsonNode whoAmI = SimpleHttpDefault + .doGet(whoAmiUrl(AuthRealm.MASTER), client) + .header("Accept", "application/json") + .auth(response.getAccessToken()) + .asJson(); + Assert.assertEquals(AuthRealm.MASTER, whoAmI.get("realm").asText()); + Assert.assertEquals(DEFAULT_LOCALE, whoAmI.get("locale").asText()); + checkRealmAccess(AuthRealm.MASTER, whoAmI); + oauth.doLogout(response.getRefreshToken(), null); + } + + @Test + public void testMasterRealmCurrentRealm() throws Exception { + OAuthClient.AccessTokenResponse response = accessToken(AuthRealm.MASTER, AuthRealm.ADMIN, AuthRealm.ADMIN); + JsonNode whoAmI = SimpleHttpDefault + .doGet(whoAmiUrl(AuthRealm.MASTER, REALM_I18N_ON), client) + .header("Accept", "application/json") + .auth(response.getAccessToken()) + .asJson(); + Assert.assertEquals(AuthRealm.MASTER, whoAmI.get("realm").asText()); + Assert.assertEquals(DEFAULT_LOCALE, whoAmI.get("locale").asText()); + checkRealmAccess(REALM_I18N_ON, whoAmI); + oauth.doLogout(response.getRefreshToken(), null); + } + + @Test + public void testLocaleRealmNoToken() throws Exception { + try (SimpleHttp.Response response = SimpleHttpDefault + .doGet(whoAmiUrl(REALM_I18N_ON), client) + .header("Accept", "application/json") + .asResponse()) { + Assert.assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), response.getStatus()); + } + } + + @Test + public void testLocaleRealmUserNoAccess() throws Exception { + OAuthClient.AccessTokenResponse response = accessToken(REALM_I18N_ON, USER_NO_ACCESS, PASSWORD); + try (SimpleHttp.Response res = SimpleHttpDefault + .doGet(whoAmiUrl(REALM_I18N_ON), client) + .header("Accept", "application/json") + .auth(response.getAccessToken()) + .asResponse()) { + Assert.assertEquals(Response.Status.FORBIDDEN.getStatusCode(), res.getStatus()); + } + oauth.doLogout(response.getRefreshToken(), null); + } + + @Test + public void testLocaleRealmTokenForOtherClient() throws Exception { + try (Keycloak adminCliClient = AdminClientUtil.createAdminClient(true, REALM_I18N_ON, + USER_WITH_LOCALE, PASSWORD, Constants.ADMIN_CLI_CLIENT_ID, null)) { + AccessTokenResponse accessToken = adminCliClient.tokenManager().getAccessToken(); + Assert.assertNotNull(accessToken); + String token = accessToken.getToken(); + try (SimpleHttp.Response response = SimpleHttpDefault + .doGet(whoAmiUrl(REALM_I18N_ON), client) + .header("Accept", "application/json") + .auth(token) + .asResponse()) { + Assert.assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus()); + } + } } }