From 07b0df8f6299de1179096b71e7be083e2ac9e478 Mon Sep 17 00:00:00 2001 From: cgeorgilakis <55974447+cgeorgilakis@users.noreply.github.com> Date: Wed, 7 Sep 2022 12:25:31 +0300 Subject: [PATCH] View groups from account console (#7933) Closes #8748 --- .../migration/migrators/MigrateTo20_0_0.java | 41 +++++ .../datastore/LegacyMigrationManager.java | 4 +- .../org/keycloak/models/AccountRoles.java | 1 + .../services/managers/RealmManager.java | 3 + .../resources/account/AccountConsole.java | 6 + .../resources/account/AccountRestService.java | 12 ++ .../keycloak/testsuite/admin/ClientTest.java | 4 +- .../migration/AbstractMigrationTest.java | 24 ++- ...Import1301MigrationClientPoliciesTest.java | 1 + .../JsonFileImport198MigrationTest.java | 1 + .../JsonFileImport255MigrationTest.java | 1 + .../JsonFileImport343MigrationTest.java | 1 + .../JsonFileImport483MigrationTest.java | 1 + .../JsonFileImport903MigrationTest.java | 1 + .../testsuite/migration/MigrationTest.java | 7 + .../account/messages/messages_en.properties | 1 + .../theme/keycloak.v2/account/index.ftl | 3 +- .../account/messages/messages_en.properties | 8 + .../account/resources/content.json | 10 ++ .../src/app/content/group-page/GroupsPage.tsx | 168 ++++++++++++++++++ .../account/src/app/widgets/features.ts | 1 + 21 files changed, 292 insertions(+), 7 deletions(-) create mode 100644 model/legacy-private/src/main/java/org/keycloak/migration/migrators/MigrateTo20_0_0.java create mode 100644 themes/src/main/resources/theme/keycloak.v2/account/src/app/content/group-page/GroupsPage.tsx diff --git a/model/legacy-private/src/main/java/org/keycloak/migration/migrators/MigrateTo20_0_0.java b/model/legacy-private/src/main/java/org/keycloak/migration/migrators/MigrateTo20_0_0.java new file mode 100644 index 0000000000..7be0a72ebc --- /dev/null +++ b/model/legacy-private/src/main/java/org/keycloak/migration/migrators/MigrateTo20_0_0.java @@ -0,0 +1,41 @@ +package org.keycloak.migration.migrators; + +import org.keycloak.migration.ModelVersion; +import org.keycloak.models.AccountRoles; +import org.keycloak.models.ClientModel; +import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.representations.idm.RealmRepresentation; + +public class MigrateTo20_0_0 implements Migration { + + public static final ModelVersion VERSION = new ModelVersion("20.0.0"); + + @Override + public void migrate(KeycloakSession session) { + + session.realms().getRealmsStream().forEach(this::addViewGroupsRole); + } + + @Override + public void migrateImport(KeycloakSession session, RealmModel realm, RealmRepresentation rep, boolean skipUserDependent) { + addViewGroupsRole(realm); + } + + private void addViewGroupsRole(RealmModel realm) { + ClientModel accountClient = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID); + if (accountClient != null && accountClient.getRole(AccountRoles.VIEW_GROUPS) == null) { + RoleModel viewGroupsRole = accountClient.addRole(AccountRoles.VIEW_GROUPS); + viewGroupsRole.setDescription("${role_" + AccountRoles.VIEW_GROUPS + "}"); + ClientModel accountConsoleClient = realm.getClientByClientId(Constants.ACCOUNT_CONSOLE_CLIENT_ID); + accountConsoleClient.addScopeMapping(viewGroupsRole); + } + } + + @Override + public ModelVersion getVersion() { + return VERSION; + } +} diff --git a/model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyMigrationManager.java b/model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyMigrationManager.java index 269efafb34..caf00b2402 100644 --- a/model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyMigrationManager.java +++ b/model/legacy-private/src/main/java/org/keycloak/storage/datastore/LegacyMigrationManager.java @@ -24,6 +24,7 @@ import org.keycloak.migration.ModelVersion; import org.keycloak.migration.migrators.MigrateTo12_0_0; import org.keycloak.migration.migrators.MigrateTo14_0_0; import org.keycloak.migration.migrators.MigrateTo18_0_0; +import org.keycloak.migration.migrators.MigrateTo20_0_0; import org.keycloak.migration.migrators.MigrateTo1_2_0; import org.keycloak.migration.migrators.MigrateTo1_3_0; import org.keycloak.migration.migrators.MigrateTo1_4_0; @@ -104,7 +105,8 @@ public class LegacyMigrationManager implements MigrationManager { new MigrateTo9_0_4(), new MigrateTo12_0_0(), new MigrateTo14_0_0(), - new MigrateTo18_0_0() + new MigrateTo18_0_0(), + new MigrateTo20_0_0() }; private final KeycloakSession session; diff --git a/server-spi-private/src/main/java/org/keycloak/models/AccountRoles.java b/server-spi-private/src/main/java/org/keycloak/models/AccountRoles.java index 9c2958d5c7..f607664418 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/AccountRoles.java +++ b/server-spi-private/src/main/java/org/keycloak/models/AccountRoles.java @@ -29,6 +29,7 @@ public interface AccountRoles { String VIEW_CONSENT = "view-consent"; String MANAGE_CONSENT = "manage-consent"; String DELETE_ACCOUNT = "delete-account"; + String VIEW_GROUPS = "view-groups"; String[] DEFAULT = {VIEW_PROFILE, MANAGE_ACCOUNT}; diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java index 43f0613082..ba8c47b263 100755 --- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java +++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java @@ -438,6 +438,8 @@ public class RealmManager { RoleModel manageConsentRole = accountClient.addRole(AccountRoles.MANAGE_CONSENT); manageConsentRole.setDescription("${role_" + AccountRoles.MANAGE_CONSENT + "}"); manageConsentRole.addCompositeRole(viewConsentRole); + RoleModel viewGroups = accountClient.addRole(AccountRoles.VIEW_GROUPS); + viewGroups.setDescription("${role_" + AccountRoles.VIEW_GROUPS + "}"); KeycloakModelUtils.setupDeleteAccount(accountClient); @@ -458,6 +460,7 @@ public class RealmManager { accountConsoleClient.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); accountConsoleClient.addScopeMapping(accountClient.getRole(AccountRoles.MANAGE_ACCOUNT)); + accountConsoleClient.addScopeMapping(accountClient.getRole(AccountRoles.VIEW_GROUPS)); ProtocolMapperModel audienceMapper = new ProtocolMapperModel(); audienceMapper.setName(OIDCLoginProtocolFactory.AUDIENCE_RESOLVE); diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java b/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java index 64765e51e4..69fc1d6f18 100644 --- a/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java @@ -138,15 +138,21 @@ public class AccountConsole { boolean isTotpConfigured = false; boolean deleteAccountAllowed = false; + boolean isViewGroupsEnabled= false; if (user != null) { isTotpConfigured = user.credentialManager().isConfiguredFor(realm.getOTPPolicy().getType()); RoleModel deleteAccountRole = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).getRole(AccountRoles.DELETE_ACCOUNT); deleteAccountAllowed = deleteAccountRole != null && user.hasRole(deleteAccountRole) && realm.getRequiredActionProviderByAlias(DeleteAccount.PROVIDER_ID).isEnabled(); + RoleModel viewGrouRole = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).getRole(AccountRoles.VIEW_GROUPS); + isViewGroupsEnabled = viewGrouRole != null && user.hasRole(viewGrouRole); } map.put("isTotpConfigured", isTotpConfigured); map.put("deleteAccountAllowed", deleteAccountAllowed); + + map.put("isViewGroupsEnabled", isViewGroupsEnabled); + map.put("updateEmailFeatureEnabled", Profile.isFeatureEnabled(Profile.Feature.UPDATE_EMAIL)); RequiredActionProviderModel updateEmailActionProvider = realm.getRequiredActionProviderByAlias(UserModel.RequiredAction.UPDATE_EMAIL.name()); map.put("updateEmailActionEnabled", updateEmailActionProvider != null && updateEmailActionProvider.isEnabled()); diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java index a27f594261..96744a0c63 100755 --- a/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java @@ -35,6 +35,7 @@ import java.util.stream.Stream; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; +import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.NotFoundException; import javax.ws.rs.POST; @@ -66,6 +67,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserModel; +import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.provider.ConfiguredProvider; import org.keycloak.representations.account.ClientRepresentation; import org.keycloak.representations.account.ConsentRepresentation; @@ -74,6 +76,7 @@ import org.keycloak.representations.account.UserProfileAttributeMetadata; import org.keycloak.representations.account.UserProfileMetadata; import org.keycloak.representations.account.UserRepresentation; import org.keycloak.representations.idm.ErrorRepresentation; +import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.services.ErrorResponse; import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.UserConsentManager; @@ -484,6 +487,15 @@ public class AccountRestService { return new LinkedAccountsResource(session, request, client, auth, event, user); } + @Path("/groups") + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public Stream groupMemberships(@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) { + auth.require(AccountRoles.VIEW_GROUPS); + return ModelToRepresentation.toGroupHierarchy(user, !briefRepresentation); + } + @Path("/applications") @GET @Produces(MediaType.APPLICATION_JSON) 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 359a4d63cb..6f979b8c8e 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 @@ -627,7 +627,7 @@ public class ClientTest extends AbstractAdminTest { Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAll(), AccountRoles.VIEW_PROFILE); Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listEffective(), AccountRoles.VIEW_PROFILE); - Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAvailable(), AccountRoles.MANAGE_ACCOUNT, AccountRoles.MANAGE_ACCOUNT_LINKS, AccountRoles.VIEW_APPLICATIONS, AccountRoles.VIEW_CONSENT, AccountRoles.MANAGE_CONSENT, AccountRoles.DELETE_ACCOUNT); + Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAvailable(), AccountRoles.MANAGE_ACCOUNT, AccountRoles.MANAGE_ACCOUNT_LINKS, AccountRoles.VIEW_APPLICATIONS, AccountRoles.VIEW_CONSENT, AccountRoles.MANAGE_CONSENT, AccountRoles.DELETE_ACCOUNT, AccountRoles.VIEW_GROUPS); Assert.assertNames(scopesResource.getAll().getRealmMappings(), "realm-composite"); Assert.assertNames(scopesResource.getAll().getClientMappings().get(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).getMappings(), AccountRoles.VIEW_PROFILE); @@ -643,7 +643,7 @@ public class ClientTest extends AbstractAdminTest { Assert.assertNames(scopesResource.realmLevel().listAvailable(), "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION, "realm-composite", "realm-child", Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + REALM_NAME); Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAll()); - Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAvailable(), AccountRoles.VIEW_PROFILE, AccountRoles.MANAGE_ACCOUNT, AccountRoles.MANAGE_ACCOUNT_LINKS, AccountRoles.VIEW_APPLICATIONS, AccountRoles.VIEW_CONSENT, AccountRoles.MANAGE_CONSENT, AccountRoles.DELETE_ACCOUNT); + Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAvailable(), AccountRoles.VIEW_PROFILE, AccountRoles.MANAGE_ACCOUNT, AccountRoles.MANAGE_ACCOUNT_LINKS, AccountRoles.VIEW_APPLICATIONS, AccountRoles.VIEW_CONSENT, AccountRoles.MANAGE_CONSENT, AccountRoles.DELETE_ACCOUNT, AccountRoles.VIEW_GROUPS); Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listEffective()); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java index aa2fcc4853..d9ceca4b3c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java @@ -98,6 +98,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT; import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT_LINKS; +import static org.keycloak.models.AccountRoles.VIEW_GROUPS; import static org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_CLIENT_ID; import static org.keycloak.testsuite.Assert.assertNames; import static org.keycloak.testsuite.auth.page.AuthRealm.MASTER; @@ -319,6 +320,12 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest { testPostLogoutRedirectUrisSet(migrationRealm); } + protected void testMigrationTo20_0_0() { + testViewGroups(masterRealm); + testViewGroups(migrationRealm); + } + + protected void testDeleteAccount(RealmResource realm) { ClientRepresentation accountClient = realm.clients().findByClientId(ACCOUNT_MANAGEMENT_CLIENT_ID).get(0); ClientResource accountResource = realm.clients().get(accountClient.getId()); @@ -387,9 +394,8 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest { MappingsRepresentation scopes = clientResource.getScopeMappings().getAll(); assertNull(scopes.getRealmMappings()); assertEquals(1, scopes.getClientMappings().size()); - assertEquals(1, scopes.getClientMappings().get(ACCOUNT_MANAGEMENT_CLIENT_ID).getMappings().size()); - assertEquals(MANAGE_ACCOUNT, scopes.getClientMappings().get(ACCOUNT_MANAGEMENT_CLIENT_ID).getMappings().get(0).getName()); - + assertEquals(2, scopes.getClientMappings().get(ACCOUNT_MANAGEMENT_CLIENT_ID).getMappings().size()); + Assert.assertNames(scopes.getClientMappings().get(ACCOUNT_MANAGEMENT_CLIENT_ID).getMappings(), MANAGE_ACCOUNT, VIEW_GROUPS); List mappers = clientResource.getProtocolMappers().getMappers(); assertEquals(1, mappers.size()); assertEquals("oidc-audience-resolve-mapper", mappers.get(0).getProtocolMapper()); @@ -491,6 +497,14 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest { } } + protected void testViewGroups(RealmResource realm) { + ClientRepresentation accountClient = realm.clients().findByClientId(ACCOUNT_MANAGEMENT_CLIENT_ID).get(0); + + ClientResource accountResource = realm.clients().get(accountClient.getId()); + RoleRepresentation viewAppRole = accountResource.roles().get(VIEW_GROUPS).toRepresentation(); + assertNotNull(viewAppRole); + } + protected void testRoleManageAccountLinks(RealmResource... realms) { log.info("testing role manage account links"); for (RealmResource realm : realms) { @@ -954,6 +968,10 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest { testMigrationTo19_0_0(); } + protected void testMigrationTo20_x() { + testMigrationTo20_0_0(); + } + protected void testMigrationTo7_x(boolean supportedAuthzServices) { if (supportedAuthzServices) { testDecisionStrategySetOnResourceServer(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport1301MigrationClientPoliciesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport1301MigrationClientPoliciesTest.java index 9e40454765..3537622b8e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport1301MigrationClientPoliciesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport1301MigrationClientPoliciesTest.java @@ -66,5 +66,6 @@ public class JsonFileImport1301MigrationClientPoliciesTest extends AbstractJsonF Assert.assertTrue(clientProfiles.getProfiles().isEmpty()); ClientPoliciesRepresentation clientPolicies = adminClient.realms().realm("test").clientPoliciesPoliciesResource().getPolicies(); Assert.assertTrue(clientPolicies.getPolicies().isEmpty()); + testViewGroups(masterRealm); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport198MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport198MigrationTest.java index 2145a7b7bb..a23b3326f0 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport198MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport198MigrationTest.java @@ -70,6 +70,7 @@ public class JsonFileImport198MigrationTest extends AbstractJsonFileImportMigrat testMigrationTo9_x(); testMigrationTo12_x(false); testMigrationTo18_x(); + testMigrationTo20_x(); } @Override diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport255MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport255MigrationTest.java index e7e729f95a..2f0602ed46 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport255MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport255MigrationTest.java @@ -72,6 +72,7 @@ public class JsonFileImport255MigrationTest extends AbstractJsonFileImportMigrat testMigrationTo9_x(); testMigrationTo12_x(false); testMigrationTo18_x(); + testMigrationTo20_x(); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport343MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport343MigrationTest.java index d1ce95b118..344bc54e33 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport343MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport343MigrationTest.java @@ -67,6 +67,7 @@ public class JsonFileImport343MigrationTest extends AbstractJsonFileImportMigrat testMigrationTo9_x(); testMigrationTo12_x(true); testMigrationTo18_x(); + testMigrationTo20_x(); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport483MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport483MigrationTest.java index 00b5411d74..a5b6cf8872 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport483MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport483MigrationTest.java @@ -60,6 +60,7 @@ public class JsonFileImport483MigrationTest extends AbstractJsonFileImportMigrat testMigrationTo9_x(); testMigrationTo12_x(true); testMigrationTo18_x(); + testMigrationTo20_x(); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport903MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport903MigrationTest.java index c9b09c3145..f7fbb16ac4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport903MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport903MigrationTest.java @@ -53,6 +53,7 @@ public class JsonFileImport903MigrationTest extends AbstractJsonFileImportMigrat checkRealmsImported(); testMigrationTo12_x(true); testMigrationTo18_x(); + testMigrationTo20_x(); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java index 1f2c043aa1..93bb58e3e7 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java @@ -68,6 +68,8 @@ public class MigrationTest extends AbstractMigrationTest { // Always test offline-token login during migration test testOfflineTokenLogin(); testExtremelyLongClientAttribute(migrationRealm); + + testMigrationTo20_x(); } @Test @@ -78,6 +80,7 @@ public class MigrationTest extends AbstractMigrationTest { testMigrationTo12_x(true); testMigrationTo18_x(); testMigrationTo19_x(); + testMigrationTo20_x(); // Always test offline-token login during migration test testOfflineTokenLogin(); @@ -97,6 +100,7 @@ public class MigrationTest extends AbstractMigrationTest { testMigrationTo12_x(true); testMigrationTo18_x(); testMigrationTo19_x(); + testMigrationTo20_x(); // Always test offline-token login during migration test testOfflineTokenLogin(); @@ -117,6 +121,7 @@ public class MigrationTest extends AbstractMigrationTest { testMigrationTo12_x(true); testMigrationTo18_x(); testMigrationTo19_x(); + testMigrationTo20_x(); // Always test offline-token login during migration test testOfflineTokenLogin(); @@ -145,6 +150,7 @@ public class MigrationTest extends AbstractMigrationTest { testMigrationTo12_x(false); testMigrationTo18_x(); testMigrationTo19_x(); + testMigrationTo20_x(); // Always test offline-token login during migration test testOfflineTokenLogin(); @@ -166,6 +172,7 @@ public class MigrationTest extends AbstractMigrationTest { testMigrationTo12_x(false); testMigrationTo18_x(); testMigrationTo19_x(); + testMigrationTo20_x(); // Always test offline-token login during migration test testOfflineTokenLogin(); diff --git a/themes/src/main/resources/theme/base/account/messages/messages_en.properties b/themes/src/main/resources/theme/base/account/messages/messages_en.properties index 2c260914bf..5da255fb93 100755 --- a/themes/src/main/resources/theme/base/account/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/account/messages/messages_en.properties @@ -80,6 +80,7 @@ role_create-realm=Create realm role_view-realm=View realm role_view-users=View users role_view-applications=View applications +role_view-groups=View groups role_view-clients=View clients role_view-events=View events role_view-identity-providers=View identity providers diff --git a/themes/src/main/resources/theme/keycloak.v2/account/index.ftl b/themes/src/main/resources/theme/keycloak.v2/account/index.ftl index a8bb2ea095..bedc5b8364 100644 --- a/themes/src/main/resources/theme/keycloak.v2/account/index.ftl +++ b/themes/src/main/resources/theme/keycloak.v2/account/index.ftl @@ -48,7 +48,8 @@ isTotpConfigured : ${isTotpConfigured?c}, deleteAccountAllowed : ${deleteAccountAllowed?c}, updateEmailFeatureEnabled: ${updateEmailFeatureEnabled?c}, - updateEmailActionEnabled: ${updateEmailActionEnabled?c} + updateEmailActionEnabled: ${updateEmailActionEnabled?c}, + isViewGroupsEnabled : ${isViewGroupsEnabled?c} } var availableLocales = []; diff --git a/themes/src/main/resources/theme/keycloak.v2/account/messages/messages_en.properties b/themes/src/main/resources/theme/keycloak.v2/account/messages/messages_en.properties index 3ab5d33806..45f8bdad68 100644 --- a/themes/src/main/resources/theme/keycloak.v2/account/messages/messages_en.properties +++ b/themes/src/main/resources/theme/keycloak.v2/account/messages/messages_en.properties @@ -166,3 +166,11 @@ error-username-invalid-character=''{0}'' contains invalid character. error-person-name-invalid-character='{0}' contains invalid character. updateEmail=Update email + +#groups +groupLabel=Groups +groupDescriptionLabel=View groups that you are associated with +path=Path +directMembership=Direct membership +noGroups=No groups +noGroupsText=You are not joined in any group diff --git a/themes/src/main/resources/theme/keycloak.v2/account/resources/content.json b/themes/src/main/resources/theme/keycloak.v2/account/resources/content.json index ef26e31770..f918eb6723 100644 --- a/themes/src/main/resources/theme/keycloak.v2/account/resources/content.json +++ b/themes/src/main/resources/theme/keycloak.v2/account/resources/content.json @@ -47,6 +47,16 @@ "modulePath": "/content/applications-page/ApplicationsPage.js", "componentName": "ApplicationsPage" }, + { + "id": "groups", + "path": "groups", + "icon": "pf-icon-server-group", + "label": "groupLabel", + "descriptionLabel": "groupDescriptionLabel", + "modulePath": "/content/group-page/GroupsPage.js", + "componentName": "GroupsPage", + "hidden": "!features.isViewGroupsEnabled" + }, { "id": "resources", "icon": "pf-icon-repository", diff --git a/themes/src/main/resources/theme/keycloak.v2/account/src/app/content/group-page/GroupsPage.tsx b/themes/src/main/resources/theme/keycloak.v2/account/src/app/content/group-page/GroupsPage.tsx new file mode 100644 index 0000000000..e510ea6b3c --- /dev/null +++ b/themes/src/main/resources/theme/keycloak.v2/account/src/app/content/group-page/GroupsPage.tsx @@ -0,0 +1,168 @@ +import * as React from 'react'; + +import { + Checkbox, + DataList, + DataListItem, + DataListItemRow, + DataListCell, + DataListItemCells, +} from '@patternfly/react-core'; + +import { ContentPage } from '../ContentPage'; +import { HttpResponse } from '../../account-service/account.service'; +import { AccountServiceContext } from '../../account-service/AccountServiceContext'; +import { Msg } from '../../widgets/Msg'; + +export interface GroupsPageProps { +} + +export interface GroupsPageState { + groups: Group[]; + directGroups: Group[]; + isDirectMembership: boolean; +} + +interface Group { + id?: string; + name: string; + path: string; +} + +export class GroupsPage extends React.Component { + static contextType = AccountServiceContext; + context: React.ContextType; + + public constructor(props: GroupsPageProps, context: React.ContextType) { + super(props); + this.context = context; + this.state = { + groups: [], + directGroups: [], + isDirectMembership: false + }; + + this.fetchGroups(); + } + + private fetchGroups(): void { + this.context!.doGet("/groups") + .then((response: HttpResponse) => { + const directGroups = response.data || []; + const groups = [...directGroups]; + const groupsPaths = directGroups.map(s => s.path); + directGroups.forEach((el) => this.getParents(el, groups, groupsPaths)) + this.setState({ + groups: groups, + directGroups: directGroups + }); + }); + } + + private getParents(el: Group, groups: Group[], groupsPaths: string[]): void { + const parentPath = el.path.slice(0, el.path.lastIndexOf('/')); + if (parentPath && (groupsPaths.indexOf(parentPath) === -1)) { + + el = { + name: parentPath.slice(parentPath.lastIndexOf('/')+1), + path: parentPath + }; + groups.push(el); + groupsPaths.push(parentPath); + + this.getParents(el, groups, groupsPaths); + } + } + + private changeDirectMembership = (checked: boolean,event: React.FormEvent )=> { + this.setState({ + isDirectMembership: checked + }); + } + + private emptyGroup(): React.ReactNode { + + return ( + + + + ]} /> + + + ) + } + + private renderGroupList(group: Group, appIndex: number): React.ReactNode { + + return ( + + + + {group.name} + , + + {group.path} + , + + + + ]} + /> + + + + ) + } + + public render(): React.ReactNode { + return ( + + + + + + + + + ]} + /> + + + + + + + , + + + , + + + , + ]} + /> + + + {this.state.groups.length === 0 + ? this.emptyGroup() + : (this.state.isDirectMembership ? this.state.directGroups.map((group: Group, appIndex: number) => + this.renderGroupList(group, appIndex) + ) : this.state.groups.map((group: Group, appIndex: number) => + this.renderGroupList(group, appIndex)))} + + + ); + } +}; diff --git a/themes/src/main/resources/theme/keycloak.v2/account/src/app/widgets/features.ts b/themes/src/main/resources/theme/keycloak.v2/account/src/app/widgets/features.ts index 7e33b9d255..ba79f1e93e 100644 --- a/themes/src/main/resources/theme/keycloak.v2/account/src/app/widgets/features.ts +++ b/themes/src/main/resources/theme/keycloak.v2/account/src/app/widgets/features.ts @@ -26,6 +26,7 @@ deleteAccountAllowed: boolean; updateEmailFeatureEnabled: boolean; updateEmailActionEnabled: boolean; + isViewGroupsEnabled: boolean; }