Skip to content

Commit

Permalink
Address review comments
Browse files Browse the repository at this point in the history
Signed-off-by: Stefan Guilhen <[email protected]>
  • Loading branch information
sguilhen authored and pedroigor committed Aug 19, 2024
1 parent 6e7b36e commit fa7c2b5
Show file tree
Hide file tree
Showing 11 changed files with 98 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,5 @@ New indexes were added to the `IDENTITY_PROVIDER` table to improve the performan
If the table currently contains more than 300.000 entries,
{project_name} will skip the creation of the indexes by default during the automatic schema migration, and will instead log the SQL statements
on the console during migration. In this case, the statements must be run manually in the DB after {project_name}'s startup.

Also, the `kc.org` and `hideOnLoginPage` configuration attributes were migrated to the identity provider itself, to allow for more efficient queries when searching for providers. As such, API clients should use the `getOrganizationId/setOrganizationId` and `isHideOnLogin/setHideOnLogin` methods in the `IdentityProviderRepresentation`, and avoid setting these properties using the legacy config attributes that are now deprecated.
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ public Stream<IdentityProviderModel> getAllStream(String search, Integer first,
}

@Override
public Stream<IdentityProviderModel> getAllStream(Map<String, Object> attrs, Integer first, Integer max) {
public Stream<IdentityProviderModel> getAllStream(Map<String, String> attrs, Integer first, Integer max) {
return idpDelegate.getAllStream(attrs, first, max);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ public Stream<IdentityProviderModel> getAllStream(String search, Integer first,
}

@Override
public Stream<IdentityProviderModel> getAllStream(Map<String, Object> attrs, Integer first, Integer max) {
public Stream<IdentityProviderModel> getAllStream(Map<String, String> attrs, Integer first, Integer max) {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<IdentityProviderEntity> query = builder.createQuery(IdentityProviderEntity.class);
Root<IdentityProviderEntity> idp = query.from(IdentityProviderEntity.class);
Expand All @@ -233,9 +233,9 @@ public Stream<IdentityProviderModel> getAllStream(Map<String, Object> attrs, Int
predicates.add(builder.equal(idp.get("realmId"), getRealm().getId()));

if (attrs != null) {
for (Map.Entry<String, Object> entry : attrs.entrySet()) {
for (Map.Entry<String, String> entry : attrs.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
String value = entry.getValue();
if (StringUtil.isBlank(key)) {
continue;
}
Expand All @@ -244,7 +244,7 @@ public Stream<IdentityProviderModel> getAllStream(Map<String, Object> attrs, Int
case ENABLED:
case HIDE_ON_LOGIN:
case LINK_ONLY: {
if (Boolean.parseBoolean(value.toString())) {
if (Boolean.parseBoolean(value)) {
predicates.add(builder.isTrue(idp.get(key)));
} else {
predicates.add(builder.isFalse(idp.get(key)));
Expand All @@ -253,7 +253,7 @@ public Stream<IdentityProviderModel> getAllStream(Map<String, Object> attrs, Int
}
case FIRST_BROKER_LOGIN_FLOW_ID:
case ORGANIZATION_ID: {
if (value == null || value.toString().isEmpty()) {
if (StringUtil.isBlank(value)) {
predicates.add(builder.isNull(idp.get(key)));
} else {
predicates.add(builder.equal(idp.get(key), value));
Expand Down
82 changes: 65 additions & 17 deletions server-spi/src/main/java/org/keycloak/models/IDPProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,20 @@
*/
package org.keycloak.models;

import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.keycloak.provider.Provider;

/**
* The {@code IDPProvider} is concerned with the storage/retrieval of the configured identity providers in Keycloak. In
* other words, it is a provider of identity providers (IDPs) and, as such, handles the CRUD operations for IDPs.
*
* </p>
* It is not to be confused with the {@code IdentityProvider} found in server-spi-private as that provider is meant to be
* implemented by actual identity providers that handle the logic of authenticating users with third party brokers, such
* as Microsoft, Google, Github, LinkedIn, etc.
Expand Down Expand Up @@ -119,7 +123,7 @@ default Stream<IdentityProviderModel> getAllStream() {
* @param max the maximum number of results to be returned. Ignored if negative or {@code null}.
* @return a non-null stream of {@link IdentityProviderModel}s that match the search criteria.
*/
Stream<IdentityProviderModel> getAllStream(Map<String, Object> attrs, Integer first, Integer max);
Stream<IdentityProviderModel> getAllStream(Map<String, String> attrs, Integer first, Integer max);

/**
* Returns all identity providers associated with the organization with the provided id.
Expand Down Expand Up @@ -163,17 +167,17 @@ default Stream<IdentityProviderModel> getByOrganization(String orgId, Integer fi
* that only IDPs associated with the specified organization are to be returned.
* @return a non-null stream of {@link IdentityProviderModel}s that are suitable for being displayed in the login pages.
*/
default Stream<IdentityProviderModel> getForLogin(FETCH_MODE mode, String organizationId) {
default Stream<IdentityProviderModel> getForLogin(FetchMode mode, String organizationId) {
Stream<IdentityProviderModel> result = Stream.of();
if (mode == FETCH_MODE.REALM_ONLY || mode == FETCH_MODE.ALL) {
if (mode == FetchMode.REALM_ONLY || mode == FetchMode.ALL) {
// fetch all realm-only IDPs - i.e. those not associated with orgs.
Map<String, Object> searchOptions = getBasicSearchOptionsForLogin();
Map<String, String> searchOptions = LoginFilter.getLoginSearchOptions();
searchOptions.put(IdentityProviderModel.ORGANIZATION_ID, null);
result = Stream.concat(result, getAllStream(searchOptions, null, null));
}
if (mode == FETCH_MODE.ORG_ONLY || mode == FETCH_MODE.ALL) {
if (mode == FetchMode.ORG_ONLY || mode == FetchMode.ALL) {
// fetch IDPs associated with organizations.
Map<String, Object> searchOptions = getBasicSearchOptionsForLogin();
Map<String, String> searchOptions = LoginFilter.getLoginSearchOptions();
if (organizationId != null) {
// we want the IDPs associated with a specific org.
searchOptions.put(IdentityProviderModel.ORGANIZATION_ID, organizationId);
Expand All @@ -184,15 +188,6 @@ default Stream<IdentityProviderModel> getForLogin(FETCH_MODE mode, String organi
return result;
}

private static Map<String, Object> getBasicSearchOptionsForLogin() {
Map<String, Object> searchOptions = new LinkedHashMap<>();
searchOptions.put(IdentityProviderModel.ENABLED, "true");
searchOptions.put(IdentityProviderModel.LINK_ONLY, "false");
searchOptions.put(IdentityProviderModel.HIDE_ON_LOGIN, "false");
return searchOptions;
}


/**
* Returns the number of IDPs in the realm.
*
Expand All @@ -210,5 +205,58 @@ default boolean isIdentityFederationEnabled() {
return count() > 0;
}

enum FETCH_MODE {REALM_ONLY, ORG_ONLY, ALL}
/**
* Enum to control how login identity providers should be fetched.
*/
enum FetchMode {
/** only realm-level providers should be fetched (not linked to any organization) **/
REALM_ONLY,
/** only providers linked to organizations should be fetched **/
ORG_ONLY,
/** all providers should fetched, regardless of being linked to an organization or not **/
ALL
}

/**
* Enum that contains all fields that are considered when deciding if a provider should be available for login or not.
*/
enum LoginFilter {

ENABLED(IdentityProviderModel.ENABLED, Boolean.TRUE.toString(), IdentityProviderModel::isEnabled),

LINK_ONLY(IdentityProviderModel.LINK_ONLY, Boolean.FALSE.toString(), Predicate.not(IdentityProviderModel::isLinkOnly)),

HIDE_ON_LOGIN(IdentityProviderModel.HIDE_ON_LOGIN, Boolean.FALSE.toString(), Predicate.not(IdentityProviderModel::isHideOnLogin));

private final String key;
private final String value;
private final Predicate<IdentityProviderModel> filter;

LoginFilter(String key, String value, java.util.function.Predicate<IdentityProviderModel> filter) {
this.key = key;
this.value = value;
this.filter = filter;
}

public String getKey() {
return key;
}

public String getValue() {
return value;
}

public Predicate<IdentityProviderModel> getFilter() {
return filter;
}

public static Map<String, String> getLoginSearchOptions() {
return Stream.of(values()).collect(Collectors.toMap(LoginFilter::getKey, LoginFilter::getValue, (v1, v2) -> v1, LinkedHashMap::new));
}

public static Predicate<IdentityProviderModel> getLoginPredicate() {
return ((Predicate<IdentityProviderModel>) Objects::nonNull)
.and(Stream.of(values()).map(LoginFilter::getFilter).reduce(Predicate::and).get());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public class IdentityProviderModel implements Serializable {
public static final String FILTERED_BY_CLAIMS = "filteredByClaim";
public static final String FIRST_BROKER_LOGIN_FLOW_ID = "firstBrokerLoginFlowId";
public static final String HIDE_ON_LOGIN = "hideOnLogin";
@Deprecated
public static final String LEGACY_HIDE_ON_LOGIN_ATTR = "hideOnLoginPage";
public static final String LINK_ONLY = "linkOnly";
public static final String LOGIN_HINT = "loginHint";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public final class UsernameForm extends UsernamePasswordForm {
public void authenticate(AuthenticationFlowContext context) {
if (context.getUser() != null) {
// We can skip the form when user is re-authenticating. Unless current user has some IDP set, so he can re-authenticate with that IDP
if (!this.contextUserHasFederatedIDPs(context)) {
if (!this.hasLinkedBrokers(context)) {
context.success();
return;
}
Expand Down Expand Up @@ -79,7 +79,7 @@ protected String getDefaultChallengeMessage(AuthenticationFlowContext context) {
* @param context a reference to the {@link AuthenticationFlowContext}
* @return {@code true} if the context user has federated IDPs that can be used for authentication; {@code false} otherwise.
*/
private boolean contextUserHasFederatedIDPs(AuthenticationFlowContext context) {
private boolean hasLinkedBrokers(AuthenticationFlowContext context) {
KeycloakSession session = context.getSession();
UserModel user = context.getUser();
if (user == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
public SAMLIdentityProviderConfig() {
}

@Override
public void setHideOnLogin(boolean hideOnLogin) {
super.setHideOnLogin(hideOnLogin);
getConfig().put(LEGACY_HIDE_ON_LOGIN_ATTR, String.valueOf(hideOnLogin));
}

public SAMLIdentityProviderConfig(IdentityProviderModel identityProviderModel) {
super(identityProviderModel);
}
Expand Down Expand Up @@ -375,7 +381,7 @@ public boolean isSignSpMetadata() {
public void setSignSpMetadata(boolean signSpMetadata) {
getConfig().put(SIGN_SP_METADATA, String.valueOf(signSpMetadata));
}

public boolean isAllowCreate() {
return Boolean.valueOf(getConfig().get(ALLOW_CREATE));
}
Expand Down Expand Up @@ -448,6 +454,6 @@ public void validate(RealmModel realm) {
//transient name id format is not accepted together with principaltype SubjectnameId
if (JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get().equals(getNameIDPolicyFormat()) && SamlPrincipalType.SUBJECT == getPrincipalType())
throw new IllegalArgumentException("Can not have Transient NameID Policy Format together with SUBJECT Principal Type");

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ public Map<String, String> parseConfig(KeycloakSession session, String config) {
for (AttributeType attribute : entityType.getExtensions().getEntityAttributes().getAttribute()) {
if (MACEDIR_ENTITY_CATEGORY.equals(attribute.getName())
&& attribute.getAttributeValue().contains(REFEDS_HIDE_FROM_DISCOVERY)) {
samlIdentityProviderConfig.getConfig().put(IdentityProviderModel.LEGACY_HIDE_ON_LOGIN_ATTR, Boolean.TRUE.toString());
samlIdentityProviderConfig.setHideOnLogin(true);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public IdentityProviderBean(KeycloakSession session, RealmModel realm, URI baseU
public List<IdentityProvider> getProviders() {
if (this.providers == null) {
String existingIDP = this.getExistingIDP(session, context);
Set<String> federatedIdentities = this.getFederatedIdentities(session, realm, context);
Set<String> federatedIdentities = this.getLinkedBrokerAliases(session, realm, context);
if (federatedIdentities != null) {
this.providers = getFederatedIdentityProviders(federatedIdentities, existingIDP);
} else {
Expand Down Expand Up @@ -167,7 +167,7 @@ protected String getExistingIDP(KeycloakSession session, AuthenticationFlowConte
}

/**
* Returns the list of IDPs associated with the user's federated identities, if any. In case these IDPs exist, the login
* Returns the list of IDPs linked with the user's federated identities, if any. In case these IDPs exist, the login
* page should show only the IDPs already linked to the user. Returning {@code null} indicates that all public enabled IDPs
* should be available.
* </p>
Expand All @@ -179,7 +179,7 @@ protected String getExistingIDP(KeycloakSession session, AuthenticationFlowConte
* @return a {@link Set} containing the aliases of the IDPs that should be available for login. An empty set indicates
* that no IDPs should be available.
*/
protected Set<String> getFederatedIdentities(KeycloakSession session, RealmModel realm, AuthenticationFlowContext context) {
protected Set<String> getLinkedBrokerAliases(KeycloakSession session, RealmModel realm, AuthenticationFlowContext context) {
Set<String> result = null;
if (context != null) {
UserModel currentUser = context.getUser();
Expand Down Expand Up @@ -223,7 +223,7 @@ protected List<IdentityProvider> getFederatedIdentityProviders(Set<String> feder
* @return the custom {@link Predicate} used as a last filter before conversion into {@link IdentityProvider}
*/
protected Predicate<IdentityProviderModel> federatedProviderPredicate() {
return idp -> Objects.nonNull(idp) && idp.isEnabled() && !idp.isLinkOnly() && !idp.isHideOnLogin();
return IDPProvider.LoginFilter.getLoginPredicate();
}

/**
Expand All @@ -235,7 +235,7 @@ protected Predicate<IdentityProviderModel> federatedProviderPredicate() {
* @return a {@link List} containing the constructed {@link IdentityProvider}s.
*/
protected List<IdentityProvider> searchForIdentityProviders(String existingIDP) {
return session.identityProviders().getForLogin(IDPProvider.FETCH_MODE.REALM_ONLY, null)
return session.identityProviders().getForLogin(IDPProvider.FetchMode.REALM_ONLY, null)
.filter(idp -> !Objects.equals(existingIDP, idp.getAlias()))
.map(idp -> createIdentityProvider(this.realm, this.baseURI, idp))
.sorted(IDP_COMPARATOR_INSTANCE).toList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public OrganizationAwareIdentityProviderBean(IdentityProviderBean delegate, bool
protected List<IdentityProvider> searchForIdentityProviders(String existingIDP) {
if (onlyRealmBrokers) {
// we only want the realm-level IDPs - i.e. those not associated with any orgs.
return session.identityProviders().getForLogin(IDPProvider.FETCH_MODE.REALM_ONLY, null)
return session.identityProviders().getForLogin(IDPProvider.FetchMode.REALM_ONLY, null)
.filter(idp -> !Objects.equals(existingIDP, idp.getAlias()))
.map(idp -> createIdentityProvider(this.realm, this.baseURI, idp))
.sorted(IDP_COMPARATOR_INSTANCE).toList();
Expand All @@ -63,16 +63,17 @@ protected List<IdentityProvider> searchForIdentityProviders(String existingIDP)
return organization.getIdentityProviders()
.filter(idp -> idp.isEnabled() && !idp.isLinkOnly() && !idp.isHideOnLogin()
&& Boolean.parseBoolean(idp.getConfig().get(OrganizationModel.BROKER_PUBLIC)))
.filter(idp -> !Objects.equals(existingIDP, idp.getAlias()))
.map(idp -> createIdentityProvider(super.realm, super.baseURI, idp))
.toList();
.sorted(IDP_COMPARATOR_INSTANCE).toList();
}
// we don't have a specific organization - fetch public enabled IDPs linked to any org.
return session.identityProviders().getForLogin(IDPProvider.FETCH_MODE.ORG_ONLY, null)
return session.identityProviders().getForLogin(IDPProvider.FetchMode.ORG_ONLY, null)
.filter(idp -> !Objects.equals(existingIDP, idp.getAlias()))
.map(idp -> createIdentityProvider(this.realm, this.baseURI, idp))
.sorted(IDP_COMPARATOR_INSTANCE).toList();
}
return session.identityProviders().getForLogin(IDPProvider.FETCH_MODE.ALL, this.organization != null ? this.organization.getId() : null)
return session.identityProviders().getForLogin(IDPProvider.FetchMode.ALL, this.organization != null ? this.organization.getId() : null)
.filter(idp -> !Objects.equals(existingIDP, idp.getAlias()))
.map(idp -> createIdentityProvider(this.realm, this.baseURI, idp))
.sorted(IDP_COMPARATOR_INSTANCE).toList();
Expand Down
Loading

0 comments on commit fa7c2b5

Please sign in to comment.