mirror of
https://github.com/keycloak/keycloak.git
synced 2026-05-26 13:50:48 +00:00
Add view-realm admin role check to SCIM discovery endpoints
Closes #46859 Signed-off-by: vramik <vramik@redhat.com>
This commit is contained in:
+2
-2
@@ -76,10 +76,10 @@ public interface ScimResourceTypeProvider<R extends ResourceTypeRepresentation>
|
||||
* and should persist the updated resource and return the persisted instance.
|
||||
* The returned resource will be used in the response to the client.
|
||||
*
|
||||
* @param user the resource to update
|
||||
* @param resource the resource to update
|
||||
* @return the updated resource
|
||||
*/
|
||||
R update(R user);
|
||||
R update(R resource);
|
||||
|
||||
/**
|
||||
* Retrieves a resource of this type by its identifier. This method is invoked when a client requests a specific resource,
|
||||
|
||||
+19
@@ -1,9 +1,14 @@
|
||||
package org.keycloak.scim.model.config;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.keycloak.authorization.fgap.AdminPermissionsSchema;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.Model;
|
||||
import org.keycloak.scim.protocol.ForbiddenException;
|
||||
import org.keycloak.scim.protocol.request.SearchRequest;
|
||||
import org.keycloak.scim.resource.config.ServiceProviderConfig;
|
||||
import org.keycloak.scim.resource.config.ServiceProviderConfig.BulkSupport;
|
||||
import org.keycloak.scim.resource.config.ServiceProviderConfig.FilterSupport;
|
||||
@@ -13,6 +18,12 @@ import org.keycloak.scim.resource.spi.SingletonResourceTypeProvider;
|
||||
|
||||
public class ServiceProviderConfigResourceTypeProvider implements SingletonResourceTypeProvider<ServiceProviderConfig> {
|
||||
|
||||
private final KeycloakSession session;
|
||||
|
||||
public ServiceProviderConfigResourceTypeProvider(KeycloakSession session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServiceProviderConfig getSingleton() {
|
||||
ServiceProviderConfig config = new ServiceProviderConfig();
|
||||
@@ -30,6 +41,14 @@ public class ServiceProviderConfigResourceTypeProvider implements SingletonResou
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<ServiceProviderConfig> getAll(SearchRequest searchRequest) {
|
||||
if (!session.getContext().getPermissions().hasPermission(AdminPermissionsSchema.REALMS_RESOURCE_TYPE, AdminPermissionsSchema.VIEW)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
return Stream.of(getSingleton());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<ServiceProviderConfig> getResourceType() {
|
||||
return ServiceProviderConfig.class;
|
||||
|
||||
+1
-3
@@ -7,11 +7,9 @@ import org.keycloak.scim.resource.spi.ScimResourceTypeProviderFactory;
|
||||
|
||||
public class ServiceProviderConfigResourceTypeProviderFactory implements ScimResourceTypeProviderFactory<ServiceProviderConfigResourceTypeProvider> {
|
||||
|
||||
public static final ServiceProviderConfigResourceTypeProvider INSTANCE = new ServiceProviderConfigResourceTypeProvider();
|
||||
|
||||
@Override
|
||||
public ServiceProviderConfigResourceTypeProvider create(KeycloakSession session) {
|
||||
return INSTANCE;
|
||||
return new ServiceProviderConfigResourceTypeProvider(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+6
-1
@@ -5,8 +5,10 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.keycloak.authorization.fgap.AdminPermissionsSchema;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.Model;
|
||||
import org.keycloak.scim.protocol.ForbiddenException;
|
||||
import org.keycloak.scim.protocol.request.SearchRequest;
|
||||
import org.keycloak.scim.resource.ResourceTypeRepresentation;
|
||||
import org.keycloak.scim.resource.config.ServiceProviderConfig;
|
||||
@@ -41,7 +43,7 @@ public class ResourceTypeProvider implements ScimResourceTypeProvider<ResourceTy
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceType update(ResourceType user) {
|
||||
public ResourceType update(ResourceType resourceType) {
|
||||
throw new UnsupportedOperationException("Not supported yet.");
|
||||
}
|
||||
|
||||
@@ -52,6 +54,9 @@ public class ResourceTypeProvider implements ScimResourceTypeProvider<ResourceTy
|
||||
|
||||
@Override
|
||||
public Stream<ResourceType> getAll(SearchRequest searchRequest) {
|
||||
if (!session.getContext().getPermissions().hasPermission(AdminPermissionsSchema.REALMS_RESOURCE_TYPE, AdminPermissionsSchema.VIEW)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
return session.getKeycloakSessionFactory().getProviderFactoriesStream(ScimResourceTypeProvider.class)
|
||||
.map(ScimResourceTypeProviderFactory.class::cast)
|
||||
.map(this::toRepresentation)
|
||||
|
||||
+8
-3
@@ -7,12 +7,14 @@ import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.keycloak.authorization.fgap.AdminPermissionsSchema;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.Model;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.scim.model.config.ServiceProviderConfigResourceTypeProvider;
|
||||
import org.keycloak.scim.model.resourcetype.ResourceTypeProviderFactory;
|
||||
import org.keycloak.scim.protocol.ForbiddenException;
|
||||
import org.keycloak.scim.protocol.request.SearchRequest;
|
||||
import org.keycloak.scim.resource.Scim;
|
||||
import org.keycloak.scim.resource.schema.ModelSchema;
|
||||
@@ -149,14 +151,17 @@ public class SchemaResourceTypeProvider implements ScimResourceTypeProvider<Sche
|
||||
|
||||
@Override
|
||||
public Schema get(String id) {
|
||||
// TODO: Add `view-realm` role check for schema discovery ??
|
||||
// Currently accessible to any authenticated user with valid bearer token
|
||||
// Should be aligned with other discovery endpoints (ResourceTypes, ServiceProviderConfig)
|
||||
if (!session.getContext().getPermissions().hasPermission(AdminPermissionsSchema.REALMS_RESOURCE_TYPE, AdminPermissionsSchema.VIEW)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
return schemas.get(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<Schema> getAll(SearchRequest searchRequest) {
|
||||
if (!session.getContext().getPermissions().hasPermission(AdminPermissionsSchema.REALMS_RESOURCE_TYPE, AdminPermissionsSchema.VIEW)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
// Per RFC 7644 Section 4, /Schemas is a discovery endpoint that SHALL return all schemas.
|
||||
// Filtering, sorting, and pagination are not supported for discovery endpoints.
|
||||
// The searchRequest parameter is ignored.
|
||||
|
||||
@@ -173,6 +173,21 @@ public class AuthorizationTest extends AbstractScimTest {
|
||||
noAccessClient.groups().delete(newGroup.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDiscoveryEndpointsDeniedIfRolesNotGranted() {
|
||||
assertAccessDenied(() -> noAccessClient.config().get());
|
||||
assertAccessDenied(() -> noAccessClient.schemas().getAll());
|
||||
assertAccessDenied(() -> noAccessClient.resourceTypes().getAll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDiscoveryEndpointsAccessIfViewRealmRoleGranted() {
|
||||
grantAdminRole(AdminRoles.VIEW_REALM);
|
||||
assertNotNull(noAccessClient.config().get());
|
||||
assertNotNull(noAccessClient.schemas().getAll());
|
||||
assertNotNull(noAccessClient.resourceTypes().getAll());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGroupsCanQueryIfQueryRoleGranted() {
|
||||
createGroup();
|
||||
|
||||
+2
-1
@@ -46,9 +46,10 @@ public class ScimClientSupplier implements Supplier<ScimClient, InjectScimClient
|
||||
UserRepresentation serviceAccountUser = managedRealm.admin().clients().get(id).getServiceAccountUser();
|
||||
ClientRepresentation realmMgmtClient = managedRealm.admin().clients().findByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID).get(0);
|
||||
RoleResource manageUsersRole = managedRealm.admin().clients().get(realmMgmtClient.getId()).roles().get(AdminRoles.MANAGE_USERS);
|
||||
RoleResource viewRealmRole = managedRealm.admin().clients().get(realmMgmtClient.getId()).roles().get(AdminRoles.VIEW_REALM);
|
||||
managedRealm.admin().users().get(serviceAccountUser.getId()).roles()
|
||||
.clientLevel(realmMgmtClient.getId())
|
||||
.add(List.of(manageUsersRole.toRepresentation()));
|
||||
.add(List.of(manageUsersRole.toRepresentation(), viewRealmRole.toRepresentation()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -69,6 +69,8 @@ import org.keycloak.representations.idm.authorization.ScopeRepresentation;
|
||||
|
||||
public class AdminPermissionsSchema extends AuthorizationSchema {
|
||||
|
||||
public static final String REALMS_RESOURCE_TYPE = "Realms";
|
||||
|
||||
public static final String CLIENTS_RESOURCE_TYPE = "Clients";
|
||||
public static final String GROUPS_RESOURCE_TYPE = "Groups";
|
||||
public static final String ROLES_RESOURCE_TYPE = "Roles";
|
||||
|
||||
@@ -7,16 +7,17 @@ import org.keycloak.models.KeycloakContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.Model;
|
||||
import org.keycloak.models.Permissions;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.services.resources.admin.AdminAuth;
|
||||
import org.keycloak.services.resources.admin.fgap.AdminPermissionEvaluator;
|
||||
import org.keycloak.services.resources.admin.fgap.AdminPermissions;
|
||||
import org.keycloak.services.resources.admin.fgap.GroupPermissionEvaluator;
|
||||
import org.keycloak.services.resources.admin.fgap.RealmPermissionEvaluator;
|
||||
import org.keycloak.services.resources.admin.fgap.UserPermissionEvaluator;
|
||||
|
||||
import static org.keycloak.authorization.fgap.AdminPermissionsSchema.GROUPS_RESOURCE_TYPE;
|
||||
import static org.keycloak.authorization.fgap.AdminPermissionsSchema.REALMS_RESOURCE_TYPE;
|
||||
import static org.keycloak.authorization.fgap.AdminPermissionsSchema.USERS_RESOURCE_TYPE;
|
||||
|
||||
public class DefaultPermissions implements Permissions {
|
||||
@@ -40,6 +41,7 @@ public class DefaultPermissions implements Permissions {
|
||||
return switch (realmResourceType) {
|
||||
case USERS_RESOURCE_TYPE -> evaluateUserPermission(model, scope);
|
||||
case GROUPS_RESOURCE_TYPE -> evaluateGroupPermission(model, scope);
|
||||
case REALMS_RESOURCE_TYPE -> evaluateRealmPermission(scope);
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
@@ -82,14 +84,24 @@ public class DefaultPermissions implements Permissions {
|
||||
return false;
|
||||
}
|
||||
|
||||
private RealmModel getRealm() {
|
||||
return context.getRealm();
|
||||
private boolean evaluateRealmPermission(String scope) {
|
||||
Token token = context.getBearerToken();
|
||||
|
||||
if (token instanceof AccessToken accessToken) {
|
||||
AdminPermissionEvaluator evaluator = getEvaluator(accessToken);
|
||||
RealmPermissionEvaluator realms = evaluator.realm();
|
||||
|
||||
if (AdminPermissionsSchema.VIEW.equals(scope)) {
|
||||
return realms.canViewRealm();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private AdminPermissionEvaluator getEvaluator(AccessToken accessToken) {
|
||||
if (realmAuth == null) {
|
||||
RealmModel realm = getRealm();
|
||||
realmAuth = AdminPermissions.evaluator(session, realm, new AdminAuth(realm, accessToken, context.getUser(), context.getClient()));
|
||||
realmAuth = AdminPermissions.evaluator(session, context.getRealm(), new AdminAuth(context.getRealm(), accessToken, context.getUser(), context.getClient()));
|
||||
}
|
||||
return realmAuth;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user