Allow members joining multiple organizations
Closes #30747 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
12732333c8
commit
1f8280c71a
34 changed files with 367 additions and 324 deletions
|
@ -17,12 +17,16 @@
|
|||
|
||||
package org.keycloak.admin.client.resource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.keycloak.representations.idm.MemberRepresentation;
|
||||
import org.keycloak.representations.idm.OrganizationRepresentation;
|
||||
|
||||
public interface OrganizationMemberResource {
|
||||
|
||||
|
@ -32,4 +36,9 @@ public interface OrganizationMemberResource {
|
|||
|
||||
@DELETE
|
||||
Response delete();
|
||||
|
||||
@Path("organizations")
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
List<OrganizationRepresentation> getOrganizations();
|
||||
}
|
||||
|
|
|
@ -67,11 +67,6 @@ public interface OrganizationMembersResource {
|
|||
@QueryParam("max") Integer max
|
||||
);
|
||||
|
||||
@Path("{id}/organization")
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
OrganizationRepresentation getOrganization(@PathParam("id") String id);
|
||||
|
||||
@Path("{id}")
|
||||
OrganizationMemberResource member(@PathParam("id") String id);
|
||||
|
||||
|
|
|
@ -340,12 +340,7 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
|
|||
int notBefore = getDelegate().getNotBeforeOfUser(realm, delegate);
|
||||
|
||||
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
|
||||
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))) {
|
||||
if (isOrganizationDisabled(session, delegate)) {
|
||||
return new ReadOnlyUserModelDelegate(delegate) {
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
|
@ -981,4 +976,13 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC
|
|||
}
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -161,7 +161,7 @@ public class InfinispanOrganizationProvider implements OrganizationProvider {
|
|||
}
|
||||
|
||||
@Override
|
||||
public OrganizationModel getByMember(UserModel member) {
|
||||
public Stream<OrganizationModel> getByMember(UserModel member) {
|
||||
return orgDelegate.getByMember(member);
|
||||
}
|
||||
|
||||
|
|
|
@ -157,6 +157,11 @@ public class OrganizationAdapter implements OrganizationModel {
|
|||
return delegate.isManagedMember(this, user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMember(UserModel user) {
|
||||
return delegate.isMember(this, user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
|
|
@ -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" +
|
||||
" 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="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 {
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
package org.keycloak.organization.jpa;
|
||||
|
||||
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.jpa.PaginationUtils.paginateQuery;
|
||||
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.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
|
@ -36,7 +36,6 @@ 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.GroupModel;
|
||||
import org.keycloak.models.GroupModel.Type;
|
||||
|
@ -49,7 +48,6 @@ 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;
|
||||
|
@ -59,6 +57,7 @@ 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.organization.utils.Organizations;
|
||||
import org.keycloak.utils.StringUtil;
|
||||
|
||||
public class JpaOrganizationProvider implements OrganizationProvider {
|
||||
|
@ -163,7 +162,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
|
|||
throwExceptionIfObjectIsNull(user, "User");
|
||||
|
||||
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
|
||||
if (session.users().getUserById(session.realms().getRealm(entity.getRealmId()), user.getId()) == null) {
|
||||
|
@ -181,12 +180,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
|
|||
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.setSingleAttribute(ORGANIZATION_ATTRIBUTE, entity.getId());
|
||||
} finally {
|
||||
if (current == null) {
|
||||
session.removeAttribute(OrganizationModel.class.getName());
|
||||
|
@ -285,9 +279,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
|
|||
return null;
|
||||
}
|
||||
|
||||
String orgId = user.getFirstAttribute(ORGANIZATION_ATTRIBUTE);
|
||||
|
||||
if (organization.getId().equals(orgId)) {
|
||||
if (getByMember(user).anyMatch(organization::equals)) {
|
||||
return user;
|
||||
}
|
||||
|
||||
|
@ -295,17 +287,19 @@ public class JpaOrganizationProvider implements OrganizationProvider {
|
|||
}
|
||||
|
||||
@Override
|
||||
public OrganizationModel getByMember(UserModel member) {
|
||||
public Stream<OrganizationModel> getByMember(UserModel member) {
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
OrganizationProvider organizations = session.getProvider(OrganizationProvider.class);
|
||||
GroupProvider groups = session.groups();
|
||||
|
||||
// need to go via the session to avoid bypassing the cache
|
||||
return session.getProvider(OrganizationProvider.class).getById(orgId);
|
||||
return closing(query.getResultStream())
|
||||
.map((id) -> groups.getGroupById(getRealm(), id))
|
||||
.map((g) -> organizations.getById(g.getName()))
|
||||
.filter(Objects::nonNull);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -395,25 +389,22 @@ public class JpaOrganizationProvider implements OrganizationProvider {
|
|||
throwExceptionIfObjectIsNull(organization, "organization");
|
||||
throwExceptionIfObjectIsNull(member, "member");
|
||||
|
||||
OrganizationModel userOrg = getByMember(member);
|
||||
OrganizationModel userOrg = getByMember(member).filter(organization::equals).findAny().orElse(null);
|
||||
|
||||
if (userOrg == null || !userOrg.equals(organization)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isManagedMember(organization, member)) {
|
||||
return new UserManager(session).removeUser(getRealm(), member, userProvider);
|
||||
userProvider.removeUser(getRealm(), member);
|
||||
} else {
|
||||
OrganizationModel current = (OrganizationModel) session.getAttribute(OrganizationModel.class.getName());
|
||||
OrganizationModel current = Organizations.resolveOrganization(session);
|
||||
|
||||
if (current == null) {
|
||||
session.setAttribute(OrganizationModel.class.getName(), organization);
|
||||
}
|
||||
|
||||
try {
|
||||
List<String> organizations = member.getAttributes().get(ORGANIZATION_ATTRIBUTE);
|
||||
organizations.remove(organization.getId());
|
||||
member.setAttribute(ORGANIZATION_ATTRIBUTE, organizations);
|
||||
member.leaveGroup(getOrganizationGroup(organization));
|
||||
} finally {
|
||||
if (current == null) {
|
||||
|
|
|
@ -205,6 +205,11 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
|
|||
return provider.isManagedMember(this, user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMember(UserModel user) {
|
||||
return provider.isMember(this, user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrganizationEntity getEntity() {
|
||||
return entity;
|
||||
|
|
|
@ -469,10 +469,6 @@ public class ExportUtils {
|
|||
userRep.setGroups(groups);
|
||||
}
|
||||
|
||||
if (userRep.getAttributes() != null) {
|
||||
userRep.getAttributes().remove(OrganizationModel.ORGANIZATION_ATTRIBUTE);
|
||||
}
|
||||
|
||||
return userRep;
|
||||
}
|
||||
|
||||
|
|
|
@ -117,10 +117,7 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
|
|||
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
|
||||
OrganizationProvider organizationProvider = session.getProvider(OrganizationProvider.class);
|
||||
OrganizationModel organization = organizationProvider.getByMember(user);
|
||||
|
||||
if ((organizationProvider.isEnabled() && organization != null && organization.isManaged(user) && !organization.isEnabled()) ||
|
||||
(!organizationProvider.isEnabled() && organization != null && organization.isManaged(user))) {
|
||||
if (isOrganizationDisabled(session, user)) {
|
||||
return new ReadOnlyUserModelDelegate(user) {
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
|
@ -291,13 +288,13 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
|
|||
}
|
||||
|
||||
logger.tracef("This provider (%s) cannot provide enough users to pass firstResult so we are going to filter it out and change "
|
||||
+ "firstResult for next provider: %d - %d = %d", provider.getClass().getSimpleName(),
|
||||
+ "firstResult for next provider: %d - %d = %d", provider.getClass().getSimpleName(),
|
||||
currentFirst.get(), expectedNumberOfUsersForProvider, currentFirst.get() - expectedNumberOfUsersForProvider);
|
||||
currentFirst.set((int) (currentFirst.get() - expectedNumberOfUsersForProvider));
|
||||
return false;
|
||||
})
|
||||
// collecting stream of providers to ensure the filtering (above) is evaluated before we move forward to actual querying
|
||||
.collect(Collectors.toList()).stream();
|
||||
// collecting stream of providers to ensure the filtering (above) is evaluated before we move forward to actual querying
|
||||
.collect(Collectors.toList()).stream();
|
||||
}
|
||||
|
||||
if (needsAdditionalFirstResultFiltering.get() && currentFirst.get() > 0) {
|
||||
|
@ -926,4 +923,13 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
|
|||
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -248,7 +248,6 @@ public class ModelToRepresentation {
|
|||
}
|
||||
if (attributes != null && !copy.isEmpty()) {
|
||||
Map<String, List<String>> attrs = new HashMap<>(copy);
|
||||
attrs.remove(OrganizationModel.ORGANIZATION_ATTRIBUTE);
|
||||
rep.setAttributes(attrs);
|
||||
}
|
||||
|
||||
|
|
|
@ -77,4 +77,6 @@ public interface OrganizationModel {
|
|||
Stream<IdentityProviderModel> getIdentityProviders();
|
||||
|
||||
boolean isManaged(UserModel user);
|
||||
|
||||
boolean isMember(UserModel user);
|
||||
}
|
||||
|
|
|
@ -142,10 +142,10 @@ public interface OrganizationProvider extends Provider {
|
|||
/**
|
||||
* Returns the {@link OrganizationModel} that the {@code member} belongs to.
|
||||
*
|
||||
* @param member the member of a organization
|
||||
* @return the organization the {@code member} belongs to or {@code null} if the user doesn't belong to any.
|
||||
* @param member the member of an organization
|
||||
* @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}.
|
||||
|
@ -196,6 +196,17 @@ public interface OrganizationProvider extends Provider {
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
|
|
@ -84,14 +84,6 @@ public class InviteOrgActionTokenHandler extends AbstractActionTokenHandler<Invi
|
|||
|
||||
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) {
|
||||
event.user(user).error(Errors.ORG_NOT_FOUND);
|
||||
return session.getProvider(LoginFormsProvider.class)
|
||||
|
@ -100,6 +92,14 @@ public class InviteOrgActionTokenHandler extends AbstractActionTokenHandler<Invi
|
|||
.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 RealmModel realm = tokenContext.getRealm();
|
||||
|
||||
|
|
|
@ -549,7 +549,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
OrganizationModel organization = resolveOrganization(session, user);
|
||||
|
||||
if (organization != null) {
|
||||
attributes.put("org", new OrganizationBean(session, organization, user));
|
||||
attributes.put("org", new OrganizationBean(organization, user));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,11 +23,9 @@ import java.util.Map;
|
|||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.OrganizationDomainModel;
|
||||
import org.keycloak.models.OrganizationModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.organization.OrganizationProvider;
|
||||
|
||||
public class OrganizationBean {
|
||||
|
||||
|
@ -37,18 +35,14 @@ public class OrganizationBean {
|
|||
private final boolean isMember;
|
||||
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.alias = organization.getAlias();
|
||||
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());
|
||||
}
|
||||
|
||||
private static OrganizationProvider getOrganizationProvider(KeycloakSession session) {
|
||||
return session.getProvider(OrganizationProvider.class);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
|
|
@ -33,7 +33,6 @@ import org.keycloak.models.KeycloakSession;
|
|||
import org.keycloak.models.OrganizationModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.organization.OrganizationProvider;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||
|
@ -52,7 +51,6 @@ public class OrganizationInvitationResource {
|
|||
|
||||
private final KeycloakSession session;
|
||||
private final RealmModel realm;
|
||||
private final OrganizationProvider provider;
|
||||
private final OrganizationModel organization;
|
||||
private final AdminEventBuilder adminEvent;
|
||||
private final int tokenExpiration;
|
||||
|
@ -60,7 +58,6 @@ public class OrganizationInvitationResource {
|
|||
public OrganizationInvitationResource(KeycloakSession session, OrganizationModel organization, AdminEventBuilder adminEvent) {
|
||||
this.session = session;
|
||||
this.realm = session.getContext().getRealm();
|
||||
this.provider = session.getProvider(OrganizationProvider.class);
|
||||
this.organization = organization;
|
||||
this.adminEvent = adminEvent;
|
||||
this.tokenExpiration = getTokenExpiration();
|
||||
|
@ -74,9 +71,7 @@ public class OrganizationInvitationResource {
|
|||
UserModel user = session.users().getUserByEmail(realm, email);
|
||||
|
||||
if (user != null) {
|
||||
OrganizationModel org = provider.getByMember(user);
|
||||
|
||||
if (org != null && org.equals(organization)) {
|
||||
if (organization.isMember(user)) {
|
||||
throw ErrorResponse.error("User already a member of the organization", Status.CONFLICT);
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package org.keycloak.organization.admin.resource;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import jakarta.ws.rs.Consumes;
|
||||
|
@ -178,29 +179,27 @@ public class OrganizationMemberResource {
|
|||
throw ErrorResponse.error("Not a member of the organization", Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@Path("{id}/organization")
|
||||
@Path("{id}/organizations")
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@NoCache
|
||||
@Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS)
|
||||
@Operation(summary = "Returns the organization associated with the user that has the specified id")
|
||||
public OrganizationRepresentation getOrganization(@PathParam("id") String id) {
|
||||
@Operation(summary = "Returns the organizations associated with the user that has the specified id")
|
||||
public Stream<OrganizationRepresentation> getOrganizations(@PathParam("id") String id) {
|
||||
if (StringUtil.isBlank(id)) {
|
||||
throw ErrorResponse.error("id cannot be null", Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
UserModel member = getMember(id);
|
||||
OrganizationModel organization = provider.getByMember(member);
|
||||
|
||||
if (organization == null) {
|
||||
throw ErrorResponse.error("Not associated with an organization", Status.NOT_FOUND);
|
||||
}
|
||||
return provider.getByMember(member).map((org) -> {
|
||||
OrganizationRepresentation organization = new OrganizationRepresentation();
|
||||
|
||||
OrganizationRepresentation rep = new OrganizationRepresentation();
|
||||
organization.setId(org.getId());
|
||||
organization.setName(org.getName());
|
||||
|
||||
rep.setId(organization.getId());
|
||||
|
||||
return rep;
|
||||
return organization;
|
||||
});
|
||||
}
|
||||
|
||||
private UserModel getMember(String id) {
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.keycloak.models.OrganizationModel;
|
|||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.organization.OrganizationProvider;
|
||||
import org.keycloak.organization.utils.Organizations;
|
||||
|
||||
import static org.keycloak.organization.utils.Organizations.isEnabledAndOrganizationsPresent;
|
||||
|
||||
|
@ -41,9 +42,10 @@ public class IdpAddOrganizationMemberAuthenticator extends AbstractIdpAuthentica
|
|||
|
||||
@Override
|
||||
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();
|
||||
OrganizationModel organization = (OrganizationModel) context.getSession().getAttribute(OrganizationModel.class.getName());
|
||||
OrganizationModel organization = Organizations.resolveOrganization(session);
|
||||
|
||||
if (organization == null) {
|
||||
context.attempted();
|
||||
|
@ -75,7 +77,7 @@ public class IdpAddOrganizationMemberAuthenticator extends AbstractIdpAuthentica
|
|||
return false;
|
||||
}
|
||||
|
||||
OrganizationModel organization = (OrganizationModel) session.getAttribute(OrganizationModel.class.getName());
|
||||
OrganizationModel organization = Organizations.resolveOrganization(session);
|
||||
|
||||
if (organization == null || !organization.isEnabled()) {
|
||||
return false;
|
||||
|
@ -83,4 +85,4 @@ public class IdpAddOrganizationMemberAuthenticator extends AbstractIdpAuthentica
|
|||
|
||||
return provider.getIdentityProviders(organization).findAny().isPresent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.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.Optional;
|
||||
|
||||
import jakarta.ws.rs.core.MultivaluedMap;
|
||||
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.freemarker.model.AuthenticationContextBean;
|
||||
import org.keycloak.forms.login.freemarker.model.IdentityProviderBean;
|
||||
import org.keycloak.forms.login.freemarker.model.OrganizationBean;
|
||||
import org.keycloak.http.HttpRequest;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
@ -38,13 +39,12 @@ import org.keycloak.models.OrganizationModel;
|
|||
import org.keycloak.models.OrganizationModel.IdentityProviderRedirectMode;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserProvider;
|
||||
import org.keycloak.models.utils.FormMessage;
|
||||
import org.keycloak.organization.OrganizationProvider;
|
||||
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.OrganizationAwareRealmBean;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.validation.Validation;
|
||||
|
||||
public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
|
||||
|
||||
|
@ -63,7 +63,7 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
|
|||
return;
|
||||
}
|
||||
|
||||
challenge(context);
|
||||
initialChallenge(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -71,119 +71,120 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
|
|||
HttpRequest request = context.getHttpRequest();
|
||||
MultivaluedMap<String, String> parameters = request.getDecodedFormParameters();
|
||||
String username = parameters.getFirst(UserModel.USERNAME);
|
||||
String emailDomain = getEmailDomain(username);
|
||||
|
||||
RealmModel realm = context.getRealm();
|
||||
OrganizationProvider provider = getOrganizationProvider();
|
||||
OrganizationModel organization = null;
|
||||
UserModel user = null;
|
||||
UserModel user = resolveUser(context, username);
|
||||
String domain = getEmailDomain(username);
|
||||
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) {
|
||||
// user in not member of an organization, go to the next authentication step/sub-flow
|
||||
context.attempted();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
organization = provider.getByDomainName(emailDomain);
|
||||
}
|
||||
|
||||
if (organization != null) {
|
||||
// make sure the organization is set to the session to make it available to templates
|
||||
session.setAttribute(OrganizationModel.class.getName(), organization);
|
||||
}
|
||||
|
||||
if (user == null) {
|
||||
user = session.users().getUserByEmail(realm, username);
|
||||
}
|
||||
|
||||
if (user != null) {
|
||||
// user exists, check if enabled
|
||||
if (!user.isEnabled()) {
|
||||
context.failure(AuthenticationFlowError.INVALID_USER);
|
||||
return;
|
||||
}
|
||||
|
||||
context.setUser(user);
|
||||
|
||||
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);
|
||||
|
||||
if (broker.size() == 1) {
|
||||
// user is a managed member and associated with a broker, redirect automatically
|
||||
redirect(context, broker.get(0).getAlias(), user.getEmail());
|
||||
return;
|
||||
}
|
||||
|
||||
context.attempted();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (organization == null || !organization.isEnabled()) {
|
||||
if (organization == null) {
|
||||
// request does not map to any organization, go to the next step/sub-flow
|
||||
context.attempted();
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure the organization is set to the session to make it available to templates
|
||||
session.setAttribute(OrganizationModel.class.getName(), organization);
|
||||
|
||||
if (tryRedirectBroker(context, organization, user, username, domain)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (user == null) {
|
||||
unkownUserChallenge(context, organization, realm);
|
||||
return;
|
||||
}
|
||||
|
||||
// user exists, check if enabled
|
||||
if (!user.isEnabled()) {
|
||||
context.failure(AuthenticationFlowError.INVALID_USER);
|
||||
return;
|
||||
}
|
||||
|
||||
context.attempted();
|
||||
}
|
||||
|
||||
@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) {
|
||||
// user is a managed member and associated with a broker, redirect automatically
|
||||
redirect(context, broker.get(0).getAlias(), user.getEmail());
|
||||
return true;
|
||||
}
|
||||
|
||||
return redirect(context, organization, username, domain);
|
||||
}
|
||||
|
||||
private boolean redirect(AuthenticationFlowContext context, OrganizationModel organization, String username, String domain) {
|
||||
if (domain == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
List<IdentityProviderModel> brokers = organization.getIdentityProviders().toList();
|
||||
|
||||
if (redirect(context, brokers, username, emailDomain)) {
|
||||
return;
|
||||
for (IdentityProviderModel broker : brokers) {
|
||||
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)) {
|
||||
// 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;
|
||||
return false;
|
||||
}
|
||||
|
||||
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
|
||||
// public organization brokers for selection
|
||||
LoginFormsProvider form = context.form()
|
||||
.setAttributeMapper(attributes -> {
|
||||
attributes.computeIfPresent("social",
|
||||
(key, bean) -> new OrganizationAwareIdentityProviderBean((IdentityProviderBean) bean, session, true)
|
||||
);
|
||||
if (hasPublicBrokers(organization)) {
|
||||
attributes.computeIfPresent("social",
|
||||
(key, bean) -> new OrganizationAwareIdentityProviderBean((IdentityProviderBean) bean, session, true)
|
||||
);
|
||||
// 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
|
||||
attributes.computeIfPresent("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)
|
||||
);
|
||||
attributes.computeIfPresent("realm",
|
||||
(key, bean) -> new OrganizationAwareRealmBean(realm)
|
||||
);
|
||||
|
||||
return attributes;
|
||||
});
|
||||
|
||||
form.addError(new FormMessage("Your email domain matches the " + organization.getName() + " organization but you don't have an account yet."));
|
||||
context.challenge(form
|
||||
.createLoginUsername());
|
||||
context.challenge(form.createLoginUsername());
|
||||
}
|
||||
|
||||
private static boolean hasPublicBrokers(List<IdentityProviderModel> brokers) {
|
||||
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){
|
||||
private void initialChallenge(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
|
||||
LoginFormsProvider form = context.form()
|
||||
.setAttributeMapper(attributes -> {
|
||||
|
@ -196,31 +197,14 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
|
|||
return attributes;
|
||||
});
|
||||
|
||||
if (username != null) {
|
||||
form.addError(new FormMessage(Validation.FIELD_USERNAME, Messages.INVALID_USER));
|
||||
}
|
||||
|
||||
context.challenge(form.createLoginUsername());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
return realm.isOrganizationsEnabled();
|
||||
private boolean hasPublicBrokers(OrganizationModel organization) {
|
||||
return organization.getIdentityProviders().anyMatch(p -> Boolean.parseBoolean(p.getConfig().getOrDefault(OrganizationModel.BROKER_PUBLIC, Boolean.FALSE.toString())));
|
||||
}
|
||||
|
||||
protected boolean redirect(AuthenticationFlowContext context, List<IdentityProviderModel> brokers, String username, String emailDomain) {
|
||||
for (IdentityProviderModel broker : brokers) {
|
||||
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;
|
||||
private OrganizationProvider getOrganizationProvider() {
|
||||
return session.getProvider(OrganizationProvider.class);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import org.keycloak.models.IdentityProviderModel;
|
|||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.OrganizationModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.organization.utils.Organizations;
|
||||
|
||||
public class OrganizationAwareIdentityProviderBean extends IdentityProviderBean {
|
||||
|
||||
|
@ -74,7 +75,7 @@ public class OrganizationAwareIdentityProviderBean extends IdentityProviderBean
|
|||
return false;
|
||||
}
|
||||
|
||||
OrganizationModel organization = (OrganizationModel) session.getAttribute(OrganizationModel.class.getName());
|
||||
OrganizationModel organization = Organizations.resolveOrganization(session);
|
||||
|
||||
if (organization != null && !organization.getId().equals(model.getOrganizationId())) {
|
||||
return false;
|
||||
|
|
|
@ -21,6 +21,8 @@ import java.util.ArrayList;
|
|||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.common.Profile;
|
||||
|
@ -84,14 +86,15 @@ public class OrganizationMembershipMapper extends AbstractOIDCProtocolMapper imp
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Map<String, Map<String, Object>> claim = new HashMap<>();
|
||||
claim.put(organization.getAlias(), Map.of());
|
||||
token.getOtherClaims().put(OAuth2Constants.ORGANIZATION, claim);
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
package org.keycloak.organization.protocol.mappers.saml;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.keycloak.Config.Scope;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.Profile.Feature;
|
||||
|
@ -65,16 +67,20 @@ public class OrganizationMembershipMapper extends AbstractSAMLProtocolMapper imp
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
|
|
|
@ -21,8 +21,11 @@ import static java.util.Optional.ofNullable;
|
|||
|
||||
import jakarta.ws.rs.core.MultivaluedMap;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -57,7 +60,7 @@ public class Organizations {
|
|||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
@ -65,18 +68,24 @@ public class Organizations {
|
|||
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);
|
||||
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();
|
||||
}
|
||||
|
||||
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();
|
||||
return session.users().getFederatedIdentitiesStream(realm, user)
|
||||
session.users().getFederatedIdentitiesStream(realm, user)
|
||||
.map(f -> {
|
||||
IdentityProviderModel broker = realm.getIdentityProviderByAlias(f.getIdentityProvider());
|
||||
|
||||
|
@ -92,10 +101,10 @@ public class Organizations {
|
|||
|
||||
return null;
|
||||
}).filter(Objects::nonNull)
|
||||
.toList();
|
||||
.forEach(brokers::add);
|
||||
}
|
||||
|
||||
return List.of();
|
||||
return brokers;
|
||||
}
|
||||
|
||||
public static Consumer<GroupModel> removeGroup(KeycloakSession session, RealmModel realm) {
|
||||
|
@ -105,7 +114,7 @@ public class Organizations {
|
|||
return;
|
||||
}
|
||||
|
||||
OrganizationModel current = (OrganizationModel) session.getAttribute(OrganizationModel.class.getName());
|
||||
OrganizationModel current = resolveOrganization(session);
|
||||
|
||||
try {
|
||||
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
|
||||
|
@ -218,26 +227,37 @@ public class Organizations {
|
|||
return email.substring(domainSeparator + 1);
|
||||
}
|
||||
|
||||
public static OrganizationModel resolveOrganization(KeycloakSession session) {
|
||||
return resolveOrganization(session, null, null);
|
||||
}
|
||||
|
||||
public static OrganizationModel resolveOrganization(KeycloakSession session, UserModel user) {
|
||||
OrganizationModel organization = (OrganizationModel) session.getAttribute(OrganizationModel.class.getName());
|
||||
return resolveOrganization(session, user, null);
|
||||
}
|
||||
|
||||
if (organization != null) {
|
||||
return organization;
|
||||
}
|
||||
public static OrganizationModel resolveOrganization(KeycloakSession session, UserModel user, String domain) {
|
||||
Optional<OrganizationModel> organization = Optional.ofNullable((OrganizationModel) session.getAttribute(OrganizationModel.class.getName()));
|
||||
|
||||
if (user == null) {
|
||||
return null;
|
||||
if (organization.isPresent()) {
|
||||
return organization.get();
|
||||
}
|
||||
|
||||
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
|
||||
OrganizationModel memberOrg = provider.getByMember(user);
|
||||
|
||||
if (memberOrg != null) {
|
||||
return memberOrg;
|
||||
organization = ofNullable(user).stream().flatMap(provider::getByMember)
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
package org.keycloak.organization.validator;
|
||||
|
||||
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 java.util.Collections;
|
||||
|
@ -36,7 +36,7 @@ import org.keycloak.models.KeycloakSession;
|
|||
import org.keycloak.models.OrganizationDomainModel;
|
||||
import org.keycloak.models.OrganizationModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.organization.OrganizationProvider;
|
||||
import org.keycloak.organization.utils.Organizations;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
import org.keycloak.userprofile.AttributeContext;
|
||||
import org.keycloak.userprofile.UserProfileAttributeValidationContext;
|
||||
|
@ -59,7 +59,10 @@ public class OrganizationMemberValidator extends AbstractSimpleValidator impleme
|
|||
@Override
|
||||
protected void doValidate(Object value, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||
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) {
|
||||
return;
|
||||
|
@ -121,7 +124,7 @@ public class OrganizationMemberValidator extends AbstractSimpleValidator impleme
|
|||
}
|
||||
|
||||
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()) {
|
||||
return Set.of();
|
||||
|
@ -164,23 +167,4 @@ public class OrganizationMemberValidator extends AbstractSimpleValidator impleme
|
|||
String brokerDomain = broker.getConfig().get(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -210,7 +210,7 @@ public class LinkedAccountsResource {
|
|||
}
|
||||
|
||||
if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
|
||||
if (Organizations.resolveBroker(session, user).stream()
|
||||
if (Organizations.resolveHomeBroker(session, user).stream()
|
||||
.map(IdentityProviderModel::getAlias)
|
||||
.anyMatch(providerAlias::equals)) {
|
||||
throw ErrorResponse.error(translateErrorMessage(Messages.FEDERATED_IDENTITY_BOUND_ORGANIZATION), Response.Status.BAD_REQUEST);
|
||||
|
|
|
@ -1082,7 +1082,6 @@ public class UserResource {
|
|||
|
||||
attributes.remove(UserModel.USERNAME);
|
||||
attributes.remove(UserModel.EMAIL);
|
||||
attributes.remove(OrganizationModel.ORGANIZATION_ATTRIBUTE);
|
||||
|
||||
return attributes.entrySet().stream()
|
||||
.filter(entry -> ofNullable(entry.getValue()).orElse(emptyList()).stream().anyMatch(StringUtil::isNotBlank))
|
||||
|
|
|
@ -354,12 +354,6 @@ public class DeclarativeUserProfileProviderFactory implements UserProfileProvide
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -152,7 +152,8 @@ public class OrganizationThemeTest extends AbstractOrganizationTest {
|
|||
oauth.clientId("broker-app");
|
||||
loginPage.open(bc.consumerRealmName());
|
||||
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);
|
||||
Assert.assertTrue("Driver should be on the consumer realm page right now",
|
||||
driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
|
||||
|
|
|
@ -123,12 +123,13 @@ public abstract class AbstractBrokerSelfRegistrationTest extends AbstractOrganiz
|
|||
public void testIdentityFirstUserNotExistEmailMatchBrokerDomainNoPublicBroker() {
|
||||
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||
IdentityProviderRepresentation idpRep = organization.identityProviders().getIdentityProviders().get(0);
|
||||
idpRep.getConfig().remove(IdentityProviderRedirectMode.EMAIL_MATCH.getKey());
|
||||
idpRep.getConfig().remove(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
|
||||
testRealm().identityProviders().get(idpRep.getAlias()).update(idpRep);
|
||||
|
||||
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.assertFalse(loginPage.isPasswordInputPresent());
|
||||
// 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());
|
||||
IdentityProviderRepresentation brokerRep = broker.toRepresentation();
|
||||
brokerRep.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString());
|
||||
brokerRep.getConfig().remove(IdentityProviderRedirectMode.EMAIL_MATCH.getKey());
|
||||
testRealm().identityProviders().get(brokerRep.getAlias()).update(brokerRep);
|
||||
|
||||
openIdentityFirstLoginPage(bc.getUserEmail(), true, brokerRep, false, true);
|
||||
|
@ -243,6 +245,7 @@ public abstract class AbstractBrokerSelfRegistrationTest extends AbstractOrganiz
|
|||
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||
OrganizationIdentityProviderResource broker = organization.identityProviders().get(bc.getIDPAlias());
|
||||
IdentityProviderRepresentation brokerRep = broker.toRepresentation();
|
||||
brokerRep.getConfig().remove(IdentityProviderRedirectMode.EMAIL_MATCH.getKey());
|
||||
brokerRep.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString());
|
||||
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());
|
||||
IdentityProviderRepresentation org1Broker = org1.identityProviders().getIdentityProviders().get(0);
|
||||
org1Broker.getConfig().remove(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
|
||||
org1Broker.getConfig().remove(IdentityProviderRedirectMode.EMAIL_MATCH.getKey());
|
||||
org1Broker.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString());
|
||||
testRealm().identityProviders().get(org1Broker.getAlias()).update(org1Broker);
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ import java.util.HashMap;
|
|||
import java.util.Map;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.TokenVerifier;
|
||||
|
@ -39,6 +40,7 @@ import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
|
|||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.organization.admin.AbstractOrganizationTest;
|
||||
import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse;
|
||||
|
@ -48,8 +50,19 @@ public class OrganizationOIDCProtocolMapperTest extends AbstractOrganizationTest
|
|||
|
||||
@Test
|
||||
public void testClaim() throws Exception {
|
||||
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||
addMember(organization);
|
||||
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));
|
||||
|
||||
member = getUserRepresentation(memberEmail);
|
||||
|
||||
oauth.clientId("direct-grant");
|
||||
oauth.scope("openid organization");
|
||||
|
@ -60,10 +73,13 @@ public class OrganizationOIDCProtocolMapperTest extends AbstractOrganizationTest
|
|||
|
||||
assertThat(accessToken.getOtherClaims().keySet(), hasItem(OAuth2Constants.ORGANIZATION));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> claim = (Map<String, Object>) accessToken.getOtherClaims().get(OAuth2Constants.ORGANIZATION);
|
||||
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
|
||||
|
|
|
@ -36,6 +36,7 @@ import org.keycloak.dom.saml.v2.assertion.AttributeType;
|
|||
import org.keycloak.protocol.saml.SamlConfigAttributes;
|
||||
import org.keycloak.protocol.saml.SamlProtocol;
|
||||
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.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
|
@ -53,6 +54,8 @@ public class OrganizationSAMLProtocolMapperTest extends AbstractOrganizationTest
|
|||
@Test
|
||||
public void testAttribute() {
|
||||
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||
IdentityProviderRepresentation broker = organization.identityProviders().getIdentityProviders().get(0);
|
||||
organization.identityProviders().get(broker.getAlias()).delete().close();
|
||||
addMember(organization);
|
||||
String clientId = "saml-client";
|
||||
testRealm().clients().create(ClientBuilder.create()
|
||||
|
|
|
@ -74,7 +74,7 @@ public class OrganizationMemberAuthenticationTest extends AbstractOrganizationTe
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticateUnmanagedMemberWehnProviderDisabled() throws IOException {
|
||||
public void testAuthenticateUnmanagedMemberWhenProviderDisabled() throws IOException {
|
||||
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||
UserRepresentation member = addMember(organization, "contractor@contractor.org");
|
||||
|
||||
|
|
|
@ -30,7 +30,6 @@ import static org.junit.Assert.assertNotNull;
|
|||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.keycloak.models.OrganizationModel.ORGANIZATION_ATTRIBUTE;
|
||||
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -43,19 +42,16 @@ import jakarta.ws.rs.core.Response.Status;
|
|||
import java.io.IOException;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.admin.client.resource.OrganizationMemberResource;
|
||||
import org.keycloak.admin.client.resource.OrganizationResource;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.common.Profile.Feature;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.OrganizationModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
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.IdentityProviderRepresentation;
|
||||
import org.keycloak.representations.idm.MemberRepresentation;
|
||||
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.pages.AppPage;
|
||||
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
|
||||
@EnableFeature(Feature.ORGANIZATION)
|
||||
public class OrganizationMemberTest extends AbstractOrganizationTest {
|
||||
|
@ -91,41 +88,6 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
|
|||
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
|
||||
public void testUserAlreadyMemberOfOrganization() {
|
||||
UPConfig upConfig = testRealm().users().userProfile().getConfiguration();
|
||||
|
@ -154,9 +116,9 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
|
|||
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||
UserRepresentation member = addMember(organization);
|
||||
OrganizationRepresentation expected = organization.toRepresentation();
|
||||
OrganizationRepresentation actual = organization.members().getOrganization(member.getId());
|
||||
List<OrganizationRepresentation> actual = organization.members().member(member.getId()).getOrganizations();
|
||||
assertNotNull(actual);
|
||||
assertEquals(expected.getId(), actual.getId());
|
||||
assertTrue(actual.stream().map(OrganizationRepresentation::getId).anyMatch(expected.getId()::equals));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -290,8 +252,7 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
|
|||
upConfig.setUnmanagedAttributePolicy(UnmanagedAttributePolicy.ENABLED);
|
||||
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||
UserRepresentation expected = addMember(organization);
|
||||
assertNotNull(expected.getAttributes());
|
||||
assertNull(expected.getAttributes().get(ORGANIZATION_ATTRIBUTE));
|
||||
assertNull(expected.getAttributes());
|
||||
OrganizationMemberResource member = organization.members().member(expected.getId());
|
||||
|
||||
try (Response response = member.delete()) {
|
||||
|
@ -322,10 +283,6 @@ 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());
|
||||
|
@ -352,7 +309,7 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
|
|||
for (MemberRepresentation member : expected) {
|
||||
try {
|
||||
// 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");
|
||||
} catch (NotFoundException ignore) {
|
||||
}
|
||||
|
@ -490,7 +447,7 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
|
|||
// assign IdP to the org
|
||||
idpRep.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, orgDomain);
|
||||
idpRep.getConfig().put(OrganizationModel.IdentityProviderRedirectMode.EMAIL_MATCH.getKey(), Boolean.TRUE.toString());
|
||||
|
||||
|
||||
try (Response response = testRealm().organizations().get(id).identityProviders().addIdentityProvider(idpAlias)) {
|
||||
assertThat(response.getStatus(), equalTo(Status.NO_CONTENT.getStatusCode()));
|
||||
}
|
||||
|
@ -499,6 +456,59 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
|
|||
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) {
|
||||
oauth.clientId("broker-app");
|
||||
loginPage.open(bc.consumerRealmName());
|
||||
|
@ -512,7 +522,7 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
|
|||
// user automatically redirected to the identity provider
|
||||
assertThat("Driver should be on the provider realm page right now",
|
||||
driver.getCurrentUrl(), Matchers.containsString("/auth/realms/" + bc.providerRealmName() + "/"));
|
||||
|
||||
|
||||
loginPage.login(bc.getUserLogin(), bc.getUserPassword());
|
||||
|
||||
waitForPage(driver, "update account information", false);
|
||||
|
@ -525,4 +535,8 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
|
|||
appPage.assertCurrent();
|
||||
assertThat(appPage.getRequestType(), equalTo(AppPage.RequestType.AUTH_RESPONSE));
|
||||
}
|
||||
|
||||
private UserRepresentation getUserRepFromMemberRep(MemberRepresentation member) {
|
||||
return new UserRepresentation(member);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue