Allow members joining multiple organizations

Closes #30747

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2024-07-16 16:48:20 -03:00 committed by Michal Hajas
parent 12732333c8
commit 1f8280c71a
34 changed files with 367 additions and 324 deletions

View file

@ -17,12 +17,16 @@
package org.keycloak.admin.client.resource; package org.keycloak.admin.client.resource;
import java.util.List;
import javax.ws.rs.DELETE; import javax.ws.rs.DELETE;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import org.keycloak.representations.idm.MemberRepresentation; import org.keycloak.representations.idm.MemberRepresentation;
import org.keycloak.representations.idm.OrganizationRepresentation;
public interface OrganizationMemberResource { public interface OrganizationMemberResource {
@ -32,4 +36,9 @@ public interface OrganizationMemberResource {
@DELETE @DELETE
Response delete(); Response delete();
@Path("organizations")
@GET
@Produces(MediaType.APPLICATION_JSON)
List<OrganizationRepresentation> getOrganizations();
} }

View file

@ -67,11 +67,6 @@ public interface OrganizationMembersResource {
@QueryParam("max") Integer max @QueryParam("max") Integer max
); );
@Path("{id}/organization")
@GET
@Produces(MediaType.APPLICATION_JSON)
OrganizationRepresentation getOrganization(@PathParam("id") String id);
@Path("{id}") @Path("{id}")
OrganizationMemberResource member(@PathParam("id") String id); OrganizationMemberResource member(@PathParam("id") String id);

View file

@ -340,12 +340,7 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
int notBefore = getDelegate().getNotBeforeOfUser(realm, delegate); int notBefore = getDelegate().getNotBeforeOfUser(realm, delegate);
if (Profile.isFeatureEnabled(Profile.Feature.ORGANIZATION)) { if (Profile.isFeatureEnabled(Profile.Feature.ORGANIZATION)) {
// check if provider is enabled and user is managed member of a disabled organization OR provider is disabled and user is managed member if (isOrganizationDisabled(session, delegate)) {
OrganizationProvider organizationProvider = session.getProvider(OrganizationProvider.class);
OrganizationModel organization = organizationProvider.getByMember(delegate);
if ((organizationProvider.isEnabled() && organization != null && organization.isManaged(delegate) && !organization.isEnabled()) ||
(!organizationProvider.isEnabled() && organization != null && organization.isManaged(delegate))) {
return new ReadOnlyUserModelDelegate(delegate) { return new ReadOnlyUserModelDelegate(delegate) {
@Override @Override
public boolean isEnabled() { public boolean isEnabled() {
@ -981,4 +976,13 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
} }
return List.of(); return List.of();
} }
private boolean isOrganizationDisabled(KeycloakSession session, UserModel delegate) {
// check if provider is enabled and user is managed member of a disabled organization OR provider is disabled and user is managed member
OrganizationProvider organizationProvider = session.getProvider(OrganizationProvider.class);
return organizationProvider.getByMember(delegate)
.anyMatch((org) -> (organizationProvider.isEnabled() && org.isManaged(delegate) && !org.isEnabled()) ||
(!organizationProvider.isEnabled() && org.isManaged(delegate)));
}
} }

View file

@ -161,7 +161,7 @@ public class InfinispanOrganizationProvider implements OrganizationProvider {
} }
@Override @Override
public OrganizationModel getByMember(UserModel member) { public Stream<OrganizationModel> getByMember(UserModel member) {
return orgDelegate.getByMember(member); return orgDelegate.getByMember(member);
} }

View file

@ -157,6 +157,11 @@ public class OrganizationAdapter implements OrganizationModel {
return delegate.isManagedMember(this, user); return delegate.isManagedMember(this, user);
} }
@Override
public boolean isMember(UserModel user) {
return delegate.isMember(this, user);
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View file

@ -44,7 +44,8 @@ import jakarta.persistence.Table;
@NamedQuery(name="getByNameOrDomainContained", query="select distinct o from OrganizationEntity o inner join OrganizationDomainEntity d ON o.id = d.organization.id" + @NamedQuery(name="getByNameOrDomainContained", query="select distinct o from OrganizationEntity o inner join OrganizationDomainEntity d ON o.id = d.organization.id" +
" where o.realmId = :realmId AND (lower(o.name) like concat('%',:search,'%') OR d.name like concat('%',:search,'%')) order by o.name ASC"), " where o.realmId = :realmId AND (lower(o.name) like concat('%',:search,'%') OR d.name like concat('%',:search,'%')) order by o.name ASC"),
@NamedQuery(name="getCount", query="select count(o) from OrganizationEntity o where o.realmId = :realmId"), @NamedQuery(name="getCount", query="select count(o) from OrganizationEntity o where o.realmId = :realmId"),
@NamedQuery(name="deleteOrganizationsByRealm", query="delete from OrganizationEntity o where o.realmId = :realmId") @NamedQuery(name="deleteOrganizationsByRealm", query="delete from OrganizationEntity o where o.realmId = :realmId"),
@NamedQuery(name="getGroupsByMember", query="select m.groupId from UserGroupMembershipEntity m join GroupEntity g on g.id = m.groupId where g.type = 1 and m.user.id = :userId")
}) })
public class OrganizationEntity { public class OrganizationEntity {

View file

@ -18,7 +18,6 @@
package org.keycloak.organization.jpa; package org.keycloak.organization.jpa;
import static org.keycloak.models.OrganizationModel.BROKER_PUBLIC; import static org.keycloak.models.OrganizationModel.BROKER_PUBLIC;
import static org.keycloak.models.OrganizationModel.ORGANIZATION_ATTRIBUTE;
import static org.keycloak.models.OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE; import static org.keycloak.models.OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE;
import static org.keycloak.models.jpa.PaginationUtils.paginateQuery; import static org.keycloak.models.jpa.PaginationUtils.paginateQuery;
import static org.keycloak.utils.StreamsUtil.closing; import static org.keycloak.utils.StreamsUtil.closing;
@ -26,6 +25,7 @@ import static org.keycloak.utils.StreamsUtil.closing;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream; import java.util.stream.Stream;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
@ -36,7 +36,6 @@ import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Join; import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root; import jakarta.persistence.criteria.Root;
import java.util.stream.Collectors;
import org.keycloak.connections.jpa.JpaConnectionProvider; import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.models.GroupModel; import org.keycloak.models.GroupModel;
import org.keycloak.models.GroupModel.Type; import org.keycloak.models.GroupModel.Type;
@ -49,7 +48,6 @@ import org.keycloak.models.ModelException;
import org.keycloak.models.ModelValidationException; import org.keycloak.models.ModelValidationException;
import org.keycloak.models.OrganizationModel; import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserManager;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider; import org.keycloak.models.UserProvider;
import org.keycloak.models.jpa.entities.GroupAttributeEntity; import org.keycloak.models.jpa.entities.GroupAttributeEntity;
@ -59,6 +57,7 @@ import org.keycloak.models.jpa.entities.UserEntity;
import org.keycloak.models.jpa.entities.UserGroupMembershipEntity; import org.keycloak.models.jpa.entities.UserGroupMembershipEntity;
import org.keycloak.organization.OrganizationProvider; import org.keycloak.organization.OrganizationProvider;
import org.keycloak.representations.idm.MembershipType; import org.keycloak.representations.idm.MembershipType;
import org.keycloak.organization.utils.Organizations;
import org.keycloak.utils.StringUtil; import org.keycloak.utils.StringUtil;
public class JpaOrganizationProvider implements OrganizationProvider { public class JpaOrganizationProvider implements OrganizationProvider {
@ -163,7 +162,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
throwExceptionIfObjectIsNull(user, "User"); throwExceptionIfObjectIsNull(user, "User");
OrganizationEntity entity = getEntity(organization.getId()); OrganizationEntity entity = getEntity(organization.getId());
OrganizationModel current = (OrganizationModel) session.getAttribute(OrganizationModel.class.getName()); OrganizationModel current = Organizations.resolveOrganization(session);
// check the user and the organization belongs to the same realm // check the user and the organization belongs to the same realm
if (session.users().getUserById(session.realms().getRealm(entity.getRealmId()), user.getId()) == null) { if (session.users().getUserById(session.realms().getRealm(entity.getRealmId()), user.getId()) == null) {
@ -181,12 +180,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
return false; return false;
} }
if (user.getFirstAttribute(ORGANIZATION_ATTRIBUTE) != null) {
throw new ModelException("User [" + user.getId() + "] is a member of a different organization");
}
user.joinGroup(group, metadata); user.joinGroup(group, metadata);
user.setSingleAttribute(ORGANIZATION_ATTRIBUTE, entity.getId());
} finally { } finally {
if (current == null) { if (current == null) {
session.removeAttribute(OrganizationModel.class.getName()); session.removeAttribute(OrganizationModel.class.getName());
@ -285,9 +279,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
return null; return null;
} }
String orgId = user.getFirstAttribute(ORGANIZATION_ATTRIBUTE); if (getByMember(user).anyMatch(organization::equals)) {
if (organization.getId().equals(orgId)) {
return user; return user;
} }
@ -295,17 +287,19 @@ public class JpaOrganizationProvider implements OrganizationProvider {
} }
@Override @Override
public OrganizationModel getByMember(UserModel member) { public Stream<OrganizationModel> getByMember(UserModel member) {
throwExceptionIfObjectIsNull(member, "User"); throwExceptionIfObjectIsNull(member, "User");
TypedQuery<String> query = em.createNamedQuery("getGroupsByMember", String.class);
String orgId = member.getFirstAttribute(ORGANIZATION_ATTRIBUTE); query.setParameter("userId", member.getId());
if (orgId == null) { OrganizationProvider organizations = session.getProvider(OrganizationProvider.class);
return null; GroupProvider groups = session.groups();
}
// need to go via the session to avoid bypassing the cache return closing(query.getResultStream())
return session.getProvider(OrganizationProvider.class).getById(orgId); .map((id) -> groups.getGroupById(getRealm(), id))
.map((g) -> organizations.getById(g.getName()))
.filter(Objects::nonNull);
} }
@Override @Override
@ -395,25 +389,22 @@ public class JpaOrganizationProvider implements OrganizationProvider {
throwExceptionIfObjectIsNull(organization, "organization"); throwExceptionIfObjectIsNull(organization, "organization");
throwExceptionIfObjectIsNull(member, "member"); throwExceptionIfObjectIsNull(member, "member");
OrganizationModel userOrg = getByMember(member); OrganizationModel userOrg = getByMember(member).filter(organization::equals).findAny().orElse(null);
if (userOrg == null || !userOrg.equals(organization)) { if (userOrg == null || !userOrg.equals(organization)) {
return false; return false;
} }
if (isManagedMember(organization, member)) { if (isManagedMember(organization, member)) {
return new UserManager(session).removeUser(getRealm(), member, userProvider); userProvider.removeUser(getRealm(), member);
} else { } else {
OrganizationModel current = (OrganizationModel) session.getAttribute(OrganizationModel.class.getName()); OrganizationModel current = Organizations.resolveOrganization(session);
if (current == null) { if (current == null) {
session.setAttribute(OrganizationModel.class.getName(), organization); session.setAttribute(OrganizationModel.class.getName(), organization);
} }
try { try {
List<String> organizations = member.getAttributes().get(ORGANIZATION_ATTRIBUTE);
organizations.remove(organization.getId());
member.setAttribute(ORGANIZATION_ATTRIBUTE, organizations);
member.leaveGroup(getOrganizationGroup(organization)); member.leaveGroup(getOrganizationGroup(organization));
} finally { } finally {
if (current == null) { if (current == null) {

View file

@ -205,6 +205,11 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
return provider.isManagedMember(this, user); return provider.isManagedMember(this, user);
} }
@Override
public boolean isMember(UserModel user) {
return provider.isMember(this, user);
}
@Override @Override
public OrganizationEntity getEntity() { public OrganizationEntity getEntity() {
return entity; return entity;

View file

@ -469,10 +469,6 @@ public class ExportUtils {
userRep.setGroups(groups); userRep.setGroups(groups);
} }
if (userRep.getAttributes() != null) {
userRep.getAttributes().remove(OrganizationModel.ORGANIZATION_ATTRIBUTE);
}
return userRep; return userRep;
} }

View file

@ -117,10 +117,7 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
if (Profile.isFeatureEnabled(Profile.Feature.ORGANIZATION) && user != null) { if (Profile.isFeatureEnabled(Profile.Feature.ORGANIZATION) && user != null) {
// check if provider is enabled and user is managed member of a disabled organization OR provider is disabled and user is managed member // check if provider is enabled and user is managed member of a disabled organization OR provider is disabled and user is managed member
OrganizationProvider organizationProvider = session.getProvider(OrganizationProvider.class); OrganizationProvider organizationProvider = session.getProvider(OrganizationProvider.class);
OrganizationModel organization = organizationProvider.getByMember(user); if (isOrganizationDisabled(session, user)) {
if ((organizationProvider.isEnabled() && organization != null && organization.isManaged(user) && !organization.isEnabled()) ||
(!organizationProvider.isEnabled() && organization != null && organization.isManaged(user))) {
return new ReadOnlyUserModelDelegate(user) { return new ReadOnlyUserModelDelegate(user) {
@Override @Override
public boolean isEnabled() { public boolean isEnabled() {
@ -926,4 +923,13 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
return Collections.emptyList(); return Collections.emptyList();
} }
private boolean isOrganizationDisabled(KeycloakSession session, UserModel delegate) {
// check if provider is enabled and user is managed member of a disabled organization OR provider is disabled and user is managed member
OrganizationProvider organizationProvider = session.getProvider(OrganizationProvider.class);
return organizationProvider.getByMember(delegate)
.anyMatch((org) -> (organizationProvider.isEnabled() && org.isManaged(delegate) && !org.isEnabled()) ||
(!organizationProvider.isEnabled() && org.isManaged(delegate)));
}
} }

View file

@ -248,7 +248,6 @@ public class ModelToRepresentation {
} }
if (attributes != null && !copy.isEmpty()) { if (attributes != null && !copy.isEmpty()) {
Map<String, List<String>> attrs = new HashMap<>(copy); Map<String, List<String>> attrs = new HashMap<>(copy);
attrs.remove(OrganizationModel.ORGANIZATION_ATTRIBUTE);
rep.setAttributes(attrs); rep.setAttributes(attrs);
} }

View file

@ -77,4 +77,6 @@ public interface OrganizationModel {
Stream<IdentityProviderModel> getIdentityProviders(); Stream<IdentityProviderModel> getIdentityProviders();
boolean isManaged(UserModel user); boolean isManaged(UserModel user);
boolean isMember(UserModel user);
} }

View file

@ -142,10 +142,10 @@ public interface OrganizationProvider extends Provider {
/** /**
* Returns the {@link OrganizationModel} that the {@code member} belongs to. * Returns the {@link OrganizationModel} that the {@code member} belongs to.
* *
* @param member the member of a organization * @param member the member of an organization
* @return the organization the {@code member} belongs to or {@code null} if the user doesn't belong to any. * @return the organizations the {@code member} belongs to or an empty stream if the user doesn't belong to any.
*/ */
OrganizationModel getByMember(UserModel member); Stream<OrganizationModel> getByMember(UserModel member);
/** /**
* Associate the given {@link IdentityProviderModel} with the given {@link OrganizationModel}. * Associate the given {@link IdentityProviderModel} with the given {@link OrganizationModel}.
@ -196,6 +196,17 @@ public interface OrganizationProvider extends Provider {
*/ */
boolean isManagedMember(OrganizationModel organization, UserModel member); boolean isManagedMember(OrganizationModel organization, UserModel member);
/**
* Indicates if the given {@code user} is a member of the given {@code organization}.
*
* @param organization the organization
* @param user the member
* @return {@code true} if the user is a member. Otherwise, {@code false}
*/
default boolean isMember(OrganizationModel organization, UserModel user) {
return getMemberById(organization, user.getId()) != null;
}
/** /**
* <p>Removes a member from the organization. * <p>Removes a member from the organization.
* *

View file

@ -84,14 +84,6 @@ public class InviteOrgActionTokenHandler extends AbstractActionTokenHandler<Invi
OrganizationModel organization = orgProvider.getById(token.getOrgId()); OrganizationModel organization = orgProvider.getById(token.getOrgId());
if (orgProvider.getByMember(user) != null) {
event.user(user).error(Errors.USER_ORG_MEMBER_ALREADY);
return session.getProvider(LoginFormsProvider.class)
.setAuthenticationSession(authSession)
.setInfo(Messages.ORG_MEMBER_ALREADY, user.getUsername())
.createInfoPage();
}
if (organization == null) { if (organization == null) {
event.user(user).error(Errors.ORG_NOT_FOUND); event.user(user).error(Errors.ORG_NOT_FOUND);
return session.getProvider(LoginFormsProvider.class) return session.getProvider(LoginFormsProvider.class)
@ -100,6 +92,14 @@ public class InviteOrgActionTokenHandler extends AbstractActionTokenHandler<Invi
.createInfoPage(); .createInfoPage();
} }
if (organization.isMember(user)) {
event.user(user).error(Errors.USER_ORG_MEMBER_ALREADY);
return session.getProvider(LoginFormsProvider.class)
.setAuthenticationSession(authSession)
.setInfo(Messages.ORG_MEMBER_ALREADY, user.getUsername())
.createInfoPage();
}
final UriInfo uriInfo = tokenContext.getUriInfo(); final UriInfo uriInfo = tokenContext.getUriInfo();
final RealmModel realm = tokenContext.getRealm(); final RealmModel realm = tokenContext.getRealm();

View file

@ -549,7 +549,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
OrganizationModel organization = resolveOrganization(session, user); OrganizationModel organization = resolveOrganization(session, user);
if (organization != null) { if (organization != null) {
attributes.put("org", new OrganizationBean(session, organization, user)); attributes.put("org", new OrganizationBean(organization, user));
} }
} }
} }

View file

@ -23,11 +23,9 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OrganizationDomainModel; import org.keycloak.models.OrganizationDomainModel;
import org.keycloak.models.OrganizationModel; import org.keycloak.models.OrganizationModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.organization.OrganizationProvider;
public class OrganizationBean { public class OrganizationBean {
@ -37,18 +35,14 @@ public class OrganizationBean {
private final boolean isMember; private final boolean isMember;
private final Map<String, List<String>> attributes; private final Map<String, List<String>> attributes;
public OrganizationBean(KeycloakSession session, OrganizationModel organization, UserModel user) { public OrganizationBean(OrganizationModel organization, UserModel user) {
this.name = organization.getName(); this.name = organization.getName();
this.alias = organization.getAlias(); this.alias = organization.getAlias();
this.domains = organization.getDomains().map(OrganizationDomainModel::getName).collect(Collectors.toSet()); this.domains = organization.getDomains().map(OrganizationDomainModel::getName).collect(Collectors.toSet());
this.isMember = user != null && organization.equals(getOrganizationProvider(session).getByMember(user)); this.isMember = user != null && organization.isMember(user);
this.attributes = Collections.unmodifiableMap(organization.getAttributes()); this.attributes = Collections.unmodifiableMap(organization.getAttributes());
} }
private static OrganizationProvider getOrganizationProvider(KeycloakSession session) {
return session.getProvider(OrganizationProvider.class);
}
public String getName() { public String getName() {
return name; return name;
} }

View file

@ -33,7 +33,6 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OrganizationModel; import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.utils.OIDCResponseType; import org.keycloak.protocol.oidc.utils.OIDCResponseType;
@ -52,7 +51,6 @@ public class OrganizationInvitationResource {
private final KeycloakSession session; private final KeycloakSession session;
private final RealmModel realm; private final RealmModel realm;
private final OrganizationProvider provider;
private final OrganizationModel organization; private final OrganizationModel organization;
private final AdminEventBuilder adminEvent; private final AdminEventBuilder adminEvent;
private final int tokenExpiration; private final int tokenExpiration;
@ -60,7 +58,6 @@ public class OrganizationInvitationResource {
public OrganizationInvitationResource(KeycloakSession session, OrganizationModel organization, AdminEventBuilder adminEvent) { public OrganizationInvitationResource(KeycloakSession session, OrganizationModel organization, AdminEventBuilder adminEvent) {
this.session = session; this.session = session;
this.realm = session.getContext().getRealm(); this.realm = session.getContext().getRealm();
this.provider = session.getProvider(OrganizationProvider.class);
this.organization = organization; this.organization = organization;
this.adminEvent = adminEvent; this.adminEvent = adminEvent;
this.tokenExpiration = getTokenExpiration(); this.tokenExpiration = getTokenExpiration();
@ -74,9 +71,7 @@ public class OrganizationInvitationResource {
UserModel user = session.users().getUserByEmail(realm, email); UserModel user = session.users().getUserByEmail(realm, email);
if (user != null) { if (user != null) {
OrganizationModel org = provider.getByMember(user); if (organization.isMember(user)) {
if (org != null && org.equals(organization)) {
throw ErrorResponse.error("User already a member of the organization", Status.CONFLICT); throw ErrorResponse.error("User already a member of the organization", Status.CONFLICT);
} }

View file

@ -17,6 +17,7 @@
package org.keycloak.organization.admin.resource; package org.keycloak.organization.admin.resource;
import java.util.List;
import java.util.stream.Stream; import java.util.stream.Stream;
import jakarta.ws.rs.Consumes; import jakarta.ws.rs.Consumes;
@ -178,29 +179,27 @@ public class OrganizationMemberResource {
throw ErrorResponse.error("Not a member of the organization", Status.BAD_REQUEST); throw ErrorResponse.error("Not a member of the organization", Status.BAD_REQUEST);
} }
@Path("{id}/organization") @Path("{id}/organizations")
@GET @GET
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@NoCache @NoCache
@Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS) @Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS)
@Operation(summary = "Returns the organization associated with the user that has the specified id") @Operation(summary = "Returns the organizations associated with the user that has the specified id")
public OrganizationRepresentation getOrganization(@PathParam("id") String id) { public Stream<OrganizationRepresentation> getOrganizations(@PathParam("id") String id) {
if (StringUtil.isBlank(id)) { if (StringUtil.isBlank(id)) {
throw ErrorResponse.error("id cannot be null", Status.BAD_REQUEST); throw ErrorResponse.error("id cannot be null", Status.BAD_REQUEST);
} }
UserModel member = getMember(id); UserModel member = getMember(id);
OrganizationModel organization = provider.getByMember(member);
if (organization == null) { return provider.getByMember(member).map((org) -> {
throw ErrorResponse.error("Not associated with an organization", Status.NOT_FOUND); OrganizationRepresentation organization = new OrganizationRepresentation();
}
OrganizationRepresentation rep = new OrganizationRepresentation(); organization.setId(org.getId());
organization.setName(org.getName());
rep.setId(organization.getId()); return organization;
});
return rep;
} }
private UserModel getMember(String id) { private UserModel getMember(String id) {

View file

@ -30,6 +30,7 @@ import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.organization.OrganizationProvider; import org.keycloak.organization.OrganizationProvider;
import org.keycloak.organization.utils.Organizations;
import static org.keycloak.organization.utils.Organizations.isEnabledAndOrganizationsPresent; import static org.keycloak.organization.utils.Organizations.isEnabledAndOrganizationsPresent;
@ -41,9 +42,10 @@ public class IdpAddOrganizationMemberAuthenticator extends AbstractIdpAuthentica
@Override @Override
protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) { protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
OrganizationProvider provider = context.getSession().getProvider(OrganizationProvider.class); KeycloakSession session = context.getSession();
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
UserModel user = context.getUser(); UserModel user = context.getUser();
OrganizationModel organization = (OrganizationModel) context.getSession().getAttribute(OrganizationModel.class.getName()); OrganizationModel organization = Organizations.resolveOrganization(session);
if (organization == null) { if (organization == null) {
context.attempted(); context.attempted();
@ -75,7 +77,7 @@ public class IdpAddOrganizationMemberAuthenticator extends AbstractIdpAuthentica
return false; return false;
} }
OrganizationModel organization = (OrganizationModel) session.getAttribute(OrganizationModel.class.getName()); OrganizationModel organization = Organizations.resolveOrganization(session);
if (organization == null || !organization.isEnabled()) { if (organization == null || !organization.isEnabled()) {
return false; return false;

View file

@ -19,9 +19,11 @@ package org.keycloak.organization.authentication.authenticators.browser;
import static org.keycloak.organization.utils.Organizations.getEmailDomain; import static org.keycloak.organization.utils.Organizations.getEmailDomain;
import static org.keycloak.organization.utils.Organizations.isEnabledAndOrganizationsPresent; import static org.keycloak.organization.utils.Organizations.isEnabledAndOrganizationsPresent;
import static org.keycloak.organization.utils.Organizations.resolveBroker; import static org.keycloak.organization.utils.Organizations.resolveHomeBroker;
import static org.keycloak.organization.utils.Organizations.resolveOrganization;
import java.util.List; import java.util.List;
import java.util.Optional;
import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.MultivaluedMap;
import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowContext;
@ -30,7 +32,6 @@ import org.keycloak.authentication.authenticators.browser.IdentityProviderAuthen
import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.forms.login.freemarker.model.AuthenticationContextBean; import org.keycloak.forms.login.freemarker.model.AuthenticationContextBean;
import org.keycloak.forms.login.freemarker.model.IdentityProviderBean; import org.keycloak.forms.login.freemarker.model.IdentityProviderBean;
import org.keycloak.forms.login.freemarker.model.OrganizationBean;
import org.keycloak.http.HttpRequest; import org.keycloak.http.HttpRequest;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -38,13 +39,12 @@ import org.keycloak.models.OrganizationModel;
import org.keycloak.models.OrganizationModel.IdentityProviderRedirectMode; import org.keycloak.models.OrganizationModel.IdentityProviderRedirectMode;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.organization.OrganizationProvider; import org.keycloak.organization.OrganizationProvider;
import org.keycloak.organization.forms.login.freemarker.model.OrganizationAwareAuthenticationContextBean; import org.keycloak.organization.forms.login.freemarker.model.OrganizationAwareAuthenticationContextBean;
import org.keycloak.organization.forms.login.freemarker.model.OrganizationAwareIdentityProviderBean; import org.keycloak.organization.forms.login.freemarker.model.OrganizationAwareIdentityProviderBean;
import org.keycloak.organization.forms.login.freemarker.model.OrganizationAwareRealmBean; import org.keycloak.organization.forms.login.freemarker.model.OrganizationAwareRealmBean;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.validation.Validation;
public class OrganizationAuthenticator extends IdentityProviderAuthenticator { public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
@ -63,7 +63,7 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
return; return;
} }
challenge(context); initialChallenge(context);
} }
@Override @Override
@ -71,119 +71,120 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
HttpRequest request = context.getHttpRequest(); HttpRequest request = context.getHttpRequest();
MultivaluedMap<String, String> parameters = request.getDecodedFormParameters(); MultivaluedMap<String, String> parameters = request.getDecodedFormParameters();
String username = parameters.getFirst(UserModel.USERNAME); String username = parameters.getFirst(UserModel.USERNAME);
String emailDomain = getEmailDomain(username);
RealmModel realm = context.getRealm(); RealmModel realm = context.getRealm();
OrganizationProvider provider = getOrganizationProvider(); UserModel user = resolveUser(context, username);
OrganizationModel organization = null; String domain = getEmailDomain(username);
UserModel user = null; OrganizationModel organization = resolveOrganization(session, user, domain);
if (emailDomain == null) {
// username was provided, check if the user is already federated in the realm and onboarded in an organization
user = session.users().getUserByUsername(realm, username);
if (user != null) {
organization = getOrganizationProvider().getByMember(user);
}
if (organization == null) { if (organization == null) {
// user in not member of an organization, go to the next authentication step/sub-flow // request does not map to any organization, go to the next step/sub-flow
context.attempted(); context.attempted();
return; return;
} }
} else {
organization = provider.getByDomainName(emailDomain);
}
if (organization != null) {
// make sure the organization is set to the session to make it available to templates // make sure the organization is set to the session to make it available to templates
session.setAttribute(OrganizationModel.class.getName(), organization); session.setAttribute(OrganizationModel.class.getName(), organization);
if (tryRedirectBroker(context, organization, user, username, domain)) {
return;
} }
if (user == null) { if (user == null) {
user = session.users().getUserByEmail(realm, username); unkownUserChallenge(context, organization, realm);
return;
} }
if (user != null) {
// user exists, check if enabled // user exists, check if enabled
if (!user.isEnabled()) { if (!user.isEnabled()) {
context.failure(AuthenticationFlowError.INVALID_USER); context.failure(AuthenticationFlowError.INVALID_USER);
return; return;
} }
context.setUser(user); context.attempted();
if (organization != null) {
OrganizationBean orgBean = new OrganizationBean(session, organization, user);
context.form().setAttributeMapper(attributes -> {
attributes.put("org", orgBean);
return attributes;
});
} }
List<IdentityProviderModel> broker = resolveBroker(session, user); @Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return realm.isOrganizationsEnabled();
}
private boolean tryRedirectBroker(AuthenticationFlowContext context, OrganizationModel organization, UserModel user, String username, String domain) {
List<IdentityProviderModel> broker = resolveHomeBroker(session, user);
if (broker.size() == 1) { if (broker.size() == 1) {
// user is a managed member and associated with a broker, redirect automatically // user is a managed member and associated with a broker, redirect automatically
redirect(context, broker.get(0).getAlias(), user.getEmail()); redirect(context, broker.get(0).getAlias(), user.getEmail());
return; return true;
} }
context.attempted(); return redirect(context, organization, username, domain);
return;
} }
if (organization == null || !organization.isEnabled()) { private boolean redirect(AuthenticationFlowContext context, OrganizationModel organization, String username, String domain) {
// request does not map to any organization, go to the next step/sub-flow if (domain == null) {
context.attempted(); return false;
return;
} }
List<IdentityProviderModel> brokers = organization.getIdentityProviders().toList(); List<IdentityProviderModel> brokers = organization.getIdentityProviders().toList();
if (redirect(context, brokers, username, emailDomain)) { for (IdentityProviderModel broker : brokers) {
return; if (IdentityProviderRedirectMode.EMAIL_MATCH.isSet(broker)) {
String idpDomain = broker.getConfig().get(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
if (domain.equals(idpDomain)) {
// redirect the user using the broker that matches the email domain
redirect(context, broker.getAlias(), username);
return true;
}
}
} }
if (!hasPublicBrokers(brokers)) { return false;
// the user does not exist, and there is no broker available for selection, redirect the user to the identity-first login page at the realm
challenge(username, context);
return;
} }
private UserModel resolveUser(AuthenticationFlowContext context, String username) {
UserProvider users = session.users();
RealmModel realm = session.getContext().getRealm();
UserModel user = Optional.ofNullable(users.getUserByEmail(realm, username)).orElseGet(() -> users.getUserByUsername(realm, username));
if (user != null) {
context.setUser(user);
}
return user;
}
private void unkownUserChallenge(AuthenticationFlowContext context, OrganizationModel organization, RealmModel realm) {
// the user does not exist and is authenticating in the scope of the organization, show the identity-first login page and the // the user does not exist and is authenticating in the scope of the organization, show the identity-first login page and the
// public organization brokers for selection // public organization brokers for selection
LoginFormsProvider form = context.form() LoginFormsProvider form = context.form()
.setAttributeMapper(attributes -> { .setAttributeMapper(attributes -> {
if (hasPublicBrokers(organization)) {
attributes.computeIfPresent("social", attributes.computeIfPresent("social",
(key, bean) -> new OrganizationAwareIdentityProviderBean((IdentityProviderBean) bean, session, true) (key, bean) -> new OrganizationAwareIdentityProviderBean((IdentityProviderBean) bean, session, true)
); );
attributes.computeIfPresent("auth", // do not show the self-registration link if there are public brokers available from the organization to force the user to register using a broker
(key, bean) -> new OrganizationAwareAuthenticationContextBean((AuthenticationContextBean) bean, false)
);
attributes.computeIfPresent("realm", attributes.computeIfPresent("realm",
(key, bean) -> new OrganizationAwareRealmBean(realm) (key, bean) -> new OrganizationAwareRealmBean(realm)
); );
} else {
attributes.computeIfPresent("social",
(key, bean) -> new OrganizationAwareIdentityProviderBean((IdentityProviderBean) bean, session, false, true)
);
}
attributes.computeIfPresent("auth",
(key, bean) -> new OrganizationAwareAuthenticationContextBean((AuthenticationContextBean) bean, false)
);
return attributes; return attributes;
}); });
form.addError(new FormMessage("Your email domain matches the " + organization.getName() + " organization but you don't have an account yet.")); form.addError(new FormMessage("Your email domain matches the " + organization.getName() + " organization but you don't have an account yet."));
context.challenge(form context.challenge(form.createLoginUsername());
.createLoginUsername());
} }
private static boolean hasPublicBrokers(List<IdentityProviderModel> brokers) { private void initialChallenge(AuthenticationFlowContext context){
return brokers.stream().anyMatch(p -> Boolean.parseBoolean(p.getConfig().getOrDefault(OrganizationModel.BROKER_PUBLIC, Boolean.FALSE.toString())));
}
private OrganizationProvider getOrganizationProvider() {
return session.getProvider(OrganizationProvider.class);
}
private void challenge(AuthenticationFlowContext context) {
challenge(null, context);
}
private void challenge(String username, AuthenticationFlowContext context){
// the default challenge won't show any broker but just the identity-first login page and the option to try a different authentication mechanism // the default challenge won't show any broker but just the identity-first login page and the option to try a different authentication mechanism
LoginFormsProvider form = context.form() LoginFormsProvider form = context.form()
.setAttributeMapper(attributes -> { .setAttributeMapper(attributes -> {
@ -196,31 +197,14 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
return attributes; return attributes;
}); });
if (username != null) {
form.addError(new FormMessage(Validation.FIELD_USERNAME, Messages.INVALID_USER));
}
context.challenge(form.createLoginUsername()); context.challenge(form.createLoginUsername());
} }
@Override private boolean hasPublicBrokers(OrganizationModel organization) {
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { return organization.getIdentityProviders().anyMatch(p -> Boolean.parseBoolean(p.getConfig().getOrDefault(OrganizationModel.BROKER_PUBLIC, Boolean.FALSE.toString())));
return realm.isOrganizationsEnabled();
} }
protected boolean redirect(AuthenticationFlowContext context, List<IdentityProviderModel> brokers, String username, String emailDomain) { private OrganizationProvider getOrganizationProvider() {
for (IdentityProviderModel broker : brokers) { return session.getProvider(OrganizationProvider.class);
if (IdentityProviderRedirectMode.EMAIL_MATCH.isSet(broker)) {
String idpDomain = broker.getConfig().get(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
if (emailDomain.equals(idpDomain)) {
// redirect the user using the broker that matches the email domain
redirect(context, broker.getAlias(), username);
return true;
}
}
}
return false;
} }
} }

View file

@ -26,6 +26,7 @@ import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OrganizationModel; import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.organization.utils.Organizations;
public class OrganizationAwareIdentityProviderBean extends IdentityProviderBean { public class OrganizationAwareIdentityProviderBean extends IdentityProviderBean {
@ -74,7 +75,7 @@ public class OrganizationAwareIdentityProviderBean extends IdentityProviderBean
return false; return false;
} }
OrganizationModel organization = (OrganizationModel) session.getAttribute(OrganizationModel.class.getName()); OrganizationModel organization = Organizations.resolveOrganization(session);
if (organization != null && !organization.getId().equals(model.getOrganizationId())) { if (organization != null && !organization.getId().equals(model.getOrganizationId())) {
return false; return false;

View file

@ -21,6 +21,8 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Stream;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.common.Profile; import org.keycloak.common.Profile;
@ -84,14 +86,15 @@ public class OrganizationMembershipMapper extends AbstractOIDCProtocolMapper imp
} }
UserModel user = userSession.getUser(); UserModel user = userSession.getUser();
OrganizationModel organization = provider.getByMember(user); Stream<OrganizationModel> organizations = provider.getByMember(user).filter(OrganizationModel::isEnabled);
Map<String, Map<String, Object>> claim = new HashMap<>();
if (organization == null || !organization.isEnabled()) { organizations.forEach(organization -> claim.put(organization.getAlias(), Map.of()));
if (claim.isEmpty()) {
return; return;
} }
Map<String, Map<String, Object>> claim = new HashMap<>();
claim.put(organization.getAlias(), Map.of());
token.getOtherClaims().put(OAuth2Constants.ORGANIZATION, claim); token.getOtherClaims().put(OAuth2Constants.ORGANIZATION, claim);
} }

View file

@ -18,6 +18,8 @@
package org.keycloak.organization.protocol.mappers.saml; package org.keycloak.organization.protocol.mappers.saml;
import java.util.List; import java.util.List;
import java.util.stream.Stream;
import org.keycloak.Config.Scope; import org.keycloak.Config.Scope;
import org.keycloak.common.Profile; import org.keycloak.common.Profile;
import org.keycloak.common.Profile.Feature; import org.keycloak.common.Profile.Feature;
@ -65,16 +67,20 @@ public class OrganizationMembershipMapper extends AbstractSAMLProtocolMapper imp
} }
UserModel user = userSession.getUser(); UserModel user = userSession.getUser();
OrganizationModel organization = provider.getByMember(user); Stream<OrganizationModel> organizations = provider.getByMember(user).filter(OrganizationModel::isEnabled);
AttributeType attribute = new AttributeType(ORGANIZATION_ATTRIBUTE_NAME);
if (organization == null || !organization.isEnabled()) { attribute.setFriendlyName(ORGANIZATION_ATTRIBUTE_NAME);
attribute.setNameFormat(JBossSAMLURIConstants.ATTRIBUTE_FORMAT_BASIC.get());
organizations.forEach(organization -> {
attribute.addAttributeValue(organization.getAlias());
});
if (attribute.getAttributeValue().isEmpty()) {
return; return;
} }
AttributeType attribute = new AttributeType(ORGANIZATION_ATTRIBUTE_NAME);
attribute.setFriendlyName(ORGANIZATION_ATTRIBUTE_NAME);
attribute.setNameFormat(JBossSAMLURIConstants.ATTRIBUTE_FORMAT_BASIC.get());
attribute.addAttributeValue(organization.getAlias());
attributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(attribute)); attributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(attribute));
} }

View file

@ -21,8 +21,11 @@ import static java.util.Optional.ofNullable;
import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -57,7 +60,7 @@ public class Organizations {
} }
if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) { if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
OrganizationModel organization = (OrganizationModel) session.getAttribute(OrganizationModel.class.getName()); OrganizationModel organization = resolveOrganization(session);
return organization != null && organization.getId().equals(group.getName()); return organization != null && organization.getId().equals(group.getName());
} }
@ -65,18 +68,24 @@ public class Organizations {
return true; return true;
} }
public static List<IdentityProviderModel> resolveBroker(KeycloakSession session, UserModel user) { public static List<IdentityProviderModel> resolveHomeBroker(KeycloakSession session, UserModel user) {
OrganizationProvider provider = session.getProvider(OrganizationProvider.class); OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
RealmModel realm = session.getContext().getRealm(); RealmModel realm = session.getContext().getRealm();
OrganizationModel organization = provider.getByMember(user); List<OrganizationModel> organizations = Optional.ofNullable(user).stream().flatMap(provider::getByMember)
.filter(OrganizationModel::isEnabled)
.filter((org) -> org.isManaged(user))
.toList();
if (organization == null || !organization.isEnabled()) { if (organizations.isEmpty()) {
return List.of(); return List.of();
} }
if (provider.isManagedMember(organization, user)) { List<IdentityProviderModel> brokers = new ArrayList<>();
for (OrganizationModel organization : organizations) {
// user is a managed member, try to resolve the origin broker and redirect automatically
List<IdentityProviderModel> organizationBrokers = organization.getIdentityProviders().toList(); List<IdentityProviderModel> organizationBrokers = organization.getIdentityProviders().toList();
return session.users().getFederatedIdentitiesStream(realm, user) session.users().getFederatedIdentitiesStream(realm, user)
.map(f -> { .map(f -> {
IdentityProviderModel broker = realm.getIdentityProviderByAlias(f.getIdentityProvider()); IdentityProviderModel broker = realm.getIdentityProviderByAlias(f.getIdentityProvider());
@ -92,10 +101,10 @@ public class Organizations {
return null; return null;
}).filter(Objects::nonNull) }).filter(Objects::nonNull)
.toList(); .forEach(brokers::add);
} }
return List.of(); return brokers;
} }
public static Consumer<GroupModel> removeGroup(KeycloakSession session, RealmModel realm) { public static Consumer<GroupModel> removeGroup(KeycloakSession session, RealmModel realm) {
@ -105,7 +114,7 @@ public class Organizations {
return; return;
} }
OrganizationModel current = (OrganizationModel) session.getAttribute(OrganizationModel.class.getName()); OrganizationModel current = resolveOrganization(session);
try { try {
OrganizationProvider provider = session.getProvider(OrganizationProvider.class); OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
@ -218,26 +227,37 @@ public class Organizations {
return email.substring(domainSeparator + 1); return email.substring(domainSeparator + 1);
} }
public static OrganizationModel resolveOrganization(KeycloakSession session, UserModel user) { public static OrganizationModel resolveOrganization(KeycloakSession session) {
OrganizationModel organization = (OrganizationModel) session.getAttribute(OrganizationModel.class.getName()); return resolveOrganization(session, null, null);
if (organization != null) {
return organization;
} }
if (user == null) { public static OrganizationModel resolveOrganization(KeycloakSession session, UserModel user) {
return null; return resolveOrganization(session, user, null);
}
public static OrganizationModel resolveOrganization(KeycloakSession session, UserModel user, String domain) {
Optional<OrganizationModel> organization = Optional.ofNullable((OrganizationModel) session.getAttribute(OrganizationModel.class.getName()));
if (organization.isPresent()) {
return organization.get();
} }
OrganizationProvider provider = session.getProvider(OrganizationProvider.class); OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
OrganizationModel memberOrg = provider.getByMember(user);
if (memberOrg != null) { organization = ofNullable(user).stream().flatMap(provider::getByMember)
return memberOrg; .filter(o -> o.isEnabled() && provider.isManagedMember(o, user))
.findAny();
if (organization.isPresent()) {
return organization.get();
} }
String domain = Organizations.getEmailDomain(user.getEmail()); if (user != null && domain == null) {
domain = getEmailDomain(user.getEmail());
}
return domain == null ? null : provider.getByDomainName(domain); return ofNullable(domain)
.map(provider::getByDomainName)
.orElse(null);
} }
} }

View file

@ -18,7 +18,7 @@
package org.keycloak.organization.validator; package org.keycloak.organization.validator;
import static java.util.Optional.ofNullable; import static java.util.Optional.ofNullable;
import static org.keycloak.organization.utils.Organizations.resolveBroker; import static org.keycloak.organization.utils.Organizations.resolveHomeBroker;
import static org.keycloak.validate.BuiltinValidators.emailValidator; import static org.keycloak.validate.BuiltinValidators.emailValidator;
import java.util.Collections; import java.util.Collections;
@ -36,7 +36,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OrganizationDomainModel; import org.keycloak.models.OrganizationDomainModel;
import org.keycloak.models.OrganizationModel; import org.keycloak.models.OrganizationModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.organization.OrganizationProvider; import org.keycloak.organization.utils.Organizations;
import org.keycloak.provider.EnvironmentDependentProviderFactory; import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.userprofile.AttributeContext; import org.keycloak.userprofile.AttributeContext;
import org.keycloak.userprofile.UserProfileAttributeValidationContext; import org.keycloak.userprofile.UserProfileAttributeValidationContext;
@ -59,7 +59,10 @@ public class OrganizationMemberValidator extends AbstractSimpleValidator impleme
@Override @Override
protected void doValidate(Object value, String inputHint, ValidationContext context, ValidatorConfig config) { protected void doValidate(Object value, String inputHint, ValidationContext context, ValidatorConfig config) {
KeycloakSession session = context.getSession(); KeycloakSession session = context.getSession();
OrganizationModel organization = resolveOrganization(context, session); UserProfileAttributeValidationContext upContext = (UserProfileAttributeValidationContext) context;
AttributeContext attributeContext = upContext.getAttributeContext();
UserModel user = attributeContext.getUser();
OrganizationModel organization = Organizations.resolveOrganization(session, user);
if (organization == null) { if (organization == null) {
return; return;
@ -121,7 +124,7 @@ public class OrganizationMemberValidator extends AbstractSimpleValidator impleme
} }
private static Set<String> resolveExpectedDomainsForManagedUser(ValidationContext context, UserModel user) { private static Set<String> resolveExpectedDomainsForManagedUser(ValidationContext context, UserModel user) {
List<IdentityProviderModel> brokers = resolveBroker(context.getSession(), user); List<IdentityProviderModel> brokers = resolveHomeBroker(context.getSession(), user);
if (brokers.isEmpty()) { if (brokers.isEmpty()) {
return Set.of(); return Set.of();
@ -164,23 +167,4 @@ public class OrganizationMemberValidator extends AbstractSimpleValidator impleme
String brokerDomain = broker.getConfig().get(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE); String brokerDomain = broker.getConfig().get(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
return ofNullable(brokerDomain).map(Set::of).orElse(Set.of()); return ofNullable(brokerDomain).map(Set::of).orElse(Set.of());
} }
private OrganizationModel resolveOrganization(ValidationContext context, KeycloakSession session) {
OrganizationModel organization = (OrganizationModel) session.getAttribute(OrganizationModel.class.getName());
if (organization != null) {
return organization;
}
UserProfileAttributeValidationContext upContext = (UserProfileAttributeValidationContext) context;
AttributeContext attributeContext = upContext.getAttributeContext();
UserModel user = attributeContext.getUser();
if (user != null) {
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
return provider.getByMember(user);
}
return null;
}
} }

View file

@ -210,7 +210,7 @@ public class LinkedAccountsResource {
} }
if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) { if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
if (Organizations.resolveBroker(session, user).stream() if (Organizations.resolveHomeBroker(session, user).stream()
.map(IdentityProviderModel::getAlias) .map(IdentityProviderModel::getAlias)
.anyMatch(providerAlias::equals)) { .anyMatch(providerAlias::equals)) {
throw ErrorResponse.error(translateErrorMessage(Messages.FEDERATED_IDENTITY_BOUND_ORGANIZATION), Response.Status.BAD_REQUEST); throw ErrorResponse.error(translateErrorMessage(Messages.FEDERATED_IDENTITY_BOUND_ORGANIZATION), Response.Status.BAD_REQUEST);

View file

@ -1082,7 +1082,6 @@ public class UserResource {
attributes.remove(UserModel.USERNAME); attributes.remove(UserModel.USERNAME);
attributes.remove(UserModel.EMAIL); attributes.remove(UserModel.EMAIL);
attributes.remove(OrganizationModel.ORGANIZATION_ATTRIBUTE);
return attributes.entrySet().stream() return attributes.entrySet().stream()
.filter(entry -> ofNullable(entry.getValue()).orElse(emptyList()).stream().anyMatch(StringUtil::isNotBlank)) .filter(entry -> ofNullable(entry.getValue()).orElse(emptyList()).stream().anyMatch(StringUtil::isNotBlank))

View file

@ -354,12 +354,6 @@ public class DeclarativeUserProfileProviderFactory implements UserProfileProvide
attribute.addValidators(List.of(new AttributeValidatorMetadata(OrganizationMemberValidator.ID))); attribute.addValidators(List.of(new AttributeValidatorMetadata(OrganizationMemberValidator.ID)));
} }
} }
metadata.addAttribute(OrganizationModel.ORGANIZATION_ATTRIBUTE, -1,
new AttributeValidatorMetadata(OrganizationMemberValidator.ID),
new AttributeValidatorMetadata(ImmutableAttributeValidator.ID))
.addReadCondition((c) -> false)
.addWriteCondition((c) -> false);
} }
} }

View file

@ -152,7 +152,8 @@ public class OrganizationThemeTest extends AbstractOrganizationTest {
oauth.clientId("broker-app"); oauth.clientId("broker-app");
loginPage.open(bc.consumerRealmName()); loginPage.open(bc.consumerRealmName());
loginPage.loginUsername("tom"); loginPage.loginUsername("tom");
loginPage.login("tom", "password"); Assert.assertTrue(driver.getPageSource().contains("Sign-in to myorg organization"));
loginPage.login("password");
waitForPage(driver, "update account information", false); waitForPage(driver, "update account information", false);
Assert.assertTrue("Driver should be on the consumer realm page right now", Assert.assertTrue("Driver should be on the consumer realm page right now",
driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/")); driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));

View file

@ -123,12 +123,13 @@ public abstract class AbstractBrokerSelfRegistrationTest extends AbstractOrganiz
public void testIdentityFirstUserNotExistEmailMatchBrokerDomainNoPublicBroker() { public void testIdentityFirstUserNotExistEmailMatchBrokerDomainNoPublicBroker() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId()); OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
IdentityProviderRepresentation idpRep = organization.identityProviders().getIdentityProviders().get(0); IdentityProviderRepresentation idpRep = organization.identityProviders().getIdentityProviders().get(0);
idpRep.getConfig().remove(IdentityProviderRedirectMode.EMAIL_MATCH.getKey());
idpRep.getConfig().remove(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE); idpRep.getConfig().remove(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
testRealm().identityProviders().get(idpRep.getAlias()).update(idpRep); testRealm().identityProviders().get(idpRep.getAlias()).update(idpRep);
openIdentityFirstLoginPage("user@neworg.org", false, null, false, false); openIdentityFirstLoginPage("user@neworg.org", false, null, false, false);
Assert.assertFalse(driver.getPageSource().contains("Your email domain matches the neworg organization but you don't have an account yet.")); Assert.assertTrue(driver.getPageSource().contains("Your email domain matches the neworg organization but you don't have an account yet."));
Assert.assertTrue(loginPage.isUsernameInputPresent()); Assert.assertTrue(loginPage.isUsernameInputPresent());
Assert.assertFalse(loginPage.isPasswordInputPresent()); Assert.assertFalse(loginPage.isPasswordInputPresent());
// self-registration link shown because there is no public broker and user can choose to register // self-registration link shown because there is no public broker and user can choose to register
@ -221,6 +222,7 @@ public abstract class AbstractBrokerSelfRegistrationTest extends AbstractOrganiz
OrganizationIdentityProviderResource broker = organization.identityProviders().get(bc.getIDPAlias()); OrganizationIdentityProviderResource broker = organization.identityProviders().get(bc.getIDPAlias());
IdentityProviderRepresentation brokerRep = broker.toRepresentation(); IdentityProviderRepresentation brokerRep = broker.toRepresentation();
brokerRep.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString()); brokerRep.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString());
brokerRep.getConfig().remove(IdentityProviderRedirectMode.EMAIL_MATCH.getKey());
testRealm().identityProviders().get(brokerRep.getAlias()).update(brokerRep); testRealm().identityProviders().get(brokerRep.getAlias()).update(brokerRep);
openIdentityFirstLoginPage(bc.getUserEmail(), true, brokerRep, false, true); openIdentityFirstLoginPage(bc.getUserEmail(), true, brokerRep, false, true);
@ -243,6 +245,7 @@ public abstract class AbstractBrokerSelfRegistrationTest extends AbstractOrganiz
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId()); OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
OrganizationIdentityProviderResource broker = organization.identityProviders().get(bc.getIDPAlias()); OrganizationIdentityProviderResource broker = organization.identityProviders().get(bc.getIDPAlias());
IdentityProviderRepresentation brokerRep = broker.toRepresentation(); IdentityProviderRepresentation brokerRep = broker.toRepresentation();
brokerRep.getConfig().remove(IdentityProviderRedirectMode.EMAIL_MATCH.getKey());
brokerRep.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString()); brokerRep.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString());
testRealm().identityProviders().get(brokerRep.getAlias()).update(brokerRep); testRealm().identityProviders().get(brokerRep.getAlias()).update(brokerRep);
@ -445,6 +448,7 @@ public abstract class AbstractBrokerSelfRegistrationTest extends AbstractOrganiz
OrganizationResource org1 = testRealm().organizations().get(createOrganization(org1Name).getId()); OrganizationResource org1 = testRealm().organizations().get(createOrganization(org1Name).getId());
IdentityProviderRepresentation org1Broker = org1.identityProviders().getIdentityProviders().get(0); IdentityProviderRepresentation org1Broker = org1.identityProviders().getIdentityProviders().get(0);
org1Broker.getConfig().remove(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE); org1Broker.getConfig().remove(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
org1Broker.getConfig().remove(IdentityProviderRedirectMode.EMAIL_MATCH.getKey());
org1Broker.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString()); org1Broker.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString());
testRealm().identityProviders().get(org1Broker.getAlias()).update(org1Broker); testRealm().identityProviders().get(org1Broker.getAlias()).update(org1Broker);

View file

@ -27,6 +27,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.TokenVerifier; import org.keycloak.TokenVerifier;
@ -39,6 +40,7 @@ import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.organization.admin.AbstractOrganizationTest; import org.keycloak.testsuite.organization.admin.AbstractOrganizationTest;
import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse; import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse;
@ -48,8 +50,19 @@ public class OrganizationOIDCProtocolMapperTest extends AbstractOrganizationTest
@Test @Test
public void testClaim() throws Exception { public void testClaim() throws Exception {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId()); OrganizationResource orga = testRealm().organizations().get(createOrganization("org-a").getId());
addMember(organization); OrganizationResource orgb = testRealm().organizations().get(createOrganization("org-b").getId());
addMember(orga);
UserRepresentation member = getUserRepresentation(memberEmail);
orgb.members().addMember(member.getId()).close();
Assert.assertTrue(orga.members().getAll().stream().map(UserRepresentation::getId).anyMatch(member.getId()::equals));
Assert.assertTrue(orgb.members().getAll().stream().map(UserRepresentation::getId).anyMatch(member.getId()::equals));
member = getUserRepresentation(memberEmail);
oauth.clientId("direct-grant"); oauth.clientId("direct-grant");
oauth.scope("openid organization"); oauth.scope("openid organization");
@ -60,10 +73,13 @@ public class OrganizationOIDCProtocolMapperTest extends AbstractOrganizationTest
assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION)); assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION));
@SuppressWarnings("unchecked")
Map<String, Object> claim = (Map<String, Object>) accessToken.getOtherClaims().get(OAuth2Constants.ORGANIZATION); Map<String, Object> claim = (Map<String, Object>) accessToken.getOtherClaims().get(OAuth2Constants.ORGANIZATION);
assertThat(claim, notNullValue()); assertThat(claim, notNullValue());
assertThat(claim.get(organizationName), notNullValue()); assertThat(claim.get(orga.toRepresentation().getName()), notNullValue());
String orgaId = orga.toRepresentation().getName();
String orgbId = orgb.toRepresentation().getName();
assertThat(claim.get(orgaId), notNullValue());
assertThat(claim.get(orgbId), notNullValue());
} }
@Test @Test

View file

@ -36,6 +36,7 @@ import org.keycloak.dom.saml.v2.assertion.AttributeType;
import org.keycloak.protocol.saml.SamlConfigAttributes; import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.protocol.saml.SamlProtocol; import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.organization.protocol.mappers.saml.OrganizationMembershipMapper; import org.keycloak.organization.protocol.mappers.saml.OrganizationMembershipMapper;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.RealmsResource;
@ -53,6 +54,8 @@ public class OrganizationSAMLProtocolMapperTest extends AbstractOrganizationTest
@Test @Test
public void testAttribute() { public void testAttribute() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId()); OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
IdentityProviderRepresentation broker = organization.identityProviders().getIdentityProviders().get(0);
organization.identityProviders().get(broker.getAlias()).delete().close();
addMember(organization); addMember(organization);
String clientId = "saml-client"; String clientId = "saml-client";
testRealm().clients().create(ClientBuilder.create() testRealm().clients().create(ClientBuilder.create()

View file

@ -74,7 +74,7 @@ public class OrganizationMemberAuthenticationTest extends AbstractOrganizationTe
} }
@Test @Test
public void testAuthenticateUnmanagedMemberWehnProviderDisabled() throws IOException { public void testAuthenticateUnmanagedMemberWhenProviderDisabled() throws IOException {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId()); OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
UserRepresentation member = addMember(organization, "contractor@contractor.org"); UserRepresentation member = addMember(organization, "contractor@contractor.org");

View file

@ -30,7 +30,6 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import static org.keycloak.models.OrganizationModel.ORGANIZATION_ATTRIBUTE;
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage; import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
import java.util.ArrayList; import java.util.ArrayList;
@ -43,19 +42,16 @@ import jakarta.ws.rs.core.Response.Status;
import java.io.IOException; import java.io.IOException;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import static org.junit.Assert.assertEquals;
import org.junit.Test; import org.junit.Test;
import org.keycloak.admin.client.resource.OrganizationMemberResource; import org.keycloak.admin.client.resource.OrganizationMemberResource;
import org.keycloak.admin.client.resource.OrganizationResource; import org.keycloak.admin.client.resource.OrganizationResource;
import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.Profile.Feature; import org.keycloak.common.Profile.Feature;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OrganizationModel; import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.organization.OrganizationProvider; import org.keycloak.organization.OrganizationProvider;
import org.keycloak.representations.idm.ErrorRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.MemberRepresentation; import org.keycloak.representations.idm.MemberRepresentation;
import org.keycloak.representations.idm.OrganizationRepresentation; import org.keycloak.representations.idm.OrganizationRepresentation;
@ -68,6 +64,7 @@ import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.organization.admin.AbstractOrganizationTest; import org.keycloak.testsuite.organization.admin.AbstractOrganizationTest;
import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.updaters.RealmAttributeUpdater; import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.util.UserBuilder;
@EnableFeature(Feature.ORGANIZATION) @EnableFeature(Feature.ORGANIZATION)
public class OrganizationMemberTest extends AbstractOrganizationTest { public class OrganizationMemberTest extends AbstractOrganizationTest {
@ -91,41 +88,6 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
assertEquals(expected.getLastName(), existing.getLastName()); assertEquals(expected.getLastName(), existing.getLastName());
} }
@Test
public void testFailSetUserOrganizationAttribute() {
UPConfig upConfig = testRealm().users().userProfile().getConfiguration();
upConfig.setUnmanagedAttributePolicy(UnmanagedAttributePolicy.ENABLED);
testRealm().users().userProfile().update(upConfig);
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
UserRepresentation expected = getUserRepFromMemberRep(addMember(organization));
expected.singleAttribute(ORGANIZATION_ATTRIBUTE, "invalid");
UserResource userResource = testRealm().users().get(expected.getId());
try {
userResource.update(expected);
Assert.fail("The attribute is readonly");
} catch (BadRequestException bre) {
ErrorRepresentation error = bre.getResponse().readEntity(ErrorRepresentation.class);
assertEquals(ORGANIZATION_ATTRIBUTE, error.getField());
assertEquals("error-user-attribute-read-only", error.getErrorMessage());
}
// the attribute is readonly, removing it from the rep does not make any difference
expected.getAttributes().remove(ORGANIZATION_ATTRIBUTE);
userResource.update(expected);
expected = userResource.toRepresentation();
assertNull(expected.getAttributes());
getTestingClient().server(TEST_REALM_NAME).run(OrganizationMemberTest::assertMembersHaveOrgAttribute);
}
private static void assertMembersHaveOrgAttribute(KeycloakSession session) {
OrganizationModel organization = session.getProvider(OrganizationProvider.class).getByDomainName("neworg.org");
assertTrue(session.getProvider(OrganizationProvider.class).getMembersStream(organization, null, false, -1, -1).
anyMatch(userModel -> userModel.getAttributes().getOrDefault(ORGANIZATION_ATTRIBUTE, List.of()).contains(organization.getId())));
}
@Test @Test
public void testUserAlreadyMemberOfOrganization() { public void testUserAlreadyMemberOfOrganization() {
UPConfig upConfig = testRealm().users().userProfile().getConfiguration(); UPConfig upConfig = testRealm().users().userProfile().getConfiguration();
@ -154,9 +116,9 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId()); OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
UserRepresentation member = addMember(organization); UserRepresentation member = addMember(organization);
OrganizationRepresentation expected = organization.toRepresentation(); OrganizationRepresentation expected = organization.toRepresentation();
OrganizationRepresentation actual = organization.members().getOrganization(member.getId()); List<OrganizationRepresentation> actual = organization.members().member(member.getId()).getOrganizations();
assertNotNull(actual); assertNotNull(actual);
assertEquals(expected.getId(), actual.getId()); assertTrue(actual.stream().map(OrganizationRepresentation::getId).anyMatch(expected.getId()::equals));
} }
@Test @Test
@ -290,8 +252,7 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
upConfig.setUnmanagedAttributePolicy(UnmanagedAttributePolicy.ENABLED); upConfig.setUnmanagedAttributePolicy(UnmanagedAttributePolicy.ENABLED);
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId()); OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
UserRepresentation expected = addMember(organization); UserRepresentation expected = addMember(organization);
assertNotNull(expected.getAttributes()); assertNull(expected.getAttributes());
assertNull(expected.getAttributes().get(ORGANIZATION_ATTRIBUTE));
OrganizationMemberResource member = organization.members().member(expected.getId()); OrganizationMemberResource member = organization.members().member(expected.getId());
try (Response response = member.delete()) { try (Response response = member.delete()) {
@ -322,10 +283,6 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
} }
private UserRepresentation getUserRepFromMemberRep(MemberRepresentation member) {
return new UserRepresentation(member);
}
@Test @Test
public void testDeleteMembersOnOrganizationRemoval() { public void testDeleteMembersOnOrganizationRemoval() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId()); OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
@ -352,7 +309,7 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
for (MemberRepresentation member : expected) { for (MemberRepresentation member : expected) {
try { try {
// user no longer bound to the organization // user no longer bound to the organization
organization.members().getOrganization(member.getId()); organization.members().member(member.getId()).getOrganizations();
fail("should not be associated with the organization anymore"); fail("should not be associated with the organization anymore");
} catch (NotFoundException ignore) { } catch (NotFoundException ignore) {
} }
@ -499,6 +456,59 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
assertThat(testRealm().organizations().get(id).members().getAll(), hasSize(0)); assertThat(testRealm().organizations().get(id).members().getAll(), hasSize(0));
} }
@Test
public void testMemberInMultipleOrganizations() {
OrganizationResource orga = testRealm().organizations().get(createOrganization("org-a").getId());
OrganizationResource orgb = testRealm().organizations().get(createOrganization("org-b").getId());
addMember(orga);
UserRepresentation member = getUserRepresentation(memberEmail);
orgb.members().addMember(member.getId()).close();
Assert.assertTrue(orga.members().getAll().stream().map(UserRepresentation::getId).anyMatch(member.getId()::equals));
Assert.assertTrue(orgb.members().getAll().stream().map(UserRepresentation::getId).anyMatch(member.getId()::equals));
String orgbId = orgb.toRepresentation().getId();
String orgaId = orga.toRepresentation().getId();
List<String> memberOfOrgs = orga.members().member(member.getId()).getOrganizations().stream().map(OrganizationRepresentation::getId).toList();
assertTrue(memberOfOrgs.contains(orgaId));
assertTrue(memberOfOrgs.contains(orgbId));
}
@Test
public void testManagedMemberOnlyRemovedFromHomeOrganization() {
OrganizationResource orga = testRealm().organizations().get(createOrganization("org-a").getId());
assertBrokerRegistration(orga, bc.getUserEmail(), "managed-org-a@org-a.org");
UserRepresentation memberOrgA = orga.members().getAll().get(0);
realmsResouce().realm(bc.consumerRealmName()).users().get(memberOrgA.getId()).logout();
realmsResouce().realm(bc.providerRealmName()).logoutAll();
OrganizationResource orgb = testRealm().organizations().get(createOrganization("org-b").getId());
UserRepresentation memberOrgB = UserBuilder.create()
.username("managed-org-b")
.password("password")
.enabled(true)
.build();
realmsResouce().realm(bc.providerRealmName()).users().create(memberOrgB).close();
assertBrokerRegistration(orgb, memberOrgB.getUsername(), "managed-org-b@org-b.org");
memberOrgB = orgb.members().getAll().get(0);
orga.members().addMember(memberOrgB.getId()).close();
assertThat(orga.members().getAll().size(), is(2));
OrganizationMemberResource memberOrgBInOrgA = orga.members().member(memberOrgB.getId());
memberOrgB = memberOrgBInOrgA.toRepresentation();
memberOrgBInOrgA.delete().close();
assertThat(orga.members().getAll().size(), is(1));
assertThat(orga.members().getAll().get(0).getId(), is(memberOrgA.getId()));
assertThat(orgb.members().getAll().size(), is(1));
orgb.members().member(memberOrgB.getId()).delete().close();
assertThat(orga.members().getAll().size(), is(1));
assertThat(orga.members().getAll().get(0).getId(), is(memberOrgA.getId()));
assertThat(orgb.members().getAll().size(), is(0));
}
private void loginViaNonOrgIdP(String idpAlias) { private void loginViaNonOrgIdP(String idpAlias) {
oauth.clientId("broker-app"); oauth.clientId("broker-app");
loginPage.open(bc.consumerRealmName()); loginPage.open(bc.consumerRealmName());
@ -525,4 +535,8 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
appPage.assertCurrent(); appPage.assertCurrent();
assertThat(appPage.getRequestType(), equalTo(AppPage.RequestType.AUTH_RESPONSE)); assertThat(appPage.getRequestType(), equalTo(AppPage.RequestType.AUTH_RESPONSE));
} }
private UserRepresentation getUserRepFromMemberRep(MemberRepresentation member) {
return new UserRepresentation(member);
}
} }