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;
|
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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
@ -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 "
|
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.get(), expectedNumberOfUsersForProvider, currentFirst.get() - expectedNumberOfUsersForProvider);
|
||||||
currentFirst.set((int) (currentFirst.get() - expectedNumberOfUsersForProvider));
|
currentFirst.set((int) (currentFirst.get() - expectedNumberOfUsersForProvider));
|
||||||
return false;
|
return false;
|
||||||
})
|
})
|
||||||
// collecting stream of providers to ensure the filtering (above) is evaluated before we move forward to actual querying
|
// collecting stream of providers to ensure the filtering (above) is evaluated before we move forward to actual querying
|
||||||
.collect(Collectors.toList()).stream();
|
.collect(Collectors.toList()).stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needsAdditionalFirstResultFiltering.get() && currentFirst.get() > 0) {
|
if (needsAdditionalFirstResultFiltering.get() && currentFirst.get() > 0) {
|
||||||
|
@ -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)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -83,4 +85,4 @@ public class IdpAddOrganizationMemberAuthenticator extends AbstractIdpAuthentica
|
||||||
|
|
||||||
return provider.getIdentityProviders(organization).findAny().isPresent();
|
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.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) {
|
if (organization == 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()) {
|
|
||||||
// request does not map to any organization, go to the next step/sub-flow
|
// request does not map to any organization, go to the next step/sub-flow
|
||||||
context.attempted();
|
context.attempted();
|
||||||
return;
|
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();
|
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 -> {
|
||||||
attributes.computeIfPresent("social",
|
if (hasPublicBrokers(organization)) {
|
||||||
(key, bean) -> new OrganizationAwareIdentityProviderBean((IdentityProviderBean) bean, session, true)
|
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",
|
attributes.computeIfPresent("auth",
|
||||||
(key, bean) -> new OrganizationAwareAuthenticationContextBean((AuthenticationContextBean) bean, false)
|
(key, bean) -> new OrganizationAwareAuthenticationContextBean((AuthenticationContextBean) bean, false)
|
||||||
);
|
);
|
||||||
attributes.computeIfPresent("realm",
|
|
||||||
(key, bean) -> new OrganizationAwareRealmBean(realm)
|
|
||||||
);
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
return resolveOrganization(session, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
public static OrganizationModel resolveOrganization(KeycloakSession session, UserModel user) {
|
public static OrganizationModel resolveOrganization(KeycloakSession session, UserModel user) {
|
||||||
OrganizationModel organization = (OrganizationModel) session.getAttribute(OrganizationModel.class.getName());
|
return resolveOrganization(session, user, null);
|
||||||
|
}
|
||||||
|
|
||||||
if (organization != null) {
|
public static OrganizationModel resolveOrganization(KeycloakSession session, UserModel user, String domain) {
|
||||||
return organization;
|
Optional<OrganizationModel> organization = Optional.ofNullable((OrganizationModel) session.getAttribute(OrganizationModel.class.getName()));
|
||||||
}
|
|
||||||
|
|
||||||
if (user == null) {
|
if (organization.isPresent()) {
|
||||||
return null;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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() + "/"));
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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");
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
}
|
}
|
||||||
|
@ -490,7 +447,7 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
|
||||||
// assign IdP to the org
|
// assign IdP to the org
|
||||||
idpRep.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, orgDomain);
|
idpRep.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, orgDomain);
|
||||||
idpRep.getConfig().put(OrganizationModel.IdentityProviderRedirectMode.EMAIL_MATCH.getKey(), Boolean.TRUE.toString());
|
idpRep.getConfig().put(OrganizationModel.IdentityProviderRedirectMode.EMAIL_MATCH.getKey(), Boolean.TRUE.toString());
|
||||||
|
|
||||||
try (Response response = testRealm().organizations().get(id).identityProviders().addIdentityProvider(idpAlias)) {
|
try (Response response = testRealm().organizations().get(id).identityProviders().addIdentityProvider(idpAlias)) {
|
||||||
assertThat(response.getStatus(), equalTo(Status.NO_CONTENT.getStatusCode()));
|
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));
|
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());
|
||||||
|
@ -512,7 +522,7 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
|
||||||
// user automatically redirected to the identity provider
|
// user automatically redirected to the identity provider
|
||||||
assertThat("Driver should be on the provider realm page right now",
|
assertThat("Driver should be on the provider realm page right now",
|
||||||
driver.getCurrentUrl(), Matchers.containsString("/auth/realms/" + bc.providerRealmName() + "/"));
|
driver.getCurrentUrl(), Matchers.containsString("/auth/realms/" + bc.providerRealmName() + "/"));
|
||||||
|
|
||||||
loginPage.login(bc.getUserLogin(), bc.getUserPassword());
|
loginPage.login(bc.getUserLogin(), bc.getUserPassword());
|
||||||
|
|
||||||
waitForPage(driver, "update account information", false);
|
waitForPage(driver, "update account information", false);
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue