Make sure users created through a registration link are managed members

Closes #30743

Signed-off-by: vramik <vramik@redhat.com>
This commit is contained in:
vramik 2024-07-18 14:23:04 +02:00 committed by Pedro Igor
parent 83f8622d15
commit 649b35929e
27 changed files with 337 additions and 118 deletions

View file

@ -0,0 +1,39 @@
/*
* 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;
public class MemberRepresentation extends UserRepresentation {
private MembershipType membershipType;
public MemberRepresentation() {
super();
}
public MemberRepresentation(UserRepresentation user) {
super(user);
}
public MembershipType getMembershipType() {
return membershipType;
}
public void setMembershipType(MembershipType membershipType) {
this.membershipType = membershipType;
}
}

View file

@ -0,0 +1,30 @@
/*
* 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;
public enum MembershipType {
/**
* Indicates that member can exist without group/organization.
*/
UNMANAGED,
/**
* Indicates that member cannot exist without group/organization.
*/
MANAGED;
}

View file

@ -34,7 +34,7 @@ public class OrganizationRepresentation {
private String description;
private Map<String, List<String>> attributes;
private Set<OrganizationDomainRepresentation> domains;
private List<UserRepresentation> members;
private List<MemberRepresentation> members;
private List<IdentityProviderRepresentation> identityProviders;
public String getId() {
@ -119,19 +119,19 @@ public class OrganizationRepresentation {
getDomains().remove(domain);
}
public List<UserRepresentation> getMembers() {
public List<MemberRepresentation> getMembers() {
return members;
}
public void setMembers(List<UserRepresentation> members) {
public void setMembers(List<MemberRepresentation> members) {
this.members = members;
}
public void addMember(UserRepresentation user) {
public void addMember(MemberRepresentation member) {
if (members == null) {
members = new ArrayList<>();
}
members.add(user);
members.add(member);
}
public List<IdentityProviderRepresentation> getIdentityProviders() {

View file

@ -52,6 +52,43 @@ public class UserRepresentation extends AbstractUserRepresentation{
protected List<String> groups;
private Map<String, Boolean> access;
public UserRepresentation() {
}
public UserRepresentation(UserRepresentation rep) {
// AbstractUserRepresentation
this.id = rep.getId();
this.username = rep.getUsername();
this.firstName = rep.getFirstName();
this.lastName = rep.getLastName();
this.email = rep.getEmail();
this.emailVerified = rep.isEmailVerified();
this.attributes = rep.getAttributes();
this.setUserProfileMetadata(rep.getUserProfileMetadata());
this.self = rep.getSelf();
this.origin = rep.getOrigin();
this.createdTimestamp = rep.getCreatedTimestamp();
this.enabled = rep.isEnabled();
this.totp = rep.isTotp();
this.federationLink = rep.getFederationLink();
this.serviceAccountClientId = rep.getServiceAccountClientId();
this.credentials = rep.getCredentials();
this.disableableCredentialTypes = rep.getDisableableCredentialTypes();
this.requiredActions = rep.getRequiredActions();
this.federatedIdentities = rep.getFederatedIdentities();
this.realmRoles = rep.getRealmRoles();
this.clientRoles = rep.getClientRoles();
this.clientConsents = rep.getClientConsents();
this.notBefore = rep.getNotBefore();
this.applicationRoles = rep.getApplicationRoles();
this.socialLinks = rep.getSocialLinks();
this.groups = rep.getGroups();
this.access = rep.getAccess();
}
public String getSelf() {
return self;
}

View file

@ -22,13 +22,13 @@ import javax.ws.rs.GET;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.MemberRepresentation;
public interface OrganizationMemberResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
UserRepresentation toRepresentation();
MemberRepresentation toRepresentation();
@DELETE
Response delete();

View file

@ -29,8 +29,8 @@ import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.keycloak.representations.idm.MemberRepresentation;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
public interface OrganizationMembersResource {
@ -45,7 +45,7 @@ public interface OrganizationMembersResource {
*/
@GET
@Produces(MediaType.APPLICATION_JSON)
List<UserRepresentation> getAll();
List<MemberRepresentation> getAll();
/**
* Return all organization members that match the specified filters.
@ -60,7 +60,7 @@ public interface OrganizationMembersResource {
*/
@GET
@Produces(MediaType.APPLICATION_JSON)
List<UserRepresentation> search(
List<MemberRepresentation> search(
@QueryParam("search") String search,
@QueryParam("exact") Boolean exact,
@QueryParam("first") Integer first,

View file

@ -21,6 +21,7 @@ import java.util.Map;
import java.util.stream.Stream;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.MembershipMetadata;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
@ -134,6 +135,11 @@ public class InfinispanOrganizationProvider implements OrganizationProvider {
getAllStream().forEach(this::remove);
}
@Override
public boolean addManagedMember(OrganizationModel organization, UserModel user) {
return orgDelegate.addManagedMember(organization, user);
}
@Override
public boolean addMember(OrganizationModel organization, UserModel user) {
return orgDelegate.addMember(organization, user);

View file

@ -26,6 +26,7 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.GroupModel.GroupMemberJoinEvent;
import org.keycloak.models.GroupModel.GroupMemberLeaveEvent;
import org.keycloak.models.MembershipMetadata;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
@ -55,6 +56,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
import org.keycloak.representations.idm.MembershipType;
import static org.keycloak.utils.StreamsUtil.closing;
@ -167,7 +169,7 @@ public class UserAdapter implements UserModel, JpaModel<UserEntity> {
@Override
public void setAttribute(String name, List<String> values) {
String valueToSet = (values != null && values.size() > 0) ? values.get(0) : null;
String valueToSet = (values != null && !values.isEmpty()) ? values.get(0) : null;
if (UserModel.FIRST_NAME.equals(name)) {
user.setFirstName(valueToSet);
return;
@ -363,7 +365,7 @@ public class UserAdapter implements UserModel, JpaModel<UserEntity> {
predicates.add(builder.equal(root.get("user"), getEntity()));
queryBuilder.select(root.get("groupId"));
queryBuilder.where(predicates.toArray(new Predicate[0]));
queryBuilder.where(predicates.toArray(Predicate[]::new));
return em.createQuery(queryBuilder);
}
@ -414,20 +416,28 @@ public class UserAdapter implements UserModel, JpaModel<UserEntity> {
@Override
public void joinGroup(GroupModel group) {
if (RoleUtils.isDirectMember(getGroupsStream(), group)) return;
joinGroupImpl(group);
joinGroup(group, null);
}
@Override
public void joinGroup(GroupModel group, MembershipMetadata metadata) {
if (RoleUtils.isDirectMember(getGroupsStream(), group)) return;
joinGroupImpl(group, metadata);
}
protected void joinGroupImpl(GroupModel group) {
joinGroupImpl(group, null);
}
protected void joinGroupImpl(GroupModel group, MembershipMetadata metadata) {
UserGroupMembershipEntity entity = new UserGroupMembershipEntity();
entity.setUser(getEntity());
entity.setGroupId(group.getId());
entity.setMembershipType(metadata == null ? MembershipType.UNMANAGED : metadata.getMembershipType());
em.persist(entity);
em.flush();
em.detach(entity);
GroupMemberJoinEvent.fire(group, session);
}
@Override
@ -437,7 +447,7 @@ public class UserAdapter implements UserModel, JpaModel<UserEntity> {
TypedQuery<UserGroupMembershipEntity> query = getUserGroupMappingQuery(group);
query.setLockMode(LockModeType.PESSIMISTIC_WRITE);
List<UserGroupMembershipEntity> results = query.getResultList();
if (results.size() == 0) return;
if (results.isEmpty()) return;
for (UserGroupMembershipEntity entity : results) {
em.remove(entity);
}
@ -508,7 +518,7 @@ public class UserAdapter implements UserModel, JpaModel<UserEntity> {
TypedQuery<UserRoleMappingEntity> query = getUserRoleMappingEntityTypedQuery(role);
query.setLockMode(LockModeType.PESSIMISTIC_WRITE);
List<UserRoleMappingEntity> results = query.getResultList();
if (results.size() == 0) return;
if (results.isEmpty()) return;
for (UserRoleMappingEntity entity : results) {
em.remove(entity);
}

View file

@ -28,6 +28,7 @@ import jakarta.persistence.NamedQueries;
import jakarta.persistence.NamedQuery;
import jakarta.persistence.Table;
import java.io.Serializable;
import org.keycloak.representations.idm.MembershipType;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -62,6 +63,9 @@ public class UserGroupMembershipEntity {
@Column(name = "GROUP_ID")
protected String groupId;
@Column(name = "MEMBERSHIP_TYPE")
private String membershipType;
public UserEntity getUser() {
return user;
}
@ -78,6 +82,14 @@ public class UserGroupMembershipEntity {
this.groupId = groupId;
}
public MembershipType getMembershipType() {
return MembershipType.valueOf(membershipType);
}
public void setMembershipType(MembershipType membershipType) {
this.membershipType = membershipType.toString();
}
public static class Key implements Serializable {
protected UserEntity user;

View file

@ -36,24 +36,29 @@ import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import java.util.stream.Collectors;
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.GroupModel.Type;
import org.keycloak.models.GroupProvider;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.MembershipMetadata;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
import org.keycloak.models.ModelValidationException;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserManager;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.models.jpa.entities.GroupAttributeEntity;
import org.keycloak.models.jpa.entities.GroupEntity;
import org.keycloak.models.jpa.entities.OrganizationEntity;
import org.keycloak.models.jpa.entities.UserEntity;
import org.keycloak.models.jpa.entities.UserGroupMembershipEntity;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.representations.idm.MembershipType;
import org.keycloak.utils.StringUtil;
public class JpaOrganizationProvider implements OrganizationProvider {
@ -143,8 +148,17 @@ public class JpaOrganizationProvider implements OrganizationProvider {
getAllStream().forEach(this::remove);
}
@Override
public boolean addManagedMember(OrganizationModel organization, UserModel user) {
return addMember(organization, user, new MembershipMetadata(MembershipType.MANAGED));
}
@Override
public boolean addMember(OrganizationModel organization, UserModel user) {
return addMember(organization, user, new MembershipMetadata(MembershipType.UNMANAGED));
}
private boolean addMember(OrganizationModel organization, UserModel user, MembershipMetadata metadata) {
throwExceptionIfObjectIsNull(organization, "Organization");
throwExceptionIfObjectIsNull(user, "User");
@ -171,7 +185,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
throw new ModelException("User [" + user.getId() + "] is a member of a different organization");
}
user.joinGroup(group);
user.joinGroup(group, metadata);
user.setSingleAttribute(ORGANIZATION_ATTRIBUTE, entity.getId());
} finally {
if (current == null) {
@ -357,20 +371,23 @@ public class JpaOrganizationProvider implements OrganizationProvider {
return false;
}
List<IdentityProviderModel> brokers = organization.getIdentityProviders().toList();
if (brokers.isEmpty()) {
UserEntity userEntity = em.find(UserEntity.class, member.getId());
if (userEntity == null) {
return false;
}
RealmModel realm = getRealm();
List<FederatedIdentityModel> federatedIdentities = userProvider.getFederatedIdentitiesStream(realm, member)
.map(federatedIdentityModel -> realm.getIdentityProviderByAlias(federatedIdentityModel.getIdentityProvider()))
.filter(brokers::contains)
.map(m -> userProvider.getFederatedIdentity(realm, member, m.getAlias()))
.toList();
GroupModel organizationGroup = getOrganizationGroup(organization);
try {
UserGroupMembershipEntity membership = em.createNamedQuery("userMemberOf", UserGroupMembershipEntity.class)
.setParameter("user", userEntity)
.setParameter("groupId", organizationGroup.getId())
.getSingleResult();
em.detach(membership);
return !federatedIdentities.isEmpty();
return MembershipType.MANAGED.equals(membership.getMembershipType());
} catch (NoResultException e) {
return false;
}
}
@Override
@ -385,7 +402,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
}
if (isManagedMember(organization, member)) {
userProvider.removeUser(getRealm(), member);
return new UserManager(session).removeUser(getRealm(), member, userProvider);
} else {
OrganizationModel current = (OrganizationModel) session.getAttribute(OrganizationModel.class.getName());

View file

@ -45,4 +45,14 @@
</createIndex>
</changeSet>
<changeSet author="keycloak" id="26.0.0-org-group-membership">
<addColumn tableName="USER_GROUP_MEMBERSHIP">
<column name="MEMBERSHIP_TYPE" type="VARCHAR(255)"/>
</addColumn>
<update tableName="USER_GROUP_MEMBERSHIP">
<column name="MEMBERSHIP_TYPE" value="UNMANAGED"/>
</update>
<addNotNullConstraint tableName="USER_GROUP_MEMBERSHIP" columnName="MEMBERSHIP_TYPE" columnDataType="VARCHAR(255)"/>
</changeSet>
</databaseChangeLog>

View file

@ -68,6 +68,8 @@ import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.keycloak.representations.idm.MemberRepresentation;
import org.keycloak.representations.idm.MembershipType;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -279,12 +281,14 @@ public class ExportUtils {
return domain;
}).forEach(org::addDomain);
orgProvider.getMembersStream(m, null, null, -1, -1)
.map(user -> {
UserRepresentation member = new UserRepresentation();
orgProvider.getMembersStream(m, null, null, null, null)
.forEach(user -> {
MemberRepresentation member = new MemberRepresentation();
member.setUsername(user.getUsername());
return member;
}).forEach(org::addMember);
member.setMembershipType(orgProvider.isManagedMember(m, user) ? MembershipType.MANAGED : MembershipType.UNMANAGED);
org.addMember(member);
});
orgProvider.getIdentityProviders(m)
.map(b -> {

View file

@ -85,7 +85,6 @@ import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.OAuthClientRepresentation;
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.representations.idm.PartialImportRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
@ -134,6 +133,8 @@ import static org.keycloak.models.utils.RepresentationToModel.createRoleMappings
import static org.keycloak.models.utils.RepresentationToModel.importGroup;
import static org.keycloak.models.utils.RepresentationToModel.importRoles;
import static org.keycloak.models.utils.StripSecretsUtils.stripSecrets;
import org.keycloak.representations.idm.MemberRepresentation;
import org.keycloak.representations.idm.MembershipType;
/**
* This wraps the functionality about export/import for the storage.
@ -1598,11 +1599,15 @@ public class DefaultExportImportManager implements ExportImportManager {
provider.addIdentityProvider(org, idp);
}
for (UserRepresentation member : Optional.ofNullable(orgRep.getMembers()).orElse(Collections.emptyList())) {
for (MemberRepresentation member : Optional.ofNullable(orgRep.getMembers()).orElse(Collections.emptyList())) {
UserModel m = session.users().getUserByUsername(newRealm, member.getUsername());
if (MembershipType.MANAGED.equals(member.getMembershipType())) {
provider.addManagedMember(org, m);
} else {
provider.addMember(org, m);
}
}
}
}
}
}

View file

@ -8,34 +8,7 @@ public class BruteUser extends UserRepresentation {
Map<String, Object> bruteForceStatus;
public BruteUser(UserRepresentation user) {
this.id = user.getId();
this.origin = user.getOrigin();
this.createdTimestamp = user.getCreatedTimestamp();
this.username = user.getUsername();
this.enabled = user.isEnabled();
this.totp = user.isTotp();
this.emailVerified = user.isEmailVerified();
this.firstName = user.getFirstName();
this.lastName = user.getLastName();
this.email = user.getEmail();
this.federationLink = user.getFederationLink();
this.serviceAccountClientId = user.getServiceAccountClientId();
this.attributes = user.getAttributes();
this.credentials = user.getCredentials();
this.disableableCredentialTypes = user.getDisableableCredentialTypes();
this.requiredActions = user.getRequiredActions();
this.federatedIdentities = user.getFederatedIdentities();
this.realmRoles = user.getRealmRoles();
this.clientRoles = user.getClientRoles();
this.clientConsents = user.getClientConsents();
this.notBefore = user.getNotBefore();
this.applicationRoles = user.getApplicationRoles();
this.socialLinks = user.getSocialLinks();
this.groups = user.getGroups();
this.setAccess(user.getAccess());
super(user);
}
public Map<String, Object> getBruteForceStatus() {

View file

@ -0,0 +1,32 @@
/*
* 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.models;
import org.keycloak.representations.idm.MembershipType;
public class MembershipMetadata {
private final MembershipType membershipType;
public MembershipMetadata(MembershipType membershipType) {
this.membershipType = membershipType;
}
public MembershipType getMembershipType() {
return membershipType;
}
}

View file

@ -195,6 +195,9 @@ public interface UserModel extends RoleMapperModel {
}
void joinGroup(GroupModel group);
default void joinGroup(GroupModel group, MembershipMetadata metadata) {
joinGroup(group);
}
void leaveGroup(GroupModel group);
boolean isMemberOf(GroupModel group);

View file

@ -103,7 +103,17 @@ public interface OrganizationProvider extends Provider {
void removeAll();
/**
* Adds the given {@link UserModel} as a member of the given {@link OrganizationModel}.
* Adds the given {@link UserModel} as a managed member of the given {@link OrganizationModel}.
*
* @param organization the organization
* @param user the user
* @throws ModelException if the {@link UserModel} is member of different organization
* @return {@code true} if the user was added as a member. Otherwise, returns {@code false}
*/
boolean addManagedMember(OrganizationModel organization, UserModel user);
/**
* Adds the given {@link UserModel} as an unmanaged member of the given {@link OrganizationModel}.
*
* @param organization the organization
* @param user the user

View file

@ -342,7 +342,7 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
KeycloakSession session = context.getSession();
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
OrganizationModel orgModel = provider.getById(token.getOrgId());
provider.addMember(orgModel, user);
provider.addManagedMember(orgModel, user);
context.getEvent().detail(Details.ORG_ID, orgModel.getId());
context.getAuthenticationSession().setRedirectUri(token.getRedirectUri());
}

View file

@ -48,6 +48,8 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.representations.idm.MemberRepresentation;
import org.keycloak.representations.idm.MembershipType;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.ErrorResponse;
@ -131,7 +133,7 @@ public class OrganizationMemberResource {
@NoCache
@Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS)
@Operation( summary = "Returns a paginated list of organization members filtered according to the specified parameters")
public Stream<UserRepresentation> search(
public Stream<MemberRepresentation> search(
@Parameter(description = "A String representing either a member's username, e-mail, first name, or last name.") @QueryParam("search") String search,
@Parameter(description = "Boolean which defines whether the param 'search' must match exactly or not") @QueryParam("exact") Boolean exact,
@Parameter(description = "The position of the first result to be processed (pagination offset)") @QueryParam("first") @DefaultValue("0") Integer first,
@ -148,7 +150,7 @@ public class OrganizationMemberResource {
@Operation( summary = "Returns the member of the organization with the specified id", description = "Searches for a" +
"user with the given id. If one is found, and is currently a member of the organization, returns it. Otherwise," +
"an error response with status NOT_FOUND is returned")
public UserRepresentation get(@PathParam("id") String id) {
public MemberRepresentation get(@PathParam("id") String id) {
if (StringUtil.isBlank(id)) {
throw ErrorResponse.error("id cannot be null", Status.BAD_REQUEST);
}
@ -160,8 +162,8 @@ public class OrganizationMemberResource {
@DELETE
@Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS)
@Operation(summary = "Removes the user with the specified id from the organization", description = "Breaks the association " +
"between the user and organization. The user itself is not deleted. If no user is found, or if they are not " +
"a member of the organization, an error response is returned")
"between the user and organization. The user itself is deleted in case the membership is managed, otherwise the user is not deleted. " +
"If no user is found, or if they are not a member of the organization, an error response is returned")
public Response delete(@PathParam("id") String id) {
if (StringUtil.isBlank(id)) {
throw ErrorResponse.error("id cannot be null", Status.BAD_REQUEST);
@ -211,7 +213,9 @@ public class OrganizationMemberResource {
return member;
}
private UserRepresentation toRepresentation(UserModel member) {
return ModelToRepresentation.toRepresentation(session, realm, member);
private MemberRepresentation toRepresentation(UserModel member) {
MemberRepresentation result = new MemberRepresentation(ModelToRepresentation.toRepresentation(session, realm, member));
result.setMembershipType(provider.isManagedMember(organization, member) ? MembershipType.MANAGED : MembershipType.UNMANAGED);
return result;
}
}

View file

@ -58,7 +58,7 @@ public class IdpAddOrganizationMemberAuthenticator extends AbstractIdpAuthentica
return;
}
provider.addMember(organization, user);
provider.addManagedMember(organization, user);
context.success();
}

View file

@ -180,7 +180,7 @@ public class RealmAttributeUpdater extends ServerResourceUpdater<RealmAttributeU
return this;
}
public RealmAttributeUpdater setOrganizationEnabled(Boolean organizationsEnabled) {
public RealmAttributeUpdater setOrganizationsEnabled(Boolean organizationsEnabled) {
rep.setOrganizationsEnabled(organizationsEnabled);
return this;
}

View file

@ -29,6 +29,7 @@ import java.util.List;
import java.util.function.Function;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import org.hamcrest.Matchers;
import org.jboss.arquillian.graphene.page.Page;
import org.keycloak.admin.client.resource.OrganizationResource;
import org.keycloak.models.OrganizationModel;
@ -37,6 +38,7 @@ import org.keycloak.models.OrganizationModel.IdentityProviderRedirectMode;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.MemberRepresentation;
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
@ -79,6 +81,11 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
protected BrokerConfiguration bc = brokerConfigFunction.apply(organizationName);
@Override
protected TestCleanup getCleanup() {
return getCleanup(TEST_REALM_NAME);
}
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
testRealm.getClients().addAll(bc.createConsumerClients());
@ -144,15 +151,15 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
return org;
}
protected UserRepresentation addMember(OrganizationResource organization) {
protected MemberRepresentation addMember(OrganizationResource organization) {
return addMember(organization, memberEmail);
}
protected UserRepresentation addMember(OrganizationResource organization, String email) {
protected MemberRepresentation addMember(OrganizationResource organization, String email) {
return addMember(organization, email, null, null);
}
protected UserRepresentation addMember(OrganizationResource organization, String email, String firstName, String lastName) {
protected MemberRepresentation addMember(OrganizationResource organization, String email, String firstName, String lastName) {
UserRepresentation expected = new UserRepresentation();
expected.setEmail(email);
@ -172,7 +179,7 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
try (Response response = organization.members().addMember(userId)) {
assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
UserRepresentation actual = organization.members().member(userId).toRepresentation();
MemberRepresentation actual = organization.members().member(userId).toRepresentation();
assertNotNull(expected);
assertEquals(userId, actual.getId());
@ -210,6 +217,12 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
appPage.assertCurrent();
assertThat(appPage.getRequestType(), is(AppPage.RequestType.AUTH_RESPONSE));
}
List<UserRepresentation> users = realmsResouce().realm(bc.consumerRealmName()).users().search(username, Boolean.TRUE);
if (!users.isEmpty()) {
assertThat(users, Matchers.hasSize(1));
getCleanup(bc.consumerRealmName()).addUserId(users.get(0).getId());
}
}
protected void assertIsMember(String userEmail, OrganizationResource organization) {
@ -276,11 +289,11 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
// user automatically redirected to the organization identity provider
if (autoIDPRedirect) {
Assert.assertTrue("Driver should be on the provider realm page right now",
driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/"));
assertThat("Driver should be on the provider realm page right now",
driver.getCurrentUrl(), Matchers.containsString("/auth/realms/" + bc.providerRealmName() + "/"));
} else {
Assert.assertTrue("Driver should be on the consumer realm page right now",
driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
assertThat("Driver should be on the consumer realm page right now",
driver.getCurrentUrl(), Matchers.containsString("/auth/realms/" + bc.consumerRealmName() + "/"));
}
}
}

View file

@ -34,6 +34,7 @@ import jakarta.mail.internet.MimeMessage;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import org.hamcrest.Matchers;
import static org.hamcrest.Matchers.equalTo;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Rule;
import org.junit.Test;
@ -42,6 +43,8 @@ import org.keycloak.common.Profile.Feature;
import org.keycloak.common.util.UriUtils;
import org.keycloak.cookie.CookieType;
import org.keycloak.representations.idm.ErrorRepresentation;
import org.keycloak.representations.idm.MemberRepresentation;
import org.keycloak.representations.idm.MembershipType;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.Assert;
@ -113,7 +116,9 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
List<UserRepresentation> users = testRealm().users().searchByEmail(email, true);
assertThat(users, Matchers.not(empty()));
// user is a member
Assert.assertNotNull(organization.members().member(users.get(0).getId()).toRepresentation());
MemberRepresentation member = organization.members().member(users.get(0).getId()).toRepresentation();
Assert.assertNotNull(member);
assertThat(member.getMembershipType(), equalTo(MembershipType.MANAGED));
getCleanup().addCleanup(() -> testRealm().users().get(users.get(0).getId()).remove());
// authenticated to the account console

View file

@ -406,7 +406,7 @@ public class OrganizationTest extends AbstractOrganizationTest {
OrganizationRepresentation existing = createOrganization("acme", "acme.org", "acme.net");
// disable the organization provider and try to access REST endpoints
try (RealmAttributeUpdater rau = new RealmAttributeUpdater(testRealm())
.setOrganizationEnabled(Boolean.FALSE)
.setOrganizationsEnabled(Boolean.FALSE)
.update()) {
OrganizationRepresentation org = createRepresentation("some", "some.com");

View file

@ -27,6 +27,8 @@ import java.util.List;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.keycloak.admin.client.resource.OrganizationIdentityProviderResource;
@ -45,6 +47,7 @@ import org.keycloak.representations.idm.OrganizationDomainRepresentation;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.organization.admin.AbstractOrganizationTest;
import org.keycloak.testsuite.util.UserBuilder;
@ -212,14 +215,7 @@ public abstract class AbstractBrokerSelfRegistrationTest extends AbstractOrganiz
@Test
public void testLinkExistingAccount() {
// create a realm user in the consumer realm
realmsResouce().realm(bc.consumerRealmName()).users()
.create(UserBuilder.create()
.username(bc.getUserLogin())
.email(bc.getUserEmail())
.password(bc.getUserPassword())
.enabled(true).build()
).close();
createUserInConsumerRealm();
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
OrganizationIdentityProviderResource broker = organization.identityProviders().get(bc.getIDPAlias());
@ -242,14 +238,7 @@ public abstract class AbstractBrokerSelfRegistrationTest extends AbstractOrganiz
@Test
public void testExistingUserUsingOrgDomain() {
// create a realm user in the consumer realm
realmsResouce().realm(bc.consumerRealmName()).users()
.create(UserBuilder.create()
.username(bc.getUserLogin())
.email(bc.getUserEmail())
.password(bc.getUserPassword())
.enabled(true).build()
).close();
createUserInConsumerRealm();
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
OrganizationIdentityProviderResource broker = organization.identityProviders().get(bc.getIDPAlias());
@ -425,6 +414,7 @@ public abstract class AbstractBrokerSelfRegistrationTest extends AbstractOrganiz
List<FederatedIdentityRepresentation> federatedIdentities = testRealm().users().get(user.getId()).getFederatedIdentity();
assertEquals(1, federatedIdentities.size());
assertEquals(bc.getIDPAlias(), federatedIdentities.get(0).getIdentityProvider());
}
@Test
@ -689,4 +679,18 @@ public abstract class AbstractBrokerSelfRegistrationTest extends AbstractOrganiz
} catch (NotFoundException ignore) {
}
}
private void createUserInConsumerRealm() {
// create a realm user in the consumer realm
try (Response response = realmsResouce().realm(bc.consumerRealmName()).users()
.create(UserBuilder.create()
.username(bc.getUserLogin())
.email(bc.getUserEmail())
.password(bc.getUserPassword())
.enabled(true).build())) {
assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
String id = ApiUtil.getCreatedId(response);
getCleanup(bc.consumerRealmName()).addUserId(id);
}
}
}

View file

@ -86,7 +86,7 @@ public class OrganizationMemberAuthenticationTest extends AbstractOrganizationTe
// disable the organization provider
try (RealmAttributeUpdater rau = new RealmAttributeUpdater(testRealm())
.setOrganizationEnabled(Boolean.FALSE)
.setOrganizationsEnabled(Boolean.FALSE)
.update()) {
// access the page again, now it should be present username and password fields

View file

@ -53,6 +53,7 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.representations.idm.ErrorRepresentation;
import org.keycloak.representations.idm.MemberRepresentation;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.userprofile.config.UPConfig;
@ -68,7 +69,7 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
@Test
public void testUpdate() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
UserRepresentation expected = addMember(organization);
UserRepresentation expected = getUserRepFromMemberRep(addMember(organization));
expected.setFirstName("f");
expected.setLastName("l");
@ -90,7 +91,7 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
upConfig.setUnmanagedAttributePolicy(UnmanagedAttributePolicy.ENABLED);
testRealm().users().userProfile().update(upConfig);
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
UserRepresentation expected = addMember(organization);
UserRepresentation expected = getUserRepFromMemberRep(addMember(organization));
expected.singleAttribute(ORGANIZATION_ATTRIBUTE, "invalid");
@ -161,7 +162,7 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
expected.add(addMember(organization, "member-" + i + "@neworg.org"));
}
List<UserRepresentation> existing = organization.members().getAll();
List<MemberRepresentation> existing = organization.members().getAll();
assertFalse(existing.isEmpty());
assertEquals(expected.size(), existing.size());
for (UserRepresentation expectedRep : expected) {
@ -200,7 +201,7 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
assertThat(existingOrg.isEnabled(), is(false));
// now fetch all users from the org - unmanaged users should still be enabled, but managed ones should not.
List<UserRepresentation> existing = organization.members().getAll();
List<MemberRepresentation> existing = organization.members().getAll();
assertThat(existing, not(empty()));
assertThat(existing, hasSize(6));
for (UserRepresentation user : existing) {
@ -213,10 +214,10 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
// fetching users from the users endpoint should have the same result.
UserRepresentation disabledUser = null;
existing = testRealm().users().search("*neworg*",0, 10);
assertThat(existing, not(empty()));
assertThat(existing, hasSize(6));
for (UserRepresentation user : existing) {
List<UserRepresentation> existingUsers = testRealm().users().search("*neworg*",0, 10);
assertThat(existingUsers, not(empty()));
assertThat(existingUsers, hasSize(6));
for (UserRepresentation user : existingUsers) {
if (user.getEmail().equals(bc.getUserEmail())) {
assertThat(user.isEnabled(), is(false));
disabledUser = user;
@ -254,7 +255,7 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
// disable the organization provider
try (RealmAttributeUpdater rau = new RealmAttributeUpdater(testRealm())
.setOrganizationEnabled(Boolean.FALSE)
.setOrganizationsEnabled(Boolean.FALSE)
.update()) {
// now fetch all members from the realm - unmanaged users should still be enabled, but managed ones should not.
@ -305,7 +306,7 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
@Test
public void testUpdateEmailUnmanagedMember() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
UserRepresentation expected = addMember(organization);
UserRepresentation expected = getUserRepFromMemberRep(addMember(organization));
expected.setEmail("some@unknown.org");
UserResource userResource = testRealm().users().get(expected.getId());
userResource.update(expected);
@ -315,10 +316,14 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
}
private UserRepresentation getUserRepFromMemberRep(MemberRepresentation member) {
return new UserRepresentation(member);
}
@Test
public void testDeleteMembersOnOrganizationRemoval() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
List<UserRepresentation> expected = new ArrayList<>();
List<MemberRepresentation> expected = new ArrayList<>();
for (int i = 0; i < 5; i++) {
expected.add(addMember(organization, "member-" + i + "@neworg.org"));
@ -326,19 +331,19 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
organization.delete().close();
for (UserRepresentation member : expected) {
for (MemberRepresentation member : expected) {
try {
organization.members().member(member.getId()).toRepresentation();
fail("should be deleted");
} catch (NotFoundException ignore) {}
}
for (UserRepresentation member : expected) {
for (MemberRepresentation member : expected) {
// users should exist as they are not managed by the organization
testRealm().users().get(member.getId()).toRepresentation();
}
for (UserRepresentation member : expected) {
for (MemberRepresentation member : expected) {
try {
// user no longer bound to the organization
organization.members().getOrganization(member.getId());
@ -361,7 +366,7 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
expected.add(addMember(organization, "thejoker@neworg.org", "Jack", "White"));
// exact search - username/e-mail, first name, last name.
List<UserRepresentation> existing = organization.members().search("brucewayne@neworg.org", true, null, null);
List<MemberRepresentation> existing = organization.members().search("brucewayne@neworg.org", true, null, null);
assertThat(existing, hasSize(1));
assertThat(existing.get(0).getUsername(), is(equalTo("brucewayne@neworg.org")));
assertThat(existing.get(0).getEmail(), is(equalTo("brucewayne@neworg.org")));