8000 SCIM Authorization Bypass in User Group Management by vramik · Pull Request #47616 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@
import java.util.function.Function;
import java.util.stream.Collectors;

import org.keycloak.authorization.fgap.AdminPermissionsSchema;
import org.keycloak.common.util.TriConsumer;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelValidationException;
import org.keycloak.models.Permissions;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.scim.protocol.ForbiddenException;
import org.keycloak.scim.resource.Scim;
import org.keycloak.scim.resource.common.Email;
import org.keycloak.scim.resource.common.Name;
Expand Down Expand Up @@ -146,6 +149,7 @@ protected Map<String, Attribute<UserModel, User>> doGetAttributes() {
.withModelSetter((TriConsumer<UserModel, String, Set<GroupMembership>>) (model, name, values) -> {
KeycloakSession session = KeycloakSessionUtil.getKeycloakSession();
RealmModel realm = session.getContext().getRealm();
checkUserMembershipPermission(session.getContext().getPermissions(), model);
List<GroupModel> remove = new ArrayList<>();

for (GroupUtils.GroupMembership membership : GroupUtils.getAllMemberships(session, model.getGroupsStream().toList())) {
Expand All @@ -161,10 +165,12 @@ protected Map<String, Attribute<UserModel, User>> doGetAttributes() {
throw new ModelValidationException("Group with id " + membership.getValue() + " not found");
}

checkGroupMembershipPermission(session.getContext().getPermissions(), group);
model.joinGroup(group);
}

for (GroupModel group : remove) {
checkGroupMembershipPermission(session.getContext().getPermissions(), group);
model.leaveGroup(group);
}
}, (BiConsumer<User, Collection<GroupModel>>) (user, groups) -> {
Expand All @@ -183,6 +189,7 @@ protected Map<String, Attribute<UserModel, User>> doGetAttributes() {
.withModelRemover((TriConsumer<UserModel, String, Set<GroupMembership>>) (model, name, values) -> {
KeycloakSession session = KeycloakSessionUtil.getKeycloakSession();
RealmModel realm = session.getContext().getRealm();
checkUserMembershipPermission(session.getContext().getPermissions(), model);

for (GroupMembership membership : values) {
GroupModel group = session.groups().getGroupById(realm, membership.getValue());
Expand All @@ -191,12 +198,14 @@ protected Map<String, Attribute<UserModel, User>> doGetAttributes() {
throw new ModelValidationException("Group with id " + membership.getValue() + " not found");
}

checkGroupMembershipPermission(session.getContext().getPermissions(), group);
model.leaveGroup(group);
}
})
.withModelAdder((TriConsumer<UserModel, String, Set<GroupMembership>>) (model, name, values) -> {
KeycloakSession session = KeycloakSessionUtil.getKeycloakSession();
RealmModel realm = session.getContext().getRealm();
checkUserMembershipPermission(session.getContext().getPermissions(), model);

for (GroupMembership membership : values) {
GroupModel group = session.groups().getGroupById(realm, membership.getValue());
Expand All @@ -205,6 +214,7 @@ protected Map<String, Attribute<UserModel, User>> doGetAttributes() {
throw new ModelValidationException("Group with id " + membership.getValue() + " not found");
}

checkGroupMembershipPermission(session.getContext().getPermissions(), group);
model.joinGroup(group);
}
})
Expand All @@ -225,6 +235,21 @@ public void populate(User resource, UserModel model, List<String> requestedAttri
setTimestamps(resource, model);
}

private static void checkUserMembershipPermission(Permissions permissions, UserModel user) {
if (!permissions.hasPermission(user, AdminPermissionsSchema.USERS_RESOURCE_TYPE, AdminPermissionsSchema.MANAGE_GROUP_MEMBERSHIP)) {
throw new ForbiddenException();
}
}

private static void checkGroupMembershipPermission(Permissions permissions, GroupModel group) {
if (GroupModel.Type.ORGANIZATION.equals(group.getType()) && group.getOrganization() != null) {
throw new ModelValidationException("Cannot access organization related group via non Organization API.");
}
if (!permissions.hasPermission(group, AdminPermissionsSchema.GROUPS_RESOURCE_TYPE, AdminPermissionsSchema.MANAGE_MEMBERSHIP)) {
throw new ForbiddenException();
}
}

private void setTimestamps(User resource, UserModel model) {
Long createdTimestamp = model.getCreatedTimestamp();
if (createdTimestamp != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,59 @@ public void testAllowGroupsFGAP() {
assertEquals(1, groups.getTotalResults());
}

@Test
public void testGroupMembershipDeniedWithoutManageGroupMembershipFGAP() {
RealmRepresentation realmRep = this.realm.admin().toRepresentation();
realmRep.setAdminPermissionsEnabled(true);
realm.admin().update(realmRep);

GroupRepresentation group = createGroup();
ClientRepresentation client = getScimClient();
UserRepresentation serviceAccount = realm.admin().clients().get(client.getId()).getServiceAccountUser();

// Create policy for the SCIM service account
UserPolicyRepresentation policy = new UserPolicyRepresentation();
policy.setName("Allow SCIM user management");
policy.addUser(serviceAccount.getId());
ClientResource permissionClient = realm.admin().clients().get(
realm.admin().clients().findByClientId(Constants.ADMIN_PERMISSIONS_CLIENT_ID).get(0).getId());
permissionClient.authorization().policies().user().create(policy).close();

// Grant manage on users (but NOT manage-group-membership)
ScopePermissionRepresentation userPermission = new ScopePermissionRepresentation();
userPermission.setName("Allow SCIM manage users");
userPermission.setResourceType(AdminPermissionsSchema.USERS_RESOURCE_TYPE);
userPermission.setScopes(Set.of(AdminPermissionsSchema.MANAGE, AdminPermissionsSchema.VIEW));
userPermission.addPolicy(policy.getName());
permissionClient.authorization().permissions().scope().create(userPermission).close();

// Grant view on groups (so the SCIM client can resolve group IDs)
ScopePermissionRepresentation groupPermission = new ScopePermissionRepresentation();
groupPermission.setName("Allow SCIM view groups");
groupPermission.setResourceType(AdminPermissionsSchema.GROUPS_RESOURCE_TYPE);
groupPermission.setScopes(Set.of(AdminPermissionsSchema.VIEW));
groupPermission.addPolicy(policy.getName());
permissionClient.authorization().permissions().scope().create(groupPermission).close();

// Verify the client can manage users (e.g., update name)
User user = noAccessClient.users().get(managedUser.getId());
assertNotNull(user);
noAccessClient.users().patch(managedUser.getId(), PatchRequest.create()
.add("name.givenName", "updated name")
.build());

// Verify the client CANNOT add group membership via PATCH
assertAccessDenied(() -> noAccessClient.users().patch(managedUser.getId(), PatchRequest.create()
.add("groups", group.getId())
.build()));

// Verify the client CANNOT set group membership via PUT
user = noAccessClient.users().get(managedUser.getId());
user.addGroup(group.getId());
User finalUser = user;
assertAccessDenied(() -> noAccessClient.users().update(finalUser.getId(), finalUser));
}

private ClientRepresentation getScimClient() {
return realm.admin().clients().findByClientId("scim-client-restricted").get(0);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ private boolean evaluateGroupPermission(Model model, String scope) {
return model == null ? groups.canView() : groups.canView((GroupModel) model);
} else if (AdminPermissionsSchema.MANAGE.equals(scope)) {
return model == null ? groups.canManage() : groups.canManage((GroupModel) model);
} else if (AdminPermissionsSchema.MANAGE_MEMBERSHIP.equals(scope)) {
return model != null && groups.canManageMembership((GroupModel) model);
} else if (AdminPermissionsSchema.QUERY.equals(scope)) {
return groups.canList();
}
Expand All @@ -76,6 +78,8 @@ private boolean evaluateUserPermission(Model model, String scope) {
return model == null ? users.canView() : users.canView((UserModel) model);
} else if (AdminPermissionsSchema.MANAGE.equals(scope)) {
return model == null ? users.canManage() : users.canManage((UserModel) model);
} else if (AdminPermissionsSchema.MANAGE_GROUP_MEMBERSHIP.equals(scope)) {
return model != null && users.canManageGroupMembership((UserModel) model);
} else if (AdminPermissionsSchema.QUERY.equals(scope)) {
return users.canQuery();
}
Expand Down
Loading
0