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:
parent
b4cfebd8d5
commit
61b1eec504
14 changed files with 350 additions and 109 deletions
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -28,8 +28,8 @@ public class OrganizationRepresentation {
|
|||
|
||||
private String id;
|
||||
private String name;
|
||||
private Map<String, List<String>> attributes = new HashMap<>();
|
||||
private Set<OrganizationDomainRepresentation> domains = new HashSet<>();
|
||||
private Map<String, List<String>> attributes;
|
||||
private Set<OrganizationDomainRepresentation> domains;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
|
@ -62,15 +62,31 @@ public class OrganizationRepresentation {
|
|||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
|
|
|
@ -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<String> 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
|
||||
|
|
|
@ -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<Or
|
|||
|
||||
private final RealmModel realm;
|
||||
private final OrganizationEntity entity;
|
||||
private final OrganizationProvider provider;
|
||||
private GroupModel group;
|
||||
|
||||
public OrganizationAdapter(RealmModel realm, OrganizationEntity entity) {
|
||||
public OrganizationAdapter(RealmModel realm, OrganizationEntity entity, OrganizationProvider provider) {
|
||||
this.realm = realm;
|
||||
this.entity = entity;
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -90,10 +97,16 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
|
|||
}
|
||||
|
||||
@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()
|
||||
.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<Or
|
|||
for (OrganizationDomainModel model : modelMap.values()) {
|
||||
OrganizationDomainEntity domainEntity = new OrganizationDomainEntity();
|
||||
domainEntity.setName(model.getName().toLowerCase());
|
||||
domainEntity.setVerified(model.getVerified() == null ? Boolean.FALSE : model.getVerified());
|
||||
domainEntity.setVerified(model.getVerified());
|
||||
domainEntity.setOrganization(this.entity);
|
||||
this.entity.addDomain(domainEntity);
|
||||
}
|
||||
|
@ -120,13 +133,6 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
|
|||
return entity;
|
||||
}
|
||||
|
||||
private GroupModel getGroup() {
|
||||
if (group == null) {
|
||||
group = realm.getGroupById(getGroupId());
|
||||
}
|
||||
return group;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringBuilder()
|
||||
|
@ -146,4 +152,18 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
|
|||
private OrganizationDomainModel toModel(OrganizationDomainEntity entity) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
package org.keycloak.organization;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
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.
|
||||
* The internal ID of the organization will be created automatically.
|
||||
* @param name String name of the organization.
|
||||
* @param domains the domains
|
||||
* @throws ModelDuplicateException If there is already an organization with the given name
|
||||
* @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};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<OrganizationDomainModel> getDomains();
|
||||
|
||||
void setDomains(Collection<OrganizationDomainModel> domains);
|
||||
void setDomains(Set<OrganizationDomainModel> domains);
|
||||
}
|
||||
|
|
|
@ -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<String> 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,13 +187,6 @@ 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()));
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -15,3 +15,4 @@ org.keycloak.userprofile.validator.ImmutableAttributeValidator
|
|||
org.keycloak.userprofile.validator.UsernameProhibitedCharactersValidator
|
||||
org.keycloak.userprofile.validator.PersonNameProhibitedCharactersValidator
|
||||
org.keycloak.userprofile.validator.MultiValueValidator
|
||||
org.keycloak.organization.validator.OrganizationMemberValidator
|
|
@ -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);
|
||||
}
|
||||
|
||||
try (Response response = testRealm().organizations().create(org)) {
|
||||
assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue