From a6f0d032e08491bccb57707eddee913dd3de7707 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Thu, 12 Dec 2024 18:35:36 -0300 Subject: [PATCH] Support for initial CRUD operations when managing admin permissions Signed-off-by: Pedro Igor --- .../AbstractPolicyRepresentation.java | 12 +- .../AdminPermissionsAuthorizationSchema.java | 30 --- .../ScopePermissionRepresentation.java | 10 - .../resource/ScopePermissionsResource.java | 10 + .../AdminPermissionsAuthorizationSchema.java | 94 +++++++++ .../authorization/AuthorizationProvider.java | 22 +- .../models/utils/KeycloakModelUtils.java | 3 +- .../models/utils/ModelToRepresentation.java | 1 + .../framework/annotations/InjectClient.java | 2 + .../test/framework/realm/ClientSupplier.java | 21 +- .../framework/realm/RealmConfigBuilder.java | 5 + .../server/KeycloakServerConfigBuilder.java | 3 +- .../authz/fgap/PermissionManagementTest.java | 196 ++++++++++++++++++ 13 files changed, 354 insertions(+), 55 deletions(-) delete mode 100644 core/src/main/java/org/keycloak/representations/idm/authorization/AdminPermissionsAuthorizationSchema.java create mode 100644 server-spi-private/src/main/java/org/keycloak/authorization/AdminPermissionsAuthorizationSchema.java create mode 100644 tests/base/src/test/java/org/keycloak/test/admin/authz/fgap/PermissionManagementTest.java diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/AbstractPolicyRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/AbstractPolicyRepresentation.java index ed9bc9c30e13..079e3c7bb6e8 100644 --- a/core/src/main/java/org/keycloak/representations/idm/authorization/AbstractPolicyRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/AbstractPolicyRepresentation.java @@ -38,6 +38,7 @@ public class AbstractPolicyRepresentation { private Logic logic = Logic.POSITIVE; private DecisionStrategy decisionStrategy = DecisionStrategy.UNANIMOUS; private String owner; + private String resourceType; @JsonInclude(JsonInclude.Include.NON_EMPTY) private Set resourcesData; @@ -186,4 +187,13 @@ public void setScopesData(Set scopesData) { public Set getScopesData() { return scopesData; } -} \ No newline at end of file + + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } + + public String getResourceType() { + return resourceType; + } + +} diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/AdminPermissionsAuthorizationSchema.java b/core/src/main/java/org/keycloak/representations/idm/authorization/AdminPermissionsAuthorizationSchema.java deleted file mode 100644 index 6ce7c7025fcb..000000000000 --- a/core/src/main/java/org/keycloak/representations/idm/authorization/AdminPermissionsAuthorizationSchema.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2024 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.representations.idm.authorization; - -import java.util.Arrays; -import java.util.HashSet; - -public class AdminPermissionsAuthorizationSchema extends AuthorizationSchema { - - public static final AdminPermissionsAuthorizationSchema INSTANCE = new AdminPermissionsAuthorizationSchema(); - - private AdminPermissionsAuthorizationSchema() { - super(new ResourceType("Users", new HashSet<>(Arrays.asList("manage")))); - } - -} diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/ScopePermissionRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/ScopePermissionRepresentation.java index 1f08553f3294..b6a02b414d7c 100644 --- a/core/src/main/java/org/keycloak/representations/idm/authorization/ScopePermissionRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/ScopePermissionRepresentation.java @@ -21,18 +21,8 @@ */ public class ScopePermissionRepresentation extends AbstractPolicyRepresentation { - private String resourceType; - @Override public String getType() { return "scope"; } - - public void setResourceType(String resourceType) { - this.resourceType = resourceType; - } - - public String getResourceType() { - return resourceType; - } } diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ScopePermissionsResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ScopePermissionsResource.java index d38df84c4dd9..6ed503af2091 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ScopePermissionsResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ScopePermissionsResource.java @@ -16,6 +16,8 @@ */ package org.keycloak.admin.client.resource; +import java.util.List; + import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; @@ -45,4 +47,12 @@ public interface ScopePermissionsResource { @GET @Produces(MediaType.APPLICATION_JSON) ScopePermissionRepresentation findByName(@QueryParam("name") String name); + + @GET + @Produces(MediaType.APPLICATION_JSON) + List findAll(@QueryParam("policyId") String id, + @QueryParam("name") String name, + @QueryParam("resource") String resource, + @QueryParam("first") Integer firstResult, + @QueryParam("max") Integer maxResult); } diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/AdminPermissionsAuthorizationSchema.java b/server-spi-private/src/main/java/org/keycloak/authorization/AdminPermissionsAuthorizationSchema.java new file mode 100644 index 000000000000..c202c1c40857 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/authorization/AdminPermissionsAuthorizationSchema.java @@ -0,0 +1,94 @@ +/* + * Copyright 2024 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.authorization; + +import java.util.Arrays; +import java.util.HashSet; + +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.authorization.AuthorizationSchema; +import org.keycloak.representations.idm.authorization.ResourceType; + +public class AdminPermissionsAuthorizationSchema extends AuthorizationSchema { + + public static final ResourceType USERS = new ResourceType("Users", new HashSet<>(Arrays.asList("manage"))); + public static final AdminPermissionsAuthorizationSchema INSTANCE = new AdminPermissionsAuthorizationSchema(); + + private AdminPermissionsAuthorizationSchema() { + super(USERS); + } + + public Resource getOrCreateResource(KeycloakSession session, String type, String id) { + RealmModel realm = session.getContext().getRealm(); + + if (!realm.isAdminPermissionsEnabled()) { + return null; + } + + ClientModel permissionClient = realm.getAdminPermissionsClient(); + + if (permissionClient == null) { + throw new IllegalStateException("Permission client not found"); + } + + StoreFactory storeFactory = getStoreFactory(session); + ResourceServer resourceServer = storeFactory.getResourceServerStore().findByClient(permissionClient); + String resourceName = null; + + if (USERS.getType().equals(type)) { + resourceName = resolveUser(session, id, realm); + } + + if (resourceName == null) { + throw new IllegalStateException("Could not map resource object with type [" + type + "] and id [" + id + "]"); + } + + return getOrCreateResource(session, resourceServer, resourceName); + } + + private String resolveUser(KeycloakSession session, String id, RealmModel realm) { + UserModel user = session.users().getUserById(realm, id); + + if (user == null) { + user = session.users().getUserByUsername(realm, id); + } + + return user == null ? null : user.getId(); + } + + private Resource getOrCreateResource(KeycloakSession session, ResourceServer resourceServer, String id) { + StoreFactory storeFactory = getStoreFactory(session); + Resource resource = storeFactory.getResourceStore().findByName(resourceServer, id); + + if (resource == null) { + return storeFactory.getResourceStore().create(resourceServer, id, resourceServer.getClientId()); + } + + return resource; + } + + private StoreFactory getStoreFactory(KeycloakSession session) { + AuthorizationProvider authzProvider = session.getProvider(AuthorizationProvider.class); + return authzProvider.getStoreFactory(); + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java b/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java index 40eff1a7e5fc..68effcb1a63e 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java @@ -19,6 +19,8 @@ import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -295,18 +297,24 @@ public Policy create(ResourceServer resourceServer, AbstractPolicyRepresentation if (resources != null) { representation.setResources(resources.stream().map(id -> { - Resource resource = storeFactory.getResourceStore().findById(resourceServer, id); + Resource resource = AdminPermissionsAuthorizationSchema.INSTANCE.getOrCreateResource(keycloakSession, representation.getResourceType(), id); if (resource == null) { - resource = storeFactory.getResourceStore().findByName(resourceServer, id); - } + resource = storeFactory.getResourceStore().findById(resourceServer, id); - if (resource == null) { - throw new RuntimeException("Resource [" + id + "] does not exist or is not owned by the resource server."); + if (resource == null) { + resource = storeFactory.getResourceStore().findByName(resourceServer, id); + } + + if (resource == null) { + throw new RuntimeException("Resource [" + id + "] does not exist or is not owned by the resource server."); + } + + return resource.getId(); } - return resource.getId(); - }).collect(Collectors.toSet())); + return Optional.ofNullable(resource).map(Resource::getId).orElse(null); + }).filter(Objects::nonNull).collect(Collectors.toSet())); } Set scopes = representation.getScopes(); diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java index 60bc5f2ffa33..ad0fd4a6c11a 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java @@ -20,6 +20,7 @@ import org.jboss.logging.Logger; import org.keycloak.Config; import org.keycloak.Config.Scope; +import org.keycloak.authorization.AdminPermissionsAuthorizationSchema; import org.keycloak.broker.social.SocialIdentityProvider; import org.keycloak.broker.social.SocialIdentityProviderFactory; import org.keycloak.common.util.CertificateUtils; @@ -88,10 +89,8 @@ import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.common.Profile; -import org.keycloak.representations.idm.authorization.AdminPermissionsAuthorizationSchema; import org.keycloak.representations.idm.authorization.AuthorizationSchema; import org.keycloak.representations.idm.authorization.ResourceServerRepresentation; -import org.keycloak.representations.idm.authorization.ResourceType; import org.keycloak.representations.idm.authorization.ScopeRepresentation; import static org.keycloak.utils.StreamsUtil.closing; 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 800a2e1dcb60..01e43bae4d87 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 @@ -23,6 +23,7 @@ import org.jboss.logging.Logger; import org.keycloak.Config; import org.keycloak.authentication.otp.OTPApplicationProvider; +import org.keycloak.authorization.AdminPermissionsAuthorizationSchema; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.AuthorizationProviderFactory; import org.keycloak.authorization.model.PermissionTicket; diff --git a/test-framework/core/src/main/java/org/keycloak/test/framework/annotations/InjectClient.java b/test-framework/core/src/main/java/org/keycloak/test/framework/annotations/InjectClient.java index 67351e278059..87cd808c0d5b 100644 --- a/test-framework/core/src/main/java/org/keycloak/test/framework/annotations/InjectClient.java +++ b/test-framework/core/src/main/java/org/keycloak/test/framework/annotations/InjectClient.java @@ -20,4 +20,6 @@ String ref() default ""; String realmRef() default ""; + + boolean createClient() default true; } diff --git a/test-framework/core/src/main/java/org/keycloak/test/framework/realm/ClientSupplier.java b/test-framework/core/src/main/java/org/keycloak/test/framework/realm/ClientSupplier.java index f8d6ee73f562..b606c41255ab 100644 --- a/test-framework/core/src/main/java/org/keycloak/test/framework/realm/ClientSupplier.java +++ b/test-framework/core/src/main/java/org/keycloak/test/framework/realm/ClientSupplier.java @@ -1,5 +1,7 @@ package org.keycloak.test.framework.realm; +import java.util.List; + import jakarta.ws.rs.core.Response; import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.representations.idm.ClientRepresentation; @@ -34,11 +36,22 @@ public ManagedClient getValue(InstanceContext insta clientRepresentation.setClientId(clientId); } - Response response = realm.admin().clients().create(clientRepresentation); - String uuid = ApiUtil.handleCreatedResponse(response); - clientRepresentation.setId(uuid); + List clients = realm.admin().clients().findByClientId(clientRepresentation.getClientId()); + + if (instanceContext.getAnnotation().createClient()) { + if (!clients.isEmpty()) { + throw new IllegalStateException("Client already exist with client id " + clientRepresentation.getClientId() + ". To use the existing client configure the injection point to skip creating the client."); + } + Response response = realm.admin().clients().create(clientRepresentation); + clientRepresentation.setId(ApiUtil.handleCreatedResponse(response)); + } else { + if (clients.isEmpty()) { + throw new IllegalStateException("No client found for ref: " + instanceContext.getAnnotation().ref()); + } + clientRepresentation = clients.get(0); + } - ClientResource clientResource = realm.admin().clients().get(uuid); + ClientResource clientResource = realm.admin().clients().get(clientRepresentation.getId()); return new ManagedClient(clientRepresentation, clientResource); } diff --git a/test-framework/core/src/main/java/org/keycloak/test/framework/realm/RealmConfigBuilder.java b/test-framework/core/src/main/java/org/keycloak/test/framework/realm/RealmConfigBuilder.java index a19902abdc5a..c51cb8a00429 100644 --- a/test-framework/core/src/main/java/org/keycloak/test/framework/realm/RealmConfigBuilder.java +++ b/test-framework/core/src/main/java/org/keycloak/test/framework/realm/RealmConfigBuilder.java @@ -67,6 +67,11 @@ public RealmConfigBuilder defaultSignatureAlgorithm(String algorithm) { return this; } + public RealmConfigBuilder adminPermissionsEnabled(boolean enabled) { + rep.setAdminPermissionsEnabled(enabled); + return this; + } + public RealmConfigBuilder eventsListeners(String... eventListeners) { if (rep.getEventsListeners() == null) { rep.setEventsListeners(new LinkedList<>()); diff --git a/test-framework/core/src/main/java/org/keycloak/test/framework/server/KeycloakServerConfigBuilder.java b/test-framework/core/src/main/java/org/keycloak/test/framework/server/KeycloakServerConfigBuilder.java index ee62f11e086b..250223aa4d42 100644 --- a/test-framework/core/src/main/java/org/keycloak/test/framework/server/KeycloakServerConfigBuilder.java +++ b/test-framework/core/src/main/java/org/keycloak/test/framework/server/KeycloakServerConfigBuilder.java @@ -5,6 +5,7 @@ import io.smallrye.config.SmallRyeConfig; import org.eclipse.microprofile.config.spi.ConfigSource; import org.keycloak.common.Profile; +import org.keycloak.common.Profile.Feature; import java.util.Arrays; import java.util.HashMap; @@ -194,7 +195,7 @@ public Set toDependencies() { } private Set toFeatureStrings(Profile.Feature... features) { - return Arrays.stream(features).map(f -> f.name().toLowerCase().replace('_', '-')).collect(Collectors.toSet()); + return Arrays.stream(features).map(Feature::getVersionedKey).collect(Collectors.toSet()); } public enum LogHandlers { diff --git a/tests/base/src/test/java/org/keycloak/test/admin/authz/fgap/PermissionManagementTest.java b/tests/base/src/test/java/org/keycloak/test/admin/authz/fgap/PermissionManagementTest.java new file mode 100644 index 000000000000..a380f7114df0 --- /dev/null +++ b/tests/base/src/test/java/org/keycloak/test/admin/authz/fgap/PermissionManagementTest.java @@ -0,0 +1,196 @@ +/* + * Copyright 2024 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.test.admin.authz.fgap; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.Set; + +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.keycloak.admin.client.resource.ScopePermissionResource; +import org.keycloak.admin.client.resource.ScopePermissionsResource; +import org.keycloak.authorization.AdminPermissionsAuthorizationSchema; +import org.keycloak.common.Profile.Feature; +import org.keycloak.models.Constants; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation; +import org.keycloak.representations.idm.authorization.UserPolicyRepresentation; +import org.keycloak.test.admin.authz.fgap.PermissionManagementTest.KeycloakAdminPermissionsServerConfig; +import org.keycloak.test.framework.annotations.InjectClient; +import org.keycloak.test.framework.annotations.InjectRealm; +import org.keycloak.test.framework.annotations.InjectUser; +import org.keycloak.test.framework.annotations.KeycloakIntegrationTest; +import org.keycloak.test.framework.realm.ManagedClient; +import org.keycloak.test.framework.realm.ManagedRealm; +import org.keycloak.test.framework.realm.ManagedUser; +import org.keycloak.test.framework.realm.RealmConfig; +import org.keycloak.test.framework.realm.RealmConfigBuilder; +import org.keycloak.test.framework.server.KeycloakServerConfig; +import org.keycloak.test.framework.server.KeycloakServerConfigBuilder; + +@KeycloakIntegrationTest(config = KeycloakAdminPermissionsServerConfig.class) +public class PermissionManagementTest { + + @InjectRealm(config = RealmAdminPermissionsConfig.class) + ManagedRealm realm; + + @InjectClient(ref = Constants.ADMIN_PERMISSIONS_CLIENT_ID, createClient = false) + ManagedClient client; + + @InjectUser(ref = "alice") + ManagedUser userAlice; + + @InjectUser(ref = "bob") + ManagedUser userBob; + + @BeforeEach + public void onBefore() { + for (int i = 0; i < 3; i++) { + UserPolicyRepresentation policy = new UserPolicyRepresentation(); + + policy.setName("User Policy " + i); + + client.admin().authorization().policies().user().create(policy); + } + } + + @AfterEach + public void onAfter() { + ScopePermissionsResource scope = client.admin().authorization().permissions().scope(); + + for (ScopePermissionRepresentation permission : scope.findAll(null, null, null, -1, -1)) { + scope.findById(permission.getId()).remove(); + } + } + + @Test + public void testCreateResourceTypePermission() { + ScopePermissionRepresentation expected = new ScopePermissionRepresentation(); + + expected.setName(KeycloakModelUtils.generateId()); + expected.setResourceType(AdminPermissionsAuthorizationSchema.USERS.getType()); + expected.setScopes(AdminPermissionsAuthorizationSchema.USERS.getScopes()); + expected.setPolicies(Set.of("User Policy 0", "User Policy 1", "User Policy 2")); + + ScopePermissionsResource permissions = client.admin().authorization().permissions().scope(); + + try (Response response = permissions.create(expected)) { + assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); + List result = permissions.findAll(null, null, null, -1, -1); + assertEquals(1, result.size()); + ScopePermissionRepresentation permissionRep = result.get(0); + ScopePermissionResource permission = permissions.findById(permissionRep.getId()); + assertEquals(expected.getName(), permissionRep.getName()); + assertEquals(1, permission.scopes().size()); + assertEquals(3, permission.associatedPolicies().size()); + } + } + + @Test + public void testCreateResourceObjectPermission() { + ScopePermissionsResource permissions = client.admin().authorization().permissions().scope(); + ScopePermissionRepresentation expected = createUserPermission(userAlice); + + try (Response response = permissions.create(expected)) { + assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); + List result = permissions.findAll(null, null, null, -1, -1); + assertEquals(1, result.size()); + ScopePermissionRepresentation permissionRep = result.get(0); + ScopePermissionResource permission = permissions.findById(permissionRep.getId()); + assertEquals(expected.getName(), permissionRep.getName()); + assertEquals(1, permission.scopes().size()); + assertEquals(1, permission.resources().size()); + assertEquals(3, permission.associatedPolicies().size()); + } + } + + @Test + public void testFindByResourceObject() { + ScopePermissionsResource permissions = client.admin().authorization().permissions().scope(); + + try (Response response = permissions.create(createUserPermission(userAlice))) { + assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); + } + + try (Response response = permissions.create(createUserPermission(userBob))) { + assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); + } + + List existing = permissions.findAll(null, null, userAlice.getId(), -1, -1); + assertEquals(1, existing.size()); + existing = permissions.findAll(null, null, userBob.getId(), -1, -1); + assertEquals(1, existing.size()); + } + + @Test + public void testDelete() { + ScopePermissionsResource permissions = client.admin().authorization().permissions().scope(); + ScopePermissionRepresentation expected = createUserPermission(userAlice); + + try (Response response = permissions.create(expected)) { + assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); + } + + expected = createUserPermission(userBob); + + try (Response response = permissions.create(expected)) { + assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); + } + + List existing = permissions.findAll(null, null, userAlice.getId(), -1, -1); + assertEquals(1, existing.size()); + permissions.findById(existing.get(0).getId()).remove(); + existing = permissions.findAll(null, null, userAlice.getId(), -1, -1); + assertEquals(0, existing.size()); + + existing = permissions.findAll(null, null, userBob.getId(), -1, -1); + assertEquals(1, existing.size()); + } + + private ScopePermissionRepresentation createUserPermission(ManagedUser user) { + ScopePermissionRepresentation permission = new ScopePermissionRepresentation(); + + permission.setName(KeycloakModelUtils.generateId()); + permission.setResourceType(AdminPermissionsAuthorizationSchema.USERS.getType()); + permission.setResources(Set.of(user.getUsername())); + permission.setScopes(AdminPermissionsAuthorizationSchema.USERS.getScopes()); + permission.setPolicies(Set.of("User Policy 0", "User Policy 1", "User Policy 2")); + + return permission; + } + + private static class RealmAdminPermissionsConfig implements RealmConfig { + + @Override + public RealmConfigBuilder configure(RealmConfigBuilder realm) { + return realm.adminPermissionsEnabled(true); + } + } + + public static class KeycloakAdminPermissionsServerConfig implements KeycloakServerConfig { + + @Override + public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) { + return config.features(Feature.ADMIN_FINE_GRAINED_AUTHZ_V2); + } + } +}