Added ClientType implementation from Marek's prototype
Signed-off-by: vibrown <vibrown@redhat.com> More updates Signed-off-by: vibrown <vibrown@redhat.com> Added client type logic from Marek's prototype Signed-off-by: vibrown <vibrown@redhat.com> updates Signed-off-by: vibrown <vibrown@redhat.com> updates Signed-off-by: vibrown <vibrown@redhat.com> updates Signed-off-by: vibrown <vibrown@redhat.com> Testing to see if skipRestart was cause of test failures in MR
This commit is contained in:
parent
9c1790af68
commit
3fffc5182e
20 changed files with 664 additions and 122 deletions
|
@ -31,6 +31,7 @@ public class ClientRepresentation {
|
||||||
protected String clientId;
|
protected String clientId;
|
||||||
protected String name;
|
protected String name;
|
||||||
protected String description;
|
protected String description;
|
||||||
|
protected String type;
|
||||||
protected String rootUrl;
|
protected String rootUrl;
|
||||||
protected String adminUrl;
|
protected String adminUrl;
|
||||||
protected String baseUrl;
|
protected String baseUrl;
|
||||||
|
@ -105,6 +106,14 @@ public class ClientRepresentation {
|
||||||
this.description = description;
|
this.description = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setType(String type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
public String getClientId() {
|
public String getClientId() {
|
||||||
return clientId;
|
return clientId;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
|
|
||||||
package org.keycloak.representations.idm;
|
package org.keycloak.representations.idm;
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
@ -61,17 +60,6 @@ public class ClientTypeRepresentation {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonProperty("referenced-properties")
|
|
||||||
protected Map<String, Object> referencedProperties = new HashMap<>();
|
|
||||||
|
|
||||||
public Map<String, Object> getReferencedProperties() {
|
|
||||||
return referencedProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setReferencedProperties(Map<String, Object> referencedProperties) {
|
|
||||||
this.referencedProperties = referencedProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class PropertyConfig {
|
public static class PropertyConfig {
|
||||||
|
|
||||||
@JsonProperty("applicable")
|
@JsonProperty("applicable")
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.admin.client.resource;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.PUT;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
|
||||||
|
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||||
|
import org.keycloak.representations.idm.ClientTypesRepresentation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public interface ClientTypesResource {
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@NoCache
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
ClientTypesRepresentation getClientTypes();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update client types in the realm. The "global-client-types" field of client types is ignored as it is not possible to update global types
|
||||||
|
*
|
||||||
|
* @param clientTypes
|
||||||
|
*/
|
||||||
|
@PUT
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
void updateClientTypes(final ClientTypesRepresentation clientTypes);
|
||||||
|
}
|
|
@ -292,4 +292,7 @@ public interface RealmResource {
|
||||||
|
|
||||||
@Path("organizations")
|
@Path("organizations")
|
||||||
OrganizationsResource organizations();
|
OrganizationsResource organizations();
|
||||||
|
|
||||||
|
@Path("client-types")
|
||||||
|
ClientTypesResource clientTypes();
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,9 @@
|
||||||
package org.keycloak.models.cache.infinispan;
|
package org.keycloak.models.cache.infinispan;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.client.clienttype.ClientTypeManager;
|
||||||
import org.keycloak.cluster.ClusterProvider;
|
import org.keycloak.cluster.ClusterProvider;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
import org.keycloak.component.ComponentModel;
|
import org.keycloak.component.ComponentModel;
|
||||||
import org.keycloak.models.*;
|
import org.keycloak.models.*;
|
||||||
import org.keycloak.models.cache.CacheRealmProvider;
|
import org.keycloak.models.cache.CacheRealmProvider;
|
||||||
|
@ -1195,7 +1197,7 @@ public class RealmCacheSession implements CacheRealmProvider {
|
||||||
if (invalidations.contains(delegate.getId())) return delegate;
|
if (invalidations.contains(delegate.getId())) return delegate;
|
||||||
StorageId storageId = new StorageId(delegate.getId());
|
StorageId storageId = new StorageId(delegate.getId());
|
||||||
CachedClient cached = null;
|
CachedClient cached = null;
|
||||||
ClientAdapter adapter = null;
|
ClientModel adapter = null;
|
||||||
|
|
||||||
if (!storageId.isLocal()) {
|
if (!storageId.isLocal()) {
|
||||||
ComponentModel component = realm.getComponent(storageId.getProviderId());
|
ComponentModel component = realm.getComponent(storageId.getProviderId());
|
||||||
|
@ -1209,7 +1211,7 @@ public class RealmCacheSession implements CacheRealmProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
cached = new CachedClient(revision, realm, delegate);
|
cached = new CachedClient(revision, realm, delegate);
|
||||||
adapter = new ClientAdapter(realm, cached, this);
|
adapter = toClientModel(realm, cached);
|
||||||
|
|
||||||
long lifespan = model.getLifespan();
|
long lifespan = model.getLifespan();
|
||||||
if (lifespan > 0) {
|
if (lifespan > 0) {
|
||||||
|
@ -1219,7 +1221,7 @@ public class RealmCacheSession implements CacheRealmProvider {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
cached = new CachedClient(revision, realm, delegate);
|
cached = new CachedClient(revision, realm, delegate);
|
||||||
adapter = new ClientAdapter(realm, cached, this);
|
adapter = toClientModel(realm, cached);
|
||||||
cache.addRevisioned(cached, startupRevision);
|
cache.addRevisioned(cached, startupRevision);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1247,9 +1249,17 @@ public class RealmCacheSession implements CacheRealmProvider {
|
||||||
return getClientDelegate().getClientById(realm, cached.getId());
|
return getClientDelegate().getClientById(realm, cached.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ClientAdapter adapter = new ClientAdapter(realm, cached, this);
|
return toClientModel(realm, cached);
|
||||||
|
}
|
||||||
|
|
||||||
return adapter;
|
private ClientModel toClientModel(RealmModel realm, CachedClient cached) {
|
||||||
|
ClientAdapter client = new ClientAdapter(realm, cached, this);
|
||||||
|
|
||||||
|
if (Profile.isFeatureEnabled(Profile.Feature.CLIENT_TYPES)) {
|
||||||
|
ClientTypeManager mgr = session.getProvider(ClientTypeManager.class);
|
||||||
|
return mgr.augmentClient(client);
|
||||||
|
}
|
||||||
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -43,6 +43,8 @@ import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import org.hibernate.Session;
|
import org.hibernate.Session;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.client.clienttype.ClientTypeManager;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.connections.jpa.util.JpaUtils;
|
import org.keycloak.connections.jpa.util.JpaUtils;
|
||||||
import org.keycloak.migration.MigrationModel;
|
import org.keycloak.migration.MigrationModel;
|
||||||
|
@ -293,10 +295,11 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc
|
||||||
.filter(s -> s.get("client") != null))
|
.filter(s -> s.get("client") != null))
|
||||||
.collect(
|
.collect(
|
||||||
Collectors.groupingBy(
|
Collectors.groupingBy(
|
||||||
s -> new ClientAdapter(realm, em, session, (ClientEntity) s.get("client")),
|
s -> toClientModel(realm, (ClientEntity) s.get("client")),
|
||||||
Collectors.mapping(s -> (String) s.get("redirectUri"), Collectors.toSet())
|
Collectors.mapping(s -> (String) s.get("redirectUri"), Collectors.toSet())
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -755,6 +758,8 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ClientModel addClient(RealmModel realm, String id, String clientId) {
|
public ClientModel addClient(RealmModel realm, String id, String clientId) {
|
||||||
|
ClientModel resource;
|
||||||
|
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
id = KeycloakModelUtils.generateId();
|
id = KeycloakModelUtils.generateId();
|
||||||
}
|
}
|
||||||
|
@ -773,7 +778,7 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc
|
||||||
entity.setRealmId(realm.getId());
|
entity.setRealmId(realm.getId());
|
||||||
em.persist(entity);
|
em.persist(entity);
|
||||||
|
|
||||||
final ClientModel resource = new ClientAdapter(realm, em, session, entity);
|
resource = toClientModel(realm, entity);
|
||||||
|
|
||||||
session.getKeycloakSessionFactory().publish((ClientModel.ClientCreationEvent) () -> resource);
|
session.getKeycloakSessionFactory().publish((ClientModel.ClientCreationEvent) () -> resource);
|
||||||
return resource;
|
return resource;
|
||||||
|
@ -810,9 +815,18 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc
|
||||||
ClientEntity client = em.find(ClientEntity.class, id);
|
ClientEntity client = em.find(ClientEntity.class, id);
|
||||||
// Check if client belongs to this realm
|
// Check if client belongs to this realm
|
||||||
if (client == null || !realm.getId().equals(client.getRealmId())) return null;
|
if (client == null || !realm.getId().equals(client.getRealmId())) return null;
|
||||||
ClientAdapter adapter = new ClientAdapter(realm, em, session, client);
|
return toClientModel(realm, client);
|
||||||
return adapter;
|
}
|
||||||
|
|
||||||
|
private ClientModel toClientModel(RealmModel realm, ClientEntity client) {
|
||||||
|
ClientAdapter adapter = new ClientAdapter(realm, em, session, client);
|
||||||
|
|
||||||
|
if (Profile.isFeatureEnabled(Profile.Feature.CLIENT_TYPES)) {
|
||||||
|
ClientTypeManager mgr = session.getProvider(ClientTypeManager.class);
|
||||||
|
return mgr.augmentClient(adapter);
|
||||||
|
} else {
|
||||||
|
return adapter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -34,7 +34,7 @@ public class ClientTypeSpi implements Spi {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return "client-type";
|
return "clientType";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -119,6 +119,11 @@ public class ModelToRepresentation {
|
||||||
REALM_EXCLUDED_ATTRIBUTES.add("firstBrokerLoginFlowId");
|
REALM_EXCLUDED_ATTRIBUTES.add("firstBrokerLoginFlowId");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Set<String> CLIENT_EXCLUDED_ATTRIBUTES = new HashSet<>();
|
||||||
|
static {
|
||||||
|
CLIENT_EXCLUDED_ATTRIBUTES.add(ClientModel.TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getLogger(ModelToRepresentation.class);
|
private static final Logger LOG = Logger.getLogger(ModelToRepresentation.class);
|
||||||
|
|
||||||
public static String buildGroupPath(GroupModel group) {
|
public static String buildGroupPath(GroupModel group) {
|
||||||
|
@ -536,6 +541,20 @@ public class ModelToRepresentation {
|
||||||
|
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Map<String, String> stripClientAttributesIncludedAsFields(Map<String, String> attributes) {
|
||||||
|
Map<String, String> a = new HashMap<>();
|
||||||
|
|
||||||
|
for (Map.Entry<String, String> e : attributes.entrySet()) {
|
||||||
|
if (CLIENT_EXCLUDED_ATTRIBUTES.contains(e.getKey())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
a.put(e.getKey(), e.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
public static void exportGroups(KeycloakSession session, RealmModel realm, RealmRepresentation rep) {
|
public static void exportGroups(KeycloakSession session, RealmModel realm, RealmRepresentation rep) {
|
||||||
rep.setGroups(toGroupHierarchy(session, realm, true).collect(Collectors.toList()));
|
rep.setGroups(toGroupHierarchy(session, realm, true).collect(Collectors.toList()));
|
||||||
}
|
}
|
||||||
|
@ -686,13 +705,14 @@ public class ModelToRepresentation {
|
||||||
rep.setClientId(clientModel.getClientId());
|
rep.setClientId(clientModel.getClientId());
|
||||||
rep.setName(clientModel.getName());
|
rep.setName(clientModel.getName());
|
||||||
rep.setDescription(clientModel.getDescription());
|
rep.setDescription(clientModel.getDescription());
|
||||||
|
rep.setType(clientModel.getType());
|
||||||
rep.setEnabled(clientModel.isEnabled());
|
rep.setEnabled(clientModel.isEnabled());
|
||||||
rep.setAlwaysDisplayInConsole(clientModel.isAlwaysDisplayInConsole());
|
rep.setAlwaysDisplayInConsole(clientModel.isAlwaysDisplayInConsole());
|
||||||
rep.setAdminUrl(clientModel.getManagementUrl());
|
rep.setAdminUrl(clientModel.getManagementUrl());
|
||||||
rep.setPublicClient(clientModel.isPublicClient());
|
rep.setPublicClient(clientModel.isPublicClient());
|
||||||
rep.setFrontchannelLogout(clientModel.isFrontchannelLogout());
|
rep.setFrontchannelLogout(clientModel.isFrontchannelLogout());
|
||||||
rep.setProtocol(clientModel.getProtocol());
|
rep.setProtocol(clientModel.getProtocol());
|
||||||
rep.setAttributes(clientModel.getAttributes());
|
rep.setAttributes(stripClientAttributesIncludedAsFields(clientModel.getAttributes()));
|
||||||
rep.setAuthenticationFlowBindingOverrides(clientModel.getAuthenticationFlowBindingOverrides());
|
rep.setAuthenticationFlowBindingOverrides(clientModel.getAuthenticationFlowBindingOverrides());
|
||||||
rep.setFullScopeAllowed(clientModel.isFullScopeAllowed());
|
rep.setFullScopeAllowed(clientModel.isFullScopeAllowed());
|
||||||
rep.setBearerOnly(clientModel.isBearerOnly());
|
rep.setBearerOnly(clientModel.isBearerOnly());
|
||||||
|
|
|
@ -319,6 +319,7 @@ public class RepresentationToModel {
|
||||||
ClientModel client = resourceRep.getId() != null ? realm.addClient(resourceRep.getId(), resourceRep.getClientId()) : realm.addClient(resourceRep.getClientId());
|
ClientModel client = resourceRep.getId() != null ? realm.addClient(resourceRep.getId(), resourceRep.getClientId()) : realm.addClient(resourceRep.getClientId());
|
||||||
if (resourceRep.getName() != null) client.setName(resourceRep.getName());
|
if (resourceRep.getName() != null) client.setName(resourceRep.getName());
|
||||||
if (resourceRep.getDescription() != null) client.setDescription(resourceRep.getDescription());
|
if (resourceRep.getDescription() != null) client.setDescription(resourceRep.getDescription());
|
||||||
|
if (resourceRep.getType() != null) client.setType(resourceRep.getType());
|
||||||
if (resourceRep.isEnabled() != null) client.setEnabled(resourceRep.isEnabled());
|
if (resourceRep.isEnabled() != null) client.setEnabled(resourceRep.isEnabled());
|
||||||
if (resourceRep.isAlwaysDisplayInConsole() != null) client.setAlwaysDisplayInConsole(resourceRep.isAlwaysDisplayInConsole());
|
if (resourceRep.isAlwaysDisplayInConsole() != null) client.setAlwaysDisplayInConsole(resourceRep.isAlwaysDisplayInConsole());
|
||||||
client.setManagementUrl(resourceRep.getAdminUrl());
|
client.setManagementUrl(resourceRep.getAdminUrl());
|
||||||
|
@ -493,6 +494,7 @@ public class RepresentationToModel {
|
||||||
if (newClientId != null) resource.setClientId(newClientId);
|
if (newClientId != null) resource.setClientId(newClientId);
|
||||||
if (rep.getName() != null) resource.setName(rep.getName());
|
if (rep.getName() != null) resource.setName(rep.getName());
|
||||||
if (rep.getDescription() != null) resource.setDescription(rep.getDescription());
|
if (rep.getDescription() != null) resource.setDescription(rep.getDescription());
|
||||||
|
if (rep.getType() != null) resource.setType(rep.getType());
|
||||||
if (rep.isEnabled() != null) resource.setEnabled(rep.isEnabled());
|
if (rep.isEnabled() != null) resource.setEnabled(rep.isEnabled());
|
||||||
if (rep.isAlwaysDisplayInConsole() != null) resource.setAlwaysDisplayInConsole(rep.isAlwaysDisplayInConsole());
|
if (rep.isAlwaysDisplayInConsole() != null) resource.setAlwaysDisplayInConsole(rep.isAlwaysDisplayInConsole());
|
||||||
if (rep.isBearerOnly() != null) resource.setBearerOnly(rep.isBearerOnly());
|
if (rep.isBearerOnly() != null) resource.setBearerOnly(rep.isBearerOnly());
|
||||||
|
|
|
@ -39,6 +39,7 @@ public interface ClientModel extends ClientScopeModel, RoleContainerModel, Prot
|
||||||
String LOGO_URI ="logoUri";
|
String LOGO_URI ="logoUri";
|
||||||
String POLICY_URI ="policyUri";
|
String POLICY_URI ="policyUri";
|
||||||
String TOS_URI ="tosUri";
|
String TOS_URI ="tosUri";
|
||||||
|
String TYPE = "type";
|
||||||
|
|
||||||
interface ClientCreationEvent extends ProviderEvent {
|
interface ClientCreationEvent extends ProviderEvent {
|
||||||
ClientModel getCreatedClient();
|
ClientModel getCreatedClient();
|
||||||
|
@ -105,6 +106,14 @@ public interface ClientModel extends ClientScopeModel, RoleContainerModel, Prot
|
||||||
|
|
||||||
void setDescription(String description);
|
void setDescription(String description);
|
||||||
|
|
||||||
|
default String getType() {
|
||||||
|
return getAttribute(TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
default void setType(String type) {
|
||||||
|
setAttribute(TYPE, type);
|
||||||
|
}
|
||||||
|
|
||||||
boolean isEnabled();
|
boolean isEnabled();
|
||||||
|
|
||||||
void setEnabled(boolean enabled);
|
void setEnabled(boolean enabled);
|
||||||
|
|
|
@ -34,6 +34,7 @@ import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.representations.idm.ClientTypeRepresentation;
|
import org.keycloak.representations.idm.ClientTypeRepresentation;
|
||||||
import org.keycloak.representations.idm.ClientTypesRepresentation;
|
import org.keycloak.representations.idm.ClientTypesRepresentation;
|
||||||
|
import org.keycloak.services.clienttype.client.TypeAwareClientModelDelegate;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -66,6 +67,7 @@ public class DefaultClientTypeManager implements ClientTypeManager {
|
||||||
try {
|
try {
|
||||||
// Skip validation here for performance reasons
|
// Skip validation here for performance reasons
|
||||||
result = JsonSerialization.readValue(asStr, ClientTypesRepresentation.class);
|
result = JsonSerialization.readValue(asStr, ClientTypesRepresentation.class);
|
||||||
|
result.setGlobalClientTypes(globalClientTypes);
|
||||||
} catch (IOException ioe) {
|
} catch (IOException ioe) {
|
||||||
throw new ClientTypeException("Failed to deserialize client types from JSON string", ioe);
|
throw new ClientTypeException("Failed to deserialize client types from JSON string", ioe);
|
||||||
}
|
}
|
||||||
|
@ -104,14 +106,12 @@ public class DefaultClientTypeManager implements ClientTypeManager {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ClientModel augmentClient(ClientModel client) throws ClientTypeException {
|
public ClientModel augmentClient(ClientModel client) throws ClientTypeException {
|
||||||
//TODO:vibrown put the logic back in next Client Type PR
|
if (client.getType() == null) {
|
||||||
return client;
|
|
||||||
/*if (client.getType() == null) {
|
|
||||||
return client;
|
return client;
|
||||||
} else {
|
} else {
|
||||||
ClientType clientType = getClientType(client.getRealm(), client.getType());
|
ClientType clientType = getClientType(client.getRealm(), client.getType());
|
||||||
return new TypeAwareClientModelDelegate(clientType, () -> client);
|
return new TypeAwareClientModelDelegate(clientType, () -> client);
|
||||||
}*/
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<ClientTypeRepresentation> validateAndCastConfiguration(KeycloakSession session, List<ClientTypeRepresentation> clientTypes, List<ClientTypeRepresentation> globalTypes) {
|
static List<ClientTypeRepresentation> validateAndCastConfiguration(KeycloakSession session, List<ClientTypeRepresentation> clientTypes, List<ClientTypeRepresentation> globalTypes) {
|
||||||
|
@ -129,8 +129,8 @@ public class DefaultClientTypeManager implements ClientTypeManager {
|
||||||
private static ClientTypeRepresentation validateAndCastConfiguration(KeycloakSession session, ClientTypeRepresentation clientType, Set<String> currentNames) {
|
private static ClientTypeRepresentation validateAndCastConfiguration(KeycloakSession session, ClientTypeRepresentation clientType, Set<String> currentNames) {
|
||||||
ClientTypeProvider clientTypeProvider = session.getProvider(ClientTypeProvider.class, clientType.getProvider());
|
ClientTypeProvider clientTypeProvider = session.getProvider(ClientTypeProvider.class, clientType.getProvider());
|
||||||
if (clientTypeProvider == null) {
|
if (clientTypeProvider == null) {
|
||||||
logger.errorf("Did not found client type provider '%s' for the client type '%s'", clientType.getProvider(), clientType.getName());
|
logger.errorf("Did not find client type provider '%s' for the client type '%s'", clientType.getProvider(), clientType.getName());
|
||||||
throw new ClientTypeException("Did not found client type provider");
|
throw new ClientTypeException("Did not find client type provider");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate name is not duplicated
|
// Validate name is not duplicated
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 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.services.clienttype.client;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import org.keycloak.common.util.ObjectUtil;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.delegate.ClientModelLazyDelegate;
|
||||||
|
import org.keycloak.client.clienttype.ClientType;
|
||||||
|
import org.keycloak.client.clienttype.ClientTypeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegates to client-type and underlying delegate
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate {
|
||||||
|
|
||||||
|
private final ClientType clientType;
|
||||||
|
|
||||||
|
public TypeAwareClientModelDelegate(ClientType clientType, Supplier<ClientModel> clientModelSupplier) {
|
||||||
|
super(clientModelSupplier);
|
||||||
|
|
||||||
|
if (clientType == null) {
|
||||||
|
throw new IllegalArgumentException("Null client type not supported for client " + getClientId());
|
||||||
|
}
|
||||||
|
this.clientType = clientType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isStandardFlowEnabled() {
|
||||||
|
return getBooleanProperty("standardFlowEnabled", super::isStandardFlowEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setStandardFlowEnabled(boolean standardFlowEnabled) {
|
||||||
|
setBooleanProperty("standardFlowEnabled", standardFlowEnabled, super::setStandardFlowEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected boolean getBooleanProperty(String propertyName, Supplier<Boolean> clientGetter) {
|
||||||
|
// Check if clientType supports the feature. If not, simply return false
|
||||||
|
if (!clientType.isApplicable(propertyName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is read-only. If yes, then we just directly delegate to return stuff from the clientType rather than from client
|
||||||
|
if (clientType.isReadOnly(propertyName)) {
|
||||||
|
return clientType.getDefaultValue(propertyName, Boolean.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delegate to clientGetter
|
||||||
|
return clientGetter.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setBooleanProperty(String propertyName, Boolean newValue, Consumer<Boolean> clientSetter) {
|
||||||
|
// Check if clientType supports the feature. If not, return directly
|
||||||
|
if (!clientType.isApplicable(propertyName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is read-only. If yes and there is an attempt to change some stuff, then throw an exception
|
||||||
|
if (clientType.isReadOnly(propertyName)) {
|
||||||
|
Boolean oldVal = clientType.getDefaultValue(propertyName, Boolean.class);
|
||||||
|
if (!ObjectUtil.isEqualOrBothNull(oldVal, newValue)) {
|
||||||
|
throw new ClientTypeException("Property " + propertyName + " of client " + getClientId() + " is read-only due to client type " + clientType.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call clientSetter
|
||||||
|
clientSetter.accept(newValue);
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,11 +20,9 @@ package org.keycloak.services.clienttype.impl;
|
||||||
|
|
||||||
import java.beans.PropertyDescriptor;
|
import java.beans.PropertyDescriptor;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.Type;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JavaType;
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.common.util.ObjectUtil;
|
import org.keycloak.common.util.ObjectUtil;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
|
@ -34,7 +32,6 @@ import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
import org.keycloak.representations.idm.ClientTypeRepresentation;
|
import org.keycloak.representations.idm.ClientTypeRepresentation;
|
||||||
import org.keycloak.client.clienttype.ClientType;
|
import org.keycloak.client.clienttype.ClientType;
|
||||||
import org.keycloak.client.clienttype.ClientTypeException;
|
import org.keycloak.client.clienttype.ClientTypeException;
|
||||||
import org.keycloak.util.JsonSerialization;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
@ -43,9 +40,6 @@ public class DefaultClientType implements ClientType {
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(DefaultClientType.class);
|
private static final Logger logger = Logger.getLogger(DefaultClientType.class);
|
||||||
|
|
||||||
// Will be used as reference in JSON. Probably just temporary solution
|
|
||||||
private static final String REFERENCE_PREFIX = "ref::";
|
|
||||||
|
|
||||||
private final KeycloakSession session;
|
private final KeycloakSession session;
|
||||||
private final ClientTypeRepresentation clientType;
|
private final ClientTypeRepresentation clientType;
|
||||||
|
|
||||||
|
@ -93,40 +87,9 @@ public class DefaultClientType implements ClientType {
|
||||||
if (propertyConfig.getDefaultValue() != null) {
|
if (propertyConfig.getDefaultValue() != null) {
|
||||||
if (clientRepresentationProperties.containsKey(property.getKey())) {
|
if (clientRepresentationProperties.containsKey(property.getKey())) {
|
||||||
// Java property on client representation
|
// Java property on client representation
|
||||||
|
Method setter = clientRepresentationProperties.get(property.getKey()).getWriteMethod();
|
||||||
try {
|
try {
|
||||||
PropertyDescriptor propertyDescriptor = clientRepresentationProperties.get(property.getKey());
|
setter.invoke(createdClient, propertyConfig.getDefaultValue());
|
||||||
Method setter = propertyDescriptor.getWriteMethod();
|
|
||||||
Object defaultVal = propertyConfig.getDefaultValue();
|
|
||||||
if (defaultVal instanceof String && defaultVal.toString().startsWith(REFERENCE_PREFIX)) {
|
|
||||||
// TODO:client-types re-verify or remove support for "ref::" entirely from the codebase
|
|
||||||
throw new UnsupportedOperationException("Not supported to use ref:: references");
|
|
||||||
// Reference. We need to found referred value and call the setter with it
|
|
||||||
// String referredPropertyName = defaultVal.toString().substring(REFERENCE_PREFIX.length());
|
|
||||||
// Object referredPropertyVal = clientType.getReferencedProperties().get(referredPropertyName);
|
|
||||||
// if (referredPropertyVal == null) {
|
|
||||||
// logger.warnf("Reference '%s' not found used in property '%s' of client type '%s'", defaultVal.toString(), property.getKey(), clientType.getName());
|
|
||||||
// throw new ClientTypeException("Cannot set property on client");
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Generic collections
|
|
||||||
// Type genericType = setter.getGenericParameterTypes()[0];
|
|
||||||
// JavaType jacksonType = JsonSerialization.mapper.constructType(genericType);
|
|
||||||
// Object converted = JsonSerialization.mapper.convertValue(referredPropertyVal, jacksonType);
|
|
||||||
//
|
|
||||||
// setter.invoke(createdClient, converted);
|
|
||||||
} else {
|
|
||||||
Type genericType = setter.getGenericParameterTypes()[0];
|
|
||||||
|
|
||||||
Object converted;
|
|
||||||
if (!defaultVal.getClass().equals(genericType)) {
|
|
||||||
JavaType jacksonType = JsonSerialization.mapper.constructType(genericType);
|
|
||||||
converted = JsonSerialization.mapper.convertValue(defaultVal, jacksonType);
|
|
||||||
} else {
|
|
||||||
converted = defaultVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
setter.invoke(createdClient, converted);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.warnf("Cannot set property '%s' on client with value '%s'. Check configuration of the client type '%s'", property.getKey(), propertyConfig.getDefaultValue(), clientType.getName());
|
logger.warnf("Cannot set property '%s' on client with value '%s'. Check configuration of the client type '%s'", property.getKey(), propertyConfig.getDefaultValue(), clientType.getName());
|
||||||
throw new ClientTypeException("Cannot set property on client", e);
|
throw new ClientTypeException("Cannot set property on client", e);
|
||||||
|
|
|
@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.authentication.ClientAuthenticator;
|
import org.keycloak.authentication.ClientAuthenticator;
|
||||||
import org.keycloak.authentication.ClientAuthenticatorFactory;
|
import org.keycloak.authentication.ClientAuthenticatorFactory;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
import org.keycloak.common.constants.ServiceAccountConstants;
|
import org.keycloak.common.constants.ServiceAccountConstants;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
|
@ -41,6 +42,8 @@ import org.keycloak.protocol.saml.SamlProtocol;
|
||||||
import org.keycloak.representations.adapters.config.BaseRealmConfig;
|
import org.keycloak.representations.adapters.config.BaseRealmConfig;
|
||||||
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.client.clienttype.ClientType;
|
||||||
|
import org.keycloak.client.clienttype.ClientTypeManager;
|
||||||
import org.keycloak.sessions.AuthenticationSessionProvider;
|
import org.keycloak.sessions.AuthenticationSessionProvider;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
@ -79,6 +82,12 @@ public class ClientManager {
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public static ClientModel createClient(KeycloakSession session, RealmModel realm, ClientRepresentation rep) {
|
public static ClientModel createClient(KeycloakSession session, RealmModel realm, ClientRepresentation rep) {
|
||||||
|
if (Profile.isFeatureEnabled(Profile.Feature.CLIENT_TYPES) && rep.getType() != null) {
|
||||||
|
ClientTypeManager mgr = session.getProvider(ClientTypeManager.class);
|
||||||
|
ClientType clientType = mgr.getClientType(realm, rep.getType());
|
||||||
|
clientType.onCreate(rep);
|
||||||
|
}
|
||||||
|
|
||||||
ClientModel client = RepresentationToModel.createClient(session, realm, rep);
|
ClientModel client = RepresentationToModel.createClient(session, realm, rep);
|
||||||
|
|
||||||
if (rep.getProtocol() != null) {
|
if (rep.getProtocol() != null) {
|
||||||
|
@ -164,7 +173,13 @@ public class ClientManager {
|
||||||
user.setServiceAccountClientLink(client.getId());
|
user.setServiceAccountClientLink(client.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add protocol mappers to retrieve clientId in access token
|
// Add protocol mappers to retrieve clientId in access token. Ignore this in case type is filled (protocol mappers can be explicitly specified for particular specific type)
|
||||||
|
if (!Profile.isFeatureEnabled(Profile.Feature.CLIENT_TYPES) || client.getType() == null) {
|
||||||
|
addServiceAccountProtocolMappers(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addServiceAccountProtocolMappers(ClientModel client) {
|
||||||
if (client.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER) == null) {
|
if (client.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER) == null) {
|
||||||
logger.debugf("Creating service account protocol mapper '%s' for client '%s'", ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER, client.getClientId());
|
logger.debugf("Creating service account protocol mapper '%s' for client '%s'", ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER, client.getClientId());
|
||||||
ProtocolMapperModel protocolMapper = UserSessionNoteMapper.createClaimMapper(ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER,
|
ProtocolMapperModel protocolMapper = UserSessionNoteMapper.createClaimMapper(ServiceAccountConstants.CLIENT_ID_PROTOCOL_MAPPER,
|
||||||
|
|
|
@ -26,8 +26,12 @@ import org.jboss.logging.Logger;
|
||||||
import org.jboss.resteasy.reactive.NoCache;
|
import org.jboss.resteasy.reactive.NoCache;
|
||||||
import org.keycloak.OAuthErrorException;
|
import org.keycloak.OAuthErrorException;
|
||||||
import org.keycloak.authorization.admin.AuthorizationService;
|
import org.keycloak.authorization.admin.AuthorizationService;
|
||||||
|
import org.keycloak.client.clienttype.ClientType;
|
||||||
|
import org.keycloak.client.clienttype.ClientTypeException;
|
||||||
|
import org.keycloak.client.clienttype.ClientTypeManager;
|
||||||
import org.keycloak.common.ClientConnection;
|
import org.keycloak.common.ClientConnection;
|
||||||
import org.keycloak.common.Profile;
|
import org.keycloak.common.Profile;
|
||||||
|
import org.keycloak.common.util.ObjectUtil;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.events.admin.OperationType;
|
import org.keycloak.events.admin.OperationType;
|
||||||
|
@ -148,6 +152,17 @@ public class ClientResource {
|
||||||
session.setAttribute(ClientSecretConstants.CLIENT_SECRET_ROTATION_ENABLED,Boolean.FALSE);
|
session.setAttribute(ClientSecretConstants.CLIENT_SECRET_ROTATION_ENABLED,Boolean.FALSE);
|
||||||
session.clientPolicy().triggerOnEvent(new AdminClientUpdateContext(rep, client, auth.adminAuth()));
|
session.clientPolicy().triggerOnEvent(new AdminClientUpdateContext(rep, client, auth.adminAuth()));
|
||||||
|
|
||||||
|
if (Profile.isFeatureEnabled(Profile.Feature.CLIENT_TYPES)) {
|
||||||
|
if (!ObjectUtil.isEqualOrBothNull(rep.getType(), client.getType())) {
|
||||||
|
throw new ClientTypeException("Not supported to change client type");
|
||||||
|
}
|
||||||
|
if (rep.getType() != null) {
|
||||||
|
ClientTypeManager mgr = session.getProvider(ClientTypeManager.class);
|
||||||
|
ClientType clientType = mgr.getClientType(realm, rep.getType());
|
||||||
|
clientType.onUpdate(client, rep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateClientFromRep(rep, client, session);
|
updateClientFromRep(rep, client, session);
|
||||||
|
|
||||||
ValidationUtil.validateClient(session, client, false, r -> {
|
ValidationUtil.validateClient(session, client, false, r -> {
|
||||||
|
@ -170,6 +185,8 @@ public class ClientResource {
|
||||||
return Response.noContent().build();
|
return Response.noContent().build();
|
||||||
} catch (ModelDuplicateException e) {
|
} catch (ModelDuplicateException e) {
|
||||||
throw ErrorResponse.exists("Client already exists");
|
throw ErrorResponse.exists("Client already exists");
|
||||||
|
} catch (ClientTypeException cte) {
|
||||||
|
throw ErrorResponse.error(cte.getMessage(), Response.Status.BAD_REQUEST);
|
||||||
} catch (ClientPolicyException cpe) {
|
} catch (ClientPolicyException cpe) {
|
||||||
throw new ErrorResponseException(cpe.getError(), cpe.getErrorDetail(), Response.Status.BAD_REQUEST);
|
throw new ErrorResponseException(cpe.getError(), cpe.getErrorDetail(), Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 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.services.resources.admin;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.BadRequestException;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.PUT;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.extensions.Extension;
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.jboss.resteasy.reactive.NoCache;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.representations.idm.ClientTypesRepresentation;
|
||||||
|
import org.keycloak.services.ErrorResponse;
|
||||||
|
import org.keycloak.client.clienttype.ClientTypeException;
|
||||||
|
import org.keycloak.client.clienttype.ClientTypeManager;
|
||||||
|
import org.keycloak.services.resources.KeycloakOpenAPI;
|
||||||
|
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "")
|
||||||
|
public class ClientTypesResource {
|
||||||
|
protected static final Logger logger = Logger.getLogger(ClientTypesResource.class);
|
||||||
|
|
||||||
|
protected final ClientTypeManager manager;
|
||||||
|
protected final RealmModel realm;
|
||||||
|
|
||||||
|
private final AdminPermissionEvaluator auth;
|
||||||
|
|
||||||
|
public ClientTypesResource(ClientTypeManager manager, RealmModel realm, AdminPermissionEvaluator auth) {
|
||||||
|
this.manager = manager;
|
||||||
|
this.auth = auth;
|
||||||
|
this.realm = realm;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@NoCache
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
|
||||||
|
@Operation(summary = "List all client types available in the current realm",
|
||||||
|
description = "This endpoint returns a list of both global and realm level client types and the attributes they set"
|
||||||
|
)
|
||||||
|
public ClientTypesRepresentation getClientTypes() {
|
||||||
|
auth.realm().requireViewRealm();
|
||||||
|
|
||||||
|
try {
|
||||||
|
return manager.getClientTypes(realm);
|
||||||
|
} catch (ClientTypeException e) {
|
||||||
|
logger.error(e.getMessage(), e);
|
||||||
|
throw new BadRequestException(ErrorResponse.error(e.getMessage(), Response.Status.BAD_REQUEST));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
|
||||||
|
@Operation(summary = "Update a client type",
|
||||||
|
description = "This endpoint allows you to update a realm level client type"
|
||||||
|
)
|
||||||
|
@APIResponse(responseCode = "204", description = "No Content")
|
||||||
|
public Response updateClientTypes(final ClientTypesRepresentation clientTypes) {
|
||||||
|
auth.realm().requireManageRealm();
|
||||||
|
|
||||||
|
try {
|
||||||
|
manager.updateClientTypes(realm, clientTypes);
|
||||||
|
} catch (ClientTypeException e) {
|
||||||
|
logger.error(e.getMessage(), e);
|
||||||
|
throw ErrorResponse.error(e.getMessage(), Response.Status.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
return Response.noContent().build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -63,6 +63,7 @@ import org.keycloak.Config;
|
||||||
import org.keycloak.KeyPairVerifier;
|
import org.keycloak.KeyPairVerifier;
|
||||||
import org.keycloak.authentication.CredentialRegistrator;
|
import org.keycloak.authentication.CredentialRegistrator;
|
||||||
import org.keycloak.authentication.RequiredActionProvider;
|
import org.keycloak.authentication.RequiredActionProvider;
|
||||||
|
import org.keycloak.client.clienttype.ClientTypeManager;
|
||||||
import org.keycloak.common.ClientConnection;
|
import org.keycloak.common.ClientConnection;
|
||||||
import org.keycloak.common.Profile;
|
import org.keycloak.common.Profile;
|
||||||
import org.keycloak.common.VerificationException;
|
import org.keycloak.common.VerificationException;
|
||||||
|
@ -1228,4 +1229,11 @@ public class RealmAdminResource {
|
||||||
ProfileHelper.requireFeature(Profile.Feature.CLIENT_POLICIES);
|
ProfileHelper.requireFeature(Profile.Feature.CLIENT_POLICIES);
|
||||||
return new ClientProfilesResource(session, auth);
|
return new ClientProfilesResource(session, auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Path("client-types")
|
||||||
|
public ClientTypesResource getClientTypesResource() {
|
||||||
|
ProfileHelper.requireFeature(Profile.Feature.CLIENT_TYPES);
|
||||||
|
return new ClientTypesResource(session.getProvider(ClientTypeManager.class), realm, auth);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,53 +61,6 @@
|
||||||
"read-only": true,
|
"read-only": true,
|
||||||
"default-value": true
|
"default-value": true
|
||||||
},
|
},
|
||||||
"protocolMappers": {
|
|
||||||
"applicable": true,
|
|
||||||
"read-only": true,
|
|
||||||
"default-value": [
|
|
||||||
{
|
|
||||||
"name" : "Client IP Address",
|
|
||||||
"protocol" : "openid-connect",
|
|
||||||
"protocolMapper" : "oidc-usersessionmodel-note-mapper",
|
|
||||||
"consentRequired" : false,
|
|
||||||
"config" : {
|
|
||||||
"user.session.note" : "clientAddress",
|
|
||||||
"id.token.claim" : "true",
|
|
||||||
"access.token.claim" : "true",
|
|
||||||
"claim.name" : "clientAddress",
|
|
||||||
"jsonType.label" : "String"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name" : "Client Host",
|
|
||||||
"protocol" : "openid-connect",
|
|
||||||
"protocolMapper" : "oidc-usersessionmodel-note-mapper",
|
|
||||||
"consentRequired" : false,
|
|
||||||
"config" : {
|
|
||||||
"user.session.note" : "clientHost",
|
|
||||||
"id.token.claim" : "true",
|
|
||||||
"access.token.claim" : "true",
|
|
||||||
"claim.name" : "clientHost",
|
|
||||||
"jsonType.label" : "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"webOrigins": {
|
|
||||||
"applicable": true,
|
|
||||||
"read-only": true,
|
|
||||||
"default-value": [ "https://foo", "https://bar"]
|
|
||||||
},
|
|
||||||
"defaultClientScopes": {
|
|
||||||
"applicable": true,
|
|
||||||
"read-only": true,
|
|
||||||
"default-value": [ "address", "offline_access"]
|
|
||||||
},
|
|
||||||
"optionalClientScopes": {
|
|
||||||
"applicable": true,
|
|
||||||
"read-only": true,
|
|
||||||
"default-value": [ "profile" ]
|
|
||||||
},
|
|
||||||
"logoUri": {
|
"logoUri": {
|
||||||
"applicable": false
|
"applicable": false
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,289 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 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.testsuite.client;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.BadRequestException;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.common.util.ObjectUtil;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.idm.ClientTypeRepresentation;
|
||||||
|
|
||||||
|
import org.keycloak.representations.idm.ClientTypesRepresentation;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.client.clienttype.ClientTypeManager;
|
||||||
|
import org.keycloak.services.clienttype.impl.DefaultClientTypeProviderFactory;
|
||||||
|
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||||
|
import org.keycloak.testsuite.Assert;
|
||||||
|
import org.keycloak.testsuite.admin.ApiUtil;
|
||||||
|
import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
|
||||||
|
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||||
|
import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected;
|
||||||
|
import org.keycloak.testsuite.util.ClientBuilder;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
import static org.keycloak.common.Profile.Feature.CLIENT_TYPES;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
@EnableFeature(value = CLIENT_TYPES)
|
||||||
|
public class ClientTypesTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFeatureWorksWhenEnabled() {
|
||||||
|
checkIfFeatureWorks(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@UncaughtServerErrorExpected
|
||||||
|
@DisableFeature(value = CLIENT_TYPES, skipRestart = true)
|
||||||
|
public void testFeatureDoesntWorkWhenDisabled() {
|
||||||
|
checkIfFeatureWorks(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test create client with clientType filled. Check default properties are filled
|
||||||
|
@Test
|
||||||
|
public void testCreateClientWithClientType() {
|
||||||
|
ClientRepresentation clientRep = createClientWithType("foo", ClientTypeManager.SERVICE_ACCOUNT);
|
||||||
|
Assert.assertEquals("foo", clientRep.getClientId());
|
||||||
|
Assert.assertEquals(ClientTypeManager.SERVICE_ACCOUNT, clientRep.getType());
|
||||||
|
Assert.assertEquals(OIDCLoginProtocol.LOGIN_PROTOCOL, clientRep.getProtocol());
|
||||||
|
Assert.assertFalse(clientRep.isStandardFlowEnabled());
|
||||||
|
Assert.assertFalse(clientRep.isImplicitFlowEnabled());
|
||||||
|
Assert.assertFalse(clientRep.isDirectAccessGrantsEnabled());
|
||||||
|
Assert.assertTrue(clientRep.isServiceAccountsEnabled());
|
||||||
|
Assert.assertFalse(clientRep.isPublicClient());
|
||||||
|
Assert.assertFalse(clientRep.isBearerOnly());
|
||||||
|
|
||||||
|
// Check type not included as client attribute
|
||||||
|
Assert.assertFalse(clientRep.getAttributes().containsKey(ClientModel.TYPE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUpdateClientWithClientType() {
|
||||||
|
ClientRepresentation clientRep = createClientWithType("foo", ClientTypeManager.SERVICE_ACCOUNT);
|
||||||
|
|
||||||
|
// Changing type should fail
|
||||||
|
clientRep.setType(ClientTypeManager.STANDARD);
|
||||||
|
try {
|
||||||
|
testRealm().clients().get(clientRep.getId()).update(clientRep);
|
||||||
|
Assert.fail("Not expected to update client");
|
||||||
|
} catch (BadRequestException bre) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updating read-only attribute should fail
|
||||||
|
clientRep.setType(ClientTypeManager.SERVICE_ACCOUNT);
|
||||||
|
clientRep.setServiceAccountsEnabled(false);
|
||||||
|
try {
|
||||||
|
testRealm().clients().get(clientRep.getId()).update(clientRep);
|
||||||
|
Assert.fail("Not expected to update client");
|
||||||
|
} catch (BadRequestException bre) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adding non-applicable attribute should fail
|
||||||
|
clientRep.setServiceAccountsEnabled(true);
|
||||||
|
clientRep.getAttributes().put(ClientModel.LOGO_URI, "https://foo");
|
||||||
|
try {
|
||||||
|
testRealm().clients().get(clientRep.getId()).update(clientRep);
|
||||||
|
Assert.fail("Not expected to update client");
|
||||||
|
} catch (BadRequestException bre) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update of supported attribute should be successful
|
||||||
|
clientRep.getAttributes().remove(ClientModel.LOGO_URI);
|
||||||
|
clientRep.setRootUrl("https://foo");
|
||||||
|
testRealm().clients().get(clientRep.getId()).update(clientRep);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testClientTypesAdminRestAPI_globalTypes() {
|
||||||
|
ClientTypesRepresentation clientTypes = testRealm().clientTypes().getClientTypes();
|
||||||
|
|
||||||
|
Assert.assertEquals(0, clientTypes.getRealmClientTypes().size());
|
||||||
|
|
||||||
|
List<String> globalClientTypeNames = clientTypes.getGlobalClientTypes().stream()
|
||||||
|
.map(ClientTypeRepresentation::getName)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
Assert.assertNames(globalClientTypeNames, "sla", "service-account");
|
||||||
|
|
||||||
|
ClientTypeRepresentation serviceAccountType = clientTypes.getGlobalClientTypes().stream()
|
||||||
|
.filter(clientType -> "service-account".equals(clientType.getName()))
|
||||||
|
.findFirst()
|
||||||
|
.get();
|
||||||
|
Assert.assertEquals("default", serviceAccountType.getProvider());
|
||||||
|
|
||||||
|
ClientTypeRepresentation.PropertyConfig cfg = serviceAccountType.getConfig().get("standardFlowEnabled");
|
||||||
|
assertPropertyConfig("standardFlowEnabled", cfg, true, true, false);
|
||||||
|
|
||||||
|
cfg = serviceAccountType.getConfig().get("serviceAccountsEnabled");
|
||||||
|
assertPropertyConfig("serviceAccountsEnabled", cfg, true, true, true);
|
||||||
|
|
||||||
|
cfg = serviceAccountType.getConfig().get("tosUri");
|
||||||
|
assertPropertyConfig("tosUri", cfg, false, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testClientTypesAdminRestAPI_realmTypes() {
|
||||||
|
ClientTypesRepresentation clientTypes = testRealm().clientTypes().getClientTypes();
|
||||||
|
|
||||||
|
// Test invalid provider type should fail
|
||||||
|
ClientTypeRepresentation clientType = new ClientTypeRepresentation();
|
||||||
|
try {
|
||||||
|
clientType.setName("sla1");
|
||||||
|
clientType.setProvider("non-existent");
|
||||||
|
clientType.setConfig(new HashMap<String, ClientTypeRepresentation.PropertyConfig>());
|
||||||
|
clientTypes.setRealmClientTypes(Arrays.asList(clientType));
|
||||||
|
testRealm().clientTypes().updateClientTypes(clientTypes);
|
||||||
|
Assert.fail("Not expected to update client types");
|
||||||
|
} catch (BadRequestException bre) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test attribute without applicable should fail
|
||||||
|
try {
|
||||||
|
clientType.setProvider(DefaultClientTypeProviderFactory.PROVIDER_ID);
|
||||||
|
ClientTypeRepresentation.PropertyConfig cfg = new ClientTypeRepresentation.PropertyConfig();
|
||||||
|
clientType.getConfig().put("standardFlowEnabled", cfg);
|
||||||
|
testRealm().clientTypes().updateClientTypes(clientTypes);
|
||||||
|
Assert.fail("Not expected to update client types");
|
||||||
|
} catch (BadRequestException bre) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test non-applicable attribute with default-value should fail
|
||||||
|
try {
|
||||||
|
ClientTypeRepresentation.PropertyConfig cfg = clientType.getConfig().get("standardFlowEnabled");
|
||||||
|
cfg.setApplicable(false);
|
||||||
|
cfg.setReadOnly(true);
|
||||||
|
cfg.setDefaultValue(true);
|
||||||
|
testRealm().clientTypes().updateClientTypes(clientTypes);
|
||||||
|
Assert.fail("Not expected to update client types");
|
||||||
|
} catch (BadRequestException bre) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update should be successful
|
||||||
|
ClientTypeRepresentation.PropertyConfig cfg = clientType.getConfig().get("standardFlowEnabled");
|
||||||
|
cfg.setApplicable(true);
|
||||||
|
testRealm().clientTypes().updateClientTypes(clientTypes);
|
||||||
|
|
||||||
|
// Test duplicate name should fail
|
||||||
|
ClientTypeRepresentation clientType2 = new ClientTypeRepresentation();
|
||||||
|
try {
|
||||||
|
clientTypes = testRealm().clientTypes().getClientTypes();
|
||||||
|
clientType2 = new ClientTypeRepresentation();
|
||||||
|
clientType2.setName("sla1");
|
||||||
|
clientType2.setProvider(DefaultClientTypeProviderFactory.PROVIDER_ID);
|
||||||
|
clientType2.setConfig(new HashMap<>());
|
||||||
|
clientTypes.getRealmClientTypes().add(clientType2);
|
||||||
|
testRealm().clientTypes().updateClientTypes(clientTypes);
|
||||||
|
Assert.fail("Not expected to update client types");
|
||||||
|
} catch (BadRequestException bre) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also test duplicated global name should fail
|
||||||
|
try {
|
||||||
|
clientType2.setName("service-account");
|
||||||
|
testRealm().clientTypes().updateClientTypes(clientTypes);
|
||||||
|
Assert.fail("Not expected to update client types");
|
||||||
|
} catch (BadRequestException bre) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// Different name should be fine
|
||||||
|
clientType2.setName("different");
|
||||||
|
testRealm().clientTypes().updateClientTypes(clientTypes);
|
||||||
|
|
||||||
|
// Assert updated
|
||||||
|
clientTypes = testRealm().clientTypes().getClientTypes();
|
||||||
|
assertNames(clientTypes.getRealmClientTypes(), "sla1", "different");
|
||||||
|
assertNames(clientTypes.getGlobalClientTypes(), "sla", "service-account");
|
||||||
|
|
||||||
|
// Test updating global won't update anything. Nothing will be added to globalTypes
|
||||||
|
clientType2.setName("moreDifferent");
|
||||||
|
clientTypes.getGlobalClientTypes().add(clientType2);
|
||||||
|
testRealm().clientTypes().updateClientTypes(clientTypes);
|
||||||
|
|
||||||
|
clientTypes = testRealm().clientTypes().getClientTypes();
|
||||||
|
assertNames(clientTypes.getRealmClientTypes(), "sla1", "different");
|
||||||
|
assertNames(clientTypes.getGlobalClientTypes(), "sla", "service-account");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertNames(List<ClientTypeRepresentation> clientTypes, String... expectedNames) {
|
||||||
|
List<String> names = clientTypes.stream()
|
||||||
|
.map(ClientTypeRepresentation::getName)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
Assert.assertNames(names, expectedNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void assertPropertyConfig(String propertyName, ClientTypeRepresentation.PropertyConfig cfg, Boolean expectedApplicable, Boolean expectedReadOnly, Object expectedDefaultValue) {
|
||||||
|
assertThat("'applicable' for property " + propertyName + " not equal", ObjectUtil.isEqualOrBothNull(expectedApplicable, cfg.getApplicable()));
|
||||||
|
assertThat("'read-only' for property " + propertyName + " not equal", ObjectUtil.isEqualOrBothNull(expectedReadOnly, cfg.getReadOnly()));
|
||||||
|
assertThat("'default-value' ;for property " + propertyName + " not equal", ObjectUtil.isEqualOrBothNull(expectedDefaultValue, cfg.getDefaultValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClientRepresentation createClientWithType(String clientId, String clientType) {
|
||||||
|
ClientRepresentation clientRep = ClientBuilder.create()
|
||||||
|
.clientId(clientId)
|
||||||
|
.type(clientType)
|
||||||
|
.build();
|
||||||
|
Response response = testRealm().clients().create(clientRep);
|
||||||
|
String clientUUID = ApiUtil.getCreatedId(response);
|
||||||
|
getCleanup().addClientUuid(clientUUID);
|
||||||
|
|
||||||
|
return testRealm().clients().get(clientUUID).toRepresentation();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the feature really works
|
||||||
|
private void checkIfFeatureWorks(boolean shouldWork) {
|
||||||
|
try {
|
||||||
|
ClientTypesRepresentation clientTypes = testRealm().clientTypes().getClientTypes();
|
||||||
|
Assert.assertTrue(clientTypes.getRealmClientTypes().isEmpty());
|
||||||
|
if (!shouldWork)
|
||||||
|
fail("Feature is available, but at this moment should be disabled");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (shouldWork) {
|
||||||
|
e.printStackTrace();
|
||||||
|
fail("Feature is not available");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -63,6 +63,11 @@ public class ClientBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ClientBuilder type(String type) {
|
||||||
|
rep.setType(type);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public ClientBuilder consentRequired(boolean consentRequired) {
|
public ClientBuilder consentRequired(boolean consentRequired) {
|
||||||
rep.setConsentRequired(consentRequired);
|
rep.setConsentRequired(consentRequired);
|
||||||
return this;
|
return this;
|
||||||
|
|
Loading…
Reference in a new issue