From 61b1eec50432a3c2d4917c663be70dd72abefb7f Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Thu, 11 Apr 2024 13:58:18 -0300 Subject: [PATCH] Prevent members with an email other than the domain set to an organization Closes #28644 Signed-off-by: Pedro Igor --- .../idm/OrganizationDomainRepresentation.java | 6 +- .../idm/OrganizationRepresentation.java | 26 +++- .../jpa/JpaOrganizationProvider.java | 23 +++- .../organization/jpa/OrganizationAdapter.java | 46 +++++-- .../organization/OrganizationProvider.java | 4 +- .../models/OrganizationDomainModel.java | 12 +- .../keycloak/models/OrganizationModel.java | 4 +- .../admin/resource/OrganizationResource.java | 22 +--- .../OrganizationMemberValidator.java | 110 ++++++++++++++++ ...DeclarativeUserProfileProviderFactory.java | 12 ++ .../org.keycloak.validate.ValidatorFactory | 3 +- .../admin/AbstractOrganizationTest.java | 11 +- .../admin/OrganizationMemberTest.java | 59 ++++++++- .../organization/admin/OrganizationTest.java | 121 +++++++++++------- 14 files changed, 350 insertions(+), 109 deletions(-) create mode 100644 services/src/main/java/org/keycloak/organization/validator/OrganizationMemberValidator.java diff --git a/core/src/main/java/org/keycloak/representations/idm/OrganizationDomainRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/OrganizationDomainRepresentation.java index c0d2b053f7..1f17f4c52a 100644 --- a/core/src/main/java/org/keycloak/representations/idm/OrganizationDomainRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/OrganizationDomainRepresentation.java @@ -25,7 +25,7 @@ package org.keycloak.representations.idm; public class OrganizationDomainRepresentation { private String name; - private Boolean verified; + private boolean verified; public String getName() { return this.name; @@ -35,11 +35,11 @@ public class OrganizationDomainRepresentation { this.name = name; } - public Boolean isVerified() { + public boolean isVerified() { return this.verified; } - public void setVerified(Boolean verified) { + public void setVerified(boolean verified) { this.verified = verified; } diff --git a/core/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java index 0f38fb4837..53c214c969 100644 --- a/core/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java @@ -28,8 +28,8 @@ public class OrganizationRepresentation { private String id; private String name; - private Map> attributes = new HashMap<>(); - private Set domains = new HashSet<>(); + private Map> attributes; + private Set domains; public String getId() { return id; @@ -62,15 +62,31 @@ public class OrganizationRepresentation { } public Set 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) { - this.domains.add(domain); + if (domains == null) { + domains = new HashSet<>(); + } + domains.add(domain); } public void removeDomain(OrganizationDomainRepresentation domain) { - this.domains.remove(domain); + if (domains == null) { + return; + } + getDomains().remove(domain); } @Override diff --git a/model/jpa/src/main/java/org/keycloak/organization/jpa/JpaOrganizationProvider.java b/model/jpa/src/main/java/org/keycloak/organization/jpa/JpaOrganizationProvider.java index 77ca68f45a..98a90a3402 100644 --- a/model/jpa/src/main/java/org/keycloak/organization/jpa/JpaOrganizationProvider.java +++ b/model/jpa/src/main/java/org/keycloak/organization/jpa/JpaOrganizationProvider.java @@ -20,6 +20,8 @@ package org.keycloak.organization.jpa; import static org.keycloak.models.OrganizationModel.USER_ORGANIZATION_ATTRIBUTE; import static org.keycloak.utils.StreamsUtil.closing; +import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; import jakarta.persistence.EntityManager; @@ -32,6 +34,8 @@ import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelException; +import org.keycloak.models.ModelValidationException; +import org.keycloak.models.OrganizationDomainModel; import org.keycloak.models.OrganizationModel; import org.keycloak.models.RealmModel; 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.utils.KeycloakModelUtils; import org.keycloak.organization.OrganizationProvider; +import org.keycloak.utils.StringUtil; public class JpaOrganizationProvider implements OrganizationProvider { @@ -59,7 +64,11 @@ public class JpaOrganizationProvider implements OrganizationProvider { } @Override - public OrganizationModel create(String name) { + public OrganizationModel create(String name, Set domains) { + if (StringUtil.isBlank(name)) { + throw new ModelValidationException("Name can not be null"); + } + GroupModel group = createOrganizationGroup(name); OrganizationEntity entity = new OrganizationEntity(); @@ -70,7 +79,11 @@ public class JpaOrganizationProvider implements OrganizationProvider { 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 @@ -121,7 +134,7 @@ public class JpaOrganizationProvider implements OrganizationProvider { @Override public OrganizationModel getById(String id) { OrganizationEntity entity = getEntity(id, false); - return entity == null ? null : new OrganizationAdapter(realm, entity); + return entity == null ? null : new OrganizationAdapter(realm, entity, this); } @Override @@ -130,7 +143,7 @@ public class JpaOrganizationProvider implements OrganizationProvider { query.setParameter("name", domain.toLowerCase()); try { OrganizationDomainEntity entity = query.getSingleResult(); - return new OrganizationAdapter(realm, entity.getOrganization()); + return new OrganizationAdapter(realm, entity.getOrganization(), this); } catch (NoResultException nre) { return null; } @@ -142,7 +155,7 @@ public class JpaOrganizationProvider implements OrganizationProvider { 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 diff --git a/model/jpa/src/main/java/org/keycloak/organization/jpa/OrganizationAdapter.java b/model/jpa/src/main/java/org/keycloak/organization/jpa/OrganizationAdapter.java index cb8b674068..a664c91eca 100644 --- a/model/jpa/src/main/java/org/keycloak/organization/jpa/OrganizationAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/organization/jpa/OrganizationAdapter.java @@ -18,19 +18,24 @@ package org.keycloak.organization.jpa; import org.keycloak.models.GroupModel; -import java.util.Collection; + +import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.Objects; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.keycloak.models.ModelValidationException; import org.keycloak.models.OrganizationDomainModel; import org.keycloak.models.OrganizationModel; import org.keycloak.models.RealmModel; import org.keycloak.models.jpa.JpaModel; import org.keycloak.models.jpa.entities.OrganizationDomainEntity; import org.keycloak.models.jpa.entities.OrganizationEntity; +import org.keycloak.organization.OrganizationProvider; import java.util.List; @@ -38,11 +43,13 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel domains) { + public void setDomains(Set domains) { + if (domains == null || domains.isEmpty()) { + throw new ModelValidationException("You must provide at least one domain"); + } + Map modelMap = domains.stream() - .collect(Collectors.toMap(model -> model.getName(), Function.identity())); - for (OrganizationDomainEntity domainEntity : this.entity.getDomains()) { + .peek(this::isDomainInUse) + .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). if (modelMap.containsKey(domainEntity.getName())) { domainEntity.setVerified(modelMap.get(domainEntity.getName()).getVerified()); @@ -109,7 +122,7 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel domains); /** * Returns a {@link OrganizationModel} by its {@code id}; diff --git a/server-spi/src/main/java/org/keycloak/models/OrganizationDomainModel.java b/server-spi/src/main/java/org/keycloak/models/OrganizationDomainModel.java index 38dfdf9833..494257654d 100644 --- a/server-spi/src/main/java/org/keycloak/models/OrganizationDomainModel.java +++ b/server-spi/src/main/java/org/keycloak/models/OrganizationDomainModel.java @@ -27,9 +27,13 @@ import java.io.Serializable; public class OrganizationDomainModel implements Serializable { 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.verified = verified; } @@ -42,11 +46,11 @@ public class OrganizationDomainModel implements Serializable { this.name = name; } - public Boolean getVerified() { + public boolean getVerified() { return this.verified; } - public void setVerified(Boolean verified) { + public void setVerified(boolean verified) { this.verified = verified; } diff --git a/server-spi/src/main/java/org/keycloak/models/OrganizationModel.java b/server-spi/src/main/java/org/keycloak/models/OrganizationModel.java index 18b00f07d3..bdf8054f49 100644 --- a/server-spi/src/main/java/org/keycloak/models/OrganizationModel.java +++ b/server-spi/src/main/java/org/keycloak/models/OrganizationModel.java @@ -19,7 +19,7 @@ package org.keycloak.models; import java.util.List; import java.util.Map; -import java.util.Collection; +import java.util.Set; import java.util.stream.Stream; public interface OrganizationModel { @@ -38,5 +38,5 @@ public interface OrganizationModel { Stream getDomains(); - void setDomains(Collection domains); + void setDomains(Set domains); } diff --git a/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationResource.java b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationResource.java index a9c0de0e5f..4caa12711a 100644 --- a/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationResource.java +++ b/services/src/main/java/org/keycloak/organization/admin/resource/OrganizationResource.java @@ -17,7 +17,6 @@ package org.keycloak.organization.admin.resource; -import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.Objects; @@ -45,7 +44,6 @@ import org.keycloak.models.OrganizationModel; import org.keycloak.organization.OrganizationProvider; import org.keycloak.representations.idm.OrganizationDomainRepresentation; import org.keycloak.representations.idm.OrganizationRepresentation; -import org.keycloak.services.ErrorResponse; import org.keycloak.services.resources.admin.AdminEventBuilder; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.utils.StringUtil; @@ -77,7 +75,9 @@ public class OrganizationResource { throw new BadRequestException(); } - OrganizationModel model = provider.create(organization.getName()); + Set domains = organization.getDomains().stream().map(OrganizationDomainRepresentation::getName).collect(Collectors.toSet()); + OrganizationModel model = provider.create(organization.getName(), domains); + toModel(organization, model); return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(model.getId()).build()).build(); @@ -133,6 +133,7 @@ public class OrganizationResource { @Path("{id}/members") public OrganizationMemberResource members(@PathParam("id") String id) { OrganizationModel organization = getOrganization(id); + session.setAttribute(OrganizationModel.class.getName(), organization); return new OrganizationMemberResource(session, organization, auth, adminEvent); } @@ -186,15 +187,8 @@ public class OrganizationResource { model.setName(rep.getName()); model.setAttributes(rep.getAttributes()); model.setDomains(Optional.ofNullable(rep.getDomains()).orElse(Set.of()).stream() - .filter(this::validateDomainRepresentation) - .peek(domainRep -> { - 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())); + .map(this::toModel) + .collect(Collectors.toSet())); return model; } @@ -202,8 +196,4 @@ public class OrganizationResource { private OrganizationDomainModel toModel(OrganizationDomainRepresentation domainRepresentation) { return new OrganizationDomainModel(domainRepresentation.getName(), domainRepresentation.isVerified()); } - - private boolean validateDomainRepresentation(OrganizationDomainRepresentation rep) { - return rep != null && rep.getName() != null && !rep.getName().trim().isEmpty(); - } } diff --git a/services/src/main/java/org/keycloak/organization/validator/OrganizationMemberValidator.java b/services/src/main/java/org/keycloak/organization/validator/OrganizationMemberValidator.java new file mode 100644 index 0000000000..cebd252066 --- /dev/null +++ b/services/src/main/java/org/keycloak/organization/validator/OrganizationMemberValidator.java @@ -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 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; + } +} diff --git a/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProviderFactory.java b/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProviderFactory.java index 366706193b..77ba37ce77 100644 --- a/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProviderFactory.java +++ b/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProviderFactory.java @@ -35,6 +35,7 @@ import org.keycloak.Config; import org.keycloak.Config.Scope; import org.keycloak.authentication.requiredactions.TermsAndConditions; import org.keycloak.common.Profile; +import org.keycloak.common.Profile.Feature; import org.keycloak.component.AmphibianProviderFactory; import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentValidationException; @@ -44,6 +45,7 @@ import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredActionProviderModel; import org.keycloak.models.UserModel; +import org.keycloak.organization.validator.OrganizationMemberValidator; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.representations.userprofile.config.UPConfig; @@ -341,6 +343,16 @@ public class DeclarativeUserProfileProviderFactory implements UserProfileProvide if (contextualMetadataRegistry.putIfAbsent(metadata.getContext(), metadata) != null) { 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) { diff --git a/services/src/main/resources/META-INF/services/org.keycloak.validate.ValidatorFactory b/services/src/main/resources/META-INF/services/org.keycloak.validate.ValidatorFactory index 8784b7d291..9280e208b3 100644 --- a/services/src/main/resources/META-INF/services/org.keycloak.validate.ValidatorFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.validate.ValidatorFactory @@ -14,4 +14,5 @@ org.keycloak.userprofile.validator.BrokeringFederatedUsernameHasValueValidator org.keycloak.userprofile.validator.ImmutableAttributeValidator org.keycloak.userprofile.validator.UsernameProhibitedCharactersValidator org.keycloak.userprofile.validator.PersonNameProhibitedCharactersValidator -org.keycloak.userprofile.validator.MultiValueValidator \ No newline at end of file +org.keycloak.userprofile.validator.MultiValueValidator +org.keycloak.organization.validator.OrganizationMemberValidator \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractOrganizationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractOrganizationTest.java index 321c789eaa..8f7ec51f51 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractOrganizationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/AbstractOrganizationTest.java @@ -44,7 +44,7 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest { } protected OrganizationRepresentation createOrganization(String name) { - return createOrganization(name, null); + return createOrganization(name, name + ".org"); } protected OrganizationRepresentation createOrganization(String name, String orgDomain) { @@ -54,12 +54,9 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest { String id; - if (orgDomain != null) { - OrganizationDomainRepresentation domainRep = new OrganizationDomainRepresentation(); - domainRep.setName(orgDomain); - domainRep.setVerified(true); - org.addDomain(domainRep); - } + OrganizationDomainRepresentation domainRep = new OrganizationDomainRepresentation(); + domainRep.setName(orgDomain); + org.addDomain(domainRep); try (Response response = testRealm().organizations().create(org)) { assertEquals(Status.CREATED.getStatusCode(), response.getStatus()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationMemberTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationMemberTest.java index 796a07b9cb..f0152292dd 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationMemberTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationMemberTest.java @@ -27,6 +27,7 @@ import static org.keycloak.models.OrganizationModel.USER_ORGANIZATION_ATTRIBUTE; import java.util.ArrayList; import java.util.List; +import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.core.Response; 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.OrganizationResource; import org.keycloak.common.Profile.Feature; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.idm.OrganizationRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.userprofile.config.UPConfig; import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy; -import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.arquillian.annotation.EnableFeature; @EnableFeature(Feature.ORGANIZATION) @@ -67,14 +69,13 @@ public class OrganizationMemberTest extends AbstractOrganizationTest { } @Test - public void testFailCreateUser() { + public void testFailSetUserOrganizationAttribute() { UPConfig upConfig = testRealm().users().userProfile().getConfiguration(); upConfig.setUnmanagedAttributePolicy(UnmanagedAttributePolicy.ENABLED); testRealm().users().userProfile().update(upConfig); OrganizationResource organization = testRealm().organizations().get(createOrganization().getId()); UserRepresentation expected = new UserRepresentation(); - expected.setEmail("u@o.org"); expected.setUsername(expected.getEmail()); 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 public void testGet() { OrganizationResource organization = testRealm().organizations().get(createOrganization().getId()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationTest.java index 88bc09156a..94d9b2817b 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationTest.java @@ -50,12 +50,6 @@ public class OrganizationTest extends AbstractOrganizationTest { assertEquals(organizationName, expected.getName()); 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()); try (Response response = organization.update(expected)) { @@ -66,48 +60,6 @@ public class OrganizationTest extends AbstractOrganizationTest { assertEquals(expected.getId(), existing.getId()); assertEquals(expected.getName(), existing.getName()); 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 @@ -147,7 +99,7 @@ public class OrganizationTest extends AbstractOrganizationTest { assertEquals(1, orgRep.getDomains().size()); OrganizationDomainRepresentation domainRep = orgRep.getDomains().iterator().next(); assertEquals("testorg2.org", domainRep.getName()); - assertTrue(domainRep.isVerified()); + assertFalse(domainRep.isVerified()); // search for an organization with an non-existent domain. existing = testRealm().organizations().getAll("someother.org"); @@ -208,4 +160,75 @@ public class OrganizationTest extends AbstractOrganizationTest { updated = organization.toRepresentation(); 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")); + } }