[FGAP:V2] remove configure scope from Client resource type

Closes #38567

Signed-off-by: vramik <vramik@redhat.com>
This commit is contained in:
vramik 2025-04-02 17:23:01 +02:00 committed by Pedro Igor
parent 279e517548
commit 6488890585
7 changed files with 59 additions and 87 deletions

View File

@ -108,9 +108,7 @@ set of management operations:
| *Operation* | *Description*
| *view* | Defines if a realm administrator can view clients.
| *manage* | Defines if a realm administrator can manage clients. It allows operations, such as update certificates, manage
protocol-mappers or client scopes, regenerate access token.
| *configure* | Defines if a realm administrator can manage clients.
| *manage* | Defines if a realm administrator can manage clients.
| *map-roles* | Defines if a realm administrator can assign any role defined by a client (or multiple clients) to a user.
| *map-roles-composite* | Defines if a realm administrator can assign any role defined by a client (or multiple clients) as a composite to
another role.
@ -124,7 +122,6 @@ set of management operations:
assigning only *manage* without *view* will prevent the client from being visible.
- The *map-roles* operation does not grant the ability to manage users or assign roles arbitrarily; the administrator must also
have user role mapping permissions.
- For certain operations like updating or deleting clients it is required both *manage* and *configure* assigned.
====
===== Roles Resource Type

View File

@ -121,6 +121,7 @@ Due to fundamental changes in the permission model, **automatic migration from V
** Example: To both view and manage a resource, both *view* and *manage* scopes for a permissions must be assigned separately.
* Permission model changes:
** The *user-impersonated* user permission has been _removed_.
** The *configure* client permission has been _removed_. With the introduction of explicit operation scoping in V2, the distinction between manage and configure became ambiguous.
* Flexible resource scoping:
** Unlike V1, where permissions were granted either to *a single resource* (for clients, groups, and roles) or *all resources* (for users), V2 introduces greater flexibility.
** Administrators can now define permissions for:

View File

@ -75,7 +75,6 @@ public class AdminPermissionsSchema extends AuthorizationSchema {
public static final String VIEW = "view";
// client specific scopes
public static final String CONFIGURE = "configure";
public static final String MAP_ROLES = "map-roles";
public static final String MAP_ROLES_CLIENT_SCOPE = "map-roles-client-scope";
public static final String MAP_ROLES_COMPOSITE = "map-roles-composite";
@ -95,7 +94,7 @@ public class AdminPermissionsSchema extends AuthorizationSchema {
public static final String MANAGE_GROUP_MEMBERSHIP = "manage-group-membership";
public static final ResourceType CLIENTS = new ResourceType(CLIENTS_RESOURCE_TYPE, Set.of(CONFIGURE, MANAGE, MAP_ROLES, MAP_ROLES_CLIENT_SCOPE, MAP_ROLES_COMPOSITE, VIEW));
public static final ResourceType CLIENTS = new ResourceType(CLIENTS_RESOURCE_TYPE, Set.of(MANAGE, MAP_ROLES, MAP_ROLES_CLIENT_SCOPE, MAP_ROLES_COMPOSITE, VIEW));
public static final ResourceType GROUPS = new ResourceType(GROUPS_RESOURCE_TYPE, Set.of(MANAGE, VIEW, MANAGE_MEMBERSHIP, MANAGE_MEMBERS, VIEW_MEMBERS));
public static final ResourceType ROLES = new ResourceType(ROLES_RESOURCE_TYPE, Set.of(MAP_ROLE, MAP_ROLE_CLIENT_SCOPE, MAP_ROLE_COMPOSITE));
public static final ResourceType USERS = new ResourceType(USERS_RESOURCE_TYPE, Set.of(MANAGE, VIEW, IMPERSONATE, MAP_ROLES, MANAGE_GROUP_MEMBERSHIP), Map.of(VIEW, Set.of(VIEW_MEMBERS), MANAGE, Set.of(MANAGE_MEMBERS)), GROUPS.getType());

View File

@ -145,8 +145,6 @@ public class ClientAttributeCertificateResource {
@Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENT_ATTRIBUTE_CERTIFICATE)
@Operation( summary = "Upload certificate and eventually private key")
public CertificateRepresentation uploadJks() throws IOException {
auth.clients().requireConfigure(client);
try {
CertificateRepresentation info = updateCertFromRequest();
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();
@ -169,8 +167,6 @@ public class ClientAttributeCertificateResource {
@Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENT_ATTRIBUTE_CERTIFICATE)
@Operation( summary = "Upload only certificate, not private key")
public CertificateRepresentation uploadJksCertificate() throws IOException {
auth.clients().requireConfigure(client);
try {
CertificateRepresentation info = updateCertFromRequest();
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(info).success();

View File

@ -118,12 +118,14 @@ public interface ClientPermissionEvaluator {
* <p/>
* Or if the caller has a permission to {@link ClientPermissionManagement#CONFIGURE_SCOPE} the client.
* <p/>
* For V2 only: Also if the caller has a permission to {@link org.keycloak.authorization.AdminPermissionsSchema#CONFIGURE} all clients.
* For V2 only: the call is redirected to {@code canManage(ClientModel)}.
*/
boolean canConfigure(ClientModel client);
/**
* Throws ForbiddenException if {@link #canConfigure(ClientModel)} returns {@code false}.
* <p/>
* For V2 only: the call is redirected to {@code requireManage(ClientModel)}.
*/
void requireConfigure(ClientModel client);

View File

@ -55,10 +55,15 @@ class ClientPermissionsV2 extends ClientPermissions {
}
@Override
public boolean canConfigure(ClientModel client) {
if (canManage(client)) return true;
public void requireConfigure(ClientModel client) {
//redirecting call to manage for V2
super.requireManage(client);
}
return hasPermission(client, AdminPermissionsSchema.CONFIGURE);
@Override
public boolean canConfigure(ClientModel client) {
//redirecting call to manage for V2
return canManage(client);
}
@Override

View File

@ -24,7 +24,6 @@ import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.fail;
import static org.keycloak.authorization.AdminPermissionsSchema.CLIENTS;
import static org.keycloak.authorization.AdminPermissionsSchema.CONFIGURE;
import static org.keycloak.authorization.AdminPermissionsSchema.MANAGE;
import static org.keycloak.authorization.AdminPermissionsSchema.MAP_ROLES;
import static org.keycloak.authorization.AdminPermissionsSchema.MAP_ROLES_COMPOSITE;
@ -195,64 +194,6 @@ public class ClientResourceTypeEvaluationTest extends AbstractPermissionTest {
}
}
@Test
public void testConfigureOnlyOneClient() {
ClientRepresentation myclient = realm.admin().clients().findByClientId("myclient").get(0);
UserRepresentation myadmin = realm.admin().users().search("myadmin").get(0);
ClientResource clientResource = realmAdminClient.realm(realm.getName()).clients().get(myclient.getId());
// the following operations should fail as the permission wasn't granted yet
try {
clientResource.toRepresentation();
fail("Expected exception wasn't thrown.");
} catch (Exception ex) {
assertThat(ex, instanceOf(ForbiddenException.class));
}
try {
clientResource.generateNewSecret();
fail("Expected exception wasn't thrown.");
} catch (Exception ex) {
assertThat(ex, instanceOf(ForbiddenException.class));
}
try {
clientResource.getDefaultClientScopes();
fail("Expected exception wasn't thrown.");
} catch (Exception ex) {
assertThat(ex, instanceOf(ForbiddenException.class));
}
UserPolicyRepresentation onlyMyAdminUserPolicy = createUserPolicy(realm, client, "Only My Admin User Policy", myadmin.getId());
createPermission(client, myclient.getId(), clientsType, Set.of(VIEW, CONFIGURE), onlyMyAdminUserPolicy);
// the caller can view myclient
clientResource.toRepresentation();
// can do operations with client secrets
clientResource.getSecret();
clientResource.generateNewSecret();
clientResource.invalidateRotatedSecret();
// can get default client scopes
List<ClientScopeRepresentation> defaultClientScopes = clientResource.getDefaultClientScopes();
// can't delete default client scopes
try {
clientResource.removeDefaultClientScope(defaultClientScopes.get(0).getId());
fail("Expected exception wasn't thrown.");
} catch (Exception ex) {
assertThat(ex, instanceOf(ForbiddenException.class));
}
// can't add default client scopes
try {
clientResource.addDefaultClientScope(defaultClientScopes.get(0).getId());
fail("Expected exception wasn't thrown.");
} catch (Exception ex) {
assertThat(ex, instanceOf(ForbiddenException.class));
}
}
@Test
public void testManageAllClients() {
UserRepresentation myadmin = realm.admin().users().search("myadmin").get(0);
@ -369,23 +310,17 @@ public class ClientResourceTypeEvaluationTest extends AbstractPermissionTest {
}
@Test
public void testMapRolesAndCompositesOnlyOneClient() {
public void testMapRolesOnlyOneClient() {
UserRepresentation myadmin = realm.admin().users().search("myadmin").get(0);
ClientRepresentation myclient = realm.admin().clients().findByClientId("myclient").get(0);
// create a role and sub-role
// create a role
RoleRepresentation role = new RoleRepresentation();
role.setName("myclient-role");
role.setClientRole(true);
realm.admin().clients().get(myclient.getId()).roles().create(role);
role = realm.admin().clients().get(myclient.getId()).roles().get("myclient-role").toRepresentation();
RoleRepresentation subRole = new RoleRepresentation();
subRole.setName("myclient-subRole");
subRole.setClientRole(true);
realm.admin().clients().get(myclient.getId()).roles().create(subRole);
subRole = realm.admin().clients().get(myclient.getId()).roles().get("myclient-subRole").toRepresentation();
// the following operations should fail as the permission wasn't granted yet
try {
realmAdminClient.realm(realm.getName()).users().get(myadmin.getId()).roles().clientLevel(myclient.getId()).add(List.of(role));
@ -393,21 +328,58 @@ public class ClientResourceTypeEvaluationTest extends AbstractPermissionTest {
} catch (Exception ex) {
assertThat(ex, instanceOf(ForbiddenException.class));
}
UserPolicyRepresentation onlyMyAdminUserPolicy = createUserPolicy(realm, client, "Only My Admin User Policy", myadmin.getId());
createPermission(client, myadmin.getId(), AdminPermissionsSchema.USERS_RESOURCE_TYPE, Set.of(MAP_ROLES), onlyMyAdminUserPolicy);
createPermission(client, myclient.getId(), clientsType, Set.of(MAP_ROLES), onlyMyAdminUserPolicy);
// now those should pass
realmAdminClient.realm(realm.getName()).users().get(myadmin.getId()).roles().clientLevel(myclient.getId()).add(List.of(role));
}
@Test
public void testMapCompositesToAnotherClientRole() {
// to add a client role ('myclient') 'roleA' as a composite role of 'roleB' (client role of 'realmClient' client)
// it is required to have permission to manage the `realmClient` and to map-roles-composite of the `myclient`
UserRepresentation myadmin = realm.admin().users().search("myadmin").get(0);
ClientRepresentation myclient = realm.admin().clients().findByClientId("myclient").get(0);
// create two roles, each for seprate client
RoleRepresentation roleA = new RoleRepresentation();
roleA.setName("roleA");
roleA.setClientRole(true);
realm.admin().clients().get(myclient.getId()).roles().create(roleA);
roleA = realm.admin().clients().get(myclient.getId()).roles().get("roleA").toRepresentation();
RoleRepresentation roleB = new RoleRepresentation();
roleB.setName("roleB");
roleB.setClientRole(true);
realm.admin().clients().get(realmClient.getId()).roles().create(roleB);
// the following operations should fail as the permission wasn't granted yet
try {
realmAdminClient.realm(realm.getName()).clients().get(myclient.getId()).roles().get("myclient-role").addComposites(List.of(subRole));
realmAdminClient.realm(realm.getName()).clients().get(realmClient.getId()).roles().get("roleB").addComposites(List.of(roleA));
fail("Expected exception wasn't thrown.");
} catch (Exception ex) {
assertThat(ex, instanceOf(ForbiddenException.class));
}
UserPolicyRepresentation onlyMyAdminUserPolicy = createUserPolicy(realm, client, "Only My Admin User Policy", myadmin.getId());
createPermission(client, myadmin.getId(), AdminPermissionsSchema.USERS_RESOURCE_TYPE, Set.of(MAP_ROLES), onlyMyAdminUserPolicy);
createPermission(client, myclient.getId(), clientsType, Set.of(MAP_ROLES, MAP_ROLES_COMPOSITE, CONFIGURE), onlyMyAdminUserPolicy);
createPermission(client, myclient.getId(), clientsType, Set.of(MAP_ROLES_COMPOSITE), onlyMyAdminUserPolicy);
// the following operations should fail as the permission to manage the realmClient is missing
try {
realmAdminClient.realm(realm.getName()).clients().get(realmClient.getId()).roles().get("roleB").addComposites(List.of(roleA));
fail("Expected exception wasn't thrown.");
} catch (Exception ex) {
assertThat(ex, instanceOf(ForbiddenException.class));
}
createPermission(client, realmClient.getId(), clientsType, Set.of(MANAGE), onlyMyAdminUserPolicy);
// now those should pass
realmAdminClient.realm(realm.getName()).users().get(myadmin.getId()).roles().clientLevel(myclient.getId()).add(List.of(role));
realmAdminClient.realm(realm.getName()).clients().get(myclient.getId()).roles().get("myclient-role").addComposites(List.of(subRole));
realmAdminClient.realm(realm.getName()).clients().get(realmClient.getId()).roles().get("roleB").addComposites(List.of(roleA));
}
@Test