Allow organizations in different realms to have the same domain

Closes #29886

Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
This commit is contained in:
Stefan Guilhen 2024-05-27 17:01:09 -03:00 committed by Pedro Igor
parent 4317a474d1
commit 694ffaf289
9 changed files with 55 additions and 23 deletions

View file

@ -17,14 +17,14 @@
package org.keycloak.models.jpa.entities;
import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.NamedQueries;
import jakarta.persistence.NamedQuery;
import jakarta.persistence.Table;
/**
@ -34,12 +34,13 @@ import jakarta.persistence.Table;
*/
@Entity
@Table(name="ORG_DOMAIN")
@NamedQueries({
@NamedQuery(name="getByName", query="select o from OrganizationDomainEntity o where o.name = :name")
})
public class OrganizationDomainEntity {
@Id
@Column(name = "ID", length = 36)
@Access(AccessType.PROPERTY)
private String id;
@Column(name="NAME")
protected String name;
@ -50,6 +51,14 @@ public class OrganizationDomainEntity {
@JoinColumn(name = "ORG_ID")
private OrganizationEntity organization;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return this.name;
}
@ -81,7 +90,7 @@ public class OrganizationDomainEntity {
if (!(o instanceof OrganizationDomainEntity)) return false;
OrganizationDomainEntity that = (OrganizationDomainEntity) o;
return name != null && name.equals(that.getName());
return id != null && id.equals(that.getId());
}
@Override

View file

@ -37,6 +37,8 @@ import jakarta.persistence.Table;
@NamedQueries({
@NamedQuery(name="getByRealm", query="select o from OrganizationEntity o where o.realmId = :realmId order by o.name ASC"),
@NamedQuery(name="getByOrgName", query="select distinct o from OrganizationEntity o where o.realmId = :realmId AND o.name = :name"),
@NamedQuery(name="getByDomainName", query="select distinct o from OrganizationEntity o inner join OrganizationDomainEntity d ON o.id = d.organization.id" +
" where o.realmId = :realmId AND d.name = :name"),
@NamedQuery(name="getByNameOrDomain", query="select distinct o from OrganizationEntity o inner join OrganizationDomainEntity d ON o.id = d.organization.id" +
" where o.realmId = :realmId AND (o.name = :search OR d.name = :search) order by o.name ASC"),
@NamedQuery(name="getByNameOrDomainContained", query="select distinct o from OrganizationEntity o inner join OrganizationDomainEntity d ON o.id = d.organization.id" +

View file

@ -54,7 +54,6 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.models.jpa.entities.GroupAttributeEntity;
import org.keycloak.models.jpa.entities.GroupEntity;
import org.keycloak.models.jpa.entities.OrganizationDomainEntity;
import org.keycloak.models.jpa.entities.OrganizationEntity;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.utils.StringUtil;
@ -190,11 +189,12 @@ public class JpaOrganizationProvider implements OrganizationProvider {
@Override
public OrganizationModel getByDomainName(String domain) {
TypedQuery<OrganizationDomainEntity> query = em.createNamedQuery("getByName", OrganizationDomainEntity.class);
TypedQuery<OrganizationEntity> query = em.createNamedQuery("getByDomainName", OrganizationEntity.class);
query.setParameter("realmId", this.realm.getId());
query.setParameter("name", domain.toLowerCase());
try {
OrganizationDomainEntity entity = query.getSingleResult();
return new OrganizationAdapter(realm, entity.getOrganization(), this);
OrganizationEntity entity = query.getSingleResult();
return new OrganizationAdapter(realm, entity, this);
} catch (NoResultException nre) {
return null;
}

View file

@ -143,7 +143,7 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
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());
domainEntity.setVerified(modelMap.get(domainEntity.getName()).isVerified());
modelMap.remove(domainEntity.getName());
} else {
// remove domain that is not found in the new set.
@ -161,8 +161,9 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
// create the remaining domains.
for (OrganizationDomainModel model : modelMap.values()) {
OrganizationDomainEntity domainEntity = new OrganizationDomainEntity();
domainEntity.setId(KeycloakModelUtils.generateId());
domainEntity.setName(model.getName());
domainEntity.setVerified(model.getVerified());
domainEntity.setVerified(model.isVerified());
domainEntity.setOrganization(this.entity);
this.entity.addDomain(domainEntity);
}
@ -233,7 +234,7 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
}
OrganizationModel orgModel = provider.getByDomainName(domainName);
if (orgModel != null && !Objects.equals(getId(), orgModel.getId())) {
throw new ModelValidationException("Domain " + domainName + " is already linked to another organization");
throw new ModelValidationException("Domain " + domainName + " is already linked to another organization in realm " + realm.getName());
}
return domainModel;
}

View file

@ -101,6 +101,9 @@
<addUniqueConstraint tableName="ORG" columnNames="GROUP_ID" constraintName="UK_ORG_GROUP"/>
<createTable tableName="ORG_DOMAIN">
<column name="ID" type="VARCHAR(36)">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="NAME" type="VARCHAR(255)">
<constraints primaryKey="true" nullable="false"/>
</column>

View file

@ -42,7 +42,7 @@ public class OrganizationDomainModel implements Serializable {
return this.name;
}
public boolean getVerified() {
public boolean isVerified() {
return this.verified;
}

View file

@ -155,7 +155,7 @@ public class Organizations {
public static OrganizationDomainRepresentation toRepresentation(OrganizationDomainModel model) {
OrganizationDomainRepresentation representation = new OrganizationDomainRepresentation();
representation.setName(model.getName());
representation.setVerified(model.getVerified());
representation.setVerified(model.isVerified());
return representation;
}

View file

@ -98,19 +98,25 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
return createOrganization(name, name + ".org");
}
protected OrganizationRepresentation createOrganization(String name, String... orgDomain) {
return createOrganization(testRealm(), getCleanup(), name, brokerConfigFunction.apply(name).setUpIdentityProvider(), orgDomain);
protected OrganizationRepresentation createOrganization(String name, String... orgDomains) {
return createOrganization(testRealm(), name, orgDomains);
}
protected static OrganizationRepresentation createOrganization(RealmResource testRealm, TestCleanup testCleanup, String name, IdentityProviderRepresentation broker, String... orgDomain) {
OrganizationRepresentation org = createRepresentation(name, orgDomain);
protected OrganizationRepresentation createOrganization(RealmResource realm, String name, String... orgDomains) {
return createOrganization(realm, getCleanup(), name, brokerConfigFunction.apply(name).setUpIdentityProvider(), orgDomains);
}
protected static OrganizationRepresentation createOrganization(RealmResource testRealm, TestCleanup testCleanup, String name,
IdentityProviderRepresentation broker, String... orgDomains) {
OrganizationRepresentation org = createRepresentation(name, orgDomains);
String id;
try (Response response = testRealm.organizations().create(org)) {
assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
id = ApiUtil.getCreatedId(response);
}
broker.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, org.getDomains().iterator().next().getName());
// set the idp domain to the first domain used to create the org.
broker.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, orgDomains[0]);
testRealm.identityProviders().create(broker).close();
testCleanup.addCleanup(testRealm.identityProviders().get(broker.getAlias())::remove);
testRealm.organizations().get(id).identityProviders().addIdentityProvider(broker.getAlias()).close();
@ -226,6 +232,14 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
}
protected BrokerConfiguration createBrokerConfiguration() {
return new KcOidcBrokerConfiguration();
};
return new KcOidcBrokerConfiguration() {
@Override
public RealmRepresentation createProviderRealm() {
// enable organizations in the provider realm too just for testing purposes.
RealmRepresentation realmRep = super.createProviderRealm();
realmRep.setOrganizationsEnabled(true);
return realmRep;
}
};
}
}

View file

@ -355,7 +355,7 @@ public class OrganizationTest extends AbstractOrganizationTest {
}
expectedNewOrgBrDomain.setName("acme.com");
// create another org and attempt to set the same internet domain during update - should not be possible.
// create another org in the same realm 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());
@ -363,6 +363,9 @@ public class OrganizationTest extends AbstractOrganizationTest {
assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
}
// create another org in a different realm with the same internet domain - should be allowed.
createOrganization(adminClient.realm(bc.providerRealmName()), "testorg", "acme.com");
// try to remove a domain
organization = testRealm().organizations().get(existing.getId());
existing.removeDomain(existingNewOrgDomain);