Support for mutiple identity providers

Closes #28840

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2024-04-30 16:04:09 -03:00 committed by Alexander Schwartz
parent 051c0197db
commit 32d25f43d0
27 changed files with 991 additions and 372 deletions

View file

@ -27,6 +27,14 @@ public class OrganizationDomainRepresentation {
private String name;
private boolean verified;
public OrganizationDomainRepresentation() {
// for reflection
}
public OrganizationDomainRepresentation(String name) {
this.name = name;
}
public String getName() {
return this.name;
}

View file

@ -20,7 +20,6 @@ package org.keycloak.admin.client.resource;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@ -29,10 +28,6 @@ import org.keycloak.representations.idm.IdentityProviderRepresentation;
public interface OrganizationIdentityProviderResource {
@POST
@Consumes(MediaType.APPLICATION_JSON)
Response create(IdentityProviderRepresentation idpRepresentation);
@GET
@Produces(MediaType.APPLICATION_JSON)
IdentityProviderRepresentation toRepresentation();

View file

@ -0,0 +1,44 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.admin.client.resource;
import java.util.List;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
public interface OrganizationIdentityProvidersResource {
@POST
@Consumes(MediaType.APPLICATION_JSON)
Response create(IdentityProviderRepresentation idpRepresentation);
@GET
@Produces(MediaType.APPLICATION_JSON)
List<IdentityProviderRepresentation> getIdentityProviders();
@Path("{id}")
OrganizationIdentityProviderResource get(@PathParam("id") String id);
}

View file

@ -43,6 +43,6 @@ public interface OrganizationResource {
@Path("members")
OrganizationMembersResource members();
@Path("identity-provider")
OrganizationIdentityProviderResource identityProvider();
@Path("identity-providers")
OrganizationIdentityProvidersResource identityProviders();
}

View file

@ -57,9 +57,6 @@ public class OrganizationEntity {
@Column(name = "GROUP_ID")
private String groupId;
@Column(name = "IDP_ALIAS")
private String idpAlias;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy="organization")
protected Set<OrganizationDomainEntity> domains = new HashSet<>();
@ -95,14 +92,6 @@ public class OrganizationEntity {
return name;
}
public String getIdpAlias() {
return idpAlias;
}
public void setIdpAlias(String idpAlias) {
this.idpAlias = idpAlias;
}
public Collection<OrganizationDomainEntity> getDomains() {
if (this.domains == null) {
this.domains = new HashSet<>();

View file

@ -23,6 +23,7 @@ import static org.keycloak.utils.StreamsUtil.closing;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -98,8 +99,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
//TODO: won't scale, requires a better mechanism for bulk deleting users
userProvider.getGroupMembersStream(realm, group).forEach(userModel -> removeMember(organization, userModel));
groupProvider.removeGroup(realm, group);
realm.removeIdentityProviderByAlias(entity.getIdpAlias());
organization.getIdentityProviders().forEach((model) -> realm.removeIdentityProviderByAlias(model.getAlias()));
em.remove(entity);
@ -215,29 +215,35 @@ public class JpaOrganizationProvider implements OrganizationProvider {
throwExceptionIfObjectIsNull(identityProvider, "Identity provider");
OrganizationEntity organizationEntity = getEntity(organization.getId());
organizationEntity.setIdpAlias(identityProvider.getAlias());
identityProvider.getConfig().put(ORGANIZATION_ATTRIBUTE, organization.getId());
identityProvider.setOrganizationId(organizationEntity.getId());
realm.updateIdentityProvider(identityProvider);
return true;
}
@Override
public IdentityProviderModel getIdentityProvider(OrganizationModel organization) {
public Stream<IdentityProviderModel> getIdentityProviders(OrganizationModel organization) {
throwExceptionIfObjectIsNull(organization, "Organization");
throwExceptionIfObjectIsNull(organization.getId(), "Organization ID");
OrganizationEntity organizationEntity = getEntity(organization.getId());
// realm and its IDPs are cached
return realm.getIdentityProviderByAlias(organizationEntity.getIdpAlias());
return realm.getIdentityProvidersStream().filter(model -> organizationEntity.getId().equals(model.getOrganizationId()));
}
@Override
public boolean removeIdentityProvider(OrganizationModel organization) {
public boolean removeIdentityProvider(OrganizationModel organization, IdentityProviderModel identityProvider) {
throwExceptionIfObjectIsNull(organization, "Organization");
OrganizationEntity organizationEntity = getEntity(organization.getId());
organizationEntity.setIdpAlias(null);
if (!organizationEntity.getId().equals(identityProvider.getOrganizationId())) {
return false;
}
realm.removeIdentityProviderByAlias(identityProvider.getAlias());
return true;
}
@ -249,15 +255,19 @@ public class JpaOrganizationProvider implements OrganizationProvider {
return false;
}
IdentityProviderModel identityProvider = organization.getIdentityProvider();
List<IdentityProviderModel> brokers = organization.getIdentityProviders().toList();
if (identityProvider == null) {
if (brokers.isEmpty()) {
return false;
}
FederatedIdentityModel federatedIdentity = userProvider.getFederatedIdentity(realm, member, identityProvider.getAlias());
List<FederatedIdentityModel> federatedIdentities = userProvider.getFederatedIdentitiesStream(realm, member)
.map(federatedIdentityModel -> realm.getIdentityProviderByAlias(federatedIdentityModel.getIdentityProvider()))
.filter(brokers::contains)
.map(m -> userProvider.getFederatedIdentity(realm, member, m.getAlias()))
.toList();
return federatedIdentity != null;
return !federatedIdentities.isEmpty();
}
@Override
@ -310,7 +320,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
}
if (!realm.getId().equals(entity.getRealmId())) {
throw new ModelException("Organization [" + entity.getId() + " does not belong to realm [" + realm.getId() + "]");
throw new ModelException("Organization [" + entity.getId() + "] does not belong to realm [" + realm.getId() + "]");
}
return entity;

View file

@ -130,8 +130,8 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
}
@Override
public IdentityProviderModel getIdentityProvider() {
return provider.getIdentityProvider(this);
public Stream<IdentityProviderModel> getIdentityProviders() {
return provider.getIdentityProviders(this);
}
@Override

View file

@ -89,7 +89,6 @@
<column name="GROUP_ID" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="IDP_ALIAS" type="VARCHAR(255)" />
<column name="NAME" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>

View file

@ -16,6 +16,7 @@
*/
package org.keycloak.organization;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import org.keycloak.models.IdentityProviderModel;
@ -140,15 +141,16 @@ public interface OrganizationProvider extends Provider {
* @param organization the organization
* @return The identityProvider associated with a given {@code organization} or {@code null} if there is none.
*/
IdentityProviderModel getIdentityProvider(OrganizationModel organization);
Stream<IdentityProviderModel> getIdentityProviders(OrganizationModel organization);
/**
* Removes the link between the given {@link OrganizationModel} and identity provider associated with it if such a link exists.
*
* @param organization the organization
* @param identityProvider the identity provider
* @return {@code true} if the link was removed, {@code false} otherwise
*/
boolean removeIdentityProvider(OrganizationModel organization);
boolean removeIdentityProvider(OrganizationModel organization, IdentityProviderModel identityProvider);
/**
* Indicates if the current realm supports organization.

View file

@ -220,6 +220,14 @@ public class IdentityProviderModel implements Serializable {
return displayIconClasses;
}
public String getOrganizationId() {
return getConfig().get(OrganizationModel.ORGANIZATION_ATTRIBUTE);
}
public void setOrganizationId(String organizationId) {
getConfig().put(OrganizationModel.ORGANIZATION_ATTRIBUTE, organizationId);
}
/**
* <p>Validates this configuration.
*

View file

@ -25,6 +25,8 @@ import java.util.stream.Stream;
public interface OrganizationModel {
String ORGANIZATION_ATTRIBUTE = "kc.org";
String ORGANIZATION_DOMAIN_ATTRIBUTE = "kc.org.domain";
String BROKER_PUBLIC = "kc.org.broker.public";
String getId();
@ -40,7 +42,7 @@ public interface OrganizationModel {
void setDomains(Set<OrganizationDomainModel> domains);
IdentityProviderModel getIdentityProvider();
Stream<IdentityProviderModel> getIdentityProviders();
boolean isManaged(UserModel user);
}

View file

@ -30,6 +30,8 @@ import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticator;
import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
import org.keycloak.authentication.requiredactions.util.UserUpdateProfileContext;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.common.Profile;
import org.keycloak.common.Profile.Feature;
import org.keycloak.common.util.ObjectUtil;
import org.keycloak.forms.login.LoginFormsPages;
import org.keycloak.forms.login.LoginFormsProvider;
@ -64,6 +66,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.organization.forms.login.freemarker.model.OrganizationAwareIdentityProviderBean;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.rar.AuthorizationDetails;
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
@ -71,7 +74,6 @@ import org.keycloak.services.Urls;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.CommonClientSessionModel;
import org.keycloak.theme.FreeMarkerException;
import org.keycloak.theme.Theme;
import org.keycloak.theme.beans.AdvancedMessageFormatterMethod;
@ -474,8 +476,13 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
List<IdentityProviderModel> identityProviders = LoginFormsUtil
.filterIdentityProvidersForTheme(realm.getIdentityProvidersStream(), session, context);
attributes.put("social", new IdentityProviderBean(realm, session, identityProviders, baseUriWithCodeAndClientId));
IdentityProviderBean idpBean = new IdentityProviderBean(realm, session, identityProviders, baseUriWithCodeAndClientId);
if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
idpBean = new OrganizationAwareIdentityProviderBean(idpBean, session);
}
attributes.put("social", idpBean);
attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri));
attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri));
attributes.put("auth", new AuthenticationContextBean(context, page));

View file

@ -1,188 +0,0 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.organization.admin.resource;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.ext.Provider;
import java.util.Objects;
import java.util.Optional;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.resources.admin.AdminEventBuilder;
import org.keycloak.services.resources.admin.IdentityProviderResource;
import org.keycloak.services.resources.admin.IdentityProvidersResource;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
@Provider
public class OrganizationIdentityProviderResource {
private final KeycloakSession session;
private final RealmModel realm;
private final OrganizationProvider organizationProvider;
private final OrganizationModel organization;
private final AdminPermissionEvaluator auth;
private final AdminEventBuilder adminEvent;
public OrganizationIdentityProviderResource() {
// needed for registering to the JAX-RS stack
this(null, null, null, null);
}
public OrganizationIdentityProviderResource(KeycloakSession session, OrganizationModel organization, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
this.session = session;
this.realm = session == null ? null : session.getContext().getRealm();
this.organizationProvider = session == null ? null : session.getProvider(OrganizationProvider.class);
this.organization = organization;
this.auth = auth;
this.adminEvent = adminEvent;
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response addIdentityProvider(IdentityProviderRepresentation providerRep) {
auth.realm().requireManageRealm();
IdentityProviderModel identityProvider = organization.getIdentityProvider();
if (identityProvider != null) {
throw ErrorResponse.error("Organization already assigned with an identity provider.", Status.BAD_REQUEST);
}
//create IdP within the realm
Response response = new IdentityProvidersResource(realm, session, auth, adminEvent).create(providerRep);
if (Status.CREATED.getStatusCode() == response.getStatus()) {
//get the created IdP from session
identityProvider = realm.getIdentityProviderByAlias(providerRep.getAlias());
String errorMessage;
try {
if (organizationProvider.addIdentityProvider(organization, identityProvider)) {
return response;
}
errorMessage = "Assigning the Identity provider with the organization was not succesful.";
} catch (ModelException me) {
errorMessage = me.getMessage();
}
throw ErrorResponse.error(errorMessage, Status.BAD_REQUEST);
}
return response;
}
@GET
@Produces(MediaType.APPLICATION_JSON)
public IdentityProviderRepresentation getIdentityProvider() {
auth.realm().requireManageRealm();
return Optional.ofNullable(organization.getIdentityProvider()).map(this::toRepresentation).orElse(null);
}
@DELETE
public Response delete() {
auth.realm().requireManageRealm();
IdentityProviderModel identityProvider = getIdentityProviderModel();
Response response = getIdentityProviderResource(identityProvider).delete();
// remove link between IdP and the organization if the IdP deletetion was successful
if (Status.NO_CONTENT.getStatusCode() == response.getStatus()) {
String errorMessage;
try {
if (organizationProvider.removeIdentityProvider(organization)) {
return response;
}
errorMessage = "Removing the Identity provider from the organization was not succesful.";
} catch (ModelException me) {
errorMessage = me.getMessage();
}
throw ErrorResponse.error(errorMessage, Status.BAD_REQUEST);
}
return response;
}
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public Response update(IdentityProviderRepresentation rep) {
auth.realm().requireManageRealm();
IdentityProviderModel identityProvider = getIdentityProviderModel();
if (!rep.getAlias().equals(identityProvider.getAlias()) || (rep.getInternalId() != null && !Objects.equals(rep.getInternalId(), identityProvider.getInternalId()))) {
throw ErrorResponse.error("Identity provider not assigned to the organization.", Status.NOT_FOUND);
}
Response response = getIdentityProviderResource(identityProvider).update(rep);
//update link between IdP and the organization if the update of IdP was successful and the IdP alias differs
if (Status.NO_CONTENT.getStatusCode() == response.getStatus() &&
! Objects.equals(identityProvider.getAlias(), rep.getAlias())) {
//get the updated IdP from session
identityProvider = realm.getIdentityProviderByAlias(rep.getAlias());
String errorMessage;
try {
if (organizationProvider.removeIdentityProvider(organization) &&
organizationProvider.addIdentityProvider(organization, identityProvider)) {
return response;
}
errorMessage = "Updating the Identity provider was not succesful.";
} catch (ModelException me) {
errorMessage = me.getMessage();
}
throw ErrorResponse.error(errorMessage, Status.BAD_REQUEST);
}
return response;
}
private IdentityProviderRepresentation toRepresentation(IdentityProviderModel idp) {
return ModelToRepresentation.toRepresentation(realm, idp);
}
private IdentityProviderResource getIdentityProviderResource(IdentityProviderModel idp) {
return new IdentityProviderResource(auth, realm, session, idp, adminEvent);
}
private IdentityProviderModel getIdentityProviderModel() {
IdentityProviderModel identityProvider = organization.getIdentityProvider();
if (identityProvider == null) {
throw ErrorResponse.error("Organization doesn't have assigned an identity provider.", Status.NOT_FOUND);
}
return identityProvider;
}
}

View file

@ -0,0 +1,138 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.organization.admin.resource;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.ext.Provider;
import java.util.List;
import java.util.stream.Stream;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
import org.keycloak.models.OrganizationDomainModel;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.resources.admin.AdminEventBuilder;
import org.keycloak.services.resources.admin.IdentityProviderResource;
import org.keycloak.services.resources.admin.IdentityProvidersResource;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
@Provider
public class OrganizationIdentityProvidersResource {
private final KeycloakSession session;
private final RealmModel realm;
private final OrganizationProvider organizationProvider;
private final OrganizationModel organization;
private final AdminPermissionEvaluator auth;
private final AdminEventBuilder adminEvent;
public OrganizationIdentityProvidersResource() {
// needed for registering to the JAX-RS stack
this(null, null, null, null);
}
public OrganizationIdentityProvidersResource(KeycloakSession session, OrganizationModel organization, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
this.session = session;
this.realm = session == null ? null : session.getContext().getRealm();
this.organizationProvider = session == null ? null : session.getProvider(OrganizationProvider.class);
this.organization = organization;
this.auth = auth;
this.adminEvent = adminEvent;
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response addIdentityProvider(IdentityProviderRepresentation providerRep) {
auth.realm().requireManageRealm();
Response response = new IdentityProvidersResource(realm, session, auth, adminEvent).create(providerRep);
if (Status.CREATED.getStatusCode() == response.getStatus()) {
try {
IdentityProviderModel identityProvider = realm.getIdentityProviderByAlias(providerRep.getAlias());
if (organizationProvider.addIdentityProvider(organization, identityProvider)) {
return response;
}
throw ErrorResponse.error("Identity provider already associated to the organization", Status.BAD_REQUEST);
} catch (ModelException me) {
throw ErrorResponse.error(me.getMessage(), Status.BAD_REQUEST);
}
}
return response;
}
@GET
@Produces(MediaType.APPLICATION_JSON)
public Stream<IdentityProviderRepresentation> getIdentityProviders() {
auth.realm().requireManageRealm();
return organization.getIdentityProviders().map(this::toRepresentation);
}
@Path("{alias}")
public IdentityProviderResource getIdentityProvider(@PathParam("alias") String alias) {
IdentityProviderModel broker = realm.getIdentityProviderByAlias(alias);
return new IdentityProviderResource(auth, realm, session, broker, adminEvent) {
@Override
public Response delete() {
Response response = super.delete();
if (organizationProvider.removeIdentityProvider(organization, broker)) {
return response;
}
throw ErrorResponse.error("Identity provider not associated with the organization", Status.BAD_REQUEST);
}
@Override
public Response update(IdentityProviderRepresentation providerRep) {
if (organization.getIdentityProviders().noneMatch(model -> model.getInternalId().equals(providerRep.getInternalId()) || model.getAlias().equals(providerRep.getAlias()))) {
return Response.status(Status.NOT_FOUND).build();
}
String domain = providerRep.getConfig().get(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
if (domain != null && organization.getDomains().map(OrganizationDomainModel::getName).noneMatch(domain::equals)) {
return Response.status(Status.BAD_REQUEST).build();
}
return super.update(providerRep);
}
};
}
private IdentityProviderRepresentation toRepresentation(IdentityProviderModel idp) {
return ModelToRepresentation.toRepresentation(realm, idp);
}
}

View file

@ -142,9 +142,9 @@ public class OrganizationResource {
return new OrganizationMemberResource(session, organization, auth, adminEvent);
}
@Path("{id}/identity-provider")
public OrganizationIdentityProviderResource identityProvider(@PathParam("id") String id) {
return new OrganizationIdentityProviderResource(session, getOrganization(id), auth, adminEvent);
@Path("{id}/identity-providers")
public OrganizationIdentityProvidersResource identityProvider(@PathParam("id") String id) {
return new OrganizationIdentityProvidersResource(session, getOrganization(id), auth, adminEvent);
}
private OrganizationModel getOrganization(String id) {

View file

@ -17,6 +17,9 @@
package org.keycloak.organization.authentication.authenticators.broker;
import java.util.List;
import java.util.stream.Stream;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
@ -29,7 +32,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.organization.OrganizationProvider;
public class IdpOrganizationAuthenticator extends AbstractIdpAuthenticator {
public class IdpAddOrganizationMemberAuthenticator extends AbstractIdpAuthenticator {
@Override
protected void actionImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
@ -46,10 +49,10 @@ public class IdpOrganizationAuthenticator extends AbstractIdpAuthenticator {
return;
}
IdentityProviderModel expectedBroker = organization.getIdentityProvider();
IdentityProviderModel currentBroker = brokerContext.getIdpConfig();
Stream<IdentityProviderModel> expectedBrokers = organization.getIdentityProviders();
IdentityProviderModel broker = brokerContext.getIdpConfig();
if (!expectedBroker.getAlias().equals(currentBroker.getAlias())) {
if (expectedBrokers.noneMatch(broker::equals)) {
context.failure(AuthenticationFlowError.ACCESS_DENIED);
return;
}
@ -71,31 +74,12 @@ public class IdpOrganizationAuthenticator extends AbstractIdpAuthenticator {
return false;
}
String domain = getEmailDomain(user.getEmail());
OrganizationModel organization = (OrganizationModel) session.getAttribute(OrganizationModel.class.getName());
if (domain == null) {
if (organization == null) {
return false;
}
OrganizationModel organization = provider.getByDomainName(domain);
if (organization == null || provider.getIdentityProvider(organization) == null) {
return false;
return provider.getIdentityProviders(organization).findAny().isPresent();
}
session.setAttribute(OrganizationModel.class.getName(), organization);
return true;
}
private String getEmailDomain(String email) {
int domainSeparator = email.indexOf('@');
if (domainSeparator == -1) {
return null;
}
return email.substring(domainSeparator + 1);
}
}

View file

@ -33,11 +33,11 @@ import org.keycloak.provider.ProviderConfigProperty;
public class IdpOrganizationAuthenticatorFactory implements AuthenticatorFactory, EnvironmentDependentProviderFactory {
public static final String ID = "organization-broker";
public static final String ID = "idp-add-organization-member";
@Override
public Authenticator create(KeycloakSession session) {
return new IdpOrganizationAuthenticator();
return new IdpAddOrganizationMemberAuthenticator();
}
@Override
@ -77,7 +77,7 @@ public class IdpOrganizationAuthenticatorFactory implements AuthenticatorFactory
@Override
public String getDisplayType() {
return "Organization Member Link";
return "Organization";
}
@Override
@ -87,7 +87,7 @@ public class IdpOrganizationAuthenticatorFactory implements AuthenticatorFactory
@Override
public boolean isUserSetupAllowed() {
return false;
return true;
}
@Override

View file

@ -17,18 +17,23 @@
package org.keycloak.organization.authentication.authenticators.browser;
import java.util.function.BiFunction;
import java.util.List;
import java.util.Objects;
import jakarta.ws.rs.core.MultivaluedMap;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator;
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;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.organization.forms.login.freemarker.model.OrganizationAwareIdentityProviderBean;
public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
@ -43,7 +48,7 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
OrganizationProvider provider = getOrganizationProvider();
if (!provider.isEnabled()) {
attempted(context);
context.attempted();
return;
}
@ -55,55 +60,114 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
HttpRequest request = context.getHttpRequest();
MultivaluedMap<String, String> parameters = request.getDecodedFormParameters();
String username = parameters.getFirst(UserModel.USERNAME);
String emailDomain = getEmailDomain(username);
if (username == null) {
challenge(context);
return;
}
String domain = getEmailDomain(username);
if (domain == null) {
attempted(context);
if (emailDomain == null) {
// username does not map to any email domain, go to the next authentication step/sub-flow
context.attempted();
return;
}
OrganizationProvider provider = getOrganizationProvider();
OrganizationModel organization = provider.getByDomainName(domain);
OrganizationModel organization = null;
RealmModel realm = context.getRealm();
UserModel user = session.users().getUserByEmail(realm, username);
if (user != null) {
// user exists, check if enabled
if (!user.isEnabled()) {
context.failure(AuthenticationFlowError.INVALID_USER);
return;
}
organization = provider.getByMember(user);
if (organization != null) {
if (provider.isManagedMember(organization, user)) {
// user is a managed member, try to resolve the origin broker and redirect automatically
List<IdentityProviderModel> organizationBrokers = organization.getIdentityProviders().toList();
List<IdentityProviderModel> originBrokers = session.users().getFederatedIdentitiesStream(realm, user)
.map(f -> {
IdentityProviderModel broker = realm.getIdentityProviderByAlias(f.getIdentityProvider());
if (!organizationBrokers.contains(broker)) {
return null;
}
FederatedIdentityModel identity = session.users().getFederatedIdentity(realm, user, broker.getAlias());
if (identity != null) {
return broker;
}
return null;
}).filter(Objects::nonNull)
.toList();
if (originBrokers.size() == 1) {
redirect(context, originBrokers.get(0).getAlias());
return;
}
} else {
context.attempted();
return;
}
}
}
if (organization == null) {
attempted(context);
return;
organization = provider.getByDomainName(emailDomain);
}
IdentityProviderModel identityProvider = organization.getIdentityProvider();
if (identityProvider == null) {
attempted(context);
return;
}
redirect(context, identityProvider.getAlias(), username);
}
private void attempted(AuthenticationFlowContext context) {
context.form()
.setAttributeMapper(attributes -> {
attributes.computeIfPresent("social", createOrganizationAwareSocialBean());
return attributes;
});
if (organization == null) {
// request does not map to any organization, go to the next step/sub-flow
context.attempted();
return;
}
private BiFunction<String, Object, IdentityProviderBean> createOrganizationAwareSocialBean() {
return (key, bean) -> new OrganizationAwareIdentityProviderBean((IdentityProviderBean) bean, session);
List<IdentityProviderModel> domainBrokers = organization.getIdentityProviders().toList();
if (domainBrokers.isEmpty()) {
// no organization brokers to automatically redirect the user, go to the next step/sub-flow
context.attempted();
return;
}
if (domainBrokers.size() == 1) {
// there is a single broker, redirect the user to authenticate
redirect(context, domainBrokers.get(0).getAlias(), username);
return;
}
for (IdentityProviderModel broker : domainBrokers) {
String idpDomain = broker.getConfig().get(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
if (emailDomain.equals(idpDomain)) {
// redirect the user using the broker that matches the email domain
redirect(context, broker.getAlias(), username);
return;
}
}
// the user is authenticating in the scope of the organization, show the identity-first login page and the
// public organization brokers for selection
context.challenge(context.form()
.setAttributeMapper(attributes -> {
attributes.computeIfPresent("social",
(key, bean) -> new OrganizationAwareIdentityProviderBean((IdentityProviderBean) bean, session, true)
);
return attributes;
})
.createLoginUsername());
}
private OrganizationProvider getOrganizationProvider() {
return session.getProvider(OrganizationProvider.class);
}
private void challenge (AuthenticationFlowContext context){
private void challenge(AuthenticationFlowContext context){
// the default challenge won't show any broker but just the identity-first login page and the option to try a different authentication mechanism
context.challenge(context.form()
.setAttributeMapper(attributes -> {
// removes identity provider related attributes from forms
@ -114,6 +178,10 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
}
private String getEmailDomain(String email) {
if (email == null) {
return null;
}
int domainSeparator = email.indexOf('@');
if (domainSeparator == -1) {

View file

@ -15,10 +15,9 @@
* limitations under the License.
*/
package org.keycloak.organization.authentication.authenticators.browser;
package org.keycloak.organization.forms.login.freemarker.model;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.keycloak.forms.login.freemarker.model.IdentityProviderBean;
@ -29,30 +28,50 @@ import org.keycloak.models.RealmModel;
public class OrganizationAwareIdentityProviderBean extends IdentityProviderBean {
private final IdentityProviderBean delegate;
private final KeycloakSession session;
private final List<IdentityProvider> providers;
public OrganizationAwareIdentityProviderBean(IdentityProviderBean delegate, KeycloakSession session) {
this.delegate = delegate;
public OrganizationAwareIdentityProviderBean(IdentityProviderBean delegate, KeycloakSession session, boolean onlyOrganizationBrokers) {
this.session = session;
if (onlyOrganizationBrokers) {
providers = Optional.ofNullable(delegate.getProviders()).orElse(List.of()).stream()
.filter(this::isPublicOrganizationBroker)
.toList();
} else {
providers = Optional.ofNullable(delegate.getProviders()).orElse(List.of()).stream()
.filter(p -> isRealmBroker(p) || isPublicOrganizationBroker(p))
.toList();
}
}
public OrganizationAwareIdentityProviderBean(IdentityProviderBean delegate, KeycloakSession session) {
this(delegate, session, false);
}
@Override
public List<IdentityProvider> getProviders() {
return Optional.ofNullable(delegate.getProviders()).orElse(List.of()).stream()
.filter(this::filterOrganizationalIdentityProvider)
.toList();
return providers;
}
@Override
public boolean isDisplayInfo() {
return delegate.isDisplayInfo();
return !providers.isEmpty();
}
private boolean filterOrganizationalIdentityProvider(IdentityProvider idp) {
private boolean isPublicOrganizationBroker(IdentityProvider idp) {
RealmModel realm = session.getContext().getRealm();
IdentityProviderModel model = realm.getIdentityProviderByAlias(idp.getAlias());
Map<String, String> config = model.getConfig();
return !config.containsKey(OrganizationModel.ORGANIZATION_ATTRIBUTE);
if (model.getOrganizationId() == null) {
return false;
}
return Boolean.parseBoolean(model.getConfig().getOrDefault(OrganizationModel.BROKER_PUBLIC, Boolean.FALSE.toString()));
}
private boolean isRealmBroker(IdentityProvider idp) {
RealmModel realm = session.getContext().getRealm();
IdentityProviderModel model = realm.getIdentityProviderByAlias(idp.getAlias());
return model.getOrganizationId() == null;
}
}

View file

@ -19,11 +19,13 @@ package org.keycloak.organization.validator;
import static org.keycloak.validate.BuiltinValidators.emailValidator;
import java.util.stream.Stream;
import java.util.List;
import org.keycloak.Config.Scope;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.common.Profile;
import org.keycloak.common.Profile.Feature;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OrganizationDomainModel;
import org.keycloak.models.OrganizationModel;
@ -32,6 +34,7 @@ import org.keycloak.organization.OrganizationProvider;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.userprofile.AttributeContext;
import org.keycloak.userprofile.UserProfileAttributeValidationContext;
import org.keycloak.userprofile.UserProfileContext;
import org.keycloak.utils.StringUtil;
import org.keycloak.validate.AbstractSimpleValidator;
import org.keycloak.validate.ValidationContext;
@ -83,15 +86,38 @@ public class OrganizationMemberValidator extends AbstractSimpleValidator impleme
UserProfileAttributeValidationContext upContext = (UserProfileAttributeValidationContext) context;
AttributeContext attributeContext = upContext.getAttributeContext();
UserModel user = attributeContext.getUser();
String emailDomain = email.substring(email.indexOf('@') + 1);
List<String> expectedDomains = organization.getDomains().map(OrganizationDomainModel::getName).toList();
if (!organization.isManaged(user)) {
if (UserProfileContext.IDP_REVIEW.equals(attributeContext.getContext())) {
KeycloakSession session = attributeContext.getSession();
BrokeredIdentityContext brokerContext = (BrokeredIdentityContext) session.getAttribute(BrokeredIdentityContext.class.getName());
if (brokerContext != null) {
String alias = brokerContext.getIdpConfig().getAlias();
IdentityProviderModel broker = organization.getIdentityProviders().filter((p) -> p.getAlias().equals(alias)).findAny().orElse(null);
if (broker == null) {
return;
}
String domain = email.substring(email.indexOf('@') + 1);
Stream<OrganizationDomainModel> expectedDomains = organization.getDomains();
String brokerDomain = broker.getConfig().get(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
if (expectedDomains.map(OrganizationDomainModel::getName).noneMatch(domain::equals)) {
if (brokerDomain == null) {
return;
}
expectedDomains = List.of(brokerDomain);
}
} else if (!organization.isManaged(user)) {
return;
}
if (expectedDomains.isEmpty()) {
return;
}
if (!expectedDomains.contains(emailDomain)) {
context.addError(new ValidationError(ID, inputHint, "Email domain does not match any domain from the organization"));
}
}

View file

@ -17,6 +17,8 @@
package org.keycloak.services.resources;
import org.jboss.logging.Logger;
import org.keycloak.common.Profile;
import org.keycloak.common.Profile.Feature;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.forms.login.MessageType;
import org.keycloak.forms.login.freemarker.DetachedInfoStateChecker;
@ -52,6 +54,7 @@ import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.exceptions.TokenNotActiveException;
import org.keycloak.models.KeycloakContext;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.SingleUseObjectKeyModel;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel;
@ -67,6 +70,7 @@ import org.keycloak.models.utils.AuthenticationFlowResolver;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.SystemClientUtil;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.protocol.AuthorizationEndpointBase;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocol.Error;
@ -77,7 +81,6 @@ import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.ErrorPageException;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
@ -925,9 +928,23 @@ public class LoginActionsService {
};
configureOrganization(brokerContext);
return processFlow(checks.isActionRequest(), execution, authSession, flowPath, brokerLoginFlow, null, processor);
}
private void configureOrganization(BrokeredIdentityContext brokerContext) {
if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
String organizationId = brokerContext.getIdpConfig().getOrganizationId();
if (organizationId != null) {
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
session.setAttribute(OrganizationModel.class.getName(), provider.getById(organizationId));
session.setAttribute(BrokeredIdentityContext.class.getName(), brokerContext);
}
}
}
private Response redirectToAfterBrokerLoginEndpoint(AuthenticationSessionModel authSession, boolean firstBrokerLogin) {
return redirectToAfterBrokerLoginEndpoint(session, realm, session.getContext().getUri(), authSession, firstBrokerLogin);
}

View file

@ -27,6 +27,7 @@ import jakarta.ws.rs.core.Response;
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.representations.idm.OrganizationDomainRepresentation;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
@ -65,7 +66,15 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
.username(getUserLogin())
.email(getUserEmail())
.password(getUserPassword())
.enabled(true).build())
.enabled(true)
.build(),
UserBuilder.create()
.username("external")
.email("external@user.org")
.password("password")
.enabled(true)
.build()
)
);
return providerRealm;
@ -80,6 +89,13 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
public String getIDPAlias() {
return name + "-identity-provider";
}
@Override
public List<ClientRepresentation> createProviderClients() {
List<ClientRepresentation> clients = super.createProviderClients();
clients.get(0).setRedirectUris(List.of("*"));
return clients;
}
};
@Page
@ -126,7 +142,7 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
id = ApiUtil.getCreatedId(response);
}
testRealm().organizations().get(id).identityProvider().create(brokerConfigFunction.apply(name).setUpIdentityProvider()).close();
testRealm().organizations().get(id).identityProviders().create(brokerConfigFunction.apply(name).setUpIdentityProvider()).close();
org = testRealm().organizations().get(id).toRepresentation();
getCleanup().addCleanup(() -> testRealm().organizations().get(id).delete().close());

View file

@ -107,33 +107,33 @@ public class OrganizationAdminPermissionsTest extends AbstractOrganizationTest {
idpRep.setProviderId("oidc");
//create IdP
try (
Response userResponse = realmUserResource.organizations().get(orgId).identityProvider().create(idpRep);
Response adminResponse = realmAdminResource.organizations().get(orgId).identityProvider().create(idpRep)
Response userResponse = realmUserResource.organizations().get(orgId).identityProviders().create(idpRep);
Response adminResponse = realmAdminResource.organizations().get(orgId).identityProviders().create(idpRep)
) {
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
assertThat(adminResponse.getStatus(), equalTo(Status.CREATED.getStatusCode()));
getCleanup().addCleanup(() -> testRealm().organizations().get(orgId).identityProvider().delete().close());
getCleanup().addCleanup(() -> testRealm().organizations().get(orgId).identityProviders().get(idpRep.getAlias()).delete().close());
}
//get IdP
try {
//we should get 403, not 400 or 404 etc.
realmUserResource.organizations().get("non-existing").identityProvider().toRepresentation();
realmUserResource.organizations().get("non-existing").identityProviders().get(idpRep.getAlias()).toRepresentation();
fail("Expected ForbiddenException");
} catch (ForbiddenException expected) {}
try {
realmUserResource.organizations().get(orgId).identityProvider().toRepresentation();
realmUserResource.organizations().get(orgId).identityProviders().get(idpRep.getAlias()).toRepresentation();
fail("Expected ForbiddenException");
} catch (ForbiddenException expected) {}
assertThat(realmAdminResource.organizations().get(orgId).identityProvider().toRepresentation(), Matchers.notNullValue());
assertThat(realmAdminResource.organizations().get(orgId).identityProviders().get(idpRep.getAlias()).toRepresentation(), Matchers.notNullValue());
//update IdP
try (Response userResponse = realmUserResource.organizations().get(orgId).identityProvider().update(idpRep)) {
try (Response userResponse = realmUserResource.organizations().get(orgId).identityProviders().get(idpRep.getAlias()).update(idpRep)) {
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
}
//delete IdP
try (Response userResponse = realmUserResource.organizations().get(orgId).identityProvider().delete()) {
try (Response userResponse = realmUserResource.organizations().get(orgId).identityProviders().get(idpRep.getAlias()).delete()) {
assertThat(userResponse.getStatus(), equalTo(Status.FORBIDDEN.getStatusCode()));
}

View file

@ -18,6 +18,8 @@
package org.keycloak.testsuite.organization.admin;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
@ -26,15 +28,20 @@ import java.util.List;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.NotFoundException;
import org.junit.Test;
import org.keycloak.admin.client.resource.OrganizationIdentityProviderResource;
import org.keycloak.admin.client.resource.OrganizationMemberResource;
import org.keycloak.admin.client.resource.OrganizationResource;
import org.keycloak.admin.client.resource.UsersResource;
import org.keycloak.common.Profile.Feature;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.ErrorRepresentation;
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
@ -44,17 +51,17 @@ import org.keycloak.testsuite.util.UserBuilder;
public class OrganizationBrokerSelfRegistrationTest extends AbstractOrganizationTest {
@Test
public void testBrokerRegistration() {
public void testRegistrationRedirectWhenSingleBroker() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
assertBrokerRegistration(organization);
assertBrokerRegistration(organization, bc.getUserEmail());
}
@Test
public void testLoginHint() {
public void testLoginHintSentToBrokerWhenEnabled() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
IdentityProviderRepresentation idp = organization.identityProvider().toRepresentation();
IdentityProviderRepresentation idp = organization.identityProviders().get(bc.getIDPAlias()).toRepresentation();
idp.getConfig().put(IdentityProviderModel.LOGIN_HINT, "true");
organization.identityProvider().update(idp).close();
organization.identityProviders().get(bc.getIDPAlias()).update(idp).close();
oauth.clientId("broker-app");
loginPage.open(bc.consumerRealmName());
@ -71,6 +78,50 @@ public class OrganizationBrokerSelfRegistrationTest extends AbstractOrganization
Assert.assertEquals(bc.getUserEmail(), loginPage.getUsername());
}
@Test
public void testDefaultAuthenticationMechanismIfNotOrganizationMember() {
testRealm().organizations().get(createOrganization().getId());
oauth.clientId("broker-app");
// login with email only
loginPage.open(bc.consumerRealmName());
log.debug("Logging in");
Assert.assertFalse(loginPage.isPasswordInputPresent());
loginPage.loginUsername("user@noorg.org");
// check if the login page is shown
Assert.assertTrue(loginPage.isUsernameInputPresent());
Assert.assertTrue(loginPage.isPasswordInputPresent());
}
@Test
public void testRealmLevelBrokersAvailableIfEmailDoesNotMatchOrganization() {
testRealm().organizations().get(createOrganization().getId());
oauth.clientId("broker-app");
// login with email only
loginPage.open(bc.consumerRealmName());
log.debug("Logging in");
Assert.assertFalse(loginPage.isPasswordInputPresent());
loginPage.loginUsername("user");
// check if the login page is shown
Assert.assertTrue(loginPage.isUsernameInputPresent());
Assert.assertTrue(loginPage.isPasswordInputPresent());
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
IdentityProviderRepresentation idp = bc.setUpIdentityProvider();
idp.setAlias("realm-level-idp");
Assert.assertFalse(loginPage.isSocialButtonPresent(idp.getAlias()));
testRealm().identityProviders().create(idp).close();
driver.navigate().refresh();
Assert.assertTrue(loginPage.isUsernameInputPresent());
Assert.assertTrue(loginPage.isPasswordInputPresent());
Assert.assertTrue(loginPage.isSocialButtonPresent(idp.getAlias()));
}
@Test
public void testLinkExistingAccount() {
// create a realm user in the consumer realm
@ -102,7 +153,7 @@ public class OrganizationBrokerSelfRegistrationTest extends AbstractOrganization
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(), bc.getUserEmail(), "Firstname", "Lastname");
updateAccountInformationPage.updateAccountInformation(bc.getUserEmail(), bc.getUserEmail(), "Firstname", "Lastname");
// account with the same email exists in the realm, execute account linking
waitForPage(driver, "account already exists", false);
@ -114,11 +165,11 @@ public class OrganizationBrokerSelfRegistrationTest extends AbstractOrganization
}
@Test
public void testMemberAlreadyExists() {
public void testReAuthenticateWhenAlreadyMember() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
// add the member for the first time
assertBrokerRegistration(organization);
assertBrokerRegistration(organization, bc.getUserEmail());
// logout to force the user to authenticate again
UserRepresentation account = getUserRepresentation(bc.getUserEmail());
@ -141,11 +192,15 @@ public class OrganizationBrokerSelfRegistrationTest extends AbstractOrganization
}
@Test
public void testFailUpdateEmailWithDomainDifferentThanOrganization() {
public void testFailUpdateEmailNotAssociatedOrganizationUsingAdminAPI() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
OrganizationIdentityProviderResource idp = organization.identityProviders().get(bc.getIDPAlias());
IdentityProviderRepresentation idpRep = idp.toRepresentation();
idpRep.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, "neworg.org");
idp.update(idpRep).close();
// add the member for the first time
assertBrokerRegistration(organization);
assertBrokerRegistration(organization, bc.getUserEmail());
UserRepresentation member = getUserRepresentation(bc.getUserEmail());
member.setEmail(KeycloakModelUtils.generateId() + "@user.org");
@ -165,13 +220,12 @@ public class OrganizationBrokerSelfRegistrationTest extends AbstractOrganization
}
@Test
public void testDelete() {
public void testDeleteManagedMember() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
// add the member for the first time
assertBrokerRegistration(organization);
assertBrokerRegistration(organization, bc.getUserEmail());
UserRepresentation member = getUserRepresentation(bc.getUserEmail());
member.setEmail(KeycloakModelUtils.generateId() + "@user.org");
OrganizationMemberResource organizationMember = organization.members().member(member.getId());
organizationMember.delete().close();
@ -189,29 +243,368 @@ public class OrganizationBrokerSelfRegistrationTest extends AbstractOrganization
}
}
private void assertBrokerRegistration(OrganizationResource organization) {
// login with email only
@Test
public void testRedirectToIdentityProviderAssociatedWithOrganizationDomain() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
IdentityProviderRepresentation idp = organization.identityProviders().get(bc.getIDPAlias()).toRepresentation();
idp.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, "neworg.org");
organization.identityProviders().get(bc.getIDPAlias()).update(idp).close();
idp.setAlias("second-idp");
idp.setInternalId(null);
idp.getConfig().remove(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
organization.identityProviders().create(idp).close();
oauth.clientId("broker-app");
loginPage.open(bc.consumerRealmName());
log.debug("Logging in");
Assert.assertFalse(loginPage.isPasswordInputPresent());
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
Assert.assertFalse(loginPage.isSocialButtonPresent(idp.getAlias()));
loginPage.loginUsername(bc.getUserEmail());
// 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(bc.getUserEmail(), 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(), bc.getUserEmail(), "Firstname", "Lastname");
updateAccountInformationPage.updateAccountInformation(bc.getUserEmail(), bc.getUserEmail(), "Firstname", "Lastname");
appPage.assertCurrent();
assertIsMember(bc.getUserEmail(), organization);
UserRepresentation user = testRealm().users().search(bc.getUserEmail()).get(0);
List<FederatedIdentityRepresentation> federatedIdentities = testRealm().users().get(user.getId()).getFederatedIdentity();
assertEquals(1, federatedIdentities.size());
assertEquals(bc.getIDPAlias(), federatedIdentities.get(0).getIdentityProvider());
}
@Test
public void testIdentityFirstLoginShowsPublicOrganizationBrokers() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
OrganizationRepresentation representation = organization.toRepresentation();
representation.addDomain(new OrganizationDomainRepresentation("other.org"));
organization.update(representation).close();
IdentityProviderRepresentation idp = organization.identityProviders().get(bc.getIDPAlias()).toRepresentation();
idp.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, "neworg.org");
// set a domain to the existing broker
organization.identityProviders().get(bc.getIDPAlias()).update(idp).close();
idp = bc.setUpIdentityProvider();
idp.setAlias("second-idp");
idp.setInternalId(null);
idp.getConfig().remove(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
idp.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString());
// create a second broker without a domain set
organization.identityProviders().create(idp).close();
idp = organization.identityProviders().get(idp.getAlias()).toRepresentation();
oauth.clientId("broker-app");
loginPage.open(bc.consumerRealmName());
log.debug("Logging in");
Assert.assertFalse(loginPage.isPasswordInputPresent());
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
Assert.assertFalse(loginPage.isSocialButtonPresent(idp.getAlias()));
loginPage.loginUsername("external@user.org");
Assert.assertTrue(loginPage.isPasswordInputPresent());
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
Assert.assertTrue(loginPage.isSocialButtonPresent(idp.getAlias()));
idp.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.FALSE.toString());
organization.identityProviders().get(idp.getAlias()).update(idp).close();
driver.navigate().refresh();
Assert.assertTrue(loginPage.isPasswordInputPresent());
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
Assert.assertFalse(loginPage.isSocialButtonPresent(idp.getAlias()));
}
@Test
public void testLoginUsingBrokerWithoutDomain() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
IdentityProviderRepresentation idp = organization.identityProviders().get(bc.getIDPAlias()).toRepresentation();
idp.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, "neworg.org");
// set a domain to the existing broker
organization.identityProviders().get(bc.getIDPAlias()).update(idp).close();
idp = bc.setUpIdentityProvider();
idp.setAlias("second-idp");
idp.setInternalId(null);
idp.getConfig().remove(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE);
idp.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString());
// create a second broker without a domain set
organization.identityProviders().create(idp).close();
oauth.clientId("broker-app");
loginPage.open(bc.consumerRealmName());
log.debug("Logging in");
Assert.assertFalse(loginPage.isPasswordInputPresent());
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
String email = "external@user.org";
loginPage.loginUsername(email);
loginPage.clickSocial(idp.getAlias());
// 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() + "/"));
loginPage.login("external", "password");
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(email, email, "Firstname", "Lastname");
appPage.assertCurrent();
assertIsMember(email, organization);
// make sure the federated identity matches the expected broker
UserRepresentation user = testRealm().users().search(email).get(0);
List<FederatedIdentityRepresentation> federatedIdentities = testRealm().users().get(user.getId()).getFederatedIdentity();
assertEquals(1, federatedIdentities.size());
assertEquals(idp.getAlias(), federatedIdentities.get(0).getIdentityProvider());
}
@Test
public void testEmailDomainDoesNotMatchBrokerDomain() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
OrganizationRepresentation representation = organization.toRepresentation();
representation.addDomain(new OrganizationDomainRepresentation("other.org"));
organization.update(representation).close();
IdentityProviderRepresentation idp = organization.identityProviders().get(bc.getIDPAlias()).toRepresentation();
idp.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, "neworg.org");
// set a domain to the existing broker
organization.identityProviders().get(bc.getIDPAlias()).update(idp).close();
idp = bc.setUpIdentityProvider();
idp.setAlias("second-idp");
idp.setInternalId(null);
idp.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, "other.org");
idp.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString());
// create a second broker without a domain set
organization.identityProviders().create(idp).close();
oauth.clientId("broker-app");
loginPage.open(bc.consumerRealmName());
log.debug("Logging in");
Assert.assertFalse(loginPage.isPasswordInputPresent());
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
String email = "external@user.org";
loginPage.loginUsername(email);
loginPage.clickSocial(idp.getAlias());
// 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() + "/"));
loginPage.login(email, "password");
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(email, email, "Firstname", "Lastname");
Assert.assertTrue(driver.getPageSource().contains("Email domain does not match any domain from the organization"));
assertIsNotMember(email, organization);
updateAccountInformationPage.updateAccountInformation("external@other.org", "external@other.org", "Firstname", "Lastname");
appPage.assertCurrent();
assertIsMember("external@other.org", organization);
}
@Test
public void testAnyEmailFromBrokerWithoutDomainSet() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
OrganizationRepresentation representation = organization.toRepresentation();
representation.addDomain(new OrganizationDomainRepresentation("other.org"));
organization.update(representation).close();
IdentityProviderRepresentation idp = organization.identityProviders().get(bc.getIDPAlias()).toRepresentation();
idp.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, "neworg.org");
// set a domain to the existing broker
organization.identityProviders().get(bc.getIDPAlias()).update(idp).close();
idp = bc.setUpIdentityProvider();
idp.setAlias("second-idp");
idp.setInternalId(null);
idp.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString());
// create a second broker without a domain set
organization.identityProviders().create(idp).close();
oauth.clientId("broker-app");
loginPage.open(bc.consumerRealmName());
log.debug("Logging in");
Assert.assertFalse(loginPage.isPasswordInputPresent());
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
String email = "external@user.org";
loginPage.loginUsername(email);
loginPage.clickSocial(idp.getAlias());
// 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() + "/"));
loginPage.login(email, "password");
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("external@unknown.org", "external@unknown.org", "Firstname", "Lastname");
appPage.assertCurrent();
assertIsMember("external@unknown.org", organization);
}
@Test
public void testRealmLevelBrokerNotImpactedByOrganizationFlow() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
IdentityProviderRepresentation idp = bc.setUpIdentityProvider();
idp.setAlias("realm-idp");
idp.setInternalId(null);
// create a second broker without a domain set
testRealm().identityProviders().create(idp).close();
oauth.clientId("broker-app");
loginPage.open(bc.consumerRealmName());
log.debug("Logging in");
Assert.assertFalse(loginPage.isPasswordInputPresent());
Assert.assertFalse(loginPage.isSocialButtonPresent(bc.getIDPAlias()));
loginPage.loginUsername("some@user.org");
waitForPage(driver, "sign in to", true);
Assert.assertTrue("Driver should be on the consumer realm page right now",
driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
loginPage.clickSocial(idp.getAlias());
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() + "/"));
loginPage.login("external", "password");
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.getUserEmail(), bc.getUserEmail(), "Firstname", "Lastname");
appPage.assertCurrent();
assertTrue(organization.members().getAll().isEmpty());
UserRepresentation user = testRealm().users().search(bc.getUserEmail()).get(0);
testRealm().users().get(user.getId()).remove();
}
@Test
public void testMemberRegistrationUsingDifferentDomainThanOrganization() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
IdentityProviderRepresentation idpRep = organization.identityProviders().getIdentityProviders().get(0);
// make sure the user can select this idp from the organization when authenticating
idpRep.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString());
organization.identityProviders().get(idpRep.getAlias()).update(idpRep).close();
// create a user to the provider realm using a email that does not share the same domain as the org
UserRepresentation user = UserBuilder.create()
.username("user")
.email("user@different.org")
.password("password")
.enabled(true)
.build();
realmsResouce().realm(bc.providerRealmName()).users().create(user).close();
// select the organization broker to authenticate
oauth.clientId("broker-app");
loginPage.open(bc.consumerRealmName());
loginPage.loginUsername("user@different.org");
loginPage.clickSocial(idpRep.getAlias());
// login through the organization broker
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() + "/"));
loginPage.login("user@different.org", "password");
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(user.getUsername(), user.getEmail(), "Firstname", "Lastname");
appPage.assertCurrent();
}
@Test
public void testMemberFromBrokerRedirectedToOriginBroker() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
IdentityProviderRepresentation idpRep = organization.identityProviders().getIdentityProviders().get(0);
// make sure the user can select this idp from the organization when authenticating
idpRep.getConfig().put(OrganizationModel.BROKER_PUBLIC, Boolean.TRUE.toString());
organization.identityProviders().get(idpRep.getAlias()).update(idpRep).close();
// create a user to the provider realm using a email that does not share the same domain as the org
UserRepresentation user = UserBuilder.create()
.username("user")
.email("user@different.org")
.password("password")
.enabled(true)
.build();
realmsResouce().realm(bc.providerRealmName()).users().create(user).close();
// execute the identity-first login
oauth.clientId("broker-app");
loginPage.open(bc.consumerRealmName());
loginPage.loginUsername(user.getEmail());
waitForPage(driver, "sign in to", true);
// select the organization broker to authenticate
assertTrue(loginPage.isPasswordInputPresent());
assertTrue(loginPage.isUsernameInputPresent());
loginPage.clickSocial(idpRep.getAlias());
// login through the organization broker
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() + "/"));
loginPage.login("user@different.org", "password");
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(user.getUsername(), user.getEmail(), "Firstname", "Lastname");
UserRepresentation account = getUserRepresentation(user.getEmail());
realmsResouce().realm(bc.consumerRealmName()).users().get(account.getId()).logout();
// the flow now changed and the user should be automatically redirected to the origin broker
loginPage.open(bc.consumerRealmName());
waitForPage(driver, "sign in to", true);
loginPage.loginUsername(user.getEmail());
Assert.assertTrue("Driver should be on the provider realm page right now",
driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/"));
loginPage.login("user@different.org", "password");
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) {
@ -220,6 +613,23 @@ public class OrganizationBrokerSelfRegistrationTest extends AbstractOrganization
Assert.assertEquals(account.getId(), member.getId());
}
private void assertIsNotMember(String userEmail, OrganizationResource organization) {
UsersResource users = adminClient.realm(bc.consumerRealmName()).users();
List<UserRepresentation> reps = users.searchByEmail(userEmail, true);
if (reps.isEmpty()) {
return;
}
assertEquals(1, reps.size());
UserRepresentation account = reps.get(0);
try {
assertNull(organization.members().member(account.getId()).toRepresentation());
} catch (NotFoundException ignore) {
}
}
private UserRepresentation getUserRepresentation(String userEmail) {
UsersResource users = adminClient.realm(bc.consumerRealmName()).users();
List<UserRepresentation> reps = users.searchByEmail(userEmail, true);

View file

@ -19,8 +19,8 @@ package org.keycloak.testsuite.organization.admin;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.nullValue;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import org.junit.Assert;
@ -28,6 +28,7 @@ import org.junit.Test;
import org.keycloak.admin.client.resource.OrganizationIdentityProviderResource;
import org.keycloak.admin.client.resource.OrganizationResource;
import org.keycloak.common.Profile.Feature;
import org.keycloak.models.OrganizationModel;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
@ -38,61 +39,90 @@ public class OrganizationIdentityProviderTest extends AbstractOrganizationTest {
@Test
public void testUpdate() {
OrganizationRepresentation organization = createOrganization();
OrganizationIdentityProviderResource orgIdPResource = testRealm().organizations().get(organization.getId()).identityProvider();
OrganizationIdentityProviderResource orgIdPResource = testRealm().organizations().get(organization.getId())
.identityProviders().get(bc.getIDPAlias());
IdentityProviderRepresentation actual = orgIdPResource.toRepresentation();
IdentityProviderRepresentation expected = actual;
assertThat(expected.getAlias(), equalTo(bc.getIDPAlias()));
//update
expected.setAlias("changed-alias");
expected.setDisplayName("My Org Broker");
expected.getConfig().put("test", "value");
try (Response response = orgIdPResource.update(expected)) {
assertThat(response.getStatus(), equalTo(Response.Status.NO_CONTENT.getStatusCode()));
}
try {
orgIdPResource.toRepresentation();
Assert.fail("should fail because the alias changed");
} catch (NotFoundException ignore) {
}
orgIdPResource = testRealm().organizations().get(organization.getId()).identityProviders().get(expected.getAlias());
actual = orgIdPResource.toRepresentation();
assertThat(expected.getAlias(), equalTo(actual.getAlias()));
assertThat(expected.getDisplayName(), equalTo(actual.getDisplayName()));
Assert.assertEquals(expected.getConfig().get("test"), actual.getConfig().get("test"));
}
@Test
public void testFailUpdateAlias() {
OrganizationRepresentation organization = createOrganization();
OrganizationIdentityProviderResource orgIdPResource = testRealm().organizations().get(organization.getId()).identityProvider();
IdentityProviderRepresentation idpRepresentation = orgIdPResource.toRepresentation();
assertThat(idpRepresentation.getAlias(), equalTo(bc.getIDPAlias()));
//update
idpRepresentation.setAlias("should-fail");
try (Response response = orgIdPResource.update(idpRepresentation)) {
assertThat(response.getStatus(), equalTo(Status.NOT_FOUND.getStatusCode()));
}
}
@Test
public void testDelete() {
OrganizationRepresentation organization = createOrganization();
OrganizationIdentityProviderResource orgIdPResource = testRealm().organizations().get(organization.getId()).identityProvider();
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
IdentityProviderRepresentation idpTemplate = organization
.identityProviders().get(bc.getIDPAlias()).toRepresentation();
try (Response response = orgIdPResource.delete()) {
for (int i = 0; i < 5; i++) {
idpTemplate.setAlias("idp-" + i);
idpTemplate.setInternalId(null);
organization.identityProviders().create(idpTemplate).close();
}
Assert.assertEquals(6, organization.identityProviders().getIdentityProviders().size());
for (int i = 0; i < 5; i++) {
OrganizationIdentityProviderResource idpResource = organization.identityProviders().get("idp-" + i);
try (Response response = idpResource.delete()) {
assertThat(response.getStatus(), equalTo(Response.Status.NO_CONTENT.getStatusCode()));
}
assertThat(orgIdPResource.toRepresentation(), nullValue());
try {
idpResource.toRepresentation();
Assert.fail("should be removed");
} catch (NotFoundException expected) {
}
}
organization.identityProviders().get(bc.getIDPAlias()).delete().close();
Assert.assertTrue(testRealm().identityProviders().findAll().isEmpty());
}
@Test
public void tryCreateSecondIdp() {
OrganizationIdentityProviderResource orgIdPResource = testRealm().organizations().get(createOrganization().getId()).identityProvider();
public void testCreatingExistingIdentityProvider() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
OrganizationIdentityProviderResource orgIdPResource = organization
.identityProviders().get(bc.getIDPAlias());
IdentityProviderRepresentation idpRepresentation = orgIdPResource.toRepresentation();
String alias = idpRepresentation.getAlias();
idpRepresentation.setAlias("another-idp");
try (Response response = orgIdPResource.create(idpRepresentation)) {
assertThat(response.getStatus(), equalTo(Response.Status.BAD_REQUEST.getStatusCode()));
try (Response response = organization.identityProviders().create(idpRepresentation)) {
assertThat(response.getStatus(), equalTo(Status.CONFLICT.getStatusCode()));
}
idpRepresentation.setAlias(alias);
idpRepresentation.setInternalId(null);
try (Response response = organization.identityProviders().create(idpRepresentation)) {
assertThat(response.getStatus(), equalTo(Status.CONFLICT.getStatusCode()));
}
}
@Test(expected = jakarta.ws.rs.NotFoundException.class)
public void removingOrgShouldRemoveIdP() {
public void testRemovingOrgShouldRemoveIdP() {
OrganizationRepresentation orgRep = createOrganization();
OrganizationResource orgResource = testRealm().organizations().get(orgRep.getId());
@ -101,25 +131,27 @@ public class OrganizationIdentityProviderTest extends AbstractOrganizationTest {
}
testRealm().identityProviders().get(bc.getIDPAlias()).toRepresentation();
Assert.assertTrue(testRealm().identityProviders().findAll().isEmpty());
}
@Test
public void tryUpdateAndRemoveIdPNotAssignedToOrg() {
public void testUpdateOrDeleteIdentityProviderNotAssignedToOrganization() {
OrganizationRepresentation orgRep = createOrganization();
OrganizationResource orgResource = testRealm().organizations().get(orgRep.getId());
OrganizationIdentityProviderResource orgIdPResource = orgResource.identityProvider();
OrganizationIdentityProviderResource orgIdPResource = orgResource.identityProviders().get(bc.getIDPAlias());
IdentityProviderRepresentation idpRepresentation = createRep("some-broker", "oidc");
getCleanup().addCleanup(() -> testRealm().identityProviders().get(idpRepresentation.getAlias()).remove());
//create IdP in realm not bound to Org
testRealm().identityProviders().create(idpRepresentation).close();
try (Response response = orgIdPResource.update(idpRepresentation)) {
assertThat(response.getStatus(), equalTo(Response.Status.NOT_FOUND.getStatusCode()));
}
try (Response response = orgIdPResource.delete()) {
assertThat(response.getStatus(), equalTo(Status.NO_CONTENT.getStatusCode()));
}
try (Response response = orgIdPResource.delete()) {
assertThat(response.getStatus(), equalTo(Status.NOT_FOUND.getStatusCode()));
}
@ -130,21 +162,41 @@ public class OrganizationIdentityProviderTest extends AbstractOrganizationTest {
OrganizationRepresentation orgRep = createOrganization();
OrganizationResource orgResource = testRealm().organizations().get(orgRep.getId());
OrganizationIdentityProviderResource orgIdPResource = orgResource.identityProvider();
OrganizationIdentityProviderResource orgIdPResource = orgResource.identityProviders().get(bc.getIDPAlias());
IdentityProviderRepresentation idpRepresentation = createRep("some-broker", "oidc");
//create IdP in realm not bound to Org and get created internalId
testRealm().identityProviders().create(idpRepresentation).close();
getCleanup().addCleanup(() -> testRealm().identityProviders().get(idpRepresentation.getAlias()).remove());
String internalId = testRealm().identityProviders().get("some-broker").toRepresentation().getInternalId();
IdentityProviderRepresentation orgIdPRep = orgIdPResource.toRepresentation();
orgIdPRep.setInternalId(internalId);
try (Response response = orgIdPResource.update(orgIdPRep)) {
assertThat(response.getStatus(), equalTo(Status.CONFLICT.getStatusCode()));
}
orgIdPRep.setAlias("some-broker-alias");
try (Response response = orgIdPResource.update(orgIdPRep)) {
assertThat(response.getStatus(), equalTo(Response.Status.NOT_FOUND.getStatusCode()));
}
}
@Test
public void testAssignDomainNotBoundToOrganization() {
OrganizationRepresentation orgRep = createOrganization();
OrganizationResource orgResource = testRealm().organizations().get(orgRep.getId());
OrganizationIdentityProviderResource orgIdPResource = orgResource.identityProviders().get(bc.getIDPAlias());
IdentityProviderRepresentation idpRep = orgIdPResource.toRepresentation();
idpRep.getConfig().put(OrganizationModel.ORGANIZATION_DOMAIN_ATTRIBUTE, "unknown.org");
try (Response response = orgIdPResource.update(idpRep)) {
assertThat(response.getStatus(), equalTo(Status.BAD_REQUEST.getStatusCode()));
}
}
private IdentityProviderRepresentation createRep(String alias, String providerId) {
IdentityProviderRepresentation idp = new IdentityProviderRepresentation();

View file

@ -43,7 +43,7 @@ public class OrganizationMemberAuthenticationTest extends AbstractOrganizationTe
// the email does not match an organization so redirect to the realm's default authentication mechanism
waitForPage(driver, "sign in to", true);
Assert.assertTrue("Driver should be on the provider realm page right now",
Assert.assertTrue("Driver should be on the consumer realm page right now",
driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
Assert.assertTrue(loginPage.isPasswordInputPresent());
Assert.assertEquals(member.getEmail(), loginPage.getUsername());

View file

@ -164,7 +164,7 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
}
@Test
public void testDelete() {
public void testDeleteUnmanagedMember() {
UPConfig upConfig = testRealm().users().userProfile().getConfiguration();
upConfig.setUnmanagedAttributePolicy(UnmanagedAttributePolicy.ENABLED);
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
@ -188,6 +188,19 @@ public class OrganizationMemberTest extends AbstractOrganizationTest {
}
}
@Test
public void testUpdateEmailUnmanagedMember() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
UserRepresentation expected = addMember(organization);
expected.setEmail("some@unknown.org");
UserResource userResource = testRealm().users().get(expected.getId());
userResource.update(expected);
UserRepresentation actual = userResource.toRepresentation();
assertEquals(expected.getId(), actual.getId());
assertEquals(expected.getEmail(), actual.getEmail());
}
@Test
public void testDeleteMembersOnOrganizationRemoval() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());