Support for mutiple identity providers
Closes #28840 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
051c0197db
commit
32d25f43d0
27 changed files with 991 additions and 372 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -43,6 +43,6 @@ public interface OrganizationResource {
|
|||
@Path("members")
|
||||
OrganizationMembersResource members();
|
||||
|
||||
@Path("identity-provider")
|
||||
OrganizationIdentityProviderResource identityProvider();
|
||||
@Path("identity-providers")
|
||||
OrganizationIdentityProvidersResource identityProviders();
|
||||
}
|
||||
|
|
|
@ -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<>();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,48 +60,106 @@ 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() {
|
||||
|
@ -104,6 +167,7 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
|
|
Loading…
Reference in a new issue