diff --git a/js/apps/admin-ui/src/clients/utils.ts b/js/apps/admin-ui/src/clients/utils.ts index 0eb72b781f..df94c0bb5a 100644 --- a/js/apps/admin-ui/src/clients/utils.ts +++ b/js/apps/admin-ui/src/clients/utils.ts @@ -4,7 +4,8 @@ import type { TFunction } from "i18next"; /** * Checks if a client is intended to be used for authenticating a to a realm. */ -export const isRealmClient = (client: ClientRepresentation) => !client.protocol; +export const isRealmClient = (client: ClientRepresentation): boolean => + client.attributes?.["realm_client"] === true.toString(); /** * Gets a human readable name for the specified protocol. diff --git a/server-spi-private/src/main/java/org/keycloak/models/Constants.java b/server-spi-private/src/main/java/org/keycloak/models/Constants.java index da45df6030..11838d6f28 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/Constants.java +++ b/server-spi-private/src/main/java/org/keycloak/models/Constants.java @@ -181,4 +181,7 @@ public final class Constants { // attribute name used in apps to mark that it is an admin console and its azp is allowed public static final String SECURITY_ADMIN_CONSOLE_ATTR = "security.admin.console"; + + //attribute name used to mark a client as realm client + public static final String REALM_CLIENT = "realm_client"; } diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index ee6ce995a3..90947652d0 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -21,6 +21,7 @@ import static java.util.Optional.ofNullable; import static org.keycloak.models.utils.StripSecretsUtils.stripSecrets; import org.jboss.logging.Logger; +import org.keycloak.Config; import org.keycloak.authentication.otp.OTPApplicationProvider; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.AuthorizationProviderFactory; @@ -737,6 +738,8 @@ public class ModelToRepresentation { rep.setNodeReRegistrationTimeout(clientModel.getNodeReRegistrationTimeout()); rep.setClientAuthenticatorType(clientModel.getClientAuthenticatorType()); + rep.getAttributes().put(Constants.REALM_CLIENT, String.valueOf(isRealmClient(clientModel.getClientId(), clientModel.getRealm(), session))); + // adding the secret if non public or bearer only if (clientModel.isBearerOnly() || clientModel.isPublicClient()) { rep.setSecret(null); @@ -778,6 +781,23 @@ public class ModelToRepresentation { return rep; } + private static boolean isRealmClient(String clientId, RealmModel realm, KeycloakSession session) { + final String realmClientSuffix = "-realm"; + + if (clientId == null) { + return false; + } + if (Constants.BROKER_SERVICE_CLIENT_ID.equals(clientId)) { + return true; + } + if (Config.getAdminRealm().equals(realm.getName())) { + return clientId.endsWith(realmClientSuffix) && session.realms().getRealmByName(clientId.substring(0, clientId.length() - realmClientSuffix.length())) != null; + } + else { + return Constants.REALM_MANAGEMENT_CLIENT_ID.equals(clientId); + } + } + public static IdentityProviderRepresentation toBriefRepresentation(RealmModel realm, IdentityProviderModel identityProviderModel) { IdentityProviderRepresentation providerRep = new IdentityProviderRepresentation(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java index 34104f82f5..ff789deb1c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java @@ -96,6 +96,13 @@ public class ClientTest extends AbstractAdminTest { Assert.assertNames(realm.clients().findAll(), "account", "account-console", "realm-management", "security-admin-console", "broker", Constants.ADMIN_CLI_CLIENT_ID); } + @Test + public void getRealmClients() { + assertTrue(realm.clients().findAll().stream().filter(client-> client.getAttributes().get(Constants.REALM_CLIENT).equals("true")) + .map(ClientRepresentation::getClientId) + .allMatch(clientId -> clientId.equals(Constants.REALM_MANAGEMENT_CLIENT_ID) || clientId.equals(Constants.BROKER_SERVICE_CLIENT_ID) || clientId.endsWith("-realm"))); + } + private ClientRepresentation createClient() { return createClient(null); } @@ -122,7 +129,7 @@ public class ClientTest extends AbstractAdminTest { return rep; } - + private ClientRepresentation createClientNonPublic() { ClientRepresentation rep = new ClientRepresentation(); rep.setClientId("my-app"); @@ -142,7 +149,7 @@ public class ClientTest extends AbstractAdminTest { return rep; } - + @Test public void createClientVerifyWithSecret() { String id = createClientNonPublic().getId(); @@ -442,14 +449,14 @@ public class ClientTest extends AbstractAdminTest { assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.roleResourceCompositesPath(Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + REALM_NAME), Collections.singletonList(role), ResourceType.REALM_ROLE); - assertThat(realm.roles().get(Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + REALM_NAME).getRoleComposites().stream().map(RoleRepresentation::getName).collect(Collectors.toSet()), + assertThat(realm.roles().get(Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + REALM_NAME).getRoleComposites().stream().map(RoleRepresentation::getName).collect(Collectors.toSet()), hasItem(role.getName())); realm.clients().get(id).roles().deleteRole("test"); assertAdminEvents.assertEvent(realmId, OperationType.DELETE, AdminEventPaths.clientRoleResourcePath(id, "test"), ResourceType.CLIENT_ROLE); - assertThat(realm.roles().get(Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + REALM_NAME).getRoleComposites().stream().map(RoleRepresentation::getName).collect(Collectors.toSet()), + assertThat(realm.roles().get(Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + REALM_NAME).getRoleComposites().stream().map(RoleRepresentation::getName).collect(Collectors.toSet()), not(hasItem(role))); } @@ -558,7 +565,7 @@ public class ClientTest extends AbstractAdminTest { realm.clients().get(id).registerNode(Collections.singletonMap("node", "foo#")); } - + @Test public void nodes() { testingClient.testApp().clearAdminActions();