diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java index 79c6e07260..26d145eba0 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -214,6 +214,8 @@ public class RealmRepresentation { protected Boolean userManagedAccessAllowed; + protected Boolean organizationsEnabled; + @Deprecated protected Boolean social; @Deprecated @@ -1420,6 +1422,14 @@ public class RealmRepresentation { return userManagedAccessAllowed; } + public Boolean isOrganizationsEnabled() { + return organizationsEnabled; + } + + public void setOrganizationsEnabled(Boolean organizationsEnabled) { + this.organizationsEnabled = organizationsEnabled; + } + @JsonIgnore public Map getAttributesOrEmpty() { return (Map) (attributes == null ? Collections.emptyMap() : attributes); diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties index 9cf308ec41..869dc0eb64 100644 --- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties +++ b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties @@ -3131,4 +3131,6 @@ identityBrokeringLink=Identity brokering link searchClientRegistration=Search for policy importFileHelp=File to import a key logo=Logo -avatarImage=Avatar image \ No newline at end of file +avatarImage=Avatar image +organizationsEnabled=Organizations +organizationsEnabledHelp=If enabled, allows managing organizations. Otherwise, existing organizations are still kept but you will not be able to manage them anymore or authenticate their members. diff --git a/js/apps/admin-ui/src/realm-settings/GeneralTab.tsx b/js/apps/admin-ui/src/realm-settings/GeneralTab.tsx index c44c472504..80e89824bf 100644 --- a/js/apps/admin-ui/src/realm-settings/GeneralTab.tsx +++ b/js/apps/admin-ui/src/realm-settings/GeneralTab.tsx @@ -36,6 +36,8 @@ import { import { useFetch } from "../utils/useFetch"; import { UIRealmRepresentation } from "./RealmSettingsTabs"; +import useIsFeatureEnabled, { Feature } from "../utils/useIsFeatureEnabled"; + type RealmSettingsGeneralTabProps = { realm: UIRealmRepresentation; save: (realm: UIRealmRepresentation) => void; @@ -105,6 +107,8 @@ function RealmSettingsGeneralTabForm({ setValue, formState: { isDirty, errors }, } = form; + const isFeatureEnabled = useIsFeatureEnabled(); + const isOrganizationsEnabled = isFeatureEnabled(Feature.Organizations); const setupForm = () => { convertToFormValues(realm, setValue); @@ -212,6 +216,13 @@ function RealmSettingsGeneralTabForm({ label={t("userManagedAccess")} labelIcon={t("userManagedAccessHelp")} /> + {isOrganizationsEnabled && ( + + )} Bill Burke @@ -860,19 +862,34 @@ public class RealmAdapter implements CachedRealmModel { @Override public Stream getIdentityProvidersStream() { - if (isUpdated()) return updated.getIdentityProvidersStream(); - return cached.getIdentityProviders().stream(); + if (isUpdated()) return updated.getIdentityProvidersStream().map(this::createOrganizationAwareIdentityProviderModel); + return cached.getIdentityProviders().stream().map(this::createOrganizationAwareIdentityProviderModel); } @Override public IdentityProviderModel getIdentityProviderByAlias(String alias) { - if (isUpdated()) return updated.getIdentityProviderByAlias(alias); + if (isUpdated()) return createOrganizationAwareIdentityProviderModel(updated.getIdentityProviderByAlias(alias)); return getIdentityProvidersStream() .filter(model -> Objects.equals(model.getAlias(), alias)) .findFirst() + .map(this::createOrganizationAwareIdentityProviderModel) .orElse(null); } + private IdentityProviderModel createOrganizationAwareIdentityProviderModel(IdentityProviderModel idp) { + if (!Profile.isFeatureEnabled(Profile.Feature.ORGANIZATION)) return idp; + return new IdentityProviderModel(idp) { + @Override + public boolean isEnabled() { + // if IdP is bound to an org + if (getOrganizationId() != null) { + return session.getProvider(OrganizationProvider.class).isEnabled() && super.isEnabled(); + } + return super.isEnabled(); + } + }; + } + @Override public void addIdentityProvider(IdentityProviderModel identityProvider) { getDelegateForUpdate(); @@ -1748,4 +1765,21 @@ public class RealmAdapter implements CachedRealmModel { public String toString() { return String.format("%s@%08x", getId(), hashCode()); } + + @Override + public boolean isOrganizationsEnabled() { + if (isUpdated()) return featureAwareIsOrganizationsEnabled(updated.isOrganizationsEnabled()); + return featureAwareIsOrganizationsEnabled(cached.isOrganizationsEnabled()); + } + + @Override + public void setOrganizationsEnabled(boolean organizationsEnabled) { + getDelegateForUpdate(); + updated.setOrganizationsEnabled(organizationsEnabled); + } + + private boolean featureAwareIsOrganizationsEnabled(boolean isOrganizationsEnabled) { + if (!Profile.isFeatureEnabled(Profile.Feature.ORGANIZATION)) return false; + return isOrganizationsEnabled; + } } 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 e56a95180b..86475fa81a 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 @@ -340,10 +340,12 @@ public class UserCacheSession implements UserCache, OnCreateComponent, OnUpdateC int notBefore = getDelegate().getNotBeforeOfUser(realm, delegate); if (Profile.isFeatureEnabled(Profile.Feature.ORGANIZATION)) { - // check if user is member of a disabled organization. + // check if provider is enabled and user is managed member of a disabled organization OR provider is disabled and user is managed member OrganizationProvider organizationProvider = session.getProvider(OrganizationProvider.class); OrganizationModel organization = organizationProvider.getByMember(delegate); - if (organization != null && organization.isManaged(delegate) && !organization.isEnabled()) { + + if ((organizationProvider.isEnabled() && organization != null && organization.isManaged(delegate) && !organization.isEnabled()) || + (!organizationProvider.isEnabled() && organization != null && organization.isManaged(delegate))) { return new ReadOnlyUserModelDelegate(delegate) { @Override public boolean isEnabled() { diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java index dfccd91df3..1cc52d3e34 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java @@ -70,6 +70,7 @@ public class CachedRealm extends AbstractExtendableRevisioned { protected boolean resetPasswordAllowed; protected boolean identityFederationEnabled; protected boolean editUsernameAllowed; + protected boolean organizationsEnabled; //--- brute force settings protected boolean bruteForceProtected; protected boolean permanentLockout; @@ -191,6 +192,7 @@ public class CachedRealm extends AbstractExtendableRevisioned { resetPasswordAllowed = model.isResetPasswordAllowed(); identityFederationEnabled = model.isIdentityFederationEnabled(); editUsernameAllowed = model.isEditUsernameAllowed(); + organizationsEnabled = model.isOrganizationsEnabled(); //--- brute force settings bruteForceProtected = model.isBruteForceProtected(); permanentLockout = model.isPermanentLockout(); @@ -423,6 +425,10 @@ public class CachedRealm extends AbstractExtendableRevisioned { return editUsernameAllowed; } + public boolean isOrganizationsEnabled() { + return organizationsEnabled; + } + public String getDefaultSignatureAlgorithm() { return defaultSignatureAlgorithm; } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index 0156d20bd8..3f08d27f90 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -1173,6 +1173,16 @@ public class RealmAdapter implements StorageProviderRealmModel, JpaModel idps = this.getIdentityProviders().toList(); - Map modelMap = domains.stream() .map(this::validateDomain) .collect(Collectors.toMap(OrganizationDomainModel::getName, Function.identity())); @@ -147,17 +145,16 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel { - if (Objects.equals(domainEntity.getName(), idp.getConfig().get(ORGANIZATION_DOMAIN_ATTRIBUTE))) { - idp.getConfig().remove(ORGANIZATION_DOMAIN_ATTRIBUTE); - realm.updateIdentityProvider(idp); - } - }); + getIdentityProviders() + .filter(idp -> Objects.equals(domainEntity.getName(), idp.getConfig().get(ORGANIZATION_DOMAIN_ATTRIBUTE))) + .forEach(idp -> { + idp.getConfig().remove(ORGANIZATION_DOMAIN_ATTRIBUTE); + realm.updateIdentityProvider(idp); + }); } } 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 d00469c74c..66db3abfcc 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 @@ -68,7 +68,6 @@ import org.keycloak.organization.OrganizationProvider; import org.keycloak.storage.client.ClientStorageProvider; import org.keycloak.storage.datastore.DefaultDatastoreProvider; import org.keycloak.storage.federated.UserFederatedStorageProvider; -import org.keycloak.storage.federated.UserGroupMembershipFederatedStorage; import org.keycloak.storage.managers.UserStorageSyncManager; import org.keycloak.storage.user.ImportedUserValidation; import org.keycloak.storage.user.UserBulkUpdateProvider; @@ -116,10 +115,12 @@ public class UserStorageManager extends AbstractStorageManagerA model type representing the configuration for identity providers. It provides some common properties and also a {@link org.keycloak.models.IdentityProviderModel#config} @@ -319,4 +320,20 @@ public class IdentityProviderModel implements Serializable { public void setMetadataDescriptorUrl(String metadataDescriptorUrl) { getConfig().put(METADATA_DESCRIPTOR_URL, metadataDescriptorUrl); } + + @Override + public int hashCode() { + int hash = 5; + hash = 61 * hash + Objects.hashCode(this.internalId); + hash = 61 * hash + Objects.hashCode(this.alias); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof IdentityProviderModel)) return false; + return Objects.equals(getInternalId(), ((IdentityProviderModel) obj).getInternalId()) && + Objects.equals(getAlias(), ((IdentityProviderModel) obj).getAlias()); + } } diff --git a/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java index 07fe914f56..9569229f0c 100755 --- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java +++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java @@ -105,6 +105,10 @@ public interface RealmModel extends RoleContainerModel { void setUserManagedAccessAllowed(boolean userManagedAccessAllowed); + boolean isOrganizationsEnabled(); + + void setOrganizationsEnabled(boolean organizationsEnabled); + void setAttribute(String name, String value); default void setAttribute(String name, Boolean value) { setAttribute(name, value.toString()); 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 1710283530..69dcd99559 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 @@ -89,6 +89,7 @@ public class OrganizationResource { @Operation( summary = "Creates a new organization") public Response create(OrganizationRepresentation organization) { auth.realm().requireManageRealm(); + checkOrganizationsEnabled(); if (organization == null) { throw ErrorResponse.error("Organization cannot be null.", Response.Status.BAD_REQUEST); } @@ -126,6 +127,7 @@ public class OrganizationResource { @Parameter(description = "The maximum number of results to be returned - defaults to 10") @QueryParam("max") @DefaultValue("10") Integer max ) { auth.realm().requireManageRealm(); + checkOrganizationsEnabled(); // check if are searching orgs by attribute. if(StringUtil.isNotBlank(searchQuery)) { @@ -150,6 +152,7 @@ public class OrganizationResource { @Operation(summary = "Returns the organization associated with the specified id, or null if no organization is found") public OrganizationRepresentation get(@PathParam("id") String id) { auth.realm().requireManageRealm(); + checkOrganizationsEnabled(); if (StringUtil.isBlank(id)) { throw ErrorResponse.error("Id cannot be null.", Response.Status.BAD_REQUEST); } @@ -163,6 +166,7 @@ public class OrganizationResource { @Operation(summary = "Deletes the organization with the specified id") public Response delete(@PathParam("id") String id) { auth.realm().requireManageRealm(); + checkOrganizationsEnabled(); if (StringUtil.isBlank(id)) { throw ErrorResponse.error("Id cannot be null.", Response.Status.BAD_REQUEST); } @@ -179,6 +183,7 @@ public class OrganizationResource { @Operation(summary = "Updates the organization with the specified id") public Response update(@PathParam("id") String id, OrganizationRepresentation organization) { auth.realm().requireManageRealm(); + checkOrganizationsEnabled(); OrganizationModel model = getOrganization(id); toModel(organization, model); @@ -187,11 +192,13 @@ public class OrganizationResource { @Path("{id}/members") public OrganizationMemberResource members(@PathParam("id") String id) { + checkOrganizationsEnabled(); return new OrganizationMemberResource(session, getOrganization(id), auth, adminEvent); } @Path("{id}/identity-providers") public OrganizationIdentityProvidersResource identityProvider(@PathParam("id") String id) { + checkOrganizationsEnabled(); return new OrganizationIdentityProvidersResource(session, getOrganization(id), auth, adminEvent); } @@ -259,4 +266,10 @@ public class OrganizationResource { private OrganizationDomainModel toModel(OrganizationDomainRepresentation domainRepresentation) { return new OrganizationDomainModel(domainRepresentation.getName(), domainRepresentation.isVerified()); } + + private void checkOrganizationsEnabled() { + if (provider != null && !provider.isEnabled()) { + throw ErrorResponse.error("Organizations not enabled for this realm.", Response.Status.NOT_FOUND); + } + } } diff --git a/services/src/main/java/org/keycloak/organization/authentication/authenticators/broker/IdpAddOrganizationMemberAuthenticator.java b/services/src/main/java/org/keycloak/organization/authentication/authenticators/broker/IdpAddOrganizationMemberAuthenticator.java index 55216f39e4..51b7f4672b 100644 --- a/services/src/main/java/org/keycloak/organization/authentication/authenticators/broker/IdpAddOrganizationMemberAuthenticator.java +++ b/services/src/main/java/org/keycloak/organization/authentication/authenticators/broker/IdpAddOrganizationMemberAuthenticator.java @@ -17,7 +17,6 @@ package org.keycloak.organization.authentication.authenticators.broker; -import java.util.List; import java.util.stream.Stream; import org.keycloak.authentication.AuthenticationFlowContext; @@ -32,6 +31,8 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.organization.OrganizationProvider; +import static org.keycloak.organization.utils.Organizations.isEnabledAndOrganizationsPresent; + public class IdpAddOrganizationMemberAuthenticator extends AbstractIdpAuthenticator { @Override @@ -70,13 +71,13 @@ public class IdpAddOrganizationMemberAuthenticator extends AbstractIdpAuthentica public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { OrganizationProvider provider = session.getProvider(OrganizationProvider.class); - if (!provider.isEnabled()) { + if (!isEnabledAndOrganizationsPresent(provider)) { return false; } OrganizationModel organization = (OrganizationModel) session.getAttribute(OrganizationModel.class.getName()); - if (organization == null) { + if (organization == null || !organization.isEnabled()) { return false; } diff --git a/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java b/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java index 7fca1fab7e..8ff30c77c9 100644 --- a/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java +++ b/services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java @@ -17,11 +17,10 @@ package org.keycloak.organization.authentication.authenticators.browser; +import static org.keycloak.organization.utils.Organizations.isEnabledAndOrganizationsPresent; import static org.keycloak.organization.utils.Organizations.resolveBroker; import java.util.List; -import java.util.Objects; - import jakarta.ws.rs.core.MultivaluedMap; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; @@ -30,7 +29,6 @@ import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.forms.login.freemarker.model.AuthenticationContextBean; import org.keycloak.forms.login.freemarker.model.IdentityProviderBean; import org.keycloak.http.HttpRequest; -import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.OrganizationModel; @@ -56,7 +54,7 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator { public void authenticate(AuthenticationFlowContext context) { OrganizationProvider provider = getOrganizationProvider(); - if (!provider.isEnabled()) { + if (!isEnabledAndOrganizationsPresent(provider)) { context.attempted(); return; } diff --git a/services/src/main/java/org/keycloak/organization/protocol/mappers/oidc/OrganizationMembershipMapper.java b/services/src/main/java/org/keycloak/organization/protocol/mappers/oidc/OrganizationMembershipMapper.java index 35b2d2b8c1..0cf9ecb53a 100644 --- a/services/src/main/java/org/keycloak/organization/protocol/mappers/oidc/OrganizationMembershipMapper.java +++ b/services/src/main/java/org/keycloak/organization/protocol/mappers/oidc/OrganizationMembershipMapper.java @@ -42,6 +42,8 @@ import org.keycloak.provider.EnvironmentDependentProviderFactory; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.IDToken; +import static org.keycloak.organization.utils.Organizations.isEnabledAndOrganizationsPresent; + public class OrganizationMembershipMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper, TokenIntrospectionTokenMapper, EnvironmentDependentProviderFactory { public static final String PROVIDER_ID = "oidc-organization-membership-mapper"; @@ -77,7 +79,7 @@ public class OrganizationMembershipMapper extends AbstractOIDCProtocolMapper imp protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession keycloakSession, ClientSessionContext clientSessionCtx) { OrganizationProvider provider = keycloakSession.getProvider(OrganizationProvider.class); - if (!provider.isEnabled()) { + if (!isEnabledAndOrganizationsPresent(provider)) { return; } diff --git a/services/src/main/java/org/keycloak/organization/protocol/mappers/saml/OrganizationMembershipMapper.java b/services/src/main/java/org/keycloak/organization/protocol/mappers/saml/OrganizationMembershipMapper.java index 1dab624cd2..1957926d79 100755 --- a/services/src/main/java/org/keycloak/organization/protocol/mappers/saml/OrganizationMembershipMapper.java +++ b/services/src/main/java/org/keycloak/organization/protocol/mappers/saml/OrganizationMembershipMapper.java @@ -18,7 +18,6 @@ package org.keycloak.organization.protocol.mappers.saml; import java.util.List; - import org.keycloak.Config.Scope; import org.keycloak.common.Profile; import org.keycloak.common.Profile.Feature; @@ -39,6 +38,8 @@ import org.keycloak.provider.EnvironmentDependentProviderFactory; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; +import static org.keycloak.organization.utils.Organizations.isEnabledAndOrganizationsPresent; + public class OrganizationMembershipMapper extends AbstractSAMLProtocolMapper implements SAMLAttributeStatementMapper, EnvironmentDependentProviderFactory { public static final String ID = "saml-organization-membership-mapper"; @@ -59,7 +60,7 @@ public class OrganizationMembershipMapper extends AbstractSAMLProtocolMapper imp public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { OrganizationProvider provider = session.getProvider(OrganizationProvider.class); - if (!provider.isEnabled()) { + if (!isEnabledAndOrganizationsPresent(provider)) { return; } diff --git a/services/src/main/java/org/keycloak/organization/utils/Organizations.java b/services/src/main/java/org/keycloak/organization/utils/Organizations.java index 3aefd260cf..29e81442c6 100644 --- a/services/src/main/java/org/keycloak/organization/utils/Organizations.java +++ b/services/src/main/java/org/keycloak/organization/utils/Organizations.java @@ -114,4 +114,9 @@ public class Organizations { } }; } + + public static boolean isEnabledAndOrganizationsPresent(OrganizationProvider organizationProvider) { + // todo replace getAllStream().findAny().isPresent() with count query + return organizationProvider != null && organizationProvider.isEnabled() && organizationProvider.getAllStream().findAny().isPresent(); + } } diff --git a/services/src/main/java/org/keycloak/social/openshift/OpenshiftV4IdentityProviderConfig.java b/services/src/main/java/org/keycloak/social/openshift/OpenshiftV4IdentityProviderConfig.java index 6fe0dba4ec..9dd2f765b8 100644 --- a/services/src/main/java/org/keycloak/social/openshift/OpenshiftV4IdentityProviderConfig.java +++ b/services/src/main/java/org/keycloak/social/openshift/OpenshiftV4IdentityProviderConfig.java @@ -6,8 +6,6 @@ import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigurationBuilder; import java.util.List; -import java.util.Map; -import java.util.Optional; /** * OpenShift 4 Identity Provider configuration class. diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/RealmAttributeUpdater.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/RealmAttributeUpdater.java index 1dca3561b5..4e27d80991 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/RealmAttributeUpdater.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/RealmAttributeUpdater.java @@ -179,4 +179,9 @@ public class RealmAttributeUpdater extends ServerResourceUpdaterPedro Igor @@ -78,6 +80,7 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest { public void configureTestRealm(RealmRepresentation testRealm) { testRealm.getClients().addAll(bc.createConsumerClients()); testRealm.setSmtpServer(null); + testRealm.setOrganizationsEnabled(Boolean.TRUE); super.configureTestRealm(testRealm); } @@ -96,25 +99,28 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest { } protected OrganizationRepresentation createOrganization(String name, String... orgDomain) { + return createOrganization(testRealm(), getCleanup(), name, brokerConfigFunction.apply(name).setUpIdentityProvider(), orgDomain); + } + + protected static OrganizationRepresentation createOrganization(RealmResource testRealm, TestCleanup testCleanup, String name, IdentityProviderRepresentation broker, String... orgDomain) { OrganizationRepresentation org = createRepresentation(name, orgDomain); String id; - try (Response response = testRealm().organizations().create(org)) { + try (Response response = testRealm.organizations().create(org)) { assertEquals(Status.CREATED.getStatusCode(), response.getStatus()); id = ApiUtil.getCreatedId(response); } - IdentityProviderRepresentation broker = brokerConfigFunction.apply(name).setUpIdentityProvider(); broker.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, org.getDomains().iterator().next().getName()); - testRealm().identityProviders().create(broker).close(); - getCleanup().addCleanup(testRealm().identityProviders().get(broker.getAlias())::remove); - testRealm().organizations().get(id).identityProviders().addIdentityProvider(broker.getAlias()).close(); - org = testRealm().organizations().get(id).toRepresentation(); - getCleanup().addCleanup(() -> testRealm().organizations().get(id).delete().close()); + testRealm.identityProviders().create(broker).close(); + testCleanup.addCleanup(testRealm.identityProviders().get(broker.getAlias())::remove); + testRealm.organizations().get(id).identityProviders().addIdentityProvider(broker.getAlias()).close(); + org = testRealm.organizations().get(id).toRepresentation(); + testCleanup.addCleanup(() -> testRealm.organizations().get(id).delete().close()); return org; } - protected OrganizationRepresentation createRepresentation(String name, String... orgDomains) { + protected static OrganizationRepresentation createRepresentation(String name, String... orgDomains) { OrganizationRepresentation org = new OrganizationRepresentation(); org.setName(name); @@ -188,7 +194,8 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest { log.debug("Updating info on updateAccount page"); assertFalse(driver.getPageSource().contains("kc.org")); updateAccountInformationPage.updateAccountInformation(bc.getUserLogin(), email, "Firstname", "Lastname"); - + assertThat(appPage.getRequestType(),is(AppPage.RequestType.AUTH_RESPONSE)); + assertIsMember(email, organization); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationAdminPermissionsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationAdminPermissionsTest.java index 743ac351c7..b5c394562d 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationAdminPermissionsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationAdminPermissionsTest.java @@ -50,6 +50,7 @@ public class OrganizationAdminPermissionsTest extends AbstractOrganizationTest { .role(Constants.REALM_MANAGEMENT_CLIENT_ID, AdminRoles.MANAGE_IDENTITY_PROVIDERS) .role(Constants.REALM_MANAGEMENT_CLIENT_ID, AdminRoles.MANAGE_USERS) .build()); + super.configureTestRealm(testRealm); } @Test diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationMemberAuthenticationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationMemberAuthenticationTest.java index 43af9bd55c..91bae664e6 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationMemberAuthenticationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/admin/OrganizationMemberAuthenticationTest.java @@ -17,14 +17,18 @@ package org.keycloak.testsuite.organization.admin; +import static org.hamcrest.MatcherAssert.assertThat; import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage; +import java.io.IOException; +import org.hamcrest.Matchers; import org.junit.Test; import org.keycloak.admin.client.resource.OrganizationResource; import org.keycloak.common.Profile.Feature; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.arquillian.annotation.EnableFeature; +import org.keycloak.testsuite.updaters.RealmAttributeUpdater; @EnableFeature(Feature.ORGANIZATION) public class OrganizationMemberAuthenticationTest extends AbstractOrganizationTest { @@ -86,4 +90,36 @@ public class OrganizationMemberAuthenticationTest extends AbstractOrganizationTe Assert.assertTrue(loginPage.isUsernameInputPresent()); Assert.assertTrue(loginPage.isPasswordInputPresent()); } -} \ No newline at end of file + + @Test + public void testAuthenticateUnmanagedMemberWehnProviderDisabled() throws IOException { + OrganizationResource organization = testRealm().organizations().get(createOrganization().getId()); + UserRepresentation member = addMember(organization, "contractor@contractor.org"); + + // first try to access login page + oauth.clientId("broker-app"); + loginPage.open(bc.consumerRealmName()); + Assert.assertFalse(loginPage.isPasswordInputPresent()); + Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias())); + + // disable the organization provider + try (RealmAttributeUpdater rau = new RealmAttributeUpdater(testRealm()) + .setOrganizationEnabled(Boolean.FALSE) + .update()) { + + // access the page again, now it should be present username and password fields + loginPage.open(bc.consumerRealmName()); + + waitForPage(driver, "sign in to", true); + assertThat("Driver should be on the consumer realm page right now", + driver.getCurrentUrl(), Matchers.containsString("/auth/realms/" + bc.consumerRealmName() + "/")); + Assert.assertTrue(loginPage.isPasswordInputPresent()); + // no idp should be shown because there is only a single idp that is bound to an organization + Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias())); + + // the member should be able to log in using the credentials + loginPage.login(member.getEmail(), memberPassword); + appPage.assertCurrent(); + } + } +} 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 3b98298b2a..a92d989fc7 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 @@ -39,6 +39,7 @@ import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; +import java.io.IOException; import org.hamcrest.Matchers; import org.junit.Test; import org.keycloak.admin.client.resource.OrganizationMemberResource; @@ -57,6 +58,7 @@ import org.keycloak.representations.userprofile.config.UPConfig; import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.arquillian.annotation.EnableFeature; +import org.keycloak.testsuite.updaters.RealmAttributeUpdater; @EnableFeature(Feature.ORGANIZATION) public class OrganizationMemberTest extends AbstractOrganizationTest { @@ -194,7 +196,7 @@ public class OrganizationMemberTest extends AbstractOrganizationTest { 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();; + List existing = organization.members().getAll(); assertThat(existing, not(empty())); assertThat(existing, hasSize(6)); for (UserRepresentation user : existing) { @@ -229,6 +231,48 @@ public class OrganizationMemberTest extends AbstractOrganizationTest { } } + @Test + public void testGetAllDisabledOrganizationProvider() throws IOException { + 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()); + + // now fetch all users from the realm + List members = testRealm().users().search("*neworg*", null, null); + members.stream().forEach(user -> assertThat(user.isEnabled(), is(Boolean.TRUE))); + + // disable the organization provider + try (RealmAttributeUpdater rau = new RealmAttributeUpdater(testRealm()) + .setOrganizationEnabled(Boolean.FALSE) + .update()) { + + // now fetch all members from the realm - unmanaged users should still be enabled, but managed ones should not. + List existing = testRealm().users().search("*neworg*", null, null); + assertThat(existing, hasSize(members.size())); + for (UserRepresentation user : existing) { + if (user.getEmail().equals(bc.getUserEmail())) { + assertThat(user.isEnabled(), is(Boolean.FALSE)); + + // try to update the disabled user (for example, try to re-enable the user) - should not be possible. + user.setEnabled(Boolean.TRUE); + try { + testRealm().users().get(user.getId()).update(user); + fail("Should not be possible to update disabled org user"); + } catch(BadRequestException expected) {} + } else { + assertThat("User " + user.getUsername(), user.isEnabled(), is(true)); + } + } + } + } + @Test public void testDeleteUnmanagedMember() { UPConfig upConfig = testRealm().users().userProfile().getConfiguration(); 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 26a5ca2a76..cbf8b6e64e 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 @@ -41,6 +41,7 @@ import java.util.stream.Collectors; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; +import java.io.IOException; import org.junit.Test; import org.keycloak.admin.client.resource.OrganizationResource; import org.keycloak.admin.client.resource.RealmResource; @@ -51,6 +52,7 @@ import org.keycloak.representations.idm.OrganizationDomainRepresentation; import org.keycloak.representations.idm.OrganizationRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.arquillian.annotation.EnableFeature; +import org.keycloak.testsuite.updaters.RealmAttributeUpdater; import org.keycloak.testsuite.util.RealmBuilder; @EnableFeature(Feature.ORGANIZATION) @@ -373,35 +375,67 @@ public class OrganizationTest extends AbstractOrganizationTest { assertNotNull(existing.getDomain("acme.com")); } + @Test + public void testDisabledOrganizationProvider() throws IOException { + OrganizationRepresentation existing = createOrganization("acme", "acme.org", "acme.net"); + // disable the organization provider and try to access REST endpoints + try (RealmAttributeUpdater rau = new RealmAttributeUpdater(testRealm()) + .setOrganizationEnabled(Boolean.FALSE) + .update()) { + OrganizationRepresentation org = createRepresentation("some", "some.com"); + + try (Response response = testRealm().organizations().create(org)) { + assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus()); + } + try { + testRealm().organizations().getAll(); + fail("Expected NotFoundException"); + } catch (NotFoundException expected) {} + try { + testRealm().organizations().search("*"); + fail("Expected NotFoundException"); + } catch (NotFoundException expected) {} + try { + testRealm().organizations().get(existing.getId()).toRepresentation(); + fail("Expected NotFoundException"); + } catch (NotFoundException expected) {} + } + } + @Test public void testDeleteRealm() { - RealmRepresentation realmRep = RealmBuilder.create().name(KeycloakModelUtils.generateId()).build(); - RealmResource realm = realmsResouce().realm(realmRep.getRealm()); + RealmRepresentation realmRep = RealmBuilder.create() + .name(KeycloakModelUtils.generateId()) + .organizationEnabled(true) + .build(); + RealmResource realmRes = realmsResouce().realm(realmRep.getRealm()); try { realmRep.setEnabled(true); realmsResouce().create(realmRep); - realm = realmsResouce().realm(realmRep.getRealm()); - realm.toRepresentation(); + realmRes = realmsResouce().realm(realmRep.getRealm()); + realmRes.toRepresentation(); OrganizationRepresentation org = new OrganizationRepresentation(); org.setName("test-org"); org.addDomain(new OrganizationDomainRepresentation("test.org")); org.setEnabled(true); - Response response = realm.organizations().create(org); - response.close(); - assertEquals(Status.CREATED.getStatusCode(), response.getStatus()); - List orgs = realm.organizations().getAll(); - assertEquals(1, orgs.size()); + try (Response response = realmRes.organizations().create(org)) { + assertEquals(Status.CREATED.getStatusCode(), response.getStatus()); + } + + List orgs = realmRes.organizations().getAll(); + assertThat(orgs, hasSize(1)); + IdentityProviderRepresentation broker = bc.setUpIdentityProvider(); broker.setAlias(KeycloakModelUtils.generateId()); - response = realm.identityProviders().create(broker); - response.close(); - assertEquals(Status.CREATED.getStatusCode(), response.getStatus()); - response = realm.organizations().get(orgs.get(0).getId()).identityProviders().addIdentityProvider(broker.getAlias()); - response.close(); - assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus()); + try (Response response = realmRes.identityProviders().create(broker)) { + assertEquals(Status.CREATED.getStatusCode(), response.getStatus()); + } + try (Response response = realmRes.organizations().get(orgs.get(0).getId()).identityProviders().addIdentityProvider(broker.getAlias())) { + assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus()); + } } finally { - realm.remove(); + realmRes.remove(); } } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java index 1b164ac72e..9fa31fbe28 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java @@ -326,4 +326,9 @@ public class RealmBuilder { rep.setDefaultLocale(defaultLocale); return this; } + + public RealmBuilder organizationEnabled(boolean enabled) { + rep.setOrganizationsEnabled(enabled); + return this; + } }