mirror of
https://github.com/keycloak/keycloak.git
synced 2026-05-26 13:50:48 +00:00
fix: initial projection implementation (#48460)
closes: #48734 Signed-off-by: Steve Hawkins <shawkins@redhat.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
+71
-56
@@ -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
-2
@@ -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> {
|
||||
}
|
||||
|
||||
-9
@@ -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> {
|
||||
}
|
||||
-30
@@ -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;
|
||||
}
|
||||
}
|
||||
+26
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
+25
-32
@@ -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)
|
||||
|
||||
-33
@@ -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);
|
||||
}
|
||||
|
||||
+26
-47
@@ -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) {
|
||||
|
||||
-51
@@ -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() {
|
||||
}
|
||||
}
|
||||
+6
-5
@@ -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
|
||||
|
||||
+15
-10
@@ -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);
|
||||
|
||||
|
||||
+23
-9
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
org.keycloak.models.mapper.OIDCClientModelMapperFactory
|
||||
org.keycloak.models.mapper.SAMLClientModelMapperFactory
|
||||
@@ -1 +0,0 @@
|
||||
org.keycloak.models.mapper.ClientModelMapperSpi
|
||||
+24
@@ -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
|
||||
|
||||
+18
-22
@@ -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");
|
||||
|
||||
+18
-22
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user