Add organizations enabled/disabled capability

Closes #28804

Signed-off-by: vramik <vramik@redhat.com>
This commit is contained in:
vramik 2024-04-29 15:21:56 +02:00 committed by Pedro Igor
parent 80de3a0a71
commit 278341aff9
34 changed files with 331 additions and 64 deletions

View file

@ -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<String, String> getAttributesOrEmpty() {
return (Map<String, String>) (attributes == null ? Collections.emptyMap() : attributes);

View file

@ -3131,4 +3131,6 @@ identityBrokeringLink=Identity brokering link
searchClientRegistration=Search for policy
importFileHelp=File to import a key
logo=Logo
avatarImage=Avatar image
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.

View file

@ -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 && (
<DefaultSwitchControl
name="organizationsEnabled"
label={t("organizationsEnabled")}
labelIcon={t("organizationsEnabledHelp")}
/>
)}
<SelectControl
name="unmanagedAttributePolicy"
label={t("unmanagedAttributes")}

View file

@ -10,6 +10,7 @@ export enum Feature {
TransientUsers = "TRANSIENT_USERS",
ClientTypes = "CLIENT_TYPES",
DeclarativeUI = "DECLARATIVE_UI",
Organizations = "ORGANIZATION",
}
export default function useIsFeatureEnabled() {

View file

@ -81,6 +81,7 @@ export default interface RealmRepresentation {
offlineSessionIdleTimeout?: number;
offlineSessionMaxLifespan?: number;
offlineSessionMaxLifespanEnabled?: boolean;
organizationsEnabled?: boolean;
otpPolicyAlgorithm?: string;
otpPolicyDigits?: number;
otpPolicyInitialCounter?: number;

View file

@ -32,6 +32,8 @@ import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.stream.Stream;
import org.keycloak.common.Profile;
import org.keycloak.organization.OrganizationProvider;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -860,19 +862,34 @@ public class RealmAdapter implements CachedRealmModel {
@Override
public Stream<IdentityProviderModel> 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;
}
}

View file

@ -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() {

View file

@ -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;
}

View file

@ -1173,6 +1173,16 @@ public class RealmAdapter implements StorageProviderRealmModel, JpaModel<RealmEn
em.flush();
}
@Override
public boolean isOrganizationsEnabled() {
return getAttribute(RealmAttributes.ORGANIZATIONS_ENABLED, Boolean.FALSE);
}
@Override
public void setOrganizationsEnabled(boolean organizationsEnabled) {
setAttribute(RealmAttributes.ORGANIZATIONS_ENABLED, organizationsEnabled);
}
@Override
public ClientModel getMasterAdminClient() {
String masterAdminClientId = realm.getMasterAdminClient();

View file

@ -56,4 +56,5 @@ public interface RealmAttributes {
String FIRST_BROKER_LOGIN_FLOW_ID = "firstBrokerLoginFlowId";
String ORGANIZATIONS_ENABLED = "organizationsEnabled";
}

View file

@ -398,7 +398,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
@Override
public boolean isEnabled() {
return getAllStream().findAny().isPresent();
return realm.isOrganizationsEnabled();
}
@Override

View file

@ -91,7 +91,7 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
@Override
public boolean isEnabled() {
return entity.isEnabled();
return provider.isEnabled() && entity.isEnabled();
}
@Override
@ -136,8 +136,6 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
throw new ModelValidationException("You must provide at least one domain");
}
List<IdentityProviderModel> idps = this.getIdentityProviders().toList();
Map<String, OrganizationDomainModel> modelMap = domains.stream()
.map(this::validateDomain)
.collect(Collectors.toMap(OrganizationDomainModel::getName, Function.identity()));
@ -147,17 +145,16 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
if (modelMap.containsKey(domainEntity.getName())) {
domainEntity.setVerified(modelMap.get(domainEntity.getName()).getVerified());
modelMap.remove(domainEntity.getName());
}
// remove domain that is not found in the new set.
else {
} else {
// remove domain that is not found in the new set.
this.entity.removeDomain(domainEntity);
// check if any idp is assigned to the removed domain, and unset the domain if that's the case.
idps.forEach(idp -> {
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);
});
}
}

View file

@ -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 AbstractStorageManager<UserStorageProvid
protected UserModel importValidation(RealmModel realm, UserModel user) {
if (Profile.isFeatureEnabled(Profile.Feature.ORGANIZATION) && user != null) {
// check if user belongs to 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(user);
if (organization != null && organization.isManaged(user) && !organization.isEnabled()) {
if ((organizationProvider.isEnabled() && organization != null && organization.isManaged(user) && !organization.isEnabled()) ||
(!organizationProvider.isEnabled() && organization != null && organization.isManaged(user))) {
return new ReadOnlyUserModelDelegate(user) {
@Override
public boolean isEnabled() {
@ -128,6 +129,7 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
};
}
}
if (user == null || user.getFederationLink() == null) return user;
UserStorageProviderModel model = getStorageProviderModel(realm, user.getFederationLink());

View file

@ -275,6 +275,7 @@ public class DefaultExportImportManager implements ExportImportManager {
if (rep.isDuplicateEmailsAllowed() != null) newRealm.setDuplicateEmailsAllowed(rep.isDuplicateEmailsAllowed());
if (rep.isResetPasswordAllowed() != null) newRealm.setResetPasswordAllowed(rep.isResetPasswordAllowed());
if (rep.isEditUsernameAllowed() != null) newRealm.setEditUsernameAllowed(rep.isEditUsernameAllowed());
if (rep.isOrganizationsEnabled() != null) newRealm.setOrganizationsEnabled(rep.isOrganizationsEnabled());
if (rep.getLoginTheme() != null) newRealm.setLoginTheme(rep.getLoginTheme());
if (rep.getAccountTheme() != null) newRealm.setAccountTheme(rep.getAccountTheme());
if (rep.getAdminTheme() != null) newRealm.setAdminTheme(rep.getAdminTheme());
@ -754,6 +755,7 @@ public class DefaultExportImportManager implements ExportImportManager {
if (rep.isDuplicateEmailsAllowed() != null) realm.setDuplicateEmailsAllowed(rep.isDuplicateEmailsAllowed());
if (rep.isResetPasswordAllowed() != null) realm.setResetPasswordAllowed(rep.isResetPasswordAllowed());
if (rep.isEditUsernameAllowed() != null) realm.setEditUsernameAllowed(rep.isEditUsernameAllowed());
if (rep.isOrganizationsEnabled() != null) realm.setOrganizationsEnabled(rep.isOrganizationsEnabled());
if (rep.getSslRequired() != null) realm.setSslRequired(SslRequired.valueOf(rep.getSslRequired().toUpperCase()));
if (rep.getAccessCodeLifespan() != null) realm.setAccessCodeLifespan(rep.getAccessCodeLifespan());
if (rep.getAccessCodeLifespanUserAction() != null)

View file

@ -387,6 +387,7 @@ public class ModelToRepresentation {
rep.setDuplicateEmailsAllowed(realm.isDuplicateEmailsAllowed());
rep.setResetPasswordAllowed(realm.isResetPasswordAllowed());
rep.setEditUsernameAllowed(realm.isEditUsernameAllowed());
rep.setOrganizationsEnabled(realm.isOrganizationsEnabled());
rep.setDefaultSignatureAlgorithm(realm.getDefaultSignatureAlgorithm());
rep.setRevokeRefreshToken(realm.isRevokeRefreshToken());
rep.setRefreshTokenMaxReuse(realm.getRefreshTokenMaxReuse());

View file

@ -1120,4 +1120,13 @@ public class RealmModelDelegate implements RealmModel {
return delegate.searchForRolesStream(search, first, max);
}
@Override
public boolean isOrganizationsEnabled() {
return delegate.isOrganizationsEnabled();
}
@Override
public void setOrganizationsEnabled(boolean organizationsEnabled) {
delegate.setOrganizationsEnabled(organizationsEnabled);
}
}

View file

@ -16,7 +16,6 @@
*/
package org.keycloak.organization;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

View file

@ -1773,6 +1773,14 @@ public class IdentityBrokerStateTestHelpers {
public void decreaseRemainingCount(ClientInitialAccessModel clientInitialAccess) {
}
}
@Override
public boolean isOrganizationsEnabled() {
return false;
}
@Override
public void setOrganizationsEnabled(boolean organizationsEnabled) {
}
}
}

View file

@ -21,6 +21,7 @@ import org.keycloak.common.Profile.Feature;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* <p>A 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());
}
}

View file

@ -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());

View file

@ -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);
}
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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();
}
}

View file

@ -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.

View file

@ -179,4 +179,9 @@ public class RealmAttributeUpdater extends ServerResourceUpdater<RealmAttributeU
rep.getBrowserSecurityHeaders().put(name, value);
return this;
}
public RealmAttributeUpdater setOrganizationEnabled(Boolean organizationsEnabled) {
rep.setOrganizationsEnabled(organizationsEnabled);
return this;
}
}

View file

@ -17,6 +17,8 @@
package org.keycloak.testsuite.organization.admin;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@ -24,7 +26,6 @@ import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
import java.util.List;
import java.util.function.Function;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import org.jboss.arquillian.graphene.page.Page;
@ -48,6 +49,7 @@ import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.IdpConfirmLinkPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.UpdateAccountInformationPage;
import org.keycloak.testsuite.util.TestCleanup;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -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);
}

View file

@ -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

View file

@ -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());
}
}
@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();
}
}
}

View file

@ -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<UserRepresentation> existing = organization.members().getAll();;
List<UserRepresentation> 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<UserRepresentation> 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<UserRepresentation> 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();

View file

@ -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<OrganizationRepresentation> orgs = realm.organizations().getAll();
assertEquals(1, orgs.size());
try (Response response = realmRes.organizations().create(org)) {
assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
}
List<OrganizationRepresentation> 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();
}
}
}

View file

@ -326,4 +326,9 @@ public class RealmBuilder {
rep.setDefaultLocale(defaultLocale);
return this;
}
public RealmBuilder organizationEnabled(boolean enabled) {
rep.setOrganizationsEnabled(enabled);
return this;
}
}