From b1ff9511d118099340e58ac7238b15864d22a22b Mon Sep 17 00:00:00 2001 From: vramik Date: Fri, 1 Nov 2024 09:53:51 +0100 Subject: [PATCH] Fine grained admin permissions feature V2 Closes #34563 Signed-off-by: vramik --- .github/workflows/js-ci.yml | 2 +- .../src/main/java/org/keycloak/common/Profile.java | 4 +++- .../test/java/org/keycloak/common/ProfileTest.java | 2 +- .../org/keycloak/it/cli/dist/FeaturesDistTest.java | 4 ++-- .../services/resources/admin/ClientResource.java | 6 ++++-- .../services/resources/admin/GroupResource.java | 12 ++++++++---- .../resources/admin/IdentityProviderResource.java | 9 ++++++--- .../resources/admin/RealmAdminResource.java | 13 +++++++------ .../services/resources/admin/RoleByIdResource.java | 7 +++++-- .../resources/admin/RoleContainerResource.java | 6 +++++- 10 files changed, 42 insertions(+), 23 deletions(-) diff --git a/.github/workflows/js-ci.yml b/.github/workflows/js-ci.yml index f48ae5abf6..b24eabca52 100644 --- a/.github/workflows/js-ci.yml +++ b/.github/workflows/js-ci.yml @@ -240,7 +240,7 @@ jobs: - name: Start Keycloak server run: | tar xfvz keycloak-999.0.0-SNAPSHOT.tar.gz - keycloak-999.0.0-SNAPSHOT/bin/kc.sh start-dev --features=admin-fine-grained-authz,transient-users &> ~/server.log & + keycloak-999.0.0-SNAPSHOT/bin/kc.sh start-dev --features=admin-fine-grained-authz:v1,transient-users &> ~/server.log & env: KC_BOOTSTRAP_ADMIN_USERNAME: admin KC_BOOTSTRAP_ADMIN_PASSWORD: admin diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java index 99572e5f93..a8e11e69c9 100755 --- a/common/src/main/java/org/keycloak/common/Profile.java +++ b/common/src/main/java/org/keycloak/common/Profile.java @@ -53,7 +53,9 @@ public class Profile { ACCOUNT_V3("Account Console version 3", Type.DEFAULT, 3, Feature.ACCOUNT_API), - ADMIN_FINE_GRAINED_AUTHZ("Fine-Grained Admin Permissions", Type.PREVIEW), + ADMIN_FINE_GRAINED_AUTHZ("Fine-Grained Admin Permissions", Type.PREVIEW, 1), + + ADMIN_FINE_GRAINED_AUTHZ_V2("Fine-Grained Admin Permissions version 2", Type.EXPERIMENTAL, 2, Feature.AUTHORIZATION), ADMIN_API("Admin API", Type.DEFAULT), diff --git a/common/src/test/java/org/keycloak/common/ProfileTest.java b/common/src/test/java/org/keycloak/common/ProfileTest.java index 42cde43424..a45584a4d5 100644 --- a/common/src/test/java/org/keycloak/common/ProfileTest.java +++ b/common/src/test/java/org/keycloak/common/ProfileTest.java @@ -29,7 +29,7 @@ public class ProfileTest { private static final Profile.Feature DEFAULT_FEATURE = Profile.Feature.AUTHORIZATION; private static final Profile.Feature DISABLED_BY_DEFAULT_FEATURE = Profile.Feature.DOCKER; - private static final Profile.Feature PREVIEW_FEATURE = Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ; + private static final Profile.Feature PREVIEW_FEATURE = Profile.Feature.TOKEN_EXCHANGE; private static final Profile.Feature EXPERIMENTAL_FEATURE = Profile.Feature.DYNAMIC_SCOPES; private static Profile.Feature DEPRECATED_FEATURE = Profile.Feature.LOGIN_V1; diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/FeaturesDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/FeaturesDistTest.java index 4533702cd4..6f64994dfb 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/FeaturesDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/FeaturesDistTest.java @@ -89,7 +89,7 @@ public class FeaturesDistTest { @Test @EnabledOnOs(value = { OS.LINUX, OS.MAC }, disabledReason = "different shell escaping behaviour on Windows.") - @Launch({StartDev.NAME, "--features=token-exchange,admin-fine-grained-authz"}) + @Launch({StartDev.NAME, "--features=token-exchange,admin-fine-grained-authz:v1"}) public void testEnableMultipleFeatures(LaunchResult result) { CLIResult cliResult = (CLIResult) result; cliResult.assertStartedDevMode(); @@ -100,7 +100,7 @@ public class FeaturesDistTest { @Test @EnabledOnOs(value = { OS.WINDOWS }, disabledReason = "different shell escaping behaviour on Windows.") - @Launch({StartDev.NAME, "--features=\"token-exchange,admin-fine-grained-authz\""}) + @Launch({StartDev.NAME, "--features=\"token-exchange,admin-fine-grained-authz:v1\""}) public void testWinEnableMultipleFeatures(LaunchResult result) { CLIResult cliResult = (CLIResult) result; cliResult.assertStartedDevMode(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java index 6989e67d79..1c4f984c14 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java @@ -20,7 +20,6 @@ import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.extensions.Extension; import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; -import jakarta.ws.rs.core.Response.Status; import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.NoCache; @@ -90,6 +89,7 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -702,6 +702,7 @@ public class ClientResource { @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) @Operation( summary = "Return object stating whether client Authorization permissions have been initialized or not and a reference") public ManagementPermissionReference getManagementPermissions() { + ProfileHelper.requireFeature(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ); auth.roles().requireView(client); AdminPermissionManagement permissions = AdminPermissions.management(session, realm); @@ -711,7 +712,7 @@ public class ClientResource { return toMgmtRef(client, permissions); } - public static ManagementPermissionReference toMgmtRef(ClientModel client, AdminPermissionManagement permissions) { + private ManagementPermissionReference toMgmtRef(ClientModel client, AdminPermissionManagement permissions) { ManagementPermissionReference ref = new ManagementPermissionReference(); ref.setEnabled(true); ref.setResource(permissions.clients().resource(client).getId()); @@ -734,6 +735,7 @@ public class ClientResource { @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) @Operation( summary = "Return object stating whether client Authorization permissions have been initialized or not and a reference") public ManagementPermissionReference setManagementPermissionsEnabled(ManagementPermissionReference ref) { + ProfileHelper.requireFeature(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ); auth.clients().requireManage(client); AdminPermissionManagement permissions = AdminPermissions.management(session, realm); permissions.clients().setPermissionsEnabled(client, ref.isEnabled()); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java b/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java index afd283ab2e..3f7f2e1deb 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java @@ -16,13 +16,12 @@ */ package org.keycloak.services.resources.admin; -import jakarta.ws.rs.DefaultValue; import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.extensions.Extension; import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.resteasy.reactive.NoCache; -import jakarta.ws.rs.NotFoundException; +import org.keycloak.common.Profile; import org.keycloak.common.util.ObjectUtil; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; @@ -42,10 +41,14 @@ import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement; import org.keycloak.services.resources.admin.permissions.AdminPermissions; +import org.keycloak.utils.GroupUtils; +import org.keycloak.utils.ProfileHelper; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.POST; import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; @@ -60,7 +63,6 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Stream; -import org.keycloak.utils.GroupUtils; import static org.keycloak.utils.StreamsUtil.paginatedStream; @@ -318,6 +320,7 @@ public class GroupResource { @Tag(name = KeycloakOpenAPI.Admin.Tags.GROUPS) @Operation( summary = "Return object stating whether client Authorization permissions have been initialized or not and a reference") public ManagementPermissionReference getManagementPermissions() { + ProfileHelper.requireFeature(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ); auth.groups().requireView(group); AdminPermissionManagement permissions = AdminPermissions.management(session, realm); @@ -327,7 +330,7 @@ public class GroupResource { return toMgmtRef(group, permissions); } - public static ManagementPermissionReference toMgmtRef(GroupModel group, AdminPermissionManagement permissions) { + private ManagementPermissionReference toMgmtRef(GroupModel group, AdminPermissionManagement permissions) { ManagementPermissionReference ref = new ManagementPermissionReference(); ref.setEnabled(true); ref.setResource(permissions.groups().resource(group).getId()); @@ -350,6 +353,7 @@ public class GroupResource { @Tag(name = KeycloakOpenAPI.Admin.Tags.GROUPS) @Operation( summary = "Return object stating whether client Authorization permissions have been initialized or not and a reference") public ManagementPermissionReference setManagementPermissionsEnabled(ManagementPermissionReference ref) { + ProfileHelper.requireFeature(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ); auth.groups().requireManage(group); AdminPermissionManagement permissions = AdminPermissions.management(session, realm); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java index 39227edfc0..4e6eae8378 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java @@ -24,11 +24,11 @@ import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.NoCache; -import jakarta.ws.rs.NotFoundException; import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProviderFactory; import org.keycloak.broker.provider.IdentityProviderMapper; import org.keycloak.broker.social.SocialIdentityProvider; +import org.keycloak.common.Profile; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; import org.keycloak.models.FederatedIdentityModel; @@ -53,10 +53,12 @@ import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement; import org.keycloak.services.resources.admin.permissions.AdminPermissions; +import org.keycloak.utils.ProfileHelper; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.POST; import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; @@ -69,7 +71,6 @@ import jakarta.ws.rs.core.Response; import java.util.Collections; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -440,6 +441,7 @@ public class IdentityProviderResource { @Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS) @Operation( summary = "Return object stating whether client Authorization permissions have been initialized or not and a reference") public ManagementPermissionReference getManagementPermissions() { + ProfileHelper.requireFeature(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ); this.auth.realm().requireViewIdentityProviders(); AdminPermissionManagement permissions = AdminPermissions.management(session, realm); @@ -449,7 +451,7 @@ public class IdentityProviderResource { return toMgmtRef(identityProviderModel, permissions); } - public static ManagementPermissionReference toMgmtRef(IdentityProviderModel model, AdminPermissionManagement permissions) { + private ManagementPermissionReference toMgmtRef(IdentityProviderModel model, AdminPermissionManagement permissions) { ManagementPermissionReference ref = new ManagementPermissionReference(); ref.setEnabled(true); ref.setResource(permissions.idps().resource(model).getId()); @@ -472,6 +474,7 @@ public class IdentityProviderResource { @Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS) @Operation( summary = "Return object stating whether client Authorization permissions have been initialized or not and a reference") public ManagementPermissionReference setManagementPermissionsEnabled(ManagementPermissionReference ref) { + ProfileHelper.requireFeature(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ); this.auth.realm().requireManageIdentityProviders(); AdminPermissionManagement permissions = AdminPermissions.management(session, realm); permissions.idps().setPermissionsEnabled(identityProviderModel, ref.isEnabled()); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index 62879eec18..ee91926d3a 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -30,12 +30,9 @@ import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; -import jakarta.ws.rs.DefaultValue; -import org.eclipse.microprofile.openapi.annotations.Operation; -import org.eclipse.microprofile.openapi.annotations.extensions.Extension; -import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.FormParam; import jakarta.ws.rs.GET; @@ -54,6 +51,9 @@ import jakarta.ws.rs.core.StreamingOutput; import com.fasterxml.jackson.core.type.TypeReference; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.NoCache; @@ -515,6 +515,7 @@ public class RealmAdminResource { @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) @Operation() public ManagementPermissionReference getUserMgmtPermissions() { + ProfileHelper.requireFeature(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ); auth.realm().requireViewRealm(); AdminPermissionManagement permissions = AdminPermissions.management(session, realm); @@ -534,6 +535,7 @@ public class RealmAdminResource { @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) @Operation() public ManagementPermissionReference setUsersManagementPermissionsEnabled(ManagementPermissionReference ref) { + ProfileHelper.requireFeature(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ); auth.realm().requireManageRealm(); AdminPermissionManagement permissions = AdminPermissions.management(session, realm); @@ -545,8 +547,7 @@ public class RealmAdminResource { } } - - public static ManagementPermissionReference toUsersMgmtRef(AdminPermissionManagement permissions) { + private ManagementPermissionReference toUsersMgmtRef(AdminPermissionManagement permissions) { ManagementPermissionReference ref = new ManagementPermissionReference(); ref.setEnabled(true); ref.setResource(permissions.users().resource().getId()); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RoleByIdResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RoleByIdResource.java index 63e4c6a395..05402f9e97 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RoleByIdResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RoleByIdResource.java @@ -23,7 +23,7 @@ import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.NoCache; -import jakarta.ws.rs.NotFoundException; +import org.keycloak.common.Profile; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; import org.keycloak.models.ClientModel; @@ -34,15 +34,16 @@ import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.representations.idm.ManagementPermissionReference; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.services.ErrorResponse; -import org.keycloak.services.ErrorResponseException; import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement; import org.keycloak.services.resources.admin.permissions.AdminPermissions; +import org.keycloak.utils.ProfileHelper; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.POST; import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; @@ -285,6 +286,7 @@ public class RoleByIdResource extends RoleResource { @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES_BY_ID) @Operation( summary = "Return object stating whether role Authorization permissions have been initialized or not and a reference") public ManagementPermissionReference getManagementPermissions(final @PathParam("role-id") String id) { + ProfileHelper.requireFeature(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ); RoleModel role = getRoleModel(id); auth.roles().requireView(role); @@ -318,6 +320,7 @@ public class RoleByIdResource extends RoleResource { @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES_BY_ID) @Operation( summary = "Return object stating whether role Authorization permissions have been initialized or not and a reference") public ManagementPermissionReference setManagementPermissionsEnabled(final @PathParam("role-id") String id, ManagementPermissionReference ref) { + ProfileHelper.requireFeature(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ); RoleModel role = getRoleModel(id); auth.roles().requireManage(role); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java index d864a95312..904af7af53 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java @@ -23,7 +23,7 @@ import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.resteasy.reactive.NoCache; -import jakarta.ws.rs.NotFoundException; +import org.keycloak.common.Profile; import org.keycloak.common.util.Encode; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; @@ -45,12 +45,14 @@ import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement; import org.keycloak.services.resources.admin.permissions.AdminPermissions; +import org.keycloak.utils.ProfileHelper; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.POST; import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; @@ -428,6 +430,7 @@ public class RoleContainerResource extends RoleResource { @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES) @Operation( summary = "Return object stating whether role Authorization permissions have been initialized or not and a reference") public ManagementPermissionReference getManagementPermissions(final @PathParam("role-name") String roleName) { + ProfileHelper.requireFeature(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ); auth.roles().requireView(roleContainer); RoleModel role = roleContainer.getRole(roleName); if (role == null) { @@ -456,6 +459,7 @@ public class RoleContainerResource extends RoleResource { @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES) @Operation( summary = "Return object stating whether role Authorization permissions have been initialized or not and a reference") public ManagementPermissionReference setManagementPermissionsEnabled(final @PathParam("role-name") String roleName, ManagementPermissionReference ref) { + ProfileHelper.requireFeature(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ); auth.roles().requireManage(roleContainer); RoleModel role = roleContainer.getRole(roleName); if (role == null) {