Allow adding realm users as an organization member
Closes #29023 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
d55a8b0b17
commit
51352622aa
19 changed files with 663 additions and 314 deletions
|
@ -17,10 +17,8 @@
|
||||||
|
|
||||||
package org.keycloak.admin.client.resource;
|
package org.keycloak.admin.client.resource;
|
||||||
|
|
||||||
import jakarta.ws.rs.Consumes;
|
|
||||||
import jakarta.ws.rs.DELETE;
|
import jakarta.ws.rs.DELETE;
|
||||||
import jakarta.ws.rs.GET;
|
import jakarta.ws.rs.GET;
|
||||||
import jakarta.ws.rs.PUT;
|
|
||||||
import jakarta.ws.rs.Produces;
|
import jakarta.ws.rs.Produces;
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
|
@ -32,10 +30,6 @@ public interface OrganizationMemberResource {
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
UserRepresentation toRepresentation();
|
UserRepresentation toRepresentation();
|
||||||
|
|
||||||
@PUT
|
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
|
||||||
Response update(UserRepresentation organization);
|
|
||||||
|
|
||||||
@DELETE
|
@DELETE
|
||||||
Response delete();
|
Response delete();
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ public interface OrganizationMembersResource {
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
Response addMember(UserRepresentation member);
|
Response addMember(String userId);
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
|
|
@ -67,4 +67,14 @@ public interface OrganizationsResource {
|
||||||
@QueryParam("first") Integer first,
|
@QueryParam("first") Integer first,
|
||||||
@QueryParam("max") Integer max
|
@QueryParam("max") Integer max
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all organizations that match the specified filter.
|
||||||
|
*
|
||||||
|
* @param search a {@code String} representing either an organization name or domain.
|
||||||
|
* @return a list containing the matched organizations.
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
List<OrganizationRepresentation> search(@QueryParam("search") String search);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,10 +17,11 @@
|
||||||
|
|
||||||
package org.keycloak.organization.jpa;
|
package org.keycloak.organization.jpa;
|
||||||
|
|
||||||
import static org.keycloak.models.OrganizationModel.USER_ORGANIZATION_ATTRIBUTE;
|
import static org.keycloak.models.OrganizationModel.ORGANIZATION_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;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
@ -29,6 +30,7 @@ import jakarta.persistence.EntityManager;
|
||||||
import jakarta.persistence.NoResultException;
|
import jakarta.persistence.NoResultException;
|
||||||
import jakarta.persistence.TypedQuery;
|
import jakarta.persistence.TypedQuery;
|
||||||
import org.keycloak.connections.jpa.JpaConnectionProvider;
|
import org.keycloak.connections.jpa.JpaConnectionProvider;
|
||||||
|
import org.keycloak.models.FederatedIdentityModel;
|
||||||
import org.keycloak.models.GroupModel;
|
import org.keycloak.models.GroupModel;
|
||||||
import org.keycloak.models.GroupProvider;
|
import org.keycloak.models.GroupProvider;
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
|
@ -94,7 +96,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
|
||||||
GroupModel group = getOrganizationGroup(organization);
|
GroupModel group = getOrganizationGroup(organization);
|
||||||
|
|
||||||
//TODO: won't scale, requires a better mechanism for bulk deleting users
|
//TODO: won't scale, requires a better mechanism for bulk deleting users
|
||||||
userProvider.getGroupMembersStream(realm, group).forEach(userModel -> userProvider.removeUser(realm, userModel));
|
userProvider.getGroupMembersStream(realm, group).forEach(userModel -> removeMember(organization, userModel));
|
||||||
groupProvider.removeGroup(realm, group);
|
groupProvider.removeGroup(realm, group);
|
||||||
|
|
||||||
realm.removeIdentityProviderByAlias(entity.getIdpAlias());
|
realm.removeIdentityProviderByAlias(entity.getIdpAlias());
|
||||||
|
@ -122,12 +124,12 @@ public class JpaOrganizationProvider implements OrganizationProvider {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.getFirstAttribute(USER_ORGANIZATION_ATTRIBUTE) != null) {
|
if (user.getFirstAttribute(ORGANIZATION_ATTRIBUTE) != null) {
|
||||||
throw new ModelException("User [" + user.getId() + "] is a member of a different organization");
|
throw new ModelException("User [" + user.getId() + "] is a member of a different organization");
|
||||||
}
|
}
|
||||||
|
|
||||||
user.joinGroup(group);
|
user.joinGroup(group);
|
||||||
user.setSingleAttribute(USER_ORGANIZATION_ATTRIBUTE, entity.getId());
|
user.setSingleAttribute(ORGANIZATION_ATTRIBUTE, entity.getId());
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -185,7 +187,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
String orgId = user.getFirstAttribute(USER_ORGANIZATION_ATTRIBUTE);
|
String orgId = user.getFirstAttribute(ORGANIZATION_ATTRIBUTE);
|
||||||
|
|
||||||
if (organization.getId().equals(orgId)) {
|
if (organization.getId().equals(orgId)) {
|
||||||
return user;
|
return user;
|
||||||
|
@ -198,7 +200,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
|
||||||
public OrganizationModel getByMember(UserModel member) {
|
public OrganizationModel getByMember(UserModel member) {
|
||||||
throwExceptionIfObjectIsNull(member, "User");
|
throwExceptionIfObjectIsNull(member, "User");
|
||||||
|
|
||||||
String orgId = member.getFirstAttribute(USER_ORGANIZATION_ATTRIBUTE);
|
String orgId = member.getFirstAttribute(ORGANIZATION_ATTRIBUTE);
|
||||||
|
|
||||||
if (orgId == null) {
|
if (orgId == null) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -214,6 +216,9 @@ public class JpaOrganizationProvider implements OrganizationProvider {
|
||||||
|
|
||||||
OrganizationEntity organizationEntity = getEntity(organization.getId());
|
OrganizationEntity organizationEntity = getEntity(organization.getId());
|
||||||
organizationEntity.setIdpAlias(identityProvider.getAlias());
|
organizationEntity.setIdpAlias(identityProvider.getAlias());
|
||||||
|
identityProvider.getConfig().put(ORGANIZATION_ATTRIBUTE, organization.getId());
|
||||||
|
realm.updateIdentityProvider(identityProvider);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,6 +241,48 @@ public class JpaOrganizationProvider implements OrganizationProvider {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isManagedMember(OrganizationModel organization, UserModel member) {
|
||||||
|
throwExceptionIfObjectIsNull(organization, "organization");
|
||||||
|
|
||||||
|
if (member == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
IdentityProviderModel identityProvider = organization.getIdentityProvider();
|
||||||
|
|
||||||
|
if (identityProvider == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FederatedIdentityModel federatedIdentity = userProvider.getFederatedIdentity(realm, member, identityProvider.getAlias());
|
||||||
|
|
||||||
|
return federatedIdentity != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean removeMember(OrganizationModel organization, UserModel member) {
|
||||||
|
throwExceptionIfObjectIsNull(organization, "organization");
|
||||||
|
throwExceptionIfObjectIsNull(member, "member");
|
||||||
|
|
||||||
|
OrganizationModel userOrg = getByMember(member);
|
||||||
|
|
||||||
|
if (userOrg == null || !userOrg.equals(organization)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isManagedMember(organization, member)) {
|
||||||
|
userProvider.removeUser(realm, member);
|
||||||
|
} else {
|
||||||
|
List<String> organizations = member.getAttributes().get(ORGANIZATION_ATTRIBUTE);
|
||||||
|
organizations.remove(organization.getId());
|
||||||
|
member.setAttribute(ORGANIZATION_ATTRIBUTE, organizations);
|
||||||
|
member.leaveGroup(getOrganizationGroup(organization));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
return getAllStream().findAny().isPresent();
|
return getAllStream().findAny().isPresent();
|
||||||
|
|
|
@ -32,6 +32,7 @@ import org.keycloak.models.ModelValidationException;
|
||||||
import org.keycloak.models.OrganizationDomainModel;
|
import org.keycloak.models.OrganizationDomainModel;
|
||||||
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.jpa.JpaModel;
|
import org.keycloak.models.jpa.JpaModel;
|
||||||
import org.keycloak.models.jpa.entities.OrganizationDomainEntity;
|
import org.keycloak.models.jpa.entities.OrganizationDomainEntity;
|
||||||
import org.keycloak.models.jpa.entities.OrganizationEntity;
|
import org.keycloak.models.jpa.entities.OrganizationEntity;
|
||||||
|
@ -133,6 +134,11 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
|
||||||
return provider.getIdentityProvider(this);
|
return provider.getIdentityProvider(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isManaged(UserModel user) {
|
||||||
|
return provider.isManagedMember(this, user);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public OrganizationEntity getEntity() {
|
public OrganizationEntity getEntity() {
|
||||||
return entity;
|
return entity;
|
||||||
|
@ -154,6 +160,20 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
|
||||||
.append(getGroupId()).toString();
|
.append(getGroupId()).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (!(o instanceof OrganizationModel)) return false;
|
||||||
|
|
||||||
|
OrganizationModel that = (OrganizationModel) o;
|
||||||
|
return that.getId().equals(getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return getId().hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
private OrganizationDomainModel toModel(OrganizationDomainEntity entity) {
|
private OrganizationDomainModel toModel(OrganizationDomainEntity entity) {
|
||||||
return new OrganizationDomainModel(entity.getName(), entity.isVerified());
|
return new OrganizationDomainModel(entity.getName(), entity.isVerified());
|
||||||
}
|
}
|
||||||
|
|
|
@ -156,4 +156,35 @@ public interface OrganizationProvider extends Provider {
|
||||||
* @return {@code true} if organization is supported. Otherwise, returns {@code false}
|
* @return {@code true} if organization is supported. Otherwise, returns {@code false}
|
||||||
*/
|
*/
|
||||||
boolean isEnabled();
|
boolean isEnabled();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Indicates if the given {@code member} is managed by the organization.
|
||||||
|
*
|
||||||
|
* <p>A member is managed by the organization whenever the member cannot exist without the organization they belong
|
||||||
|
* to so that their lifecycle is bound to the organization lifecycle. For instance, when a member is federated from
|
||||||
|
* the identity provider associated with an organization, there is a strong relationship between the member identity
|
||||||
|
* and the organization.
|
||||||
|
*
|
||||||
|
* <p>On the other hand, existing realm users whose identities are not intrinsically linked to an organization but
|
||||||
|
* are eventually joining an organization are not managed by the organization. They have a lifecycle that does not
|
||||||
|
* depend on the organization they are linked to.
|
||||||
|
*
|
||||||
|
* @param organization the organization
|
||||||
|
* @param member the member
|
||||||
|
* @return {@code true} if the {@code member} is managed by the given {@code organization}. Otherwise, returns {@code false}
|
||||||
|
*/
|
||||||
|
boolean isManagedMember(OrganizationModel organization, UserModel member);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Removes a member from the organization.
|
||||||
|
*
|
||||||
|
* <p>This method can either remove the given {@code member} entirely from the realm (and the organization) or only
|
||||||
|
* remove the link to the {@code organization} so that the user still exists but is no longer a member of the organization.
|
||||||
|
* The decision to remove the user entirely or only the link depends on whether the user is managed by the organization or not, respectively.
|
||||||
|
*
|
||||||
|
* @param organization the organization
|
||||||
|
* @param member the member
|
||||||
|
* @return {@code true} if the given {@code member} is a member and was successfully removed from the organization. Otherwise, returns {@code false}
|
||||||
|
*/
|
||||||
|
boolean removeMember(OrganizationModel organization, UserModel member);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ import java.util.stream.Stream;
|
||||||
|
|
||||||
public interface OrganizationModel {
|
public interface OrganizationModel {
|
||||||
|
|
||||||
String USER_ORGANIZATION_ATTRIBUTE = "kc.org";
|
String ORGANIZATION_ATTRIBUTE = "kc.org";
|
||||||
|
|
||||||
String getId();
|
String getId();
|
||||||
|
|
||||||
|
@ -41,4 +41,6 @@ public interface OrganizationModel {
|
||||||
void setDomains(Set<OrganizationDomainModel> domains);
|
void setDomains(Set<OrganizationDomainModel> domains);
|
||||||
|
|
||||||
IdentityProviderModel getIdentityProvider();
|
IdentityProviderModel getIdentityProvider();
|
||||||
|
|
||||||
|
boolean isManaged(UserModel user);
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,10 @@ public class IdentityProviderBean {
|
||||||
private RealmModel realm;
|
private RealmModel realm;
|
||||||
private final KeycloakSession session;
|
private final KeycloakSession session;
|
||||||
|
|
||||||
|
public IdentityProviderBean() {
|
||||||
|
this.session = null;
|
||||||
|
}
|
||||||
|
|
||||||
public IdentityProviderBean(RealmModel realm, KeycloakSession session, List<IdentityProviderModel> identityProviders, URI baseURI) {
|
public IdentityProviderBean(RealmModel realm, KeycloakSession session, List<IdentityProviderModel> identityProviders, URI baseURI) {
|
||||||
this.realm = realm;
|
this.realm = realm;
|
||||||
this.session = session;
|
this.session = session;
|
||||||
|
|
|
@ -79,32 +79,23 @@ public class OrganizationMemberResource {
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
public Response addMember(UserRepresentation rep) {
|
public Response addMember(String id) {
|
||||||
auth.realm().requireManageRealm();
|
auth.realm().requireManageRealm();
|
||||||
if (rep == null || !Objects.equals(rep.getUsername(), rep.getEmail())) {
|
UserModel user = session.users().getUserById(realm, id);
|
||||||
throw ErrorResponse.error("To add a member to the organization it is expected the username and the email is the same.", Status.BAD_REQUEST);
|
|
||||||
|
if (user == null) {
|
||||||
|
throw ErrorResponse.error("User does not exist", Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
UsersResource usersResource = new UsersResource(session, auth, adminEvent);
|
try {
|
||||||
Response response = usersResource.createUser(rep);
|
if (provider.addMember(organization, user)) {
|
||||||
|
return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(user.getId()).build()).build();
|
||||||
if (Status.CREATED.getStatusCode() == response.getStatus()) {
|
|
||||||
|
|
||||||
UserModel member = session.users().getUserByUsername(realm, rep.getEmail());
|
|
||||||
|
|
||||||
String errorMessage;
|
|
||||||
try {
|
|
||||||
if (provider.addMember(organization, member)) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
errorMessage = "Assigning the User as member of the organization was not succesful.";
|
|
||||||
} catch (ModelException me) {
|
|
||||||
errorMessage = me.getMessage();
|
|
||||||
}
|
}
|
||||||
throw ErrorResponse.error(errorMessage, Status.BAD_REQUEST);
|
} catch (ModelException me) {
|
||||||
|
throw ErrorResponse.error(me.getMessage(), Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
throw ErrorResponse.error("User is already a member of the organization.", Status.CONFLICT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
|
@ -136,7 +127,11 @@ public class OrganizationMemberResource {
|
||||||
|
|
||||||
UserModel member = getMember(id);
|
UserModel member = getMember(id);
|
||||||
|
|
||||||
return new UserResource(session, member, auth, adminEvent).deleteUser();
|
if (provider.removeMember(organization, member)) {
|
||||||
|
return Response.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ErrorResponse.error("Not a member of the organization", Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Path("{id}")
|
@Path("{id}")
|
||||||
|
@ -158,6 +153,11 @@ public class OrganizationMemberResource {
|
||||||
|
|
||||||
UserModel member = getMember(id);
|
UserModel member = getMember(id);
|
||||||
OrganizationModel organization = provider.getByMember(member);
|
OrganizationModel organization = provider.getByMember(member);
|
||||||
|
|
||||||
|
if (organization == null) {
|
||||||
|
throw ErrorResponse.error("Not associated with an organization", Status.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
OrganizationRepresentation rep = new OrganizationRepresentation();
|
OrganizationRepresentation rep = new OrganizationRepresentation();
|
||||||
|
|
||||||
rep.setId(organization.getId());
|
rep.setId(organization.getId());
|
||||||
|
|
|
@ -17,9 +17,12 @@
|
||||||
|
|
||||||
package org.keycloak.organization.authentication.authenticators.browser;
|
package org.keycloak.organization.authentication.authenticators.browser;
|
||||||
|
|
||||||
|
import java.util.function.BiFunction;
|
||||||
|
|
||||||
import jakarta.ws.rs.core.MultivaluedMap;
|
import jakarta.ws.rs.core.MultivaluedMap;
|
||||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||||
import org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator;
|
import org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator;
|
||||||
|
import org.keycloak.forms.login.freemarker.model.IdentityProviderBean;
|
||||||
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;
|
||||||
|
@ -40,7 +43,7 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
|
||||||
OrganizationProvider provider = getOrganizationProvider();
|
OrganizationProvider provider = getOrganizationProvider();
|
||||||
|
|
||||||
if (!provider.isEnabled()) {
|
if (!provider.isEnabled()) {
|
||||||
context.attempted();
|
attempted(context);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +64,7 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
|
||||||
String domain = getEmailDomain(username);
|
String domain = getEmailDomain(username);
|
||||||
|
|
||||||
if (domain == null) {
|
if (domain == null) {
|
||||||
context.attempted();
|
attempted(context);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,20 +72,33 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
|
||||||
OrganizationModel organization = provider.getByDomainName(domain);
|
OrganizationModel organization = provider.getByDomainName(domain);
|
||||||
|
|
||||||
if (organization == null) {
|
if (organization == null) {
|
||||||
context.attempted();
|
attempted(context);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
IdentityProviderModel identityProvider = organization.getIdentityProvider();
|
IdentityProviderModel identityProvider = organization.getIdentityProvider();
|
||||||
|
|
||||||
if (identityProvider == null) {
|
if (identityProvider == null) {
|
||||||
context.attempted();
|
attempted(context);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
redirect(context, identityProvider.getAlias(), username);
|
redirect(context, identityProvider.getAlias(), username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void attempted(AuthenticationFlowContext context) {
|
||||||
|
context.form()
|
||||||
|
.setAttributeMapper(attributes -> {
|
||||||
|
attributes.computeIfPresent("social", createOrganizationAwareSocialBean());
|
||||||
|
return attributes;
|
||||||
|
});
|
||||||
|
context.attempted();
|
||||||
|
}
|
||||||
|
|
||||||
|
private BiFunction<String, Object, IdentityProviderBean> createOrganizationAwareSocialBean() {
|
||||||
|
return (key, bean) -> new OrganizationAwareIdentityProviderBean((IdentityProviderBean) bean, session);
|
||||||
|
}
|
||||||
|
|
||||||
private OrganizationProvider getOrganizationProvider() {
|
private OrganizationProvider getOrganizationProvider() {
|
||||||
return session.getProvider(OrganizationProvider.class);
|
return session.getProvider(OrganizationProvider.class);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.organization.authentication.authenticators.browser;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.keycloak.forms.login.freemarker.model.IdentityProviderBean;
|
||||||
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.OrganizationModel;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
|
||||||
|
public class OrganizationAwareIdentityProviderBean extends IdentityProviderBean {
|
||||||
|
|
||||||
|
private final IdentityProviderBean delegate;
|
||||||
|
private final KeycloakSession session;
|
||||||
|
|
||||||
|
public OrganizationAwareIdentityProviderBean(IdentityProviderBean delegate, KeycloakSession session) {
|
||||||
|
this.delegate = delegate;
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<IdentityProvider> getProviders() {
|
||||||
|
return Optional.ofNullable(delegate.getProviders()).orElse(List.of()).stream()
|
||||||
|
.filter(this::filterOrganizationalIdentityProvider)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDisplayInfo() {
|
||||||
|
return delegate.isDisplayInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean filterOrganizationalIdentityProvider(IdentityProvider idp) {
|
||||||
|
RealmModel realm = session.getContext().getRealm();
|
||||||
|
IdentityProviderModel model = realm.getIdentityProviderByAlias(idp.getAlias());
|
||||||
|
Map<String, String> config = model.getConfig();
|
||||||
|
return !config.containsKey(OrganizationModel.ORGANIZATION_ATTRIBUTE);
|
||||||
|
}
|
||||||
|
}
|
|
@ -70,7 +70,7 @@ public class OrganizationMemberValidator extends AbstractSimpleValidator impleme
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateEmailDomain(String email, String inputHint, ValidationContext context, OrganizationModel organization) {
|
private void validateEmailDomain(String email, String inputHint, ValidationContext context, OrganizationModel organization) {
|
||||||
if (UserModel.USERNAME.equals(inputHint) || UserModel.EMAIL.equals(inputHint)) {
|
if (UserModel.EMAIL.equals(inputHint)) {
|
||||||
if (StringUtil.isBlank(email)) {
|
if (StringUtil.isBlank(email)) {
|
||||||
context.addError(new ValidationError(ID, inputHint, "Email not set"));
|
context.addError(new ValidationError(ID, inputHint, "Email not set"));
|
||||||
return;
|
return;
|
||||||
|
@ -80,6 +80,14 @@ public class OrganizationMemberValidator extends AbstractSimpleValidator impleme
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UserProfileAttributeValidationContext upContext = (UserProfileAttributeValidationContext) context;
|
||||||
|
AttributeContext attributeContext = upContext.getAttributeContext();
|
||||||
|
UserModel user = attributeContext.getUser();
|
||||||
|
|
||||||
|
if (!organization.isManaged(user)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
String domain = email.substring(email.indexOf('@') + 1);
|
String domain = email.substring(email.indexOf('@') + 1);
|
||||||
Stream<OrganizationDomainModel> expectedDomains = organization.getDomains();
|
Stream<OrganizationDomainModel> expectedDomains = organization.getDomains();
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,7 @@ import org.keycloak.component.ComponentValidationException;
|
||||||
import org.keycloak.models.KeycloakContext;
|
import org.keycloak.models.KeycloakContext;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.models.OrganizationModel;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RequiredActionProviderModel;
|
import org.keycloak.models.RequiredActionProviderModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
@ -56,6 +57,7 @@ import org.keycloak.userprofile.validator.BrokeringFederatedUsernameHasValueVali
|
||||||
import org.keycloak.userprofile.validator.DuplicateEmailValidator;
|
import org.keycloak.userprofile.validator.DuplicateEmailValidator;
|
||||||
import org.keycloak.userprofile.validator.DuplicateUsernameValidator;
|
import org.keycloak.userprofile.validator.DuplicateUsernameValidator;
|
||||||
import org.keycloak.userprofile.validator.EmailExistsAsUsernameValidator;
|
import org.keycloak.userprofile.validator.EmailExistsAsUsernameValidator;
|
||||||
|
import org.keycloak.userprofile.validator.ImmutableAttributeValidator;
|
||||||
import org.keycloak.userprofile.validator.ReadOnlyAttributeUnchangedValidator;
|
import org.keycloak.userprofile.validator.ReadOnlyAttributeUnchangedValidator;
|
||||||
import org.keycloak.userprofile.validator.RegistrationEmailAsUsernameEmailValueValidator;
|
import org.keycloak.userprofile.validator.RegistrationEmailAsUsernameEmailValueValidator;
|
||||||
import org.keycloak.userprofile.validator.RegistrationEmailAsUsernameUsernameValueValidator;
|
import org.keycloak.userprofile.validator.RegistrationEmailAsUsernameUsernameValueValidator;
|
||||||
|
@ -348,10 +350,20 @@ public class DeclarativeUserProfileProviderFactory implements UserProfileProvide
|
||||||
for (AttributeMetadata attribute : metadata.getAttributes()) {
|
for (AttributeMetadata attribute : metadata.getAttributes()) {
|
||||||
String name = attribute.getName();
|
String name = attribute.getName();
|
||||||
|
|
||||||
if (UserModel.EMAIL.equals(name) || UserModel.USERNAME.equals(name)) {
|
if (UserModel.EMAIL.equals(name)) {
|
||||||
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))
|
||||||
|
.addWriteCondition(context -> {
|
||||||
|
// the attribute can only be managed within the scope of the Organization API
|
||||||
|
// we assume, for now, that if the organization is set as a session attribute, we are operating within the scope if the Organization API
|
||||||
|
KeycloakSession session = context.getSession();
|
||||||
|
return session.getAttribute(OrganizationModel.class.getName()) != null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ import java.util.function.Function;
|
||||||
|
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import jakarta.ws.rs.core.Response.Status;
|
import jakarta.ws.rs.core.Response.Status;
|
||||||
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
import org.keycloak.admin.client.resource.OrganizationResource;
|
import org.keycloak.admin.client.resource.OrganizationResource;
|
||||||
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
|
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
|
||||||
import org.keycloak.representations.idm.OrganizationRepresentation;
|
import org.keycloak.representations.idm.OrganizationRepresentation;
|
||||||
|
@ -34,6 +35,10 @@ import org.keycloak.testsuite.admin.AbstractAdminTest;
|
||||||
import org.keycloak.testsuite.admin.ApiUtil;
|
import org.keycloak.testsuite.admin.ApiUtil;
|
||||||
import org.keycloak.testsuite.admin.Users;
|
import org.keycloak.testsuite.admin.Users;
|
||||||
import org.keycloak.testsuite.broker.KcOidcBrokerConfiguration;
|
import org.keycloak.testsuite.broker.KcOidcBrokerConfiguration;
|
||||||
|
import org.keycloak.testsuite.pages.AppPage;
|
||||||
|
import org.keycloak.testsuite.pages.IdpConfirmLinkPage;
|
||||||
|
import org.keycloak.testsuite.pages.LoginPage;
|
||||||
|
import org.keycloak.testsuite.pages.UpdateAccountInformationPage;
|
||||||
import org.keycloak.testsuite.util.UserBuilder;
|
import org.keycloak.testsuite.util.UserBuilder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -77,6 +82,18 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected LoginPage loginPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected IdpConfirmLinkPage idpConfirmLinkPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected UpdateAccountInformationPage updateAccountInformationPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected AppPage appPage;
|
||||||
|
|
||||||
protected KcOidcBrokerConfiguration bc = brokerConfigFunction.apply(organizationName);
|
protected KcOidcBrokerConfiguration bc = brokerConfigFunction.apply(organizationName);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -141,13 +158,20 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
|
||||||
expected.setEnabled(true);
|
expected.setEnabled(true);
|
||||||
Users.setPasswordFor(expected, memberPassword);
|
Users.setPasswordFor(expected, memberPassword);
|
||||||
|
|
||||||
try (Response response = organization.members().addMember(expected)) {
|
try (Response response = testRealm().users().create(expected)) {
|
||||||
|
expected.setId(ApiUtil.getCreatedId(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
getCleanup().addCleanup(() -> testRealm().users().get(expected.getId()).remove());
|
||||||
|
|
||||||
|
String userId = expected.getId();
|
||||||
|
|
||||||
|
try (Response response = organization.members().addMember(userId)) {
|
||||||
assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
|
assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
|
||||||
String id = ApiUtil.getCreatedId(response);
|
UserRepresentation actual = organization.members().member(userId).toRepresentation();
|
||||||
UserRepresentation actual = organization.members().member(id).toRepresentation();
|
|
||||||
|
|
||||||
assertNotNull(expected);
|
assertNotNull(expected);
|
||||||
assertEquals(id, actual.getId());
|
assertEquals(userId, actual.getId());
|
||||||
assertEquals(expected.getUsername(), actual.getUsername());
|
assertEquals(expected.getUsername(), actual.getUsername());
|
||||||
assertEquals(expected.getEmail(), actual.getEmail());
|
assertEquals(expected.getEmail(), actual.getEmail());
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,189 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.testsuite.organization.admin;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.ForbiddenException;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import jakarta.ws.rs.core.Response.Status;
|
||||||
|
import org.hamcrest.Matchers;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.admin.client.Keycloak;
|
||||||
|
import org.keycloak.admin.client.resource.RealmResource;
|
||||||
|
import org.keycloak.common.Profile.Feature;
|
||||||
|
import org.keycloak.models.AdminRoles;
|
||||||
|
import org.keycloak.models.Constants;
|
||||||
|
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||||
|
import org.keycloak.representations.idm.OrganizationRepresentation;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
import org.keycloak.testsuite.admin.ApiUtil;
|
||||||
|
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||||
|
import org.keycloak.testsuite.util.AdminClientUtil;
|
||||||
|
import org.keycloak.testsuite.util.UserBuilder;
|
||||||
|
|
||||||
|
@EnableFeature(Feature.ORGANIZATION)
|
||||||
|
public class OrganizationAdminPermissionsTest extends AbstractOrganizationTest {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
|
testRealm.getUsers().add(UserBuilder.create().username("realmAdmin").password("password")
|
||||||
|
.role(Constants.REALM_MANAGEMENT_CLIENT_ID, AdminRoles.MANAGE_REALM)
|
||||||
|
.role(Constants.REALM_MANAGEMENT_CLIENT_ID, AdminRoles.MANAGE_IDENTITY_PROVIDERS)
|
||||||
|
.role(Constants.REALM_MANAGEMENT_CLIENT_ID, AdminRoles.MANAGE_USERS)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testManageRealmRole() throws Exception {
|
||||||
|
try (
|
||||||
|
Keycloak manageRealmAdminClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(),
|
||||||
|
TEST_REALM_NAME, "realmAdmin", "password", Constants.ADMIN_CLI_CLIENT_ID, null);
|
||||||
|
Keycloak userAdminClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(),
|
||||||
|
TEST_REALM_NAME, "test-user@localhost", "password", Constants.ADMIN_CLI_CLIENT_ID, null)
|
||||||
|
) {
|
||||||
|
RealmResource realmAdminResource = manageRealmAdminClient.realm(TEST_REALM_NAME);
|
||||||
|
RealmResource realmUserResource = userAdminClient.realm(TEST_REALM_NAME);
|
||||||
|
|
||||||
|
/* Org */
|
||||||
|
//create org
|
||||||
|
OrganizationRepresentation orgRep = createRepresentation("testOrg", "testOrg.org");
|
||||||
|
String orgId;
|
||||||
|
try (
|
||||||
|
Response userResponse = realmUserResource.organizations().create(orgRep);
|
||||||
|
Response adminResponse = realmAdminResource.organizations().create(orgRep)
|
||||||
|
) {
|
||||||
|
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
|
||||||
|
assertThat(adminResponse.getStatus(), equalTo(Status.CREATED.getStatusCode()));
|
||||||
|
orgId = ApiUtil.getCreatedId(adminResponse);
|
||||||
|
getCleanup().addCleanup(() -> testRealm().organizations().get(orgId).delete().close());
|
||||||
|
}
|
||||||
|
|
||||||
|
//search for org
|
||||||
|
try {
|
||||||
|
realmUserResource.organizations().search("testOrg.org");
|
||||||
|
fail("Expected ForbiddenException");
|
||||||
|
} catch (ForbiddenException expected) {}
|
||||||
|
assertThat(realmAdminResource.organizations().search("testOrg.org"), Matchers.notNullValue());
|
||||||
|
|
||||||
|
//get org
|
||||||
|
try {
|
||||||
|
realmUserResource.organizations().get(orgId).toRepresentation();
|
||||||
|
fail("Expected ForbiddenException");
|
||||||
|
} catch (ForbiddenException expected) {}
|
||||||
|
assertThat(realmAdminResource.organizations().get(orgId).toRepresentation(), Matchers.notNullValue());
|
||||||
|
|
||||||
|
//update org
|
||||||
|
try (Response userResponse = realmUserResource.organizations().get(orgId).update(orgRep)) {
|
||||||
|
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
//delete org
|
||||||
|
try (Response userResponse = realmUserResource.organizations().get(orgId).delete()) {
|
||||||
|
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* IdP */
|
||||||
|
IdentityProviderRepresentation idpRep = new IdentityProviderRepresentation();
|
||||||
|
idpRep.setAlias("dummy");
|
||||||
|
idpRep.setProviderId("oidc");
|
||||||
|
//create IdP
|
||||||
|
try (
|
||||||
|
Response userResponse = realmUserResource.organizations().get(orgId).identityProvider().create(idpRep);
|
||||||
|
Response adminResponse = realmAdminResource.organizations().get(orgId).identityProvider().create(idpRep)
|
||||||
|
) {
|
||||||
|
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
|
||||||
|
assertThat(adminResponse.getStatus(), equalTo(Status.CREATED.getStatusCode()));
|
||||||
|
getCleanup().addCleanup(() -> testRealm().organizations().get(orgId).identityProvider().delete().close());
|
||||||
|
}
|
||||||
|
|
||||||
|
//get IdP
|
||||||
|
try {
|
||||||
|
//we should get 403, not 400 or 404 etc.
|
||||||
|
realmUserResource.organizations().get("non-existing").identityProvider().toRepresentation();
|
||||||
|
fail("Expected ForbiddenException");
|
||||||
|
} catch (ForbiddenException expected) {}
|
||||||
|
try {
|
||||||
|
realmUserResource.organizations().get(orgId).identityProvider().toRepresentation();
|
||||||
|
fail("Expected ForbiddenException");
|
||||||
|
} catch (ForbiddenException expected) {}
|
||||||
|
assertThat(realmAdminResource.organizations().get(orgId).identityProvider().toRepresentation(), Matchers.notNullValue());
|
||||||
|
|
||||||
|
//update IdP
|
||||||
|
try (Response userResponse = realmUserResource.organizations().get(orgId).identityProvider().update(idpRep)) {
|
||||||
|
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
//delete IdP
|
||||||
|
try (Response userResponse = realmUserResource.organizations().get(orgId).identityProvider().delete()) {
|
||||||
|
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Members */
|
||||||
|
UserRepresentation userRep = UserBuilder.create()
|
||||||
|
.username("user@testOrg.org")
|
||||||
|
.email("user@testOrg.org")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try (Response response = realmAdminResource.users().create(userRep)) {
|
||||||
|
userRep.setId(ApiUtil.getCreatedId(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
String userId;
|
||||||
|
|
||||||
|
//create member
|
||||||
|
try (
|
||||||
|
Response userResponse = realmUserResource.organizations().get(orgId).members().addMember(userRep.getId());
|
||||||
|
Response adminResponse = realmAdminResource.organizations().get(orgId).members().addMember(userRep.getId())
|
||||||
|
) {
|
||||||
|
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
|
||||||
|
assertThat(adminResponse.getStatus(), equalTo(Status.CREATED.getStatusCode()));
|
||||||
|
userId = ApiUtil.getCreatedId(adminResponse);
|
||||||
|
assertThat(userId, Matchers.notNullValue());
|
||||||
|
getCleanup().addCleanup(() -> testRealm().organizations().get(orgId).members().member(userId).delete().close());
|
||||||
|
}
|
||||||
|
|
||||||
|
//get members
|
||||||
|
try {
|
||||||
|
//we should get 403, not 400 or 404 etc.
|
||||||
|
realmUserResource.organizations().get("non-existing").members().getAll();
|
||||||
|
fail("Expected ForbiddenException");
|
||||||
|
} catch (ForbiddenException expected) {}
|
||||||
|
try {
|
||||||
|
realmUserResource.organizations().get(orgId).members().getAll();
|
||||||
|
fail("Expected ForbiddenException");
|
||||||
|
} catch (ForbiddenException expected) {}
|
||||||
|
assertThat(realmAdminResource.organizations().get(orgId).members().getAll(), Matchers.notNullValue());
|
||||||
|
|
||||||
|
//get member
|
||||||
|
try {
|
||||||
|
realmUserResource.organizations().get(orgId).members().member(userId).toRepresentation();
|
||||||
|
fail("Expected ForbiddenException");
|
||||||
|
} catch (ForbiddenException expected) {}
|
||||||
|
assertThat(realmAdminResource.organizations().get(orgId).members().member(userId).toRepresentation(), Matchers.notNullValue());
|
||||||
|
|
||||||
|
//delete member
|
||||||
|
try (Response userResponse = realmUserResource.organizations().get(orgId).members().member(userId).delete()) {
|
||||||
|
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,41 +17,32 @@
|
||||||
|
|
||||||
package org.keycloak.testsuite.organization.admin;
|
package org.keycloak.testsuite.organization.admin;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
|
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.jboss.arquillian.graphene.page.Page;
|
import jakarta.ws.rs.BadRequestException;
|
||||||
|
import jakarta.ws.rs.NotFoundException;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
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.UsersResource;
|
import org.keycloak.admin.client.resource.UsersResource;
|
||||||
import org.keycloak.common.Profile.Feature;
|
import org.keycloak.common.Profile.Feature;
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
import org.keycloak.representations.idm.ErrorRepresentation;
|
||||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||||
import org.keycloak.representations.idm.UserRepresentation;
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
import org.keycloak.testsuite.Assert;
|
import org.keycloak.testsuite.Assert;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||||
import org.keycloak.testsuite.pages.AppPage;
|
|
||||||
import org.keycloak.testsuite.pages.IdpConfirmLinkPage;
|
|
||||||
import org.keycloak.testsuite.pages.LoginPage;
|
|
||||||
import org.keycloak.testsuite.pages.UpdateAccountInformationPage;
|
|
||||||
import org.keycloak.testsuite.util.UserBuilder;
|
import org.keycloak.testsuite.util.UserBuilder;
|
||||||
|
|
||||||
@EnableFeature(Feature.ORGANIZATION)
|
@EnableFeature(Feature.ORGANIZATION)
|
||||||
public class OrganizationBrokerSelfRegistrationTest extends AbstractOrganizationTest {
|
public class OrganizationBrokerSelfRegistrationTest extends AbstractOrganizationTest {
|
||||||
|
|
||||||
@Page
|
|
||||||
protected LoginPage loginPage;
|
|
||||||
|
|
||||||
@Page
|
|
||||||
protected IdpConfirmLinkPage idpConfirmLinkPage;
|
|
||||||
|
|
||||||
@Page
|
|
||||||
protected UpdateAccountInformationPage updateAccountInformationPage;
|
|
||||||
|
|
||||||
@Page
|
|
||||||
protected AppPage appPage;
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testBrokerRegistration() {
|
public void testBrokerRegistration() {
|
||||||
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
|
@ -80,39 +71,6 @@ public class OrganizationBrokerSelfRegistrationTest extends AbstractOrganization
|
||||||
Assert.assertEquals(bc.getUserEmail(), loginPage.getUsername());
|
Assert.assertEquals(bc.getUserEmail(), loginPage.getUsername());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testDefaultAuthenticationMechanismIfNotOrganizationMember() {
|
|
||||||
testRealm().organizations().get(createOrganization().getId());
|
|
||||||
oauth.clientId("broker-app");
|
|
||||||
|
|
||||||
// login with email only
|
|
||||||
loginPage.open(bc.consumerRealmName());
|
|
||||||
log.debug("Logging in");
|
|
||||||
Assert.assertFalse(loginPage.isPasswordInputPresent());
|
|
||||||
loginPage.loginUsername("user@noorg.org");
|
|
||||||
|
|
||||||
// check if the login page is shown
|
|
||||||
Assert.assertTrue(loginPage.isUsernameInputPresent());
|
|
||||||
Assert.assertTrue(loginPage.isPasswordInputPresent());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testTryLoginWithUsernameNotAnEmail() {
|
|
||||||
testRealm().organizations().get(createOrganization().getId());
|
|
||||||
oauth.clientId("broker-app");
|
|
||||||
|
|
||||||
// login with email only
|
|
||||||
loginPage.open(bc.consumerRealmName());
|
|
||||||
log.debug("Logging in");
|
|
||||||
Assert.assertFalse(loginPage.isPasswordInputPresent());
|
|
||||||
loginPage.loginUsername("user");
|
|
||||||
|
|
||||||
// check if the login page is shown
|
|
||||||
Assert.assertTrue(loginPage.isUsernameInputPresent());
|
|
||||||
Assert.assertTrue(loginPage.isPasswordInputPresent());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testLinkExistingAccount() {
|
public void testLinkExistingAccount() {
|
||||||
// create a realm user in the consumer realm
|
// create a realm user in the consumer realm
|
||||||
|
@ -182,6 +140,55 @@ public class OrganizationBrokerSelfRegistrationTest extends AbstractOrganization
|
||||||
assertIsMember(bc.getUserEmail(), organization);
|
assertIsMember(bc.getUserEmail(), organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFailUpdateEmailWithDomainDifferentThanOrganization() {
|
||||||
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
|
|
||||||
|
// add the member for the first time
|
||||||
|
assertBrokerRegistration(organization);
|
||||||
|
UserRepresentation member = getUserRepresentation(bc.getUserEmail());
|
||||||
|
|
||||||
|
member.setEmail(KeycloakModelUtils.generateId() + "@user.org");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// member has a hard link with the organization, and the email must match the domains set to the organization
|
||||||
|
testRealm().users().get(member.getId()).update(member);
|
||||||
|
fail("Should fail because email domain does not match any from organization");
|
||||||
|
} catch (BadRequestException expected) {
|
||||||
|
ErrorRepresentation error = expected.getResponse().readEntity(ErrorRepresentation.class);
|
||||||
|
assertEquals(UserModel.EMAIL, error.getField());
|
||||||
|
assertEquals("Email domain does not match any domain from the organization", error.getErrorMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
member.setEmail(member.getEmail().replace("@user.org", "@" + organizationName + ".org"));
|
||||||
|
testRealm().users().get(member.getId()).update(member);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDelete() {
|
||||||
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
|
|
||||||
|
// add the member for the first time
|
||||||
|
assertBrokerRegistration(organization);
|
||||||
|
UserRepresentation member = getUserRepresentation(bc.getUserEmail());
|
||||||
|
member.setEmail(KeycloakModelUtils.generateId() + "@user.org");
|
||||||
|
OrganizationMemberResource organizationMember = organization.members().member(member.getId());
|
||||||
|
|
||||||
|
organizationMember.delete().close();
|
||||||
|
|
||||||
|
try {
|
||||||
|
testRealm().users().get(member.getId()).toRepresentation();
|
||||||
|
fail("it is managed member should be removed from the realm");
|
||||||
|
} catch (NotFoundException expected) {
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
organizationMember.toRepresentation();
|
||||||
|
fail("it is managed member should be removed from the realm");
|
||||||
|
} catch (NotFoundException expected) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void assertBrokerRegistration(OrganizationResource organization) {
|
private void assertBrokerRegistration(OrganizationResource organization) {
|
||||||
// login with email only
|
// login with email only
|
||||||
oauth.clientId("broker-app");
|
oauth.clientId("broker-app");
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.testsuite.organization.admin;
|
||||||
|
|
||||||
|
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.admin.client.resource.OrganizationResource;
|
||||||
|
import org.keycloak.common.Profile.Feature;
|
||||||
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
import org.keycloak.testsuite.Assert;
|
||||||
|
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||||
|
|
||||||
|
@EnableFeature(Feature.ORGANIZATION)
|
||||||
|
public class OrganizationMemberAuthenticationTest extends AbstractOrganizationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAuthenticateUnmanagedMember() {
|
||||||
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
|
UserRepresentation member = addMember(organization, "contractor@contractor.org");
|
||||||
|
|
||||||
|
// first try to log in using only the email
|
||||||
|
oauth.clientId("broker-app");
|
||||||
|
loginPage.open(bc.consumerRealmName());
|
||||||
|
Assert.assertFalse(loginPage.isPasswordInputPresent());
|
||||||
|
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
|
||||||
|
loginPage.loginUsername(member.getEmail());
|
||||||
|
|
||||||
|
// the email does not match an organization so redirect to the realm's default authentication mechanism
|
||||||
|
waitForPage(driver, "sign in to", true);
|
||||||
|
Assert.assertTrue("Driver should be on the provider realm page right now",
|
||||||
|
driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
|
||||||
|
Assert.assertTrue(loginPage.isPasswordInputPresent());
|
||||||
|
Assert.assertEquals(member.getEmail(), loginPage.getUsername());
|
||||||
|
// no idp should be shown because there is only a single idp that is bound to an organization
|
||||||
|
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
|
||||||
|
|
||||||
|
// the member should be able to log in using the credentials
|
||||||
|
loginPage.login(member.getEmail(), memberPassword);
|
||||||
|
appPage.assertCurrent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTryLoginWithUsernameNotAnEmail() {
|
||||||
|
testRealm().organizations().get(createOrganization().getId());
|
||||||
|
oauth.clientId("broker-app");
|
||||||
|
|
||||||
|
// login with email only
|
||||||
|
loginPage.open(bc.consumerRealmName());
|
||||||
|
log.debug("Logging in");
|
||||||
|
Assert.assertFalse(loginPage.isPasswordInputPresent());
|
||||||
|
loginPage.loginUsername("user");
|
||||||
|
|
||||||
|
// check if the login page is shown
|
||||||
|
Assert.assertTrue(loginPage.isUsernameInputPresent());
|
||||||
|
Assert.assertTrue(loginPage.isPasswordInputPresent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDefaultAuthenticationMechanismIfNotOrganizationMember() {
|
||||||
|
testRealm().organizations().get(createOrganization().getId());
|
||||||
|
oauth.clientId("broker-app");
|
||||||
|
|
||||||
|
// login with email only
|
||||||
|
loginPage.open(bc.consumerRealmName());
|
||||||
|
log.debug("Logging in");
|
||||||
|
Assert.assertFalse(loginPage.isPasswordInputPresent());
|
||||||
|
loginPage.loginUsername("user@noorg.org");
|
||||||
|
|
||||||
|
// check if the login page is shown
|
||||||
|
Assert.assertTrue(loginPage.isUsernameInputPresent());
|
||||||
|
Assert.assertTrue(loginPage.isPasswordInputPresent());
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,12 +17,14 @@
|
||||||
|
|
||||||
package org.keycloak.testsuite.organization.admin;
|
package org.keycloak.testsuite.organization.admin;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
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.USER_ORGANIZATION_ATTRIBUTE;
|
import static org.keycloak.models.OrganizationModel.ORGANIZATION_ATTRIBUTE;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -31,16 +33,19 @@ import jakarta.ws.rs.BadRequestException;
|
||||||
import jakarta.ws.rs.NotFoundException;
|
import jakarta.ws.rs.NotFoundException;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import jakarta.ws.rs.core.Response.Status;
|
import jakarta.ws.rs.core.Response.Status;
|
||||||
|
import org.hamcrest.Matchers;
|
||||||
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.common.Profile.Feature;
|
import org.keycloak.common.Profile.Feature;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
import org.keycloak.representations.idm.ErrorRepresentation;
|
||||||
import org.keycloak.representations.idm.OrganizationRepresentation;
|
import org.keycloak.representations.idm.OrganizationRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
|
||||||
import org.keycloak.representations.idm.UserRepresentation;
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||||
import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy;
|
import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy;
|
||||||
|
import org.keycloak.testsuite.Assert;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||||
|
|
||||||
@EnableFeature(Feature.ORGANIZATION)
|
@EnableFeature(Feature.ORGANIZATION)
|
||||||
|
@ -53,14 +58,11 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
|
||||||
|
|
||||||
expected.setFirstName("f");
|
expected.setFirstName("f");
|
||||||
expected.setLastName("l");
|
expected.setLastName("l");
|
||||||
|
expected.setEmail("some@differentthanorg.com");
|
||||||
|
|
||||||
OrganizationMemberResource member = organization.members().member(expected.getId());
|
testRealm().users().get(expected.getId()).update(expected);
|
||||||
|
|
||||||
try (Response response = member.update(expected)) {
|
UserRepresentation existing = organization.members().member(expected.getId()).toRepresentation();
|
||||||
assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
|
|
||||||
}
|
|
||||||
|
|
||||||
UserRepresentation existing = member.toRepresentation();
|
|
||||||
assertEquals(expected.getId(), existing.getId());
|
assertEquals(expected.getId(), existing.getId());
|
||||||
assertEquals(expected.getUsername(), existing.getUsername());
|
assertEquals(expected.getUsername(), existing.getUsername());
|
||||||
assertEquals(expected.getEmail(), existing.getEmail());
|
assertEquals(expected.getEmail(), existing.getEmail());
|
||||||
|
@ -74,67 +76,44 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
|
||||||
upConfig.setUnmanagedAttributePolicy(UnmanagedAttributePolicy.ENABLED);
|
upConfig.setUnmanagedAttributePolicy(UnmanagedAttributePolicy.ENABLED);
|
||||||
testRealm().users().userProfile().update(upConfig);
|
testRealm().users().userProfile().update(upConfig);
|
||||||
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
UserRepresentation expected = new UserRepresentation();
|
UserRepresentation expected = addMember(organization);
|
||||||
|
List<String> expectedOrganizations = expected.getAttributes().get(ORGANIZATION_ATTRIBUTE);
|
||||||
|
|
||||||
expected.setUsername(expected.getEmail());
|
expected.singleAttribute(ORGANIZATION_ATTRIBUTE, "invalid");
|
||||||
expected.singleAttribute(USER_ORGANIZATION_ATTRIBUTE, "invalid");
|
|
||||||
|
|
||||||
try (Response response = organization.members().addMember(expected)) {
|
UserResource userResource = testRealm().users().get(expected.getId());
|
||||||
assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
|
|
||||||
assertTrue(testRealm().users().search("u@o.org").isEmpty());
|
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();
|
||||||
|
assertThat(expected.getAttributes().get(ORGANIZATION_ATTRIBUTE), Matchers.containsInAnyOrder(expectedOrganizations.toArray()));
|
||||||
|
|
||||||
|
userResource.update(expected);
|
||||||
|
expected = userResource.toRepresentation();
|
||||||
|
assertThat(expected.getAttributes().get(ORGANIZATION_ATTRIBUTE), Matchers.containsInAnyOrder(expectedOrganizations.toArray()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testFailSetEmailDomainOtherThanOrganizationDomain() {
|
public void testUserAlreadyMemberOfOrganization() {
|
||||||
UPConfig upConfig = testRealm().users().userProfile().getConfiguration();
|
UPConfig upConfig = testRealm().users().userProfile().getConfiguration();
|
||||||
upConfig.setUnmanagedAttributePolicy(UnmanagedAttributePolicy.ENABLED);
|
upConfig.setUnmanagedAttributePolicy(UnmanagedAttributePolicy.ENABLED);
|
||||||
testRealm().users().userProfile().update(upConfig);
|
testRealm().users().userProfile().update(upConfig);
|
||||||
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
UserRepresentation expected = new UserRepresentation();
|
UserRepresentation expected = addMember(organization, KeycloakModelUtils.generateId() + "@user.org");
|
||||||
|
|
||||||
expected.setUsername(KeycloakModelUtils.generateId() + "@user.org");
|
try (Response response = organization.members().addMember(expected.getId())) {
|
||||||
expected.setEmail(expected.getUsername());
|
assertEquals(Status.CONFLICT.getStatusCode(), response.getStatus());
|
||||||
|
|
||||||
try (Response response = organization.members().addMember(expected)) {
|
|
||||||
assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
|
|
||||||
assertTrue(testRealm().users().search(expected.getUsername()).isEmpty());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
expected.setUsername(expected.getUsername().replace("@user.org", "@" + organizationName + ".org"));
|
|
||||||
expected.setEmail(expected.getUsername());
|
|
||||||
|
|
||||||
try (Response response = organization.members().addMember(expected)) {
|
|
||||||
assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
|
|
||||||
assertFalse(testRealm().users().search(expected.getUsername()).isEmpty());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testFailSetEmailDomainOtherThanOrganizationDomainViaUserApi() {
|
|
||||||
RealmRepresentation representation = testRealm().toRepresentation();
|
|
||||||
representation.setEditUsernameAllowed(true);
|
|
||||||
testRealm().update(representation);
|
|
||||||
OrganizationRepresentation organization = createOrganization();
|
|
||||||
UserRepresentation member = addMember(testRealm().organizations().get(organization.getId()));
|
|
||||||
|
|
||||||
member.setUsername(KeycloakModelUtils.generateId() + "@user.org");
|
|
||||||
member.setEmail(member.getUsername());
|
|
||||||
member.setFirstName("f");
|
|
||||||
member.setLastName("l");
|
|
||||||
member.setEnabled(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
testRealm().users().get(member.getId()).update(member);
|
|
||||||
fail("Should fail because email domain does not match any from organization");
|
|
||||||
} catch (BadRequestException expected) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
member.setUsername(member.getUsername().replace("@user.org", "@" + organizationName + ".org"));
|
|
||||||
member.setEmail(member.getUsername());
|
|
||||||
|
|
||||||
testRealm().users().get(member.getId()).update(member);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -182,18 +161,27 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testDelete() {
|
public void testDelete() {
|
||||||
|
UPConfig upConfig = testRealm().users().userProfile().getConfiguration();
|
||||||
|
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());
|
||||||
|
assertTrue(expected.getAttributes().get(ORGANIZATION_ATTRIBUTE).contains(organization.toRepresentation().getId()));
|
||||||
OrganizationMemberResource member = organization.members().member(expected.getId());
|
OrganizationMemberResource member = organization.members().member(expected.getId());
|
||||||
|
|
||||||
try (Response response = member.delete()) {
|
try (Response response = member.delete()) {
|
||||||
assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
|
assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// user should exist but no longer an organization member
|
||||||
|
expected = testRealm().users().get(expected.getId()).toRepresentation();
|
||||||
|
assertNull(expected.getAttributes());
|
||||||
try {
|
try {
|
||||||
member.toRepresentation();
|
member.toRepresentation();
|
||||||
fail("should be deleted");
|
fail("should not be an organization member");
|
||||||
} catch (NotFoundException ignore) {}
|
} catch (NotFoundException ignore) {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -214,11 +202,18 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
|
||||||
} catch (NotFoundException ignore) {}
|
} catch (NotFoundException ignore) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (UserRepresentation member : expected) {
|
||||||
|
// users should exist as they are not managed by the organization
|
||||||
|
testRealm().users().get(member.getId()).toRepresentation();
|
||||||
|
}
|
||||||
|
|
||||||
for (UserRepresentation member : expected) {
|
for (UserRepresentation member : expected) {
|
||||||
try {
|
try {
|
||||||
testRealm().users().get(member.getId()).toRepresentation();
|
// user no longer bound to the organization
|
||||||
fail("should be deleted");
|
organization.members().getOrganization(member.getId());
|
||||||
} catch (NotFoundException ignore) {}
|
fail("should not be associated with the organization anymore");
|
||||||
|
} catch (NotFoundException ignore) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
package org.keycloak.testsuite.organization.admin;
|
package org.keycloak.testsuite.organization.admin;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.contains;
|
|
||||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
import static org.hamcrest.Matchers.empty;
|
import static org.hamcrest.Matchers.empty;
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
@ -37,40 +36,19 @@ import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import jakarta.ws.rs.ForbiddenException;
|
|
||||||
import jakarta.ws.rs.NotFoundException;
|
import jakarta.ws.rs.NotFoundException;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import jakarta.ws.rs.core.Response.Status;
|
import jakarta.ws.rs.core.Response.Status;
|
||||||
import org.hamcrest.Matchers;
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.admin.client.Keycloak;
|
|
||||||
import org.keycloak.admin.client.resource.OrganizationResource;
|
import org.keycloak.admin.client.resource.OrganizationResource;
|
||||||
import org.keycloak.admin.client.resource.RealmResource;
|
|
||||||
import org.keycloak.common.Profile.Feature;
|
import org.keycloak.common.Profile.Feature;
|
||||||
import org.keycloak.models.AdminRoles;
|
|
||||||
import org.keycloak.models.Constants;
|
|
||||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
|
||||||
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
|
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
|
||||||
import org.keycloak.representations.idm.OrganizationRepresentation;
|
import org.keycloak.representations.idm.OrganizationRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
|
||||||
import org.keycloak.representations.idm.UserRepresentation;
|
|
||||||
import org.keycloak.testsuite.admin.ApiUtil;
|
|
||||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||||
import org.keycloak.testsuite.util.AdminClientUtil;
|
|
||||||
import org.keycloak.testsuite.util.UserBuilder;
|
|
||||||
|
|
||||||
@EnableFeature(Feature.ORGANIZATION)
|
@EnableFeature(Feature.ORGANIZATION)
|
||||||
public class OrganizationTest extends AbstractOrganizationTest {
|
public class OrganizationTest extends AbstractOrganizationTest {
|
||||||
|
|
||||||
@Override
|
|
||||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
|
||||||
testRealm.getUsers().add(UserBuilder.create().username("realmAdmin").password("password")
|
|
||||||
.role(Constants.REALM_MANAGEMENT_CLIENT_ID, AdminRoles.MANAGE_REALM)
|
|
||||||
.role(Constants.REALM_MANAGEMENT_CLIENT_ID, AdminRoles.MANAGE_IDENTITY_PROVIDERS)
|
|
||||||
.role(Constants.REALM_MANAGEMENT_CLIENT_ID, AdminRoles.MANAGE_USERS)
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testUpdate() {
|
public void testUpdate() {
|
||||||
OrganizationRepresentation expected = createOrganization();
|
OrganizationRepresentation expected = createOrganization();
|
||||||
|
@ -312,139 +290,4 @@ public class OrganizationTest extends AbstractOrganizationTest {
|
||||||
assertEquals(1, existing.getDomains().size());
|
assertEquals(1, existing.getDomains().size());
|
||||||
assertNotNull(existing.getDomain("acme.com"));
|
assertNotNull(existing.getDomain("acme.com"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void permissionsTest() throws Exception {
|
|
||||||
try (
|
|
||||||
Keycloak manageRealmAdminClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(),
|
|
||||||
TEST_REALM_NAME, "realmAdmin", "password", Constants.ADMIN_CLI_CLIENT_ID, null);
|
|
||||||
Keycloak userAdminClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(),
|
|
||||||
TEST_REALM_NAME, "test-user@localhost", "password", Constants.ADMIN_CLI_CLIENT_ID, null)
|
|
||||||
) {
|
|
||||||
RealmResource realmAdminResource = manageRealmAdminClient.realm(TEST_REALM_NAME);
|
|
||||||
RealmResource realmUserResource = userAdminClient.realm(TEST_REALM_NAME);
|
|
||||||
|
|
||||||
/* Org */
|
|
||||||
//create org
|
|
||||||
OrganizationRepresentation orgRep = createRepresentation("testOrg", "testOrg.org");
|
|
||||||
String orgId;
|
|
||||||
try (
|
|
||||||
Response userResponse = realmUserResource.organizations().create(orgRep);
|
|
||||||
Response adminResponse = realmAdminResource.organizations().create(orgRep)
|
|
||||||
) {
|
|
||||||
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
|
|
||||||
assertThat(adminResponse.getStatus(), equalTo(Status.CREATED.getStatusCode()));
|
|
||||||
orgId = ApiUtil.getCreatedId(adminResponse);
|
|
||||||
getCleanup().addCleanup(() -> testRealm().organizations().get(orgId).delete().close());
|
|
||||||
}
|
|
||||||
|
|
||||||
//search for org
|
|
||||||
try {
|
|
||||||
realmUserResource.organizations().search("testOrg.org", true, 0, 1);
|
|
||||||
fail("Expected ForbiddenException");
|
|
||||||
} catch (ForbiddenException expected) {}
|
|
||||||
assertThat(realmAdminResource.organizations().search("testOrg.org", true, 0, 1), Matchers.notNullValue());
|
|
||||||
|
|
||||||
//get org
|
|
||||||
try {
|
|
||||||
realmUserResource.organizations().get(orgId).toRepresentation();
|
|
||||||
fail("Expected ForbiddenException");
|
|
||||||
} catch (ForbiddenException expected) {}
|
|
||||||
assertThat(realmAdminResource.organizations().get(orgId).toRepresentation(), Matchers.notNullValue());
|
|
||||||
|
|
||||||
//update org
|
|
||||||
try (Response userResponse = realmUserResource.organizations().get(orgId).update(orgRep)) {
|
|
||||||
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
|
|
||||||
}
|
|
||||||
|
|
||||||
//delete org
|
|
||||||
try (Response userResponse = realmUserResource.organizations().get(orgId).delete()) {
|
|
||||||
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* IdP */
|
|
||||||
IdentityProviderRepresentation idpRep = new IdentityProviderRepresentation();
|
|
||||||
idpRep.setAlias("dummy");
|
|
||||||
idpRep.setProviderId("oidc");
|
|
||||||
//create IdP
|
|
||||||
try (
|
|
||||||
Response userResponse = realmUserResource.organizations().get(orgId).identityProvider().create(idpRep);
|
|
||||||
Response adminResponse = realmAdminResource.organizations().get(orgId).identityProvider().create(idpRep)
|
|
||||||
) {
|
|
||||||
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
|
|
||||||
assertThat(adminResponse.getStatus(), equalTo(Status.CREATED.getStatusCode()));
|
|
||||||
getCleanup().addCleanup(() -> testRealm().organizations().get(orgId).identityProvider().delete().close());
|
|
||||||
}
|
|
||||||
|
|
||||||
//get IdP
|
|
||||||
try {
|
|
||||||
//we should get 403, not 400 or 404 etc.
|
|
||||||
realmUserResource.organizations().get("non-existing").identityProvider().toRepresentation();
|
|
||||||
fail("Expected ForbiddenException");
|
|
||||||
} catch (ForbiddenException expected) {}
|
|
||||||
try {
|
|
||||||
realmUserResource.organizations().get(orgId).identityProvider().toRepresentation();
|
|
||||||
fail("Expected ForbiddenException");
|
|
||||||
} catch (ForbiddenException expected) {}
|
|
||||||
assertThat(realmAdminResource.organizations().get(orgId).identityProvider().toRepresentation(), Matchers.notNullValue());
|
|
||||||
|
|
||||||
//update IdP
|
|
||||||
try (Response userResponse = realmUserResource.organizations().get(orgId).identityProvider().update(idpRep)) {
|
|
||||||
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
|
|
||||||
}
|
|
||||||
|
|
||||||
//delete IdP
|
|
||||||
try (Response userResponse = realmUserResource.organizations().get(orgId).identityProvider().delete()) {
|
|
||||||
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Members */
|
|
||||||
UserRepresentation userRep = UserBuilder.create()
|
|
||||||
.username("user@testOrg.org")
|
|
||||||
.email("user@testOrg.org")
|
|
||||||
.build();
|
|
||||||
String userId;
|
|
||||||
|
|
||||||
//create member
|
|
||||||
try (
|
|
||||||
Response userResponse = realmUserResource.organizations().get(orgId).members().addMember(userRep);
|
|
||||||
Response adminResponse = realmAdminResource.organizations().get(orgId).members().addMember(userRep)
|
|
||||||
) {
|
|
||||||
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
|
|
||||||
assertThat(adminResponse.getStatus(), equalTo(Status.CREATED.getStatusCode()));
|
|
||||||
userId = ApiUtil.getCreatedId(adminResponse);
|
|
||||||
assertThat(userId, Matchers.notNullValue());
|
|
||||||
getCleanup().addCleanup(() -> testRealm().organizations().get(orgId).members().member(userId).delete().close());
|
|
||||||
}
|
|
||||||
|
|
||||||
//get members
|
|
||||||
try {
|
|
||||||
//we should get 403, not 400 or 404 etc.
|
|
||||||
realmUserResource.organizations().get("non-existing").members().getAll();
|
|
||||||
fail("Expected ForbiddenException");
|
|
||||||
} catch (ForbiddenException expected) {}
|
|
||||||
try {
|
|
||||||
realmUserResource.organizations().get(orgId).members().getAll();
|
|
||||||
fail("Expected ForbiddenException");
|
|
||||||
} catch (ForbiddenException expected) {}
|
|
||||||
assertThat(realmAdminResource.organizations().get(orgId).members().getAll(), Matchers.notNullValue());
|
|
||||||
|
|
||||||
//get member
|
|
||||||
try {
|
|
||||||
realmUserResource.organizations().get(orgId).members().member(userId).toRepresentation();
|
|
||||||
fail("Expected ForbiddenException");
|
|
||||||
} catch (ForbiddenException expected) {}
|
|
||||||
assertThat(realmAdminResource.organizations().get(orgId).members().member(userId).toRepresentation(), Matchers.notNullValue());
|
|
||||||
|
|
||||||
//update member
|
|
||||||
try (Response userResponse = realmUserResource.organizations().get(orgId).members().member(userId).update(userRep)) {
|
|
||||||
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
|
|
||||||
}
|
|
||||||
|
|
||||||
//delete member
|
|
||||||
try (Response userResponse = realmUserResource.organizations().get(orgId).members().member(userId).delete()) {
|
|
||||||
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue