Skip to content

Commit

Permalink
Prevent to manage groups associated with organizations from different…
Browse files Browse the repository at this point in the history
… APIs

Closes keycloak#28734

Signed-off-by: Martin Kanis <mkanis@redhat.com>
  • Loading branch information
martin-kanis committed Apr 30, 2024
1 parent 99cb437 commit 5bb1f1f
Show file tree
Hide file tree
Showing 10 changed files with 282 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
import static org.keycloak.utils.StreamsUtil.closing;

import java.util.List;
import java.util.HashMap;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand Down Expand Up @@ -319,18 +321,17 @@ private OrganizationEntity getEntity(String id, boolean failIfNotFound) {
private GroupModel createOrganizationGroup(String name) {
throwExceptionIfObjectIsNull(name, "Name of the group");

String groupName = getCanonicalGroupName(name);
GroupModel group = groupProvider.getGroupByName(realm, null, name);

if (group != null) {
throw new ModelDuplicateException("A group with the same name already exist and it is bound to different organization");
HashMap<String, String> attr = new HashMap<>();
attr.put(ORGANIZATION_ATTRIBUTE, name);
if (groupProvider.searchGroupsByAttributes(realm, attr, 0, 1).findAny().isPresent()) {
throw new ModelDuplicateException("A group with the same name (attribute) already exist and it is bound to different organization");
}

return groupProvider.createGroup(realm, groupName);
}
String orgId = UUID.randomUUID().toString();
GroupModel group = groupProvider.createGroup(realm, orgId, orgId);
group.setSingleAttribute(ORGANIZATION_ATTRIBUTE, name);

private String getCanonicalGroupName(String name) {
return "kc.org." + name;
return group;
}

private GroupModel getOrganizationGroup(OrganizationModel organization) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import org.jboss.resteasy.reactive.NoCache;

import jakarta.ws.rs.NotFoundException;
import org.keycloak.models.GroupModel;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.services.resources.KeycloakOpenAPI;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.events.admin.OperationType;
Expand Down Expand Up @@ -59,6 +61,8 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.keycloak.services.resources.admin.GroupResource.isOrganizationRelatedGroup;

/**
* @resource Client Role Mappings
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
Expand Down Expand Up @@ -166,6 +170,12 @@ public Stream<RoleRepresentation> getAvailableClientRoleMappings() {
public void addClientRoleMapping(List<RoleRepresentation> roles) {
managePermission.require();


OrganizationProvider orgProvider = session.getProvider(OrganizationProvider.class);
if (orgProvider.isEnabled() && user instanceof GroupModel && isOrganizationRelatedGroup((GroupModel) user)) {
throw ErrorResponse.error("Cannot manage organization related group via Client Role Mapper API.", Response.Status.FORBIDDEN);
}

try {
for (RoleRepresentation role : roles) {
RoleModel roleModel = client.getRole(role.getName());
Expand Down Expand Up @@ -199,6 +209,11 @@ public void addClientRoleMapping(List<RoleRepresentation> roles) {
public void deleteClientRoleMapping(List<RoleRepresentation> roles) {
managePermission.require();

OrganizationProvider orgProvider = session.getProvider(OrganizationProvider.class);
if (orgProvider.isEnabled() && user instanceof GroupModel && isOrganizationRelatedGroup((GroupModel) user)) {
throw ErrorResponse.error("Cannot manage organization related group via Client Role Mapper API.", Response.Status.FORBIDDEN);
}

if (roles == null) {
roles = user.getClientRoleMappingsStream(client)
.peek(roleModel -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.ManagementPermissionReference;
import org.keycloak.representations.idm.UserRepresentation;
Expand Down Expand Up @@ -121,6 +123,15 @@ public Response updateGroup(GroupRepresentation rep) {
throw ErrorResponse.error("Group name is missing", Response.Status.BAD_REQUEST);
}

OrganizationProvider orgProvider = session.getProvider(OrganizationProvider.class);
if (orgProvider.isEnabled()) {
if (isOrganizationRelatedGroup(group)) {
throw ErrorResponse.error("Cannot manage organization related group via Group API.", Response.Status.FORBIDDEN);
} else if (isOrganizationRelatedGroup(rep)) {
throw ErrorResponse.error("Cannot use group attribute reserved for organizations.", Response.Status.FORBIDDEN);
}
}

if (!Objects.equals(groupName, group.getName())) {
boolean exists = siblings().filter(s -> !Objects.equals(s.getId(), group.getId()))
.anyMatch(s -> Objects.equals(s.getName(), groupName));
Expand Down Expand Up @@ -149,6 +160,11 @@ private Stream<GroupModel> siblings() {
public void deleteGroup() {
this.auth.groups().requireManage(group);

OrganizationProvider orgProvider = session.getProvider(OrganizationProvider.class);
if (orgProvider.isEnabled() && isOrganizationRelatedGroup(group)) {
throw ErrorResponse.error("Cannot manage organization related group via Group API.", Response.Status.FORBIDDEN);
}

realm.removeGroup(group);
adminEvent.operation(OperationType.DELETE).resourcePath(session.getContext().getUri()).success();
}
Expand Down Expand Up @@ -194,6 +210,15 @@ public Response addChild(GroupRepresentation rep) {
throw ErrorResponse.error("Group name is missing", Response.Status.BAD_REQUEST);
}

OrganizationProvider orgProvider = session.getProvider(OrganizationProvider.class);
if (orgProvider.isEnabled()) {
if (isOrganizationRelatedGroup(group)) {
throw ErrorResponse.error("Cannot manage organization related group via Group API.", Response.Status.FORBIDDEN);
} else if (isOrganizationRelatedGroup(rep)) {
throw ErrorResponse.error("Cannot use group attribute reserved for organizations.", Response.Status.FORBIDDEN);
}
}

try {
Response.ResponseBuilder builder = Response.status(204);
GroupModel child = null;
Expand Down Expand Up @@ -367,6 +392,12 @@ public static ManagementPermissionReference toMgmtRef(GroupModel group, AdminPer
@Operation( summary = "Return object stating whether client Authorization permissions have been initialized or not and a reference")
public ManagementPermissionReference setManagementPermissionsEnabled(ManagementPermissionReference ref) {
auth.groups().requireManage(group);

OrganizationProvider orgProvider = session.getProvider(OrganizationProvider.class);
if (orgProvider.isEnabled() && isOrganizationRelatedGroup(group)) {
throw ErrorResponse.error("Cannot manage organization related group via Group API.", Response.Status.FORBIDDEN);
}

AdminPermissionManagement permissions = AdminPermissions.management(session, realm);
permissions.groups().setPermissionsEnabled(group, ref.isEnabled());
if (ref.isEnabled()) {
Expand All @@ -376,5 +407,16 @@ public ManagementPermissionReference setManagementPermissionsEnabled(ManagementP
}
}

static protected boolean isOrganizationRelatedGroup(GroupModel group) {
Map<String, List<String>> attributes = group.getAttributes();

return attributes != null && attributes.containsKey(OrganizationModel.ORGANIZATION_ATTRIBUTE);
}

static protected boolean isOrganizationRelatedGroup(GroupRepresentation group) {
Map<String, List<String>> attributes = group.getAttributes();

return attributes != null && attributes.containsKey(OrganizationModel.ORGANIZATION_ATTRIBUTE);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.resources.KeycloakOpenAPI;
Expand All @@ -52,6 +53,7 @@
import org.keycloak.utils.GroupUtils;
import org.keycloak.utils.SearchQueryUtils;

import static org.keycloak.services.resources.admin.GroupResource.isOrganizationRelatedGroup;


/**
Expand Down Expand Up @@ -176,6 +178,11 @@ public Response addTopLevelGroup(GroupRepresentation rep) {
throw ErrorResponse.error("Group name is missing", Response.Status.BAD_REQUEST);
}

OrganizationProvider orgProvider = session.getProvider(OrganizationProvider.class);
if (orgProvider.isEnabled() && isOrganizationRelatedGroup(rep)) {
throw ErrorResponse.error("Cannot use group attribute reserved for organizations.", Response.Status.FORBIDDEN);
}

try {
if (rep.getId() != null) {
child = realm.getGroupById(rep.getId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package org.keycloak.services.resources.admin;

import static org.keycloak.services.resources.admin.GroupResource.isOrganizationRelatedGroup;
import static org.keycloak.util.JsonSerialization.readValue;

import java.io.InputStream;
Expand All @@ -30,7 +31,6 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import jakarta.enterprise.inject.Default;
import jakarta.ws.rs.DefaultValue;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.extensions.Extension;
Expand All @@ -49,7 +49,6 @@
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.PathSegment;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.core.StreamingOutput;
Expand Down Expand Up @@ -94,7 +93,7 @@
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.models.utils.StripSecretsUtils;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.partialimport.ErrorResponseException;
import org.keycloak.partialimport.PartialImportResult;
import org.keycloak.partialimport.PartialImportResults;
Expand Down Expand Up @@ -1053,6 +1052,12 @@ public void addDefaultGroup(@PathParam("groupId") String groupId) {
if (group == null) {
throw new NotFoundException("Group not found");
}

OrganizationProvider orgProvider = session.getProvider(OrganizationProvider.class);
if (orgProvider.isEnabled() && isOrganizationRelatedGroup(group)) {
throw ErrorResponse.error("Cannot manage organization related group via Realm API.", Response.Status.FORBIDDEN);
}

realm.addDefaultGroup(group);

adminEvent.operation(OperationType.CREATE).resource(ResourceType.GROUP).resourcePath(session.getContext().getUri()).success();
Expand All @@ -1070,6 +1075,12 @@ public void removeDefaultGroup(@PathParam("groupId") String groupId) {
if (group == null) {
throw new NotFoundException("Group not found");
}

OrganizationProvider orgProvider = session.getProvider(OrganizationProvider.class);
if (orgProvider.isEnabled() && isOrganizationRelatedGroup(group)) {
throw ErrorResponse.error("Cannot manage organization related group via Group API.", Response.Status.FORBIDDEN);
}

realm.removeDefaultGroup(group);

adminEvent.operation(OperationType.DELETE).resource(ResourceType.GROUP).resourcePath(session.getContext().getUri()).success();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
import org.keycloak.models.ModelIllegalStateException;
Expand All @@ -37,6 +38,7 @@
import org.keycloak.models.RoleMapperModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.representations.idm.ClientMappingsRepresentation;
import org.keycloak.representations.idm.MappingsRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
Expand Down Expand Up @@ -68,6 +70,8 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.keycloak.services.resources.admin.GroupResource.isOrganizationRelatedGroup;

/**
* Base resource for managing users
*
Expand Down Expand Up @@ -234,6 +238,11 @@ public Stream<RoleRepresentation> getAvailableRealmRoleMappings() {
public void addRealmRoleMappings(@Parameter(description = "Roles to add") List<RoleRepresentation> roles) {
managePermission.require();

OrganizationProvider orgProvider = session.getProvider(OrganizationProvider.class);
if (orgProvider.isEnabled() && roleMapper instanceof GroupModel && isOrganizationRelatedGroup((GroupModel) roleMapper)) {
throw ErrorResponse.error("Cannot manage organization related group via Role Mapper API.", Response.Status.FORBIDDEN);
}

logger.debugv("** addRealmRoleMappings: {0}", roles);

try {
Expand Down Expand Up @@ -269,6 +278,11 @@ public void addRealmRoleMappings(@Parameter(description = "Roles to add") List<R
public void deleteRealmRoleMappings(List<RoleRepresentation> roles) {
managePermission.require();

OrganizationProvider orgProvider = session.getProvider(OrganizationProvider.class);
if (orgProvider.isEnabled() && roleMapper instanceof GroupModel && isOrganizationRelatedGroup((GroupModel) roleMapper)) {
throw ErrorResponse.error("Cannot manage organization related group via Role Mapper API.", Response.Status.FORBIDDEN);
}

logger.debug("deleteRealmRoleMappings");
if (roles == null) {
roles = roleMapper.getRealmRoleMappingsStream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.models.utils.RoleUtils;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.policy.PasswordPolicyNotMetException;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
Expand Down Expand Up @@ -127,6 +128,7 @@

import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID;
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;
import static org.keycloak.services.resources.admin.GroupResource.isOrganizationRelatedGroup;
import static org.keycloak.userprofile.UserProfileContext.USER_API;

/**
Expand Down Expand Up @@ -1017,6 +1019,11 @@ public void removeMembership(@PathParam("groupId") String groupId) {
}
auth.groups().requireManageMembership(group);

OrganizationProvider orgProvider = session.getProvider(OrganizationProvider.class);
if (orgProvider.isEnabled() && isOrganizationRelatedGroup(group)) {
throw ErrorResponse.error("Cannot manage organization related group via User API.", Response.Status.FORBIDDEN);
}

try {
if (user.isMemberOf(group)){
user.leaveGroup(group);
Expand Down Expand Up @@ -1044,6 +1051,12 @@ public void joinGroup(@PathParam("groupId") String groupId) {
throw new NotFoundException("Group not found");
}
auth.groups().requireManageMembership(group);

OrganizationProvider orgProvider = session.getProvider(OrganizationProvider.class);
if (orgProvider.isEnabled() && isOrganizationRelatedGroup(group)) {
throw ErrorResponse.error("Cannot manage organization related group via User API.", Response.Status.FORBIDDEN);
}

if (!RoleUtils.isDirectMember(user.getGroupsStream(),group)){
user.joinGroup(group);
adminEvent.operation(OperationType.CREATE).resource(ResourceType.GROUP_MEMBERSHIP).representation(ModelToRepresentation.toRepresentation(group, true)).resourcePath(session.getContext().getUri()).success();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ void resetSearchableAttributes() throws Exception {
reconnectAdminClient();
}

private static String buildSearchQuery(String firstAttrName, String firstAttrValue, String... furtherAttrKeysAndValues) {
public static String buildSearchQuery(String firstAttrName, String firstAttrValue, String... furtherAttrKeysAndValues) {
if (furtherAttrKeysAndValues.length % 2 != 0) {
throw new IllegalArgumentException("Invalid length of furtherAttrKeysAndValues. Must be even, but is: " + furtherAttrKeysAndValues.length);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import jakarta.ws.rs.core.Response.Status;
import org.jboss.arquillian.graphene.page.Page;
import org.keycloak.admin.client.resource.OrganizationResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
Expand Down Expand Up @@ -178,4 +180,16 @@ protected UserRepresentation addMember(OrganizationResource organization, String
return actual;
}
}

protected GroupRepresentation createGroup(RealmResource realm, String name) {
GroupRepresentation group = new GroupRepresentation();
group.setName(name);
try (Response response = realm.groups().add(group)) {
String groupId = ApiUtil.getCreatedId(response);

// Set ID to the original rep
group.setId(groupId);
return group;
}
}
}

0 comments on commit 5bb1f1f

Please sign in to comment.