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 53c214c969..f5c5497559 100644 --- a/core/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/OrganizationRepresentation.java @@ -28,6 +28,7 @@ public class OrganizationRepresentation { private String id; private String name; + private boolean enabled = true; private Map> attributes; private Set domains; @@ -47,6 +48,14 @@ public class OrganizationRepresentation { return name; } + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + public Map> getAttributes() { return attributes; } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java index f0ed6314db..e56a95180b 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java @@ -19,10 +19,12 @@ package org.keycloak.models.cache.infinispan; import org.jboss.logging.Logger; import org.keycloak.cluster.ClusterProvider; +import org.keycloak.common.Profile; import org.keycloak.credential.CredentialInput; import org.keycloak.models.ClientScopeModel; import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.OrganizationModel; import org.keycloak.models.cache.infinispan.events.InvalidationEvent; import org.keycloak.common.constants.ServiceAccountConstants; import org.keycloak.component.ComponentModel; @@ -54,6 +56,7 @@ import org.keycloak.models.cache.infinispan.events.UserUpdatedEvent; import org.keycloak.models.cache.infinispan.stream.InIdentityProviderPredicate; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.ReadOnlyUserModelDelegate; +import org.keycloak.organization.OrganizationProvider; import org.keycloak.storage.CacheableStorageProviderModel; import org.keycloak.storage.DatastoreProvider; import org.keycloak.storage.StoreManagers; @@ -336,6 +339,20 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC protected UserModel cacheUser(RealmModel realm, UserModel delegate, Long revision) { int notBefore = getDelegate().getNotBeforeOfUser(realm, delegate); + if (Profile.isFeatureEnabled(Profile.Feature.ORGANIZATION)) { + // check if user is member of a disabled organization. + OrganizationProvider organizationProvider = session.getProvider(OrganizationProvider.class); + OrganizationModel organization = organizationProvider.getByMember(delegate); + if (organization != null && organization.isManaged(delegate) && !organization.isEnabled()) { + return new ReadOnlyUserModelDelegate(delegate) { + @Override + public boolean isEnabled() { + return false; + } + }; + } + } + StorageId storageId = delegate.getFederationLink() != null ? new StorageId(delegate.getFederationLink(), delegate.getId()) : new StorageId(delegate.getId()); CachedUser cached = null; diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OrganizationEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OrganizationEntity.java index 5ea596af79..b912a2b835 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OrganizationEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OrganizationEntity.java @@ -51,6 +51,9 @@ public class OrganizationEntity { @Column(name = "NAME") private String name; + @Column(name = "ENABLED") + private boolean enabled; + @Column(name = "REALM_ID") private String realmId; @@ -72,6 +75,14 @@ public class OrganizationEntity { this.name = name; } + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + public String getRealmId() { return realmId; } 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 f314dce940..6b98876734 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 @@ -80,6 +80,7 @@ public class JpaOrganizationProvider implements OrganizationProvider { entity.setGroupId(group.getId()); entity.setRealmId(realm.getId()); entity.setName(name); + entity.setEnabled(true); em.persist(entity); 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 52a1342d67..d3698dc8c8 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 @@ -76,6 +76,16 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel> attributes) { if (attributes == null) { diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-25.0.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-25.0.0.xml index 5cc3ad8c31..2502b16674 100644 --- a/model/jpa/src/main/resources/META-INF/jpa-changelog-25.0.0.xml +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-25.0.0.xml @@ -83,6 +83,9 @@ + + + diff --git a/model/storage-private/src/main/java/org/keycloak/storage/UserStorageManager.java b/model/storage-private/src/main/java/org/keycloak/storage/UserStorageManager.java index e1244cea07..d00469c74c 100755 --- a/model/storage-private/src/main/java/org/keycloak/storage/UserStorageManager.java +++ b/model/storage-private/src/main/java/org/keycloak/storage/UserStorageManager.java @@ -33,6 +33,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.jboss.logging.Logger; +import org.keycloak.common.Profile; import org.keycloak.common.constants.ServiceAccountConstants; import org.keycloak.common.util.reflections.Types; import org.keycloak.component.ComponentFactory; @@ -50,6 +51,7 @@ import org.keycloak.models.GroupModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelException; +import org.keycloak.models.OrganizationModel; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; @@ -62,6 +64,7 @@ import org.keycloak.models.cache.OnUserCache; import org.keycloak.models.cache.UserCache; import org.keycloak.models.utils.ComponentUtil; import org.keycloak.models.utils.ReadOnlyUserModelDelegate; +import org.keycloak.organization.OrganizationProvider; import org.keycloak.storage.client.ClientStorageProvider; import org.keycloak.storage.datastore.DefaultDatastoreProvider; import org.keycloak.storage.federated.UserFederatedStorageProvider; @@ -111,6 +114,20 @@ public class UserStorageManager extends AbstractStorageManager> getAttributes(); void setAttributes(Map> attributes); 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 5fb2fe8630..bfe80349e2 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 @@ -173,6 +173,7 @@ public class OrganizationResource { rep.setId(model.getId()); rep.setName(model.getName()); + rep.setEnabled(model.isEnabled()); rep.setAttributes(model.getAttributes()); model.getDomains().filter(Objects::nonNull).map(this::toRepresentation) .forEach(rep::addDomain); @@ -193,6 +194,7 @@ public class OrganizationResource { } model.setName(rep.getName()); + model.setEnabled(rep.isEnabled()); model.setAttributes(rep.getAttributes()); model.setDomains(Optional.ofNullable(rep.getDomains()).orElse(Set.of()).stream() .filter(Objects::nonNull) 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 e870b94d15..45be6f7fa2 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 @@ -19,6 +19,7 @@ package org.keycloak.testsuite.organization.admin; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage; import java.util.List; import java.util.function.Function; @@ -28,10 +29,12 @@ import jakarta.ws.rs.core.Response.Status; import org.jboss.arquillian.graphene.page.Page; import org.keycloak.admin.client.resource.OrganizationResource; import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.admin.client.resource.UsersResource; import org.keycloak.representations.idm.OrganizationDomainRepresentation; import org.keycloak.representations.idm.OrganizationRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.admin.AbstractAdminTest; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.Users; @@ -200,4 +203,43 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest { return actual; } } + + protected void assertBrokerRegistration(OrganizationResource organization, String email) { + // login with email only + oauth.clientId("broker-app"); + loginPage.open(bc.consumerRealmName()); + log.debug("Logging in"); + Assert.assertFalse(loginPage.isPasswordInputPresent()); + Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias())); + loginPage.loginUsername(email); + + // user automatically redirected to the organization identity provider + waitForPage(driver, "sign in to", true); + Assert.assertTrue("Driver should be on the provider realm page right now", + driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/")); + // login to the organization identity provider and run the configured first broker login flow + loginPage.login(email, bc.getUserPassword()); + waitForPage(driver, "update account information", false); + updateAccountInformationPage.assertCurrent(); + Assert.assertTrue("We must be on correct realm right now", + driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/")); + log.debug("Updating info on updateAccount page"); + updateAccountInformationPage.updateAccountInformation(bc.getUserLogin(), email, "Firstname", "Lastname"); + + assertIsMember(email, organization); + } + + protected void assertIsMember(String userEmail, OrganizationResource organization) { + UserRepresentation account = getUserRepresentation(userEmail); + UserRepresentation member = organization.members().member(account.getId()).toRepresentation(); + Assert.assertEquals(account.getId(), member.getId()); + } + + protected UserRepresentation getUserRepresentation(String userEmail) { + UsersResource users = adminClient.realm(bc.consumerRealmName()).users(); + List reps = users.searchByEmail(userEmail, true); + Assert.assertFalse(reps.isEmpty()); + Assert.assertEquals(1, reps.size()); + return reps.get(0); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationBrokerSelfRegistrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationBrokerSelfRegistrationTest.java index e5690d1641..b47cb95b3a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationBrokerSelfRegistrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationBrokerSelfRegistrationTest.java @@ -582,37 +582,6 @@ public class OrganizationBrokerSelfRegistrationTest extends AbstractOrganization appPage.assertCurrent(); } - private void assertBrokerRegistration(OrganizationResource organization, String email) { - // login with email only - oauth.clientId("broker-app"); - loginPage.open(bc.consumerRealmName()); - log.debug("Logging in"); - Assert.assertFalse(loginPage.isPasswordInputPresent()); - Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias())); - loginPage.loginUsername(email); - - // user automatically redirected to the organization identity provider - waitForPage(driver, "sign in to", true); - Assert.assertTrue("Driver should be on the provider realm page right now", - driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/")); - // login to the organization identity provider and run the configured first broker login flow - loginPage.login(email, bc.getUserPassword()); - waitForPage(driver, "update account information", false); - updateAccountInformationPage.assertCurrent(); - Assert.assertTrue("We must be on correct realm right now", - driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/")); - log.debug("Updating info on updateAccount page"); - updateAccountInformationPage.updateAccountInformation(bc.getUserLogin(), email, "Firstname", "Lastname"); - - assertIsMember(email, organization); - } - - private void assertIsMember(String userEmail, OrganizationResource organization) { - UserRepresentation account = getUserRepresentation(userEmail); - UserRepresentation member = organization.members().member(account.getId()).toRepresentation(); - Assert.assertEquals(account.getId(), member.getId()); - } - private void assertIsNotMember(String userEmail, OrganizationResource organization) { UsersResource users = adminClient.realm(bc.consumerRealmName()).users(); List reps = users.searchByEmail(userEmail, true); @@ -629,12 +598,4 @@ public class OrganizationBrokerSelfRegistrationTest extends AbstractOrganization } catch (NotFoundException ignore) { } } - - private UserRepresentation getUserRepresentation(String userEmail) { - UsersResource users = adminClient.realm(bc.consumerRealmName()).users(); - List reps = users.searchByEmail(userEmail, true); - Assert.assertFalse(reps.isEmpty()); - Assert.assertEquals(1, reps.size()); - return reps.get(0); - } } 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 4cea02b590..c18dfe1258 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 @@ -22,6 +22,7 @@ import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -160,6 +161,55 @@ public class OrganizationMemberTest extends AbstractOrganizationTest { assertEquals(expectedRep.getEmail(), existingRep.getEmail()); assertEquals(expectedRep.getFirstName(), existingRep.getFirstName()); assertEquals(expectedRep.getLastName(), existingRep.getLastName()); + assertTrue(expectedRep.isEnabled()); + } + } + + @Test + public void testGetAllDisabledOrganization() { + OrganizationRepresentation orgRep = createOrganization(); + OrganizationResource organization = testRealm().organizations().get(orgRep.getId()); + + // add some unmanaged members to the organization. + for (int i = 0; i < 5; i++) { + addMember(organization, "member-" + i + "@neworg.org"); + } + + // onboard a test user by authenticating using the organization's provider. + super.assertBrokerRegistration(organization, bc.getUserEmail()); + + // disable the organization and check that fetching its representation has it disabled. + orgRep.setEnabled(false); + try (Response response = organization.update(orgRep)) { + assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus()); + } + OrganizationRepresentation existingOrg = organization.toRepresentation(); + assertThat(orgRep.getId(), is(equalTo(existingOrg.getId()))); + assertThat(orgRep.getName(), is(equalTo(existingOrg.getName()))); + assertThat(existingOrg.isEnabled(), is(false)); + + // now fetch all users from the org - unmanaged users should still be enabled, but managed ones should not. + List existing = organization.members().getAll();; + assertThat(existing, not(empty())); + assertThat(existing, hasSize(6)); + for (UserRepresentation user : existing) { + if (user.getEmail().equals(bc.getUserEmail())) { + assertThat(user.isEnabled(), is(false)); + } else { + assertThat(user.isEnabled(), is(true)); + } + } + + // fetching users from the users endpoint should have the same result. + existing = testRealm().users().search("*neworg*",0, 10); + assertThat(existing, not(empty())); + assertThat(existing, hasSize(6)); + for (UserRepresentation user : existing) { + if (user.getEmail().equals(bc.getUserEmail())) { + assertThat(user.isEnabled(), is(false)); + } else { + assertThat(user.isEnabled(), is(true)); + } } } 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 52f34f5786..a105e92267 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 @@ -55,6 +55,7 @@ public class OrganizationTest extends AbstractOrganizationTest { assertEquals(organizationName, expected.getName()); expected.setName("acme"); + expected.setEnabled(false); OrganizationResource organization = testRealm().organizations().get(expected.getId()); @@ -66,6 +67,7 @@ public class OrganizationTest extends AbstractOrganizationTest { assertEquals(expected.getId(), existing.getId()); assertEquals(expected.getName(), existing.getName()); assertEquals(1, existing.getDomains().size()); + assertThat(existing.isEnabled(), is(false)); } @Test @@ -75,6 +77,7 @@ public class OrganizationTest extends AbstractOrganizationTest { assertNotNull(existing); assertEquals(expected.getId(), existing.getId()); assertEquals(expected.getName(), existing.getName()); + assertThat(expected.isEnabled(), is(true)); } @Test @@ -103,6 +106,7 @@ public class OrganizationTest extends AbstractOrganizationTest { assertThat(existing, hasSize(1)); OrganizationRepresentation orgRep = existing.get(0); assertThat(orgRep.getName(), is(equalTo("wayne-industries"))); + assertThat(orgRep.isEnabled(), is(true)); assertThat(orgRep.getDomains(), hasSize(2)); assertThat(orgRep.getDomain("wayneind.com"), not(nullValue())); assertThat(orgRep.getDomain("wayneind-gotham.com"), not(nullValue())); @@ -111,6 +115,7 @@ public class OrganizationTest extends AbstractOrganizationTest { assertThat(existing, hasSize(1)); orgRep = existing.get(0); assertThat(orgRep.getName(), is(equalTo("Gotham-Bank"))); + assertThat(orgRep.isEnabled(), is(true)); assertThat(orgRep.getDomains(), hasSize(2)); assertThat(orgRep.getDomain("gtbank.com"), not(nullValue())); assertThat(orgRep.getDomain("gtbank.net"), not(nullValue()));