Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for multiple identity providers #29064

Merged
merged 1 commit into from
May 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
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;
Expand All @@ -29,10 +28,6 @@

public interface OrganizationIdentityProviderResource {

@POST
@Consumes(MediaType.APPLICATION_JSON)
Response create(IdentityProviderRepresentation idpRepresentation);

@GET
@Produces(MediaType.APPLICATION_JSON)
IdentityProviderRepresentation toRepresentation();
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@ public interface OrganizationResource {
@Path("members")
OrganizationMembersResource members();

@Path("identity-provider")
OrganizationIdentityProviderResource identityProvider();
@Path("identity-providers")
OrganizationIdentityProvidersResource identityProviders();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<>();

Expand Down Expand Up @@ -95,14 +92,6 @@ public String getName() {
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<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand Down Expand Up @@ -98,8 +99,7 @@ public boolean remove(OrganizationModel organization) {
//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);

Expand Down Expand Up @@ -215,29 +215,35 @@ public boolean addIdentityProvider(OrganizationModel organization, IdentityProvi
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());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we call this function do we want to delete the identity provider from the realm or only remove the association between the idp and the organization?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, we remove it because we create it in the scope of the organization. However, we are going to change the Organization API to work similarly as users when linking brokers to an organization.

For that, we are going to link an existing broker instead therefore not removing it when removing from the organization, only the link as you are suggesting.


return true;
}

Expand All @@ -249,15 +255,19 @@ public boolean isManagedMember(OrganizationModel organization, UserModel member)
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
Expand Down Expand Up @@ -310,12 +320,12 @@ private OrganizationEntity getEntity(String id, boolean failIfNotFound) {
}

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

private GroupModel createOrganizationGroup(String name) {
throwExceptionIfObjectIsNull(name, "Name of the group");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ public void setDomains(Set<OrganizationDomainModel> domains) {
}

@Override
public IdentityProviderModel getIdentityProvider() {
return provider.getIdentityProvider(this);
public Stream<IdentityProviderModel> getIdentityProviders() {
return provider.getIdentityProviders(this);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -140,15 +141,16 @@ default Stream<OrganizationModel> getAllStream() {
* @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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,14 @@ public String getDisplayIconClasses() {
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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
public interface OrganizationModel {

String ORGANIZATION_ATTRIBUTE = "kc.org";
String ORGANIZATION_DOMAIN_ATTRIBUTE = "kc.org.domain";
String BROKER_PUBLIC = "kc.org.broker.public";

String getId();

Expand All @@ -40,7 +42,7 @@ public interface OrganizationModel {

void setDomains(Set<OrganizationDomainModel> domains);

IdentityProviderModel getIdentityProvider();
Stream<IdentityProviderModel> getIdentityProviders();

boolean isManaged(UserModel user);
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
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;
Expand Down Expand Up @@ -64,14 +66,14 @@
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;
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;
Expand Down Expand Up @@ -474,8 +476,13 @@ protected void createCommonAttributes(Theme theme, Locale locale, Properties mes

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));
Expand Down