Prevent members with an email other than the domain set to an organization

Closes #28644

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2024-04-11 13:58:18 -03:00
parent b4cfebd8d5
commit 61b1eec504
14 changed files with 350 additions and 109 deletions

View file

@ -25,7 +25,7 @@ package org.keycloak.representations.idm;
public class OrganizationDomainRepresentation { public class OrganizationDomainRepresentation {
private String name; private String name;
private Boolean verified; private boolean verified;
public String getName() { public String getName() {
return this.name; return this.name;
@ -35,11 +35,11 @@ public class OrganizationDomainRepresentation {
this.name = name; this.name = name;
} }
public Boolean isVerified() { public boolean isVerified() {
return this.verified; return this.verified;
} }
public void setVerified(Boolean verified) { public void setVerified(boolean verified) {
this.verified = verified; this.verified = verified;
} }

View file

@ -28,8 +28,8 @@ public class OrganizationRepresentation {
private String id; private String id;
private String name; private String name;
private Map<String, List<String>> attributes = new HashMap<>(); private Map<String, List<String>> attributes;
private Set<OrganizationDomainRepresentation> domains = new HashSet<>(); private Set<OrganizationDomainRepresentation> domains;
public String getId() { public String getId() {
return id; return id;
@ -62,15 +62,31 @@ public class OrganizationRepresentation {
} }
public Set<OrganizationDomainRepresentation> getDomains() { public Set<OrganizationDomainRepresentation> getDomains() {
return this.domains; return domains;
}
public OrganizationDomainRepresentation getDomain(String name) {
if (domains == null) {
return null;
}
return domains.stream()
.filter(organizationDomainRepresentation -> name.equals(organizationDomainRepresentation.getName()))
.findAny()
.orElse(null);
} }
public void addDomain(OrganizationDomainRepresentation domain) { public void addDomain(OrganizationDomainRepresentation domain) {
this.domains.add(domain); if (domains == null) {
domains = new HashSet<>();
}
domains.add(domain);
} }
public void removeDomain(OrganizationDomainRepresentation domain) { public void removeDomain(OrganizationDomainRepresentation domain) {
this.domains.remove(domain); if (domains == null) {
return;
}
getDomains().remove(domain);
} }
@Override @Override

View file

@ -20,6 +20,8 @@ package org.keycloak.organization.jpa;
import static org.keycloak.models.OrganizationModel.USER_ORGANIZATION_ATTRIBUTE; import static org.keycloak.models.OrganizationModel.USER_ORGANIZATION_ATTRIBUTE;
import static org.keycloak.utils.StreamsUtil.closing; import static org.keycloak.utils.StreamsUtil.closing;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
@ -32,6 +34,8 @@ import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException; import org.keycloak.models.ModelException;
import org.keycloak.models.ModelValidationException;
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.UserModel;
@ -40,6 +44,7 @@ import org.keycloak.models.jpa.entities.OrganizationDomainEntity;
import org.keycloak.models.jpa.entities.OrganizationEntity; import org.keycloak.models.jpa.entities.OrganizationEntity;
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.utils.StringUtil;
public class JpaOrganizationProvider implements OrganizationProvider { public class JpaOrganizationProvider implements OrganizationProvider {
@ -59,7 +64,11 @@ public class JpaOrganizationProvider implements OrganizationProvider {
} }
@Override @Override
public OrganizationModel create(String name) { public OrganizationModel create(String name, Set<String> domains) {
if (StringUtil.isBlank(name)) {
throw new ModelValidationException("Name can not be null");
}
GroupModel group = createOrganizationGroup(name); GroupModel group = createOrganizationGroup(name);
OrganizationEntity entity = new OrganizationEntity(); OrganizationEntity entity = new OrganizationEntity();
@ -70,7 +79,11 @@ public class JpaOrganizationProvider implements OrganizationProvider {
em.persist(entity); em.persist(entity);
return new OrganizationAdapter(realm, entity); OrganizationAdapter adapter = new OrganizationAdapter(realm, entity, this);
adapter.setDomains(domains.stream().map(OrganizationDomainModel::new).collect(Collectors.toSet()));
return adapter;
} }
@Override @Override
@ -121,7 +134,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
@Override @Override
public OrganizationModel getById(String id) { public OrganizationModel getById(String id) {
OrganizationEntity entity = getEntity(id, false); OrganizationEntity entity = getEntity(id, false);
return entity == null ? null : new OrganizationAdapter(realm, entity); return entity == null ? null : new OrganizationAdapter(realm, entity, this);
} }
@Override @Override
@ -130,7 +143,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
query.setParameter("name", domain.toLowerCase()); query.setParameter("name", domain.toLowerCase());
try { try {
OrganizationDomainEntity entity = query.getSingleResult(); OrganizationDomainEntity entity = query.getSingleResult();
return new OrganizationAdapter(realm, entity.getOrganization()); return new OrganizationAdapter(realm, entity.getOrganization(), this);
} catch (NoResultException nre) { } catch (NoResultException nre) {
return null; return null;
} }
@ -142,7 +155,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
query.setParameter("realmId", realm.getId()); query.setParameter("realmId", realm.getId());
return closing(query.getResultStream().map(entity -> new OrganizationAdapter(realm, entity))); return closing(query.getResultStream().map(entity -> new OrganizationAdapter(realm, entity, this)));
} }
@Override @Override

View file

@ -18,19 +18,24 @@
package org.keycloak.organization.jpa; package org.keycloak.organization.jpa;
import org.keycloak.models.GroupModel; import org.keycloak.models.GroupModel;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
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.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;
import org.keycloak.organization.OrganizationProvider;
import java.util.List; import java.util.List;
@ -38,11 +43,13 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
private final RealmModel realm; private final RealmModel realm;
private final OrganizationEntity entity; private final OrganizationEntity entity;
private final OrganizationProvider provider;
private GroupModel group; private GroupModel group;
public OrganizationAdapter(RealmModel realm, OrganizationEntity entity) { public OrganizationAdapter(RealmModel realm, OrganizationEntity entity, OrganizationProvider provider) {
this.realm = realm; this.realm = realm;
this.entity = entity; this.entity = entity;
this.provider = provider;
} }
@Override @Override
@ -90,10 +97,16 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
} }
@Override @Override
public void setDomains(Collection<OrganizationDomainModel> domains) { public void setDomains(Set<OrganizationDomainModel> domains) {
if (domains == null || domains.isEmpty()) {
throw new ModelValidationException("You must provide at least one domain");
}
Map<String, OrganizationDomainModel> modelMap = domains.stream() Map<String, OrganizationDomainModel> modelMap = domains.stream()
.collect(Collectors.toMap(model -> model.getName(), Function.identity())); .peek(this::isDomainInUse)
for (OrganizationDomainEntity domainEntity : this.entity.getDomains()) { .collect(Collectors.toMap(OrganizationDomainModel::getName, Function.identity()));
for (OrganizationDomainEntity domainEntity : new HashSet<>(this.entity.getDomains())) {
// update the existing domain (for now, only the verified flag can be changed). // update the existing domain (for now, only the verified flag can be changed).
if (modelMap.containsKey(domainEntity.getName())) { if (modelMap.containsKey(domainEntity.getName())) {
domainEntity.setVerified(modelMap.get(domainEntity.getName()).getVerified()); domainEntity.setVerified(modelMap.get(domainEntity.getName()).getVerified());
@ -109,7 +122,7 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
for (OrganizationDomainModel model : modelMap.values()) { for (OrganizationDomainModel model : modelMap.values()) {
OrganizationDomainEntity domainEntity = new OrganizationDomainEntity(); OrganizationDomainEntity domainEntity = new OrganizationDomainEntity();
domainEntity.setName(model.getName().toLowerCase()); domainEntity.setName(model.getName().toLowerCase());
domainEntity.setVerified(model.getVerified() == null ? Boolean.FALSE : model.getVerified()); domainEntity.setVerified(model.getVerified());
domainEntity.setOrganization(this.entity); domainEntity.setOrganization(this.entity);
this.entity.addDomain(domainEntity); this.entity.addDomain(domainEntity);
} }
@ -120,13 +133,6 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
return entity; return entity;
} }
private GroupModel getGroup() {
if (group == null) {
group = realm.getGroupById(getGroupId());
}
return group;
}
@Override @Override
public String toString() { public String toString() {
return new StringBuilder() return new StringBuilder()
@ -146,4 +152,18 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
private OrganizationDomainModel toModel(OrganizationDomainEntity entity) { private OrganizationDomainModel toModel(OrganizationDomainEntity entity) {
return new OrganizationDomainModel(entity.getName(), entity.isVerified()); return new OrganizationDomainModel(entity.getName(), entity.isVerified());
} }
private void isDomainInUse(OrganizationDomainModel domainRep) {
OrganizationModel orgModel = provider.getByDomainName(domainRep.getName());
if (orgModel != null && !Objects.equals(getId(), orgModel.getId())) {
throw new ModelValidationException("Domain " + domainRep.getName() + " is already linked to another organization");
}
}
private GroupModel getGroup() {
if (group == null) {
group = realm.getGroupById(getGroupId());
}
return group;
}
} }

View file

@ -16,6 +16,7 @@
*/ */
package org.keycloak.organization; package org.keycloak.organization;
import java.util.Set;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelDuplicateException;
@ -32,10 +33,11 @@ public interface OrganizationProvider extends Provider {
* Creates a new organization with given {@code name} to the realm. * Creates a new organization with given {@code name} to the realm.
* The internal ID of the organization will be created automatically. * The internal ID of the organization will be created automatically.
* @param name String name of the organization. * @param name String name of the organization.
* @param domains the domains
* @throws ModelDuplicateException If there is already an organization with the given name * @throws ModelDuplicateException If there is already an organization with the given name
* @return Model of the created organization. * @return Model of the created organization.
*/ */
OrganizationModel create(String name); OrganizationModel create(String name, Set<String> domains);
/** /**
* Returns a {@link OrganizationModel} by its {@code id}; * Returns a {@link OrganizationModel} by its {@code id};

View file

@ -27,9 +27,13 @@ import java.io.Serializable;
public class OrganizationDomainModel implements Serializable { public class OrganizationDomainModel implements Serializable {
private String name; private String name;
private Boolean verified; private boolean verified;
public OrganizationDomainModel(String name, Boolean verified) { public OrganizationDomainModel(String name) {
this(name, false);
}
public OrganizationDomainModel(String name, boolean verified) {
this.name = name; this.name = name;
this.verified = verified; this.verified = verified;
} }
@ -42,11 +46,11 @@ public class OrganizationDomainModel implements Serializable {
this.name = name; this.name = name;
} }
public Boolean getVerified() { public boolean getVerified() {
return this.verified; return this.verified;
} }
public void setVerified(Boolean verified) { public void setVerified(boolean verified) {
this.verified = verified; this.verified = verified;
} }

View file

@ -19,7 +19,7 @@ package org.keycloak.models;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Collection; import java.util.Set;
import java.util.stream.Stream; import java.util.stream.Stream;
public interface OrganizationModel { public interface OrganizationModel {
@ -38,5 +38,5 @@ public interface OrganizationModel {
Stream<OrganizationDomainModel> getDomains(); Stream<OrganizationDomainModel> getDomains();
void setDomains(Collection<OrganizationDomainModel> domains); void setDomains(Set<OrganizationDomainModel> domains);
} }

View file

@ -17,7 +17,6 @@
package org.keycloak.organization.admin.resource; package org.keycloak.organization.admin.resource;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.Objects; import java.util.Objects;
@ -45,7 +44,6 @@ import org.keycloak.models.OrganizationModel;
import org.keycloak.organization.OrganizationProvider; import org.keycloak.organization.OrganizationProvider;
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.services.ErrorResponse;
import org.keycloak.services.resources.admin.AdminEventBuilder; import org.keycloak.services.resources.admin.AdminEventBuilder;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.utils.StringUtil; import org.keycloak.utils.StringUtil;
@ -77,7 +75,9 @@ public class OrganizationResource {
throw new BadRequestException(); throw new BadRequestException();
} }
OrganizationModel model = provider.create(organization.getName()); Set<String> domains = organization.getDomains().stream().map(OrganizationDomainRepresentation::getName).collect(Collectors.toSet());
OrganizationModel model = provider.create(organization.getName(), domains);
toModel(organization, model); toModel(organization, model);
return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(model.getId()).build()).build(); return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(model.getId()).build()).build();
@ -133,6 +133,7 @@ public class OrganizationResource {
@Path("{id}/members") @Path("{id}/members")
public OrganizationMemberResource members(@PathParam("id") String id) { public OrganizationMemberResource members(@PathParam("id") String id) {
OrganizationModel organization = getOrganization(id); OrganizationModel organization = getOrganization(id);
session.setAttribute(OrganizationModel.class.getName(), organization);
return new OrganizationMemberResource(session, organization, auth, adminEvent); return new OrganizationMemberResource(session, organization, auth, adminEvent);
} }
@ -186,15 +187,8 @@ public class OrganizationResource {
model.setName(rep.getName()); model.setName(rep.getName());
model.setAttributes(rep.getAttributes()); model.setAttributes(rep.getAttributes());
model.setDomains(Optional.ofNullable(rep.getDomains()).orElse(Set.of()).stream() model.setDomains(Optional.ofNullable(rep.getDomains()).orElse(Set.of()).stream()
.filter(this::validateDomainRepresentation) .map(this::toModel)
.peek(domainRep -> { .collect(Collectors.toSet()));
OrganizationModel orgModel = provider.getByDomainName(domainRep.getName());
if (orgModel != null && !Objects.equals(model.getId(), orgModel.getId())) {
throw ErrorResponse.error("Domain " + domainRep.getName() + " is already linked to another organization", Response.Status.BAD_REQUEST);
}
})
.map(this::toModel)
.collect(Collectors.toSet()));
return model; return model;
} }
@ -202,8 +196,4 @@ public class OrganizationResource {
private OrganizationDomainModel toModel(OrganizationDomainRepresentation domainRepresentation) { private OrganizationDomainModel toModel(OrganizationDomainRepresentation domainRepresentation) {
return new OrganizationDomainModel(domainRepresentation.getName(), domainRepresentation.isVerified()); return new OrganizationDomainModel(domainRepresentation.getName(), domainRepresentation.isVerified());
} }
private boolean validateDomainRepresentation(OrganizationDomainRepresentation rep) {
return rep != null && rep.getName() != null && !rep.getName().trim().isEmpty();
}
} }

View file

@ -0,0 +1,110 @@
/*
* 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.validator;
import static org.keycloak.validate.BuiltinValidators.emailValidator;
import java.util.stream.Stream;
import org.keycloak.Config.Scope;
import org.keycloak.common.Profile;
import org.keycloak.common.Profile.Feature;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OrganizationDomainModel;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.UserModel;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.userprofile.AttributeContext;
import org.keycloak.userprofile.UserProfileAttributeValidationContext;
import org.keycloak.utils.StringUtil;
import org.keycloak.validate.AbstractSimpleValidator;
import org.keycloak.validate.ValidationContext;
import org.keycloak.validate.ValidationError;
import org.keycloak.validate.ValidatorConfig;
public class OrganizationMemberValidator extends AbstractSimpleValidator implements EnvironmentDependentProviderFactory {
public static final String ID = "organization-member-validator";
@Override
public String getId() {
return ID;
}
@Override
protected void doValidate(Object value, String inputHint, ValidationContext context, ValidatorConfig config) {
KeycloakSession session = context.getSession();
OrganizationModel organization = resolveOrganization(context, session);
if (organization == null) {
return;
}
validateEmailDomain((String) value, inputHint, context, organization);
}
@Override
protected boolean skipValidation(Object value, ValidatorConfig config) {
return false;
}
@Override
public boolean isSupported(Scope config) {
return Profile.isFeatureEnabled(Feature.ORGANIZATION);
}
private void validateEmailDomain(String email, String inputHint, ValidationContext context, OrganizationModel organization) {
if (UserModel.USERNAME.equals(inputHint) || UserModel.EMAIL.equals(inputHint)) {
if (StringUtil.isBlank(email)) {
context.addError(new ValidationError(ID, inputHint, "Email not set"));
return;
}
if (!emailValidator().validate(email, inputHint, context).isValid()) {
return;
}
String domain = email.substring(email.indexOf('@') + 1);
Stream<OrganizationDomainModel> expectedDomains = organization.getDomains();
if (expectedDomains.map(OrganizationDomainModel::getName).noneMatch(domain::equals)) {
context.addError(new ValidationError(ID, inputHint, "Email domain does not match any domain from the organization"));
}
}
}
private OrganizationModel resolveOrganization(ValidationContext context, KeycloakSession session) {
OrganizationModel organization = (OrganizationModel) session.getAttribute(OrganizationModel.class.getName());
if (organization != null) {
return organization;
}
UserProfileAttributeValidationContext upContext = (UserProfileAttributeValidationContext) context;
AttributeContext attributeContext = upContext.getAttributeContext();
UserModel user = attributeContext.getUser();
if (user != null) {
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
return provider.getByMember(user);
}
return null;
}
}

View file

@ -35,6 +35,7 @@ import org.keycloak.Config;
import org.keycloak.Config.Scope; import org.keycloak.Config.Scope;
import org.keycloak.authentication.requiredactions.TermsAndConditions; import org.keycloak.authentication.requiredactions.TermsAndConditions;
import org.keycloak.common.Profile; import org.keycloak.common.Profile;
import org.keycloak.common.Profile.Feature;
import org.keycloak.component.AmphibianProviderFactory; import org.keycloak.component.AmphibianProviderFactory;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException; import org.keycloak.component.ComponentValidationException;
@ -44,6 +45,7 @@ import org.keycloak.models.KeycloakSessionFactory;
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;
import org.keycloak.organization.validator.OrganizationMemberValidator;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.representations.userprofile.config.UPConfig; import org.keycloak.representations.userprofile.config.UPConfig;
@ -341,6 +343,16 @@ public class DeclarativeUserProfileProviderFactory implements UserProfileProvide
if (contextualMetadataRegistry.putIfAbsent(metadata.getContext(), metadata) != null) { if (contextualMetadataRegistry.putIfAbsent(metadata.getContext(), metadata) != null) {
throw new IllegalStateException("Multiple profile metadata found for context " + metadata.getContext()); throw new IllegalStateException("Multiple profile metadata found for context " + metadata.getContext());
} }
if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
for (AttributeMetadata attribute : metadata.getAttributes()) {
String name = attribute.getName();
if (UserModel.EMAIL.equals(name) || UserModel.USERNAME.equals(name)) {
attribute.addValidators(List.of(new AttributeValidatorMetadata(OrganizationMemberValidator.ID)));
}
}
}
} }
private UserProfileMetadata createBrokeringProfile(AttributeValidatorMetadata readOnlyValidator) { private UserProfileMetadata createBrokeringProfile(AttributeValidatorMetadata readOnlyValidator) {

View file

@ -15,3 +15,4 @@ org.keycloak.userprofile.validator.ImmutableAttributeValidator
org.keycloak.userprofile.validator.UsernameProhibitedCharactersValidator org.keycloak.userprofile.validator.UsernameProhibitedCharactersValidator
org.keycloak.userprofile.validator.PersonNameProhibitedCharactersValidator org.keycloak.userprofile.validator.PersonNameProhibitedCharactersValidator
org.keycloak.userprofile.validator.MultiValueValidator org.keycloak.userprofile.validator.MultiValueValidator
org.keycloak.organization.validator.OrganizationMemberValidator

View file

@ -44,7 +44,7 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
} }
protected OrganizationRepresentation createOrganization(String name) { protected OrganizationRepresentation createOrganization(String name) {
return createOrganization(name, null); return createOrganization(name, name + ".org");
} }
protected OrganizationRepresentation createOrganization(String name, String orgDomain) { protected OrganizationRepresentation createOrganization(String name, String orgDomain) {
@ -54,12 +54,9 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
String id; String id;
if (orgDomain != null) { OrganizationDomainRepresentation domainRep = new OrganizationDomainRepresentation();
OrganizationDomainRepresentation domainRep = new OrganizationDomainRepresentation(); domainRep.setName(orgDomain);
domainRep.setName(orgDomain); org.addDomain(domainRep);
domainRep.setVerified(true);
org.addDomain(domainRep);
}
try (Response response = testRealm().organizations().create(org)) { try (Response response = testRealm().organizations().create(org)) {
assertEquals(Status.CREATED.getStatusCode(), response.getStatus()); assertEquals(Status.CREATED.getStatusCode(), response.getStatus());

View file

@ -27,6 +27,7 @@ import static org.keycloak.models.OrganizationModel.USER_ORGANIZATION_ATTRIBUTE;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
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;
@ -34,11 +35,12 @@ 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.common.Profile.Feature; import org.keycloak.common.Profile.Feature;
import org.keycloak.models.utils.KeycloakModelUtils;
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.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
@EnableFeature(Feature.ORGANIZATION) @EnableFeature(Feature.ORGANIZATION)
@ -67,14 +69,13 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
} }
@Test @Test
public void testFailCreateUser() { public void testFailSetUserOrganizationAttribute() {
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 = new UserRepresentation();
expected.setEmail("u@o.org");
expected.setUsername(expected.getEmail()); expected.setUsername(expected.getEmail());
expected.singleAttribute(USER_ORGANIZATION_ATTRIBUTE, "invalid"); expected.singleAttribute(USER_ORGANIZATION_ATTRIBUTE, "invalid");
@ -84,6 +85,58 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
} }
} }
@Test
public void testFailSetEmailDomainOtherThanOrganizationDomain() {
UPConfig upConfig = testRealm().users().userProfile().getConfiguration();
upConfig.setUnmanagedAttributePolicy(UnmanagedAttributePolicy.ENABLED);
testRealm().users().userProfile().update(upConfig);
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
UserRepresentation expected = new UserRepresentation();
expected.setUsername(KeycloakModelUtils.generateId() + "@user.org");
expected.setEmail(expected.getUsername());
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
public void testGet() { public void testGet() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId()); OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());

View file

@ -50,12 +50,6 @@ public class OrganizationTest extends AbstractOrganizationTest {
assertEquals(organizationName, expected.getName()); assertEquals(organizationName, expected.getName());
expected.setName("acme"); expected.setName("acme");
// add an internet domain to the organization.
OrganizationDomainRepresentation orgDomain = new OrganizationDomainRepresentation();
orgDomain.setName("neworg.org");
orgDomain.setVerified(true);
expected.addDomain(orgDomain);
OrganizationResource organization = testRealm().organizations().get(expected.getId()); OrganizationResource organization = testRealm().organizations().get(expected.getId());
try (Response response = organization.update(expected)) { try (Response response = organization.update(expected)) {
@ -66,48 +60,6 @@ public class OrganizationTest extends AbstractOrganizationTest {
assertEquals(expected.getId(), existing.getId()); assertEquals(expected.getId(), existing.getId());
assertEquals(expected.getName(), existing.getName()); assertEquals(expected.getName(), existing.getName());
assertEquals(1, existing.getDomains().size()); assertEquals(1, existing.getDomains().size());
OrganizationDomainRepresentation existingDomain = existing.getDomains().iterator().next();
assertEquals(orgDomain.getName(), existingDomain.getName());
assertEquals(orgDomain.isVerified(), existingDomain.isVerified());
// now test updating an existing internet domain (change verified to false and check the model was updated).
orgDomain.setVerified(false);
try (Response response = organization.update(expected)) {
assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
}
existing = organization.toRepresentation();
assertEquals(1, existing.getDomains().size());
existingDomain = existing.getDomains().iterator().next();
assertEquals(false, existingDomain.isVerified());
// now replace the internet domain for a different one.
orgDomain.setName("acme.com");
try (Response response = organization.update(expected)) {
assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
}
existing = organization.toRepresentation();
assertEquals(1, existing.getDomains().size());
existingDomain = existing.getDomains().iterator().next();
assertEquals("acme.com", existingDomain.getName());
assertEquals(false, existingDomain.isVerified());
// create another org and attempt to set the same internet domain during update - should not be possible.
OrganizationRepresentation anotherOrg = createOrganization("another-org");
anotherOrg.addDomain(orgDomain);
organization = testRealm().organizations().get(anotherOrg.getId());
try (Response response = organization.update(anotherOrg)) {
assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
}
// finally, attempt to create a new org with an existing internet domain in the representation - should not be possible.
OrganizationRepresentation newOrg = new OrganizationRepresentation();
newOrg.setName("new-org");
newOrg.addDomain(orgDomain);
try (Response response = testRealm().organizations().create(newOrg)) {
assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
}
} }
@Test @Test
@ -147,7 +99,7 @@ public class OrganizationTest extends AbstractOrganizationTest {
assertEquals(1, orgRep.getDomains().size()); assertEquals(1, orgRep.getDomains().size());
OrganizationDomainRepresentation domainRep = orgRep.getDomains().iterator().next(); OrganizationDomainRepresentation domainRep = orgRep.getDomains().iterator().next();
assertEquals("testorg2.org", domainRep.getName()); assertEquals("testorg2.org", domainRep.getName());
assertTrue(domainRep.isVerified()); assertFalse(domainRep.isVerified());
// search for an organization with an non-existent domain. // search for an organization with an non-existent domain.
existing = testRealm().organizations().getAll("someother.org"); existing = testRealm().organizations().getAll("someother.org");
@ -208,4 +160,75 @@ public class OrganizationTest extends AbstractOrganizationTest {
updated = organization.toRepresentation(); updated = organization.toRepresentation();
assertEquals(0, updated.getAttributes().size()); assertEquals(0, updated.getAttributes().size());
} }
@Test
public void testDomains() {
// test create org with default domain settings
OrganizationRepresentation expected = createOrganization();
OrganizationDomainRepresentation expectedNewOrgDomain = expected.getDomains().iterator().next();
OrganizationResource organization = testRealm().organizations().get(expected.getId());
OrganizationRepresentation existing = organization.toRepresentation();
assertEquals(1, existing.getDomains().size());
OrganizationDomainRepresentation existingNewOrgDomain = existing.getDomain("neworg.org");
assertEquals(expectedNewOrgDomain.getName(), existingNewOrgDomain.getName());
assertFalse(existingNewOrgDomain.isVerified());
// create a second domain with verified true
OrganizationDomainRepresentation expectedNewOrgBrDomain = new OrganizationDomainRepresentation();
expectedNewOrgBrDomain.setName("neworg.org.br");
expectedNewOrgBrDomain.setVerified(true);
expected.addDomain(expectedNewOrgBrDomain);
try (Response response = organization.update(expected)) {
assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
}
existing = organization.toRepresentation();
assertEquals(2, existing.getDomains().size());
OrganizationDomainRepresentation existingNewOrgBrDomain = existing.getDomain("neworg.org.br");
assertEquals(expectedNewOrgBrDomain.getName(), existingNewOrgBrDomain.getName());
assertEquals(expectedNewOrgBrDomain.isVerified(), existingNewOrgBrDomain.isVerified());
// now test updating an existing internet domain (change verified to false and check the model was updated).
expectedNewOrgDomain.setVerified(true);
try (Response response = organization.update(expected)) {
assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
}
existing = organization.toRepresentation();
existingNewOrgDomain = existing.getDomain("neworg.org");
assertEquals(expectedNewOrgDomain.isVerified(), existingNewOrgDomain.isVerified());
existingNewOrgBrDomain = existing.getDomain("neworg.org.br");
assertNotNull(existingNewOrgBrDomain);
assertEquals(expectedNewOrgBrDomain.isVerified(), existingNewOrgBrDomain.isVerified());
// now replace the internet domain for a different one.
expectedNewOrgBrDomain.setName("acme.com");
expectedNewOrgBrDomain.setVerified(false);
try (Response response = organization.update(expected)) {
assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
}
existing = organization.toRepresentation();
assertEquals(2, existing.getDomains().size());
existingNewOrgBrDomain = existing.getDomain("acme.com");
assertNotNull(existingNewOrgBrDomain);
assertEquals(expectedNewOrgBrDomain.getName(), existingNewOrgBrDomain.getName());
assertEquals(expectedNewOrgBrDomain.isVerified(), existingNewOrgBrDomain.isVerified());
// create another org and attempt to set the same internet domain during update - should not be possible.
OrganizationRepresentation anotherOrg = createOrganization("another-org");
anotherOrg.addDomain(expectedNewOrgDomain);
organization = testRealm().organizations().get(anotherOrg.getId());
try (Response response = organization.update(anotherOrg)) {
assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
}
// try to remove a domain
organization = testRealm().organizations().get(existing.getId());
existing.removeDomain(existingNewOrgDomain);
try (Response response = organization.update(existing)) {
assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
}
existing = organization.toRepresentation();
assertFalse(existing.getDomains().isEmpty());
assertEquals(1, existing.getDomains().size());
assertNotNull(existing.getDomain("acme.com"));
}
} }