fix: initial projection implementation (#48460)

closes: #48734

Signed-off-by: Steve Hawkins <shawkins@redhat.com>
This commit is contained in:
Steven Hawkins
2026-05-06 11:01:29 -04:00
committed by GitHub
parent c5eacd473e
commit a3e3feb726
20 changed files with 281 additions and 338 deletions
@@ -179,6 +179,17 @@ paths:
operationId: getClients
tags:
- Clients (v2)
parameters:
- description: "Set of fields to include in the response. Must be top-level\
\ fields on one of the client types. If omitted or empty, all fields will\
\ be populated."
name: fields
in: query
schema:
type: array
uniqueItems: true
items:
type: string
responses:
"200":
description: OK
@@ -1,5 +1,6 @@
package org.keycloak.admin.api.client;
import java.util.Set;
import java.util.stream.Stream;
import jakarta.validation.Valid;
@@ -9,6 +10,7 @@ import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
@@ -20,6 +22,7 @@ import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.extensions.Extension;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
@@ -35,7 +38,11 @@ public interface ClientsApi {
@APIResponses(value = {
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = BaseClientRepresentation.class)))
})
Stream<BaseClientRepresentation> getClients();
Stream<BaseClientRepresentation> getClients(@Parameter(description = "Set of fields to include in the response. Must be top-level fields on one of the client types. If omitted or empty, all fields will be populated.") @QueryParam("fields") Set<String> fields);
default Stream<BaseClientRepresentation> getClients() {
return getClients(Set.of());
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@@ -1,81 +1,96 @@
package org.keycloak.models.mapper;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.representations.admin.v2.BaseClientRepresentation;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public abstract class BaseClientModelMapper<T extends BaseClientRepresentation> implements ClientModelMapper {
protected final KeycloakSession session;
public BaseClientModelMapper(KeycloakSession session) {
this.session = session;
public static class MappedField<T> {
Function<T, Object> repGetter;
BiConsumer<T, Object> repSetter;
Function<ClientModel, Object> modelGetter;
BiConsumer<ClientModel, Object> modelSetter;
void fromModel(ClientModel model, T rep) {
if (repSetter != null && modelGetter != null) {
repSetter.accept(rep, modelGetter.apply(model));
}
}
void toModel(T rep, ClientModel model) {
if (hasGetter() && modelSetter != null) {
// TODO: exception handling to make things clearer when things fail
modelSetter.accept(model, getValue(rep));
}
}
public boolean hasGetter() {
return repGetter != null;
}
public <V> V getValue(T rep) {
if (repGetter != null) {
return (V) repGetter.apply(rep);
}
return null;
}
}
final Map<String, MappedField<BaseClientRepresentation>> fields = new LinkedHashMap<String, MappedField<BaseClientRepresentation>>();
protected <F> void addMapping(String name, Function<T, F> repGetter, BiConsumer<T, F> repSetter, Function<ClientModel, F> modelGetter, BiConsumer<ClientModel, F> modelSetter) {
MappedField prop = new MappedField<>();
prop.repGetter = repGetter;
prop.repSetter = repSetter;
prop.modelGetter = modelGetter;
prop.modelSetter = modelSetter;
this.fields.put(name, prop);
}
public BaseClientModelMapper() {
this.addMapping("protocol", BaseClientRepresentation::getProtocol, null, ClientModel::getProtocol, ClientModel::setProtocol);
this.addMapping("uuid", BaseClientRepresentation::getUuid, BaseClientRepresentation::setUuid, ClientModel::getId, null);
this.addMapping("enabled", BaseClientRepresentation::getEnabled, BaseClientRepresentation::setEnabled, ClientModel::isEnabled, (model, enabled) -> model.setEnabled(Boolean.TRUE.equals(enabled)));
this.addMapping("clientId", BaseClientRepresentation::getClientId, BaseClientRepresentation::setClientId, ClientModel::getClientId, ClientModel::setClientId);
this.addMapping("description", BaseClientRepresentation::getDescription, BaseClientRepresentation::setDescription, ClientModel::getDescription, ClientModel::setDescription);
this.addMapping("displayName", BaseClientRepresentation::getDisplayName, BaseClientRepresentation::setDisplayName, ClientModel::getName, ClientModel::setName);
this.addMapping("appUrl", BaseClientRepresentation::getAppUrl, BaseClientRepresentation::setAppUrl, ClientModel::getBaseUrl, ClientModel::setBaseUrl);
// TODO: consider built-in logic for copying collections
this.addMapping("redirectUris", BaseClientRepresentation::getRedirectUris, BaseClientRepresentation::setRedirectUris, model -> new LinkedHashSet<>(model.getRedirectUris()), (model, uris) -> model.setRedirectUris(new LinkedHashSet<>(uris)));
this.addMapping("roles", BaseClientRepresentation::getRoles, BaseClientRepresentation::setRoles, model -> model.getRolesStream().map(RoleModel::getName).collect(Collectors.toSet()), null);
}
@Override
public BaseClientRepresentation fromModel(ClientModel model) {
public BaseClientRepresentation fromModel(ClientModel model, Set<String> includeFields) {
// We don't want reps to depend on any unnecessary fields deps, hence no generated builder.
T rep = createClientRepresentation();
rep.setUuid(model.getId());
rep.setEnabled(model.isEnabled());
rep.setClientId(model.getClientId());
rep.setDescription(model.getDescription());
rep.setDisplayName(model.getName());
rep.setAppUrl(model.getBaseUrl());
rep.setRedirectUris(new HashSet<>(model.getRedirectUris()));
rep.setRoles(model.getRolesStream().map(RoleModel::getName).collect(Collectors.toSet()));
fromModelSpecific(model, rep);
var stream = fields.entrySet().stream();
if (includeFields != null && !includeFields.isEmpty()) {
stream = stream.filter(e -> includeFields.contains(e.getKey()));
}
stream.forEach(e -> e.getValue().fromModel(model, rep));
return rep;
}
@Override
@SuppressWarnings("unchecked")
public ClientModel toModel(BaseClientRepresentation rep, ClientModel existingModel) {
if (existingModel == null) {
existingModel = createClientModel(rep);
}
existingModel.setProtocol(rep.getProtocol());
existingModel.setEnabled(Boolean.TRUE.equals(rep.getEnabled()));
existingModel.setClientId(rep.getClientId());
existingModel.setDescription(rep.getDescription());
existingModel.setName(rep.getDisplayName());
existingModel.setBaseUrl(rep.getAppUrl());
existingModel.setRedirectUris(new HashSet<>(rep.getRedirectUris()));
// Roles are not handled here
toModelSpecific((T) rep, existingModel);
return existingModel;
}
protected ClientModel createClientModel(BaseClientRepresentation rep) {
RealmModel realm = session.getContext().getRealm();
// dummy add/remove to obtain a detached model
var model = realm.addClient(rep.getClientId());
realm.removeClient(model.getId());
return model;
public void toModel(BaseClientRepresentation rep, ClientModel existingModel) {
fields.values().forEach(m -> m.toModel(rep, existingModel));
}
protected abstract T createClientRepresentation();
protected abstract void fromModelSpecific(ClientModel model, T rep);
protected abstract void toModelSpecific(T rep, ClientModel model);
@Override
public void close() {
}
}
@@ -1,11 +1,10 @@
package org.keycloak.models.mapper;
import org.keycloak.models.ClientModel;
import org.keycloak.provider.Provider;
import org.keycloak.representations.admin.v2.BaseClientRepresentation;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public interface ClientModelMapper extends Provider, RepModelMapper<BaseClientRepresentation, ClientModel> {
public interface ClientModelMapper extends RepModelMapper<BaseClientRepresentation, ClientModel> {
}
@@ -1,9 +0,0 @@
package org.keycloak.models.mapper;
import org.keycloak.provider.ProviderFactory;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public interface ClientModelMapperFactory extends ProviderFactory<ClientModelMapper> {
}
@@ -1,30 +0,0 @@
package org.keycloak.models.mapper;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public class ClientModelMapperSpi implements Spi {
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return "client-model-mapper";
}
@Override
public Class<? extends Provider> getProviderClass() {
return ClientModelMapper.class;
}
@Override
public Class<? extends ProviderFactory<ClientModelMapper>> getProviderFactoryClass() {
return ClientModelMapperFactory.class;
}
}
@@ -0,0 +1,26 @@
package org.keycloak.models.mapper;
import java.util.Map;
import java.util.Optional;
import org.keycloak.representations.admin.v2.OIDCClientRepresentation;
import org.keycloak.representations.admin.v2.SAMLClientRepresentation;
public class ClientModelMappers {
private final Map<String, BaseClientModelMapper<?>> mappers;
public ClientModelMappers() {
// TODO: this may be done via discovery later
mappers = Map.of(OIDCClientRepresentation.PROTOCOL, new OIDCClientModelMapper(),
SAMLClientRepresentation.PROTOCOL, new SAMLClientModelMapper());
}
public boolean isKnownField(String name) {
return mappers.values().stream().anyMatch(f -> f.fields.containsKey(name));
}
public Optional<BaseClientModelMapper<?>> getMapper(String protocol) {
return Optional.ofNullable(mappers.get(protocol));
}
}
@@ -2,58 +2,51 @@ package org.keycloak.models.mapper;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.stream.Collectors;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RoleModel;
import org.keycloak.representations.admin.v2.OIDCClientRepresentation;
import org.keycloak.representations.admin.v2.OIDCClientRepresentation.Auth;
import org.keycloak.utils.KeycloakSessionUtil;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public class OIDCClientModelMapper extends BaseClientModelMapper<OIDCClientRepresentation> {
public OIDCClientModelMapper(KeycloakSession session) {
super(session);
}
@Override
protected OIDCClientRepresentation createClientRepresentation() {
return new OIDCClientRepresentation();
}
@Override
protected void fromModelSpecific(ClientModel model, OIDCClientRepresentation rep) {
rep.setLoginFlows(createLoginFlows(model));
if (!model.isPublicClient()) {
OIDCClientRepresentation.Auth auth = new OIDCClientRepresentation.Auth();
auth.setMethod(model.getClientAuthenticatorType());
auth.setSecret(model.getSecret());
rep.setAuth(auth);
// TODO: auth.certificate
}
rep.setWebOrigins(new HashSet<>(model.getWebOrigins()));
rep.setServiceAccountRoles(getServiceAccountRoles(model));
public OIDCClientModelMapper() {
addMapping("loginFlows", OIDCClientRepresentation::getLoginFlows, OIDCClientRepresentation::setLoginFlows, model -> createLoginFlows(model), (model, flows) -> setModelFromFlows(flows, model));
addMapping("auth", OIDCClientRepresentation::getAuth, OIDCClientRepresentation::setAuth, model -> getAuth(model), (model, auth) -> setAuth(model, auth));
addMapping("webOrigins", OIDCClientRepresentation::getWebOrigins, OIDCClientRepresentation::setWebOrigins, model -> new LinkedHashSet<>(model.getWebOrigins()), (model, webOrigins) -> model.setWebOrigins(new LinkedHashSet<>(webOrigins)));
addMapping("serviceAccountRoles", OIDCClientRepresentation::getServiceAccountRoles, OIDCClientRepresentation::setServiceAccountRoles, model -> getServiceAccountRoles(model), null);
}
@Override
protected void toModelSpecific(OIDCClientRepresentation rep, ClientModel model) {
if (rep.getAuth() != null) {
private OIDCClientRepresentation.Auth getAuth(ClientModel model) {
OIDCClientRepresentation.Auth auth = null;
if (!model.isPublicClient()) {
auth = new OIDCClientRepresentation.Auth();
auth.setMethod(model.getClientAuthenticatorType());
auth.setSecret(model.getSecret());
// TODO: auth.certificate
}
return auth;
}
private void setAuth(ClientModel model, Auth auth) {
if (auth != null) {
model.setPublicClient(false);
model.setClientAuthenticatorType(rep.getAuth().getMethod());
model.setSecret(rep.getAuth().getSecret());
model.setClientAuthenticatorType(auth.getMethod());
model.setSecret(auth.getSecret());
} else {
model.setPublicClient(true);
}
setModelFromFlows(rep.getLoginFlows(), model);
model.setWebOrigins(new HashSet<>(rep.getWebOrigins()));
// Service account roles are not handled here
}
private Set<OIDCClientRepresentation.Flow> createLoginFlows(ClientModel model) {
@@ -82,7 +75,7 @@ public class OIDCClientModelMapper extends BaseClientModelMapper<OIDCClientRepre
private Set<String> getServiceAccountRoles(ClientModel client) {
if (client.isServiceAccountsEnabled()) {
var serviceAccount = session.users().getServiceAccount(client);
var serviceAccount = KeycloakSessionUtil.getKeycloakSession().users().getServiceAccount(client);
if (serviceAccount != null) {
return serviceAccount.getRoleMappingsStream()
.map(RoleModel::getName)
@@ -1,33 +0,0 @@
package org.keycloak.models.mapper;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.representations.admin.v2.OIDCClientRepresentation;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public class OIDCClientModelMapperFactory implements ClientModelMapperFactory {
@Override
public ClientModelMapper create(KeycloakSession session) {
return new OIDCClientModelMapper(session);
}
@Override
public String getId() {
return OIDCClientRepresentation.PROTOCOL;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
}
@@ -1,14 +1,17 @@
package org.keycloak.models.mapper;
import java.util.Set;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public interface RepModelMapper <T, U> {
T fromModel(U model);
default U toModel(T rep) {
return toModel(rep, null);
default T fromModel(U model) {
return fromModel(model, null);
}
U toModel(T rep, U existingModel);
T fromModel(U model, Set<String> includeFields);
void toModel(T rep, U existingModel);
}
@@ -17,8 +17,10 @@
package org.keycloak.models.mapper;
import java.util.function.BiConsumer;
import java.util.function.Function;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.representations.admin.v2.SAMLClientRepresentation;
/**
@@ -39,63 +41,40 @@ public class SAMLClientModelMapper extends BaseClientModelMapper<SAMLClientRepre
private static final String SAML_SIGNING_CERTIFICATE = "saml.signing.certificate";
private static final String SAML_ALLOW_ECP_FLOW = "saml.allow.ecp.flow";
public SAMLClientModelMapper(KeycloakSession session) {
super(session);
}
@Override
protected SAMLClientRepresentation createClientRepresentation() {
return new SAMLClientRepresentation();
}
@Override
protected void fromModelSpecific(ClientModel model, SAMLClientRepresentation rep) {
// Name ID settings
rep.setNameIdFormat(model.getAttribute(SAML_NAME_ID_FORMAT));
rep.setForceNameIdFormat(getBooleanAttribute(model, SAML_FORCE_NAME_ID_FORMAT));
// Signature settings
rep.setIncludeAuthnStatement(getBooleanAttribute(model, SAML_AUTHN_STATEMENT));
rep.setSignDocuments(getBooleanAttribute(model, SAML_SERVER_SIGNATURE));
rep.setSignAssertions(getBooleanAttribute(model, SAML_ASSERTION_SIGNATURE));
rep.setClientSignatureRequired(getBooleanAttribute(model, SAML_CLIENT_SIGNATURE));
rep.setSignatureAlgorithm(model.getAttribute(SAML_SIGNATURE_ALGORITHM));
rep.setSignatureCanonicalizationMethod(model.getAttribute(SAML_SIGNATURE_CANONICALIZATION));
rep.setSigningCertificate(model.getAttribute(SAML_SIGNING_CERTIFICATE));
// Binding and logout settings
rep.setForcePostBinding(getBooleanAttribute(model, SAML_FORCE_POST_BINDING));
rep.setFrontChannelLogout(model.isFrontchannelLogout());
// ECP flow
rep.setAllowEcpFlow(getBooleanAttribute(model, SAML_ALLOW_ECP_FLOW));
protected void addBooleanAttributeMapping(String name, String attribute, Function<SAMLClientRepresentation, Boolean> repGetter, BiConsumer<SAMLClientRepresentation, Boolean> repSetter) {
this.addMapping(name, repGetter, repSetter, model -> getBooleanAttribute(model, attribute), (model, value) -> setBooleanAttributeIfNotNull(model, attribute, value));
}
@Override
protected void toModelSpecific(SAMLClientRepresentation rep, ClientModel model) {
model.setProtocol(SAMLClientRepresentation.PROTOCOL);
protected void addAttributeMapping(String name, String attribute, Function<SAMLClientRepresentation, String> repGetter, BiConsumer<SAMLClientRepresentation, String> repSetter) {
this.addMapping(name, repGetter, repSetter, model -> model.getAttribute(attribute), (model, value) -> setAttributeIfNotNull(model, attribute, value));
}
public SAMLClientModelMapper() {
// Name ID settings
setAttributeIfNotNull(model, SAML_NAME_ID_FORMAT, rep.getNameIdFormat());
setBooleanAttributeIfNotNull(model, SAML_FORCE_NAME_ID_FORMAT, rep.getForceNameIdFormat());
addAttributeMapping("nameIdFormat", SAML_NAME_ID_FORMAT, SAMLClientRepresentation::getNameIdFormat, SAMLClientRepresentation::setNameIdFormat);
addBooleanAttributeMapping("forceNameIdFormat", SAML_FORCE_NAME_ID_FORMAT, SAMLClientRepresentation::getForceNameIdFormat, SAMLClientRepresentation::setForceNameIdFormat);
// Signature settings
setBooleanAttributeIfNotNull(model, SAML_AUTHN_STATEMENT, rep.getIncludeAuthnStatement());
setBooleanAttributeIfNotNull(model, SAML_SERVER_SIGNATURE, rep.getSignDocuments());
setBooleanAttributeIfNotNull(model, SAML_ASSERTION_SIGNATURE, rep.getSignAssertions());
setBooleanAttributeIfNotNull(model, SAML_CLIENT_SIGNATURE, rep.getClientSignatureRequired());
setAttributeIfNotNull(model, SAML_SIGNATURE_ALGORITHM, rep.getSignatureAlgorithm());
setAttributeIfNotNull(model, SAML_SIGNATURE_CANONICALIZATION, rep.getSignatureCanonicalizationMethod());
setAttributeIfNotNull(model, SAML_SIGNING_CERTIFICATE, rep.getSigningCertificate());
addBooleanAttributeMapping("includeAuthnStatement", SAML_AUTHN_STATEMENT, SAMLClientRepresentation::getIncludeAuthnStatement, SAMLClientRepresentation::setIncludeAuthnStatement);
addBooleanAttributeMapping("signDocuments", SAML_SERVER_SIGNATURE, SAMLClientRepresentation::getSignDocuments, SAMLClientRepresentation::setSignDocuments);
addBooleanAttributeMapping("signAssertions", SAML_ASSERTION_SIGNATURE, SAMLClientRepresentation::getSignAssertions, SAMLClientRepresentation::setSignAssertions);
addBooleanAttributeMapping("clientSignatureRequired", SAML_CLIENT_SIGNATURE, SAMLClientRepresentation::getClientSignatureRequired, SAMLClientRepresentation::setClientSignatureRequired);
addAttributeMapping("signatureAlgorithm", SAML_SIGNATURE_ALGORITHM, SAMLClientRepresentation::getSignatureAlgorithm, SAMLClientRepresentation::setSignatureAlgorithm);
addAttributeMapping("signatureCanonicalizationMethod", SAML_SIGNATURE_CANONICALIZATION, SAMLClientRepresentation::getSignatureCanonicalizationMethod, SAMLClientRepresentation::setSignatureCanonicalizationMethod);
addAttributeMapping("signingCertificate", SAML_SIGNING_CERTIFICATE, SAMLClientRepresentation::getSigningCertificate, SAMLClientRepresentation::setSigningCertificate);
// Binding and logout settings
setBooleanAttributeIfNotNull(model, SAML_FORCE_POST_BINDING, rep.getForcePostBinding());
if (rep.getFrontChannelLogout() != null) {
model.setFrontchannelLogout(rep.getFrontChannelLogout());
}
addBooleanAttributeMapping("forcePostBinding", SAML_FORCE_POST_BINDING, SAMLClientRepresentation::getForcePostBinding, SAMLClientRepresentation::setForcePostBinding);
// TODO: mapping from 3 value to 2 value boolean can be confusing from a patching perspective
addMapping("frontChannelLogout", SAMLClientRepresentation::getFrontChannelLogout, SAMLClientRepresentation::setFrontChannelLogout, ClientModel::isFrontchannelLogout, (model, logout) -> model.setFrontchannelLogout(Boolean.TRUE.equals(logout)));
// ECP flow
setBooleanAttributeIfNotNull(model, SAML_ALLOW_ECP_FLOW, rep.getAllowEcpFlow());
addBooleanAttributeMapping("allowEcpFlow", SAML_ALLOW_ECP_FLOW, SAMLClientRepresentation::getAllowEcpFlow, SAMLClientRepresentation::setAllowEcpFlow);
}
private Boolean getBooleanAttribute(ClientModel model, String key) {
@@ -1,51 +0,0 @@
/*
* Copyright 2025 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.models.mapper;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.representations.admin.v2.SAMLClientRepresentation;
/**
* Factory for creating SAMLClientModelMapper instances.
*/
public class SAMLClientModelMapperFactory implements ClientModelMapperFactory {
@Override
public ClientModelMapper create(KeycloakSession session) {
return new SAMLClientModelMapper(session);
}
@Override
public String getId() {
return SAMLClientRepresentation.PROTOCOL;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
}
@@ -1,11 +1,11 @@
package org.keycloak.rest.admin.api.client;
import java.util.Set;
import java.util.stream.Stream;
import jakarta.annotation.Nonnull;
import jakarta.validation.Valid;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
@@ -17,11 +17,13 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.admin.v2.BaseClientRepresentation;
import org.keycloak.services.client.ClientService;
import org.keycloak.services.client.ClientService.ClientProjectionOptions;
import org.keycloak.services.client.DefaultClientService;
import org.keycloak.services.resources.admin.RealmAdminResource;
import org.keycloak.services.resources.admin.fgap.AdminPermissionEvaluator;
public class DefaultClientsApi implements ClientsApi {
private final KeycloakSession session;
private final AdminPermissionEvaluator permissions;
private final RealmModel realm;
@@ -41,11 +43,10 @@ public class DefaultClientsApi implements ClientsApi {
this.realmAdminResource = realmAdminResource;
this.clientService = new DefaultClientService(session, realm, permissions, realmAdminResource);
}
@GET
@Override
public Stream<BaseClientRepresentation> getClients() {
return clientService.getClients(realm);
public Stream<BaseClientRepresentation> getClients(Set<String> fields) {
return clientService.getClients(realm, new ClientProjectionOptions(fields), null, null);
}
@POST
@@ -1,7 +1,10 @@
package org.keycloak.services.client;
import java.io.InputStream;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import org.keycloak.models.RealmModel;
@@ -17,7 +20,17 @@ public interface ClientService extends Service {
}
class ClientProjectionOptions {
// TODO
private final LinkedHashSet<String> fields = new LinkedHashSet<>();
public ClientProjectionOptions(Set<String> fields) {
if (fields != null) {
this.fields.addAll(fields);
}
}
public Set<String> getFields() {
return Collections.unmodifiableSet(fields);
}
}
class ClientSortAndSliceOptions {
@@ -29,15 +42,7 @@ public interface ClientService extends Service {
record CreateOrUpdateResult(BaseClientRepresentation representation, boolean created) {}
default Optional<BaseClientRepresentation> getClient(RealmModel realm, String clientId) throws ServiceException {
return getClient(realm, clientId, null);
}
Optional<BaseClientRepresentation> getClient(RealmModel realm, String clientId, ClientProjectionOptions projectionOptions) throws ServiceException;
default Stream<BaseClientRepresentation> getClients(RealmModel realm) {
return getClients(realm, null, null, null);
}
Optional<BaseClientRepresentation> getClient(RealmModel realm, String clientId) throws ServiceException;
Stream<BaseClientRepresentation> getClients(RealmModel realm, ClientProjectionOptions projectionOptions, ClientSearchOptions searchOptions, ClientSortAndSliceOptions sortAndSliceOptions);
@@ -27,6 +27,7 @@ import org.keycloak.models.ModelValidationException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.mapper.ClientModelMapper;
import org.keycloak.models.mapper.ClientModelMappers;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.admin.v2.BaseClientRepresentation;
@@ -75,6 +76,7 @@ import static org.keycloak.utils.StringUtil.isBlank;
*/
public class DefaultClientService implements ClientService {
private static final ObjectMapper MAPPER = new ObjectMapperResolver().getContext(null);
private static final ClientModelMappers MAPPERS = new ClientModelMappers();
private final KeycloakSession session;
private final AdminPermissionEvaluator permissions;
@@ -97,8 +99,7 @@ public class DefaultClientService implements ClientService {
@Override
public Optional<BaseClientRepresentation> getClient(@Nonnull RealmModel realm,
@Nonnull String clientId,
ClientProjectionOptions projectionOptions) throws ServiceException {
@Nonnull String clientId) throws ServiceException {
ClientModel client = realm.getClientByClientId(clientId);
if (client == null) {
return Optional.empty();
@@ -119,6 +120,17 @@ public class DefaultClientService implements ClientService {
ClientSearchOptions searchOptions,
ClientSortAndSliceOptions sortAndSliceOptions) {
permissions.clients().requireList();
// TODO: this check is weak
// a stronger check is whether the remaining fields have repSetters
// however this highlights an issue we may hit with polymorphism a field may
// be projectable in one subtype, but fixed in another
projectionOptions.getFields().forEach(s -> {
if (!MAPPERS.isKnownField(s)) {
throw new ServiceException("%s is an unknown field".formatted(s), Response.Status.BAD_REQUEST);
}
});
// When FGAP is enabled, authorization filtering is applied at the JPA layer (via PartialEvaluator predicates), so we trust the DB results.
// When disabled, we fall back to in-memory filtering by VIEW_CLIENTS role.
@@ -127,7 +139,7 @@ public class DefaultClientService implements ClientService {
return realm.getClientsStream()
.filter(client -> canView || permissions.clients().canView(client))
.filter(client -> client.getProtocol() != null)
.map(client -> getMapper(client.getProtocol()).fromModel(client))
.map(client -> getMapper(client.getProtocol()).fromModel(client, projectionOptions.getFields()))
.filter(java.util.Objects::nonNull);
} catch (ModelException e) {
throw new ServiceException(e.getMessage(), Response.Status.BAD_REQUEST);
@@ -251,7 +263,7 @@ public class DefaultClientService implements ClientService {
generateClientSecretIfNeeded(client, model);
// Update model
model = mapper.toModel(client, model);
mapper.toModel(client, model);
// Validate the fully populated model
ValidationUtil.validateClient(session, model, false, r -> {
@@ -326,10 +338,12 @@ public class DefaultClientService implements ClientService {
*/
private ClientRepresentation getProposedOldRepresentation(RealmModel realm, BaseClientRepresentation client, ClientModelMapper mapper) {
String tempId = "__temp__" + client.getClientId() + "__" + System.nanoTime();
ClientModel tempModel = mapper.toModel(client, realm.addClient(tempId));
ClientModel tempModel = realm.addClient(tempId);
String clientId = client.getClientId();
mapper.toModel(client, tempModel);
try {
var proposedRepresentation = ModelToRepresentation.toRepresentation(tempModel, session);
proposedRepresentation.setClientId(client.getClientId());
proposedRepresentation.setClientId(clientId);
return proposedRepresentation;
} finally {
realm.removeClient(tempModel.getId());
@@ -450,8 +464,8 @@ public class DefaultClientService implements ClientService {
}
}
protected ClientModelMapper getMapper(String protocol) {
return Optional.ofNullable(session.getProvider(ClientModelMapper.class, protocol))
.orElseThrow(() -> new ServiceException("Mapper not found, unsupported client protocol: " + protocol, Response.Status.BAD_REQUEST));
public ClientModelMapper getMapper(String protocol) {
return MAPPERS.getMapper(protocol).orElseThrow(() -> new ServiceException("Mapper not found, unsupported client protocol: " + protocol,
Response.Status.BAD_REQUEST));
}
}
@@ -1,2 +0,0 @@
org.keycloak.models.mapper.OIDCClientModelMapperFactory
org.keycloak.models.mapper.SAMLClientModelMapperFactory
@@ -1 +0,0 @@
org.keycloak.models.mapper.ClientModelMapperSpi
@@ -23,6 +23,7 @@ import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
@@ -306,6 +307,7 @@ public class ClientApiV2Test extends AbstractClientApiV2Test{
.orElse(null);
assertThat("OIDC client should be in the list", foundOidc, is(notNullValue()));
assertThat(foundOidc.getDescription(), notNullValue());
assertThat(foundOidc.getLoginFlows(), is(Set.of(OIDCClientRepresentation.Flow.STANDARD, OIDCClientRepresentation.Flow.DIRECT_GRANT)));
assertThat(foundOidc.getWebOrigins(), is(Set.of("http://localhost:3000", "http://localhost:4000")));
@@ -317,6 +319,7 @@ public class ClientApiV2Test extends AbstractClientApiV2Test{
.orElse(null);
assertThat("SAML client should be in the list", foundSaml, is(notNullValue()));
assertThat(foundSaml.getDescription(), notNullValue());
assertThat(foundSaml.getNameIdFormat(), is("email"));
assertThat(foundSaml.getSignDocuments(), is(true));
assertThat(foundSaml.getSignAssertions(), is(true));
@@ -340,6 +343,27 @@ public class ClientApiV2Test extends AbstractClientApiV2Test{
assertThat(samlClient.getSignAssertions(), is(true));
assertThat(samlClient.getForcePostBinding(), is(true));
assertThat(samlClient.getFrontChannelLogout(), is(false));
// test projecting only id and protocol
try (Stream<BaseClientRepresentation> baseClientRepresentationStream = getClientsApi().getClients(Set.of("clientId", "protocol"))) {
List<BaseClientRepresentation> clients = baseClientRepresentationStream.toList();
for (BaseClientRepresentation client : clients) {
BaseClientRepresentation toCompare = null;
if (client.getProtocol().equals(OIDCClientRepresentation.PROTOCOL)) {
toCompare = new OIDCClientRepresentation();
} else {
toCompare = new SAMLClientRepresentation();
}
toCompare.setClientId(client.getClientId());
assertThat(client, Matchers.samePropertyValuesAs(toCompare));
}
}
}
@Test
public void invalidFieldProjection() {
BadRequestException e = assertThrows(BadRequestException.class, () -> getClientsApi().getClients(Set.of("unknown!")));
assertEquals("{\"error\":\"unknown! is an unknown field\"}", e.getResponse().readEntity(String.class));
}
@Test
@@ -25,7 +25,6 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.mapper.ClientModelMapper;
import org.keycloak.models.mapper.OIDCClientModelMapper;
import org.keycloak.representations.admin.v2.BaseClientRepresentation;
import org.keycloak.representations.admin.v2.OIDCClientRepresentation;
@@ -71,7 +70,7 @@ public class OIDCClientModelMapperTest {
clientModel.setServiceAccountsEnabled(false);
clientModel.setWebOrigins(Set.of());
OIDCClientModelMapper mapper = (OIDCClientModelMapper) session.getProvider(ClientModelMapper.class, OIDCClientRepresentation.PROTOCOL);
OIDCClientModelMapper mapper = getModelMapper(session);
BaseClientRepresentation rep = mapper.fromModel(clientModel);
assertThat(rep, instanceOf(OIDCClientRepresentation.class));
@@ -97,7 +96,7 @@ public class OIDCClientModelMapperTest {
setupBasicClientModel(clientModel);
RoleModel clientRole = clientModel.addRole("client-role");
OIDCClientModelMapper mapper = (OIDCClientModelMapper) session.getProvider(ClientModelMapper.class, OIDCClientRepresentation.PROTOCOL);
OIDCClientModelMapper mapper = getModelMapper(session);
BaseClientRepresentation rep = mapper.fromModel(clientModel);
assertThat(rep.getRoles(), contains("client-role"));
@@ -120,7 +119,7 @@ public class OIDCClientModelMapperTest {
// Note: serviceAccountsEnabled is not set here as it requires a service account user to exist
// for the mapper to work correctly. The SERVICE_ACCOUNT flow is tested separately.
OIDCClientModelMapper mapper = (OIDCClientModelMapper) session.getProvider(ClientModelMapper.class, OIDCClientRepresentation.PROTOCOL);
OIDCClientModelMapper mapper = getModelMapper(session);
OIDCClientRepresentation rep = (OIDCClientRepresentation) mapper.fromModel(clientModel);
assertThat(rep.getLoginFlows(), containsInAnyOrder(
@@ -143,7 +142,7 @@ public class OIDCClientModelMapperTest {
clientModel.setClientAuthenticatorType("client-secret");
clientModel.setSecret("my-secret");
OIDCClientModelMapper mapper = (OIDCClientModelMapper) session.getProvider(ClientModelMapper.class, OIDCClientRepresentation.PROTOCOL);
OIDCClientModelMapper mapper = getModelMapper(session);
OIDCClientRepresentation rep = (OIDCClientRepresentation) mapper.fromModel(clientModel);
assertThat(rep.getAuth(), notNullValue());
@@ -164,7 +163,7 @@ public class OIDCClientModelMapperTest {
setupBasicClientModel(clientModel);
clientModel.setPublicClient(true);
OIDCClientModelMapper mapper = (OIDCClientModelMapper) session.getProvider(ClientModelMapper.class, OIDCClientRepresentation.PROTOCOL);
OIDCClientModelMapper mapper = getModelMapper(session);
OIDCClientRepresentation rep = (OIDCClientRepresentation) mapper.fromModel(clientModel);
assertThat(rep.getAuth(), nullValue());
@@ -183,7 +182,7 @@ public class OIDCClientModelMapperTest {
setupBasicClientModel(clientModel);
clientModel.setWebOrigins(Set.of("http://localhost:3000", "http://localhost:4000"));
OIDCClientModelMapper mapper = (OIDCClientModelMapper) session.getProvider(ClientModelMapper.class, OIDCClientRepresentation.PROTOCOL);
OIDCClientModelMapper mapper = getModelMapper(session);
OIDCClientRepresentation rep = (OIDCClientRepresentation) mapper.fromModel(clientModel);
assertThat(rep.getWebOrigins(), containsInAnyOrder("http://localhost:3000", "http://localhost:4000"));
@@ -218,7 +217,7 @@ public class OIDCClientModelMapperTest {
serviceAccount.grantRole(role1);
serviceAccount.grantRole(role2);
OIDCClientModelMapper mapper = (OIDCClientModelMapper) session.getProvider(ClientModelMapper.class, OIDCClientRepresentation.PROTOCOL);
OIDCClientModelMapper mapper = getModelMapper(session);
OIDCClientRepresentation rep = (OIDCClientRepresentation) mapper.fromModel(clientModel);
assertThat(rep.getServiceAccountRoles(), hasItems("test-role-1", "test-role-2"));
@@ -239,7 +238,7 @@ public class OIDCClientModelMapperTest {
setupBasicClientModel(clientModel);
clientModel.setServiceAccountsEnabled(false);
OIDCClientModelMapper mapper = (OIDCClientModelMapper) session.getProvider(ClientModelMapper.class, OIDCClientRepresentation.PROTOCOL);
OIDCClientModelMapper mapper = getModelMapper(session);
OIDCClientRepresentation rep = (OIDCClientRepresentation) mapper.fromModel(clientModel);
assertThat(rep.getServiceAccountRoles(), empty());
@@ -265,7 +264,7 @@ public class OIDCClientModelMapperTest {
rep.setWebOrigins(Set.of("http://example.com"));
rep.setLoginFlows(Set.of());
OIDCClientModelMapper mapper = (OIDCClientModelMapper) session.getProvider(ClientModelMapper.class, OIDCClientRepresentation.PROTOCOL);
OIDCClientModelMapper mapper = getModelMapper(session);
mapper.toModel(rep, clientModel);
assertThat(clientModel.isEnabled(), is(true));
@@ -280,6 +279,10 @@ public class OIDCClientModelMapperTest {
}
}
private OIDCClientModelMapper getModelMapper(KeycloakSession session) {
return new OIDCClientModelMapper();
}
@TestOnServer
public void toModel_setsLoginFlows(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName("master");
@@ -296,7 +299,7 @@ public class OIDCClientModelMapperTest {
rep.setRedirectUris(Set.of());
rep.setWebOrigins(Set.of());
OIDCClientModelMapper mapper = (OIDCClientModelMapper) session.getProvider(ClientModelMapper.class, OIDCClientRepresentation.PROTOCOL);
OIDCClientModelMapper mapper = getModelMapper(session);
mapper.toModel(rep, clientModel);
assertThat(clientModel.isStandardFlowEnabled(), is(true));
@@ -326,7 +329,7 @@ public class OIDCClientModelMapperTest {
rep.setRedirectUris(Set.of());
rep.setWebOrigins(Set.of());
OIDCClientModelMapper mapper = (OIDCClientModelMapper) session.getProvider(ClientModelMapper.class, OIDCClientRepresentation.PROTOCOL);
OIDCClientModelMapper mapper = getModelMapper(session);
mapper.toModel(rep, clientModel);
assertThat(clientModel.isStandardFlowEnabled(), is(false));
@@ -356,7 +359,7 @@ public class OIDCClientModelMapperTest {
auth.setSecret("jwt-secret");
rep.setAuth(auth);
OIDCClientModelMapper mapper = (OIDCClientModelMapper) session.getProvider(ClientModelMapper.class, OIDCClientRepresentation.PROTOCOL);
OIDCClientModelMapper mapper = getModelMapper(session);
mapper.toModel(rep, clientModel);
assertThat(clientModel.isPublicClient(), is(false));
@@ -385,7 +388,7 @@ public class OIDCClientModelMapperTest {
rep.setLoginFlows(Set.of());
rep.setAuth(null);
OIDCClientModelMapper mapper = (OIDCClientModelMapper) session.getProvider(ClientModelMapper.class, OIDCClientRepresentation.PROTOCOL);
OIDCClientModelMapper mapper = getModelMapper(session);
mapper.toModel(rep, clientModel);
assertThat(clientModel.isPublicClient(), is(true));
@@ -411,7 +414,7 @@ public class OIDCClientModelMapperTest {
rep.setWebOrigins(Set.of());
rep.setLoginFlows(Set.of());
OIDCClientModelMapper mapper = (OIDCClientModelMapper) session.getProvider(ClientModelMapper.class, OIDCClientRepresentation.PROTOCOL);
OIDCClientModelMapper mapper = getModelMapper(session);
mapper.toModel(rep, clientModel);
assertThat(clientModel.isEnabled(), is(false));
@@ -420,13 +423,6 @@ public class OIDCClientModelMapperTest {
}
}
@TestOnServer
public void close_doesNotThrow(KeycloakSession session) {
OIDCClientModelMapper mapper = (OIDCClientModelMapper) session.getProvider(ClientModelMapper.class, OIDCClientRepresentation.PROTOCOL);
// Just verify close doesn't throw any exception
mapper.close();
}
private void setupBasicClientModel(ClientModel clientModel) {
clientModel.setEnabled(true);
clientModel.setDescription("Test description");
@@ -24,7 +24,6 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.mapper.ClientModelMapper;
import org.keycloak.models.mapper.SAMLClientModelMapper;
import org.keycloak.representations.admin.v2.BaseClientRepresentation;
import org.keycloak.representations.admin.v2.SAMLClientRepresentation;
@@ -74,7 +73,7 @@ public class SAMLClientModelMapperTest {
clientModel.setBaseUrl("http://localhost:8080/saml");
clientModel.setRedirectUris(Set.of("http://localhost:8080/saml/callback"));
SAMLClientModelMapper mapper = (SAMLClientModelMapper) session.getProvider(ClientModelMapper.class, SAMLClientRepresentation.PROTOCOL);
SAMLClientModelMapper mapper = getModelMapper(session);
BaseClientRepresentation rep = mapper.fromModel(clientModel);
assertThat(rep, instanceOf(SAMLClientRepresentation.class));
@@ -90,6 +89,10 @@ public class SAMLClientModelMapperTest {
}
}
private SAMLClientModelMapper getModelMapper(KeycloakSession session) {
return new SAMLClientModelMapper();
}
@TestOnServer
public void fromModel_mapsRoles(KeycloakSession session) {
RealmModel realm = session.realms().getRealmByName("master");
@@ -100,7 +103,7 @@ public class SAMLClientModelMapperTest {
setupBasicSamlClientModel(clientModel);
RoleModel clientRole = clientModel.addRole("saml-client-role");
SAMLClientModelMapper mapper = (SAMLClientModelMapper) session.getProvider(ClientModelMapper.class, SAMLClientRepresentation.PROTOCOL);
SAMLClientModelMapper mapper = getModelMapper(session);
BaseClientRepresentation rep = mapper.fromModel(clientModel);
assertThat(rep.getRoles(), contains("saml-client-role"));
@@ -120,7 +123,7 @@ public class SAMLClientModelMapperTest {
clientModel.setAttribute(SAML_NAME_ID_FORMAT, "username");
clientModel.setAttribute(SAML_FORCE_NAME_ID_FORMAT, "true");
SAMLClientModelMapper mapper = (SAMLClientModelMapper) session.getProvider(ClientModelMapper.class, SAMLClientRepresentation.PROTOCOL);
SAMLClientModelMapper mapper = getModelMapper(session);
SAMLClientRepresentation rep = (SAMLClientRepresentation) mapper.fromModel(clientModel);
assertThat(rep.getNameIdFormat(), is("username"));
@@ -146,7 +149,7 @@ public class SAMLClientModelMapperTest {
clientModel.setAttribute(SAML_SIGNATURE_CANONICALIZATION, "http://www.w3.org/2001/10/xml-exc-c14n#");
clientModel.setAttribute(SAML_SIGNING_CERTIFICATE, "MIICertificate");
SAMLClientModelMapper mapper = (SAMLClientModelMapper) session.getProvider(ClientModelMapper.class, SAMLClientRepresentation.PROTOCOL);
SAMLClientModelMapper mapper = getModelMapper(session);
SAMLClientRepresentation rep = (SAMLClientRepresentation) mapper.fromModel(clientModel);
assertThat(rep.getIncludeAuthnStatement(), is(true));
@@ -172,7 +175,7 @@ public class SAMLClientModelMapperTest {
clientModel.setAttribute(SAML_FORCE_POST_BINDING, "true");
clientModel.setFrontchannelLogout(true);
SAMLClientModelMapper mapper = (SAMLClientModelMapper) session.getProvider(ClientModelMapper.class, SAMLClientRepresentation.PROTOCOL);
SAMLClientModelMapper mapper = getModelMapper(session);
SAMLClientRepresentation rep = (SAMLClientRepresentation) mapper.fromModel(clientModel);
assertThat(rep.getForcePostBinding(), is(true));
@@ -192,7 +195,7 @@ public class SAMLClientModelMapperTest {
setupBasicSamlClientModel(clientModel);
clientModel.setAttribute(SAML_ALLOW_ECP_FLOW, "true");
SAMLClientModelMapper mapper = (SAMLClientModelMapper) session.getProvider(ClientModelMapper.class, SAMLClientRepresentation.PROTOCOL);
SAMLClientModelMapper mapper = getModelMapper(session);
SAMLClientRepresentation rep = (SAMLClientRepresentation) mapper.fromModel(clientModel);
assertThat(rep.getAllowEcpFlow(), is(true));
@@ -211,7 +214,7 @@ public class SAMLClientModelMapperTest {
setupBasicSamlClientModel(clientModel);
// Don't set any SAML-specific attributes
SAMLClientModelMapper mapper = (SAMLClientModelMapper) session.getProvider(ClientModelMapper.class, SAMLClientRepresentation.PROTOCOL);
SAMLClientModelMapper mapper = getModelMapper(session);
SAMLClientRepresentation rep = (SAMLClientRepresentation) mapper.fromModel(clientModel);
assertThat(rep.getNameIdFormat(), nullValue());
@@ -242,7 +245,7 @@ public class SAMLClientModelMapperTest {
rep.setAppUrl("http://example.com/saml");
rep.setRedirectUris(Set.of("http://example.com/saml/callback"));
SAMLClientModelMapper mapper = (SAMLClientModelMapper) session.getProvider(ClientModelMapper.class, SAMLClientRepresentation.PROTOCOL);
SAMLClientModelMapper mapper = getModelMapper(session);
mapper.toModel(rep, clientModel);
assertThat(clientModel.isEnabled(), is(true));
@@ -271,7 +274,7 @@ public class SAMLClientModelMapperTest {
rep.setNameIdFormat("email");
rep.setForceNameIdFormat(true);
SAMLClientModelMapper mapper = (SAMLClientModelMapper) session.getProvider(ClientModelMapper.class, SAMLClientRepresentation.PROTOCOL);
SAMLClientModelMapper mapper = getModelMapper(session);
mapper.toModel(rep, clientModel);
assertThat(clientModel.getAttribute(SAML_NAME_ID_FORMAT), is("email"));
@@ -300,7 +303,7 @@ public class SAMLClientModelMapperTest {
rep.setSignatureCanonicalizationMethod("http://www.w3.org/2001/10/xml-exc-c14n#WithComments");
rep.setSigningCertificate("MIINewCertificate");
SAMLClientModelMapper mapper = (SAMLClientModelMapper) session.getProvider(ClientModelMapper.class, SAMLClientRepresentation.PROTOCOL);
SAMLClientModelMapper mapper = getModelMapper(session);
mapper.toModel(rep, clientModel);
assertThat(clientModel.getAttribute(SAML_AUTHN_STATEMENT), is("true"));
@@ -329,7 +332,7 @@ public class SAMLClientModelMapperTest {
rep.setForcePostBinding(true);
rep.setFrontChannelLogout(true);
SAMLClientModelMapper mapper = (SAMLClientModelMapper) session.getProvider(ClientModelMapper.class, SAMLClientRepresentation.PROTOCOL);
SAMLClientModelMapper mapper = getModelMapper(session);
mapper.toModel(rep, clientModel);
assertThat(clientModel.getAttribute(SAML_FORCE_POST_BINDING), is("true"));
@@ -352,7 +355,7 @@ public class SAMLClientModelMapperTest {
rep.setRedirectUris(Set.of());
rep.setAllowEcpFlow(true);
SAMLClientModelMapper mapper = (SAMLClientModelMapper) session.getProvider(ClientModelMapper.class, SAMLClientRepresentation.PROTOCOL);
SAMLClientModelMapper mapper = getModelMapper(session);
mapper.toModel(rep, clientModel);
assertThat(clientModel.getAttribute(SAML_ALLOW_ECP_FLOW), is("true"));
@@ -378,7 +381,7 @@ public class SAMLClientModelMapperTest {
rep.setRedirectUris(Set.of());
// Leave SAML-specific fields null
SAMLClientModelMapper mapper = (SAMLClientModelMapper) session.getProvider(ClientModelMapper.class, SAMLClientRepresentation.PROTOCOL);
SAMLClientModelMapper mapper = getModelMapper(session);
mapper.toModel(rep, clientModel);
// Existing attributes should remain unchanged when rep values are null
@@ -409,7 +412,7 @@ public class SAMLClientModelMapperTest {
rep.setFrontChannelLogout(false);
rep.setAllowEcpFlow(false);
SAMLClientModelMapper mapper = (SAMLClientModelMapper) session.getProvider(ClientModelMapper.class, SAMLClientRepresentation.PROTOCOL);
SAMLClientModelMapper mapper = getModelMapper(session);
mapper.toModel(rep, clientModel);
assertThat(clientModel.isEnabled(), is(false));
@@ -426,13 +429,6 @@ public class SAMLClientModelMapperTest {
}
}
@TestOnServer
public void close_doesNotThrow(KeycloakSession session) {
SAMLClientModelMapper mapper = (SAMLClientModelMapper) session.getProvider(ClientModelMapper.class, SAMLClientRepresentation.PROTOCOL);
// Just verify close doesn't throw any exception
mapper.close();
}
private void setupBasicSamlClientModel(ClientModel clientModel) {
clientModel.setProtocol(SAMLClientRepresentation.PROTOCOL);
clientModel.setEnabled(true);