Added SPIs for ClientType and ClientTypeManager

Grabbed the SPIs for ClientType and ClientTypeManager from Marek's Client Type prototype.

Closes #26431

Signed-off-by: vibrown <vibrown@redhat.com>

Cleaned up TODOs

Signed-off-by: vibrown <vibrown@redhat.com>

Added isSupported methods

Signed-off-by: vibrown <vibrown@redhat.com>
This commit is contained in:
vibrown 2024-01-24 13:14:43 -06:00 committed by Marek Posolda
parent bb12f3fb82
commit 161d03efd2
19 changed files with 1289 additions and 0 deletions

View file

@ -0,0 +1,110 @@
/*
* 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.representations.idm;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientTypeRepresentation {
@JsonProperty("name")
private String name;
@JsonProperty("provider")
private String provider;
@JsonProperty("config")
private Map<String, PropertyConfig> config;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getProvider() {
return provider;
}
public void setProvider(String provider) {
this.provider = provider;
}
public Map<String, PropertyConfig> getConfig() {
return config;
}
public void setConfig(Map<String, PropertyConfig> 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 {
@JsonProperty("applicable")
private Boolean applicable;
@JsonProperty("read-only")
private Boolean readOnly;
@JsonProperty("default-value")
private Object defaultValue;
public Boolean getApplicable() {
return applicable;
}
public void setApplicable(Boolean applicable) {
this.applicable = applicable;
}
public Boolean getReadOnly() {
return readOnly;
}
public void setReadOnly(Boolean readOnly) {
this.readOnly = readOnly;
}
public Object getDefaultValue() {
return defaultValue;
}
public void setDefaultValue(Object defaultValue) {
this.defaultValue = defaultValue;
}
}
}

View file

@ -0,0 +1,59 @@
/*
* 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.representations.idm;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientTypesRepresentation {
@JsonProperty("client-types")
private List<ClientTypeRepresentation> realmClientTypes;
@JsonProperty("global-client-types")
private List<ClientTypeRepresentation> globalClientTypes;
public ClientTypesRepresentation() {
}
public ClientTypesRepresentation(List<ClientTypeRepresentation> realmClientTypes, List<ClientTypeRepresentation> globalClientTypes) {
this.realmClientTypes = realmClientTypes;
this.globalClientTypes = globalClientTypes;
}
public List<ClientTypeRepresentation> getRealmClientTypes() {
return realmClientTypes;
}
public void setRealmClientTypes(List<ClientTypeRepresentation> realmClientTypes) {
this.realmClientTypes = realmClientTypes;
}
public List<ClientTypeRepresentation> getGlobalClientTypes() {
return globalClientTypes;
}
public void setGlobalClientTypes(List<ClientTypeRepresentation> globalClientTypes) {
this.globalClientTypes = globalClientTypes;
}
}

View file

@ -0,0 +1,50 @@
/*
* 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.client.clienttype;
import org.keycloak.models.ClientModel;
import org.keycloak.representations.idm.ClientRepresentation;
/**
* TODO:client-types javadocs
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface ClientType {
String getName();
// Augment client type at runtime
// Can be property name (like "standardFlow" or "rootUrl") or attributeName (like "pkceEnabled")
boolean isApplicable(String optionName);
// Return if option is configurable by clientType or not...
boolean isReadOnly(String optionName);
// Return the value of particular option (if it can be provided by clientType) or return null if this option is not provided by client type
<T> T getDefaultValue(String optionName, Class<T> optionType);
// Augment at the client type
// Augment particular client on creation of client (TODO:client-types Should it be clientModel or clientRepresentation? Or something else?)
void onCreate(ClientRepresentation newClient) throws ClientTypeException;
// Augment particular client on update of client (TODO:client-types Should it be clientModel or clientRepresentation? Or something else?)
void onUpdate(ClientModel currentClient, ClientRepresentation clientToUpdate) throws ClientTypeException;
}

View file

@ -0,0 +1,43 @@
/*
* 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.client.clienttype;
import org.keycloak.models.ModelException;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientTypeException extends ModelException {
public ClientTypeException(String message) {
super(message);
}
public ClientTypeException(String message, Object ... parameters) {
super(message, parameters);
}
public ClientTypeException(String message, Throwable cause) {
super(message, cause);
}
public ClientTypeException(Throwable cause) {
super(cause);
}
}

View file

@ -0,0 +1,52 @@
/*
* 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.client.clienttype;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.Provider;
import org.keycloak.representations.idm.ClientTypesRepresentation;
/**
* TODO:client-types javadoc
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface ClientTypeManager extends Provider {
// Constants for global types
String STANDARD = "standard";
String SERVICE_ACCOUNT = "service-account";
// TODO:client-types javadoc
ClientTypesRepresentation getClientTypes(RealmModel realm) throws ClientTypeException;
// Implementation is supposed also to validate clientTypes before persisting them
void updateClientTypes(RealmModel realm, ClientTypesRepresentation clientTypes) throws ClientTypeException;
ClientType getClientType(RealmModel realm, String typeName) throws ClientTypeException;
// Create client, which delegates to the particular client type
ClientModel augmentClient(ClientModel client) throws ClientTypeException;
@Override
default void close() {
}
}

View file

@ -0,0 +1,28 @@
/*
* 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.client.clienttype;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.ProviderFactory;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface ClientTypeManagerFactory extends ProviderFactory<ClientTypeManager>, EnvironmentDependentProviderFactory {
}

View file

@ -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.client.clienttype;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientTypeManagerSpi implements Spi {
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return "client-type-manager";
}
@Override
public Class<? extends Provider> getProviderClass() {
return ClientTypeManager.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return ClientTypeManagerFactory.class;
}
}

View file

@ -0,0 +1,40 @@
/*
* 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.client.clienttype;
import org.keycloak.provider.Provider;
import org.keycloak.representations.idm.ClientTypeRepresentation;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface ClientTypeProvider extends Provider {
// Return client types for the model returned
ClientType getClientType(ClientTypeRepresentation clientTypeRep);
// TODO:client-types type-safety here. The returned clientType should have correctly casted client type configuration
// Used when creating/updating clientType. The JSON configuration is validated to be checked if it matches the good format for client type
ClientTypeRepresentation checkClientTypeConfig(ClientTypeRepresentation clientType) throws ClientTypeException;
@Override
default void close() {
}
}

View file

@ -0,0 +1,27 @@
/*
* 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.client.clienttype;
import org.keycloak.provider.ProviderFactory;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface ClientTypeProviderFactory extends ProviderFactory<ClientTypeProvider> {
}

View file

@ -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.client.clienttype;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ClientTypeSpi implements Spi {
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return "client-type";
}
@Override
public Class<? extends Provider> getProviderClass() {
return ClientTypeProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return ClientTypeProviderFactory.class;
}
}

View file

@ -15,6 +15,8 @@
# limitations under the License. # limitations under the License.
# #
org.keycloak.client.clienttype.ClientTypeManagerSpi
org.keycloak.client.clienttype.ClientTypeSpi
org.keycloak.component.ComponentFactorySpi org.keycloak.component.ComponentFactorySpi
org.keycloak.provider.ExceptionConverterSpi org.keycloak.provider.ExceptionConverterSpi
org.keycloak.models.ClientSpi org.keycloak.models.ClientSpi

View file

@ -0,0 +1,168 @@
/*
* 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;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.jboss.logging.Logger;
import org.keycloak.client.clienttype.ClientType;
import org.keycloak.client.clienttype.ClientTypeException;
import org.keycloak.client.clienttype.ClientTypeManager;
import org.keycloak.client.clienttype.ClientTypeProvider;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.ClientTypeRepresentation;
import org.keycloak.representations.idm.ClientTypesRepresentation;
import org.keycloak.util.JsonSerialization;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class DefaultClientTypeManager implements ClientTypeManager {
private static final Logger logger = Logger.getLogger(DefaultClientTypeManager.class);
// Realm attribute where are client types saved
private static final String CLIENT_TYPE_REALM_ATTRIBUTE = "client-types";
private final KeycloakSession session;
private final List<ClientTypeRepresentation> globalClientTypes;
public DefaultClientTypeManager(KeycloakSession session, List<ClientTypeRepresentation> globalClientTypes) {
this.session = session;
this.globalClientTypes = globalClientTypes;
}
@Override
public ClientTypesRepresentation getClientTypes(RealmModel realm) throws ClientTypeException {
String asStr = realm.getAttribute(CLIENT_TYPE_REALM_ATTRIBUTE);
ClientTypesRepresentation result;
if (asStr == null) {
result = new ClientTypesRepresentation(new ArrayList<>(), null);
result.setGlobalClientTypes(globalClientTypes);
} else {
try {
// Skip validation here for performance reasons
result = JsonSerialization.readValue(asStr, ClientTypesRepresentation.class);
} catch (IOException ioe) {
throw new ClientTypeException("Failed to deserialize client types from JSON string", ioe);
}
}
return result;
}
@Override
public void updateClientTypes(RealmModel realm, ClientTypesRepresentation clientTypes) throws ClientTypeException {
// Validate before save
List<ClientTypeRepresentation> validatedClientTypes = validateAndCastConfiguration(session, clientTypes.getRealmClientTypes(), globalClientTypes);
ClientTypesRepresentation noGlobalsCopy = new ClientTypesRepresentation(validatedClientTypes, null);
try {
String asStr = JsonSerialization.writeValueAsString(noGlobalsCopy);
realm.setAttribute(CLIENT_TYPE_REALM_ATTRIBUTE, asStr);
} catch (IOException ioe) {
throw new ClientTypeException("Failed to serialize client types to String", ioe);
}
}
@Override
public ClientType getClientType(RealmModel realm, String typeName) throws ClientTypeException {
ClientTypesRepresentation clientTypes = getClientTypes(realm);
ClientTypeRepresentation clientType = getClientTypeByName(clientTypes, typeName);
if (clientType == null) {
logger.errorf("Referenced client type '%s' not found");
throw new ClientTypeException("Client type not found");
}
ClientTypeProvider provider = session.getProvider(ClientTypeProvider.class, clientType.getProvider());
return provider.getClientType(clientType);
}
@Override
public ClientModel augmentClient(ClientModel client) throws ClientTypeException {
//TODO:vibrown put the logic back in next Client Type PR
return client;
/*if (client.getType() == null) {
return client;
} else {
ClientType clientType = getClientType(client.getRealm(), client.getType());
return new TypeAwareClientModelDelegate(clientType, () -> client);
}*/
}
static List<ClientTypeRepresentation> validateAndCastConfiguration(KeycloakSession session, List<ClientTypeRepresentation> clientTypes, List<ClientTypeRepresentation> globalTypes) {
Set<String> usedNames = globalTypes.stream()
.map(ClientTypeRepresentation::getName)
.collect(Collectors.toSet());
return clientTypes.stream()
.map(clientType -> validateAndCastConfiguration(session, clientType, usedNames))
.collect(Collectors.toList());
}
// TODO:client-types some javadoc or comment about how this method works
private static ClientTypeRepresentation validateAndCastConfiguration(KeycloakSession session, ClientTypeRepresentation clientType, Set<String> currentNames) {
ClientTypeProvider clientTypeProvider = session.getProvider(ClientTypeProvider.class, clientType.getProvider());
if (clientTypeProvider == null) {
logger.errorf("Did not found client type provider '%s' for the client type '%s'", clientType.getProvider(), clientType.getName());
throw new ClientTypeException("Did not found client type provider");
}
// Validate name is not duplicated
if (currentNames.contains(clientType.getName())) {
logger.errorf("Duplicated client type name '%s'", clientType.getName());
throw new ClientTypeException("Duplicated client type name");
}
clientType = clientTypeProvider.checkClientTypeConfig(clientType);
currentNames.add(clientType.getName());
return clientType;
}
private ClientTypeRepresentation getClientTypeByName(ClientTypesRepresentation clientTypes, String clientTypeName) {
// Search realm clientTypes
if (clientTypes.getRealmClientTypes() != null) {
for (ClientTypeRepresentation clientType : clientTypes.getRealmClientTypes()) {
if (clientTypeName.equals(clientType.getName())) {
return clientType;
}
}
}
// Search global clientTypes
if (clientTypes.getGlobalClientTypes() != null) {
for (ClientTypeRepresentation clientType : clientTypes.getGlobalClientTypes()) {
if (clientTypeName.equals(clientType.getName())) {
return clientType;
}
}
}
return null;
}
}

View file

@ -0,0 +1,93 @@
/*
* 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;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.client.clienttype.ClientTypeManager;
import org.keycloak.client.clienttype.ClientTypeManagerFactory;
import org.keycloak.common.Profile;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.representations.idm.ClientTypeRepresentation;
import org.keycloak.representations.idm.ClientTypesRepresentation;
import org.keycloak.util.JsonSerialization;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class DefaultClientTypeManagerFactory implements ClientTypeManagerFactory {
private static final Logger logger = Logger.getLogger(DefaultClientTypeManagerFactory.class);
private volatile List<ClientTypeRepresentation> globalClientTypes;
@Override
public ClientTypeManager create(KeycloakSession session) {
return new DefaultClientTypeManager(session, getGlobalClientTypes(session));
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "default";
}
@Override
public boolean isSupported() {
return Profile.isFeatureEnabled(Profile.Feature.CLIENT_TYPES);
}
protected List<ClientTypeRepresentation> getGlobalClientTypes(KeycloakSession session) {
if (globalClientTypes == null) {
synchronized (this) {
if (globalClientTypes == null) {
logger.info("Loading global client types");
try {
ClientTypesRepresentation globalTypesRep = JsonSerialization.readValue(getClass().getResourceAsStream("/keycloak-default-client-types.json"), ClientTypesRepresentation.class);
this.globalClientTypes = DefaultClientTypeManager.validateAndCastConfiguration(session, globalTypesRep.getRealmClientTypes(), Collections.emptyList());
} catch (IOException e) {
throw new IllegalStateException("Failed to deserialize global proposed client types from JSON.", e);
}
}
}
}
return globalClientTypes;
}
}

View file

@ -0,0 +1,182 @@
/*
* 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.impl;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.databind.JavaType;
import org.jboss.logging.Logger;
import org.keycloak.common.util.ObjectUtil;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientTypeRepresentation;
import org.keycloak.client.clienttype.ClientType;
import org.keycloak.client.clienttype.ClientTypeException;
import org.keycloak.util.JsonSerialization;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class DefaultClientType implements ClientType {
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 ClientTypeRepresentation clientType;
private final Map<String, PropertyDescriptor> clientRepresentationProperties;
public DefaultClientType(KeycloakSession session, ClientTypeRepresentation clientType, Map<String, PropertyDescriptor> clientRepresentationProperties) {
this.session = session;
this.clientType = clientType;
this.clientRepresentationProperties = clientRepresentationProperties;
}
@Override
public String getName() {
return clientType.getName();
}
@Override
public boolean isApplicable(String optionName) {
ClientTypeRepresentation.PropertyConfig cfg = clientType.getConfig().get(optionName);
// Each property is applicable by default if not configured for the particular client type
return (cfg != null && cfg.getApplicable() != null) ? cfg.getApplicable() : true;
}
@Override
public boolean isReadOnly(String optionName) {
ClientTypeRepresentation.PropertyConfig cfg = clientType.getConfig().get(optionName);
// Each property is writable by default if not configured for the particular type
return (cfg != null && cfg.getReadOnly() != null) ? cfg.getReadOnly() : false;
}
@Override
public <T> T getDefaultValue(String optionName, Class<T> optionType) {
ClientTypeRepresentation.PropertyConfig cfg = clientType.getConfig().get(optionName);
return (cfg != null && cfg.getDefaultValue() != null) ? optionType.cast(cfg.getDefaultValue()) : null;
}
@Override
public void onCreate(ClientRepresentation createdClient) throws ClientTypeException {
for (Map.Entry<String, ClientTypeRepresentation.PropertyConfig> property : clientType.getConfig().entrySet()) {
ClientTypeRepresentation.PropertyConfig propertyConfig = property.getValue();
if (!propertyConfig.getApplicable()) continue;
if (propertyConfig.getDefaultValue() != null) {
if (clientRepresentationProperties.containsKey(property.getKey())) {
// Java property on client representation
try {
PropertyDescriptor propertyDescriptor = clientRepresentationProperties.get(property.getKey());
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) {
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);
}
} else {
// Client attribute
if (createdClient.getAttributes() == null) {
createdClient.setAttributes(new HashMap<>());
}
createdClient.getAttributes().put(property.getKey(), propertyConfig.getDefaultValue().toString());
}
}
}
}
@Override
public void onUpdate(ClientModel currentClient, ClientRepresentation newClient) throws ClientTypeException{
ClientRepresentation oldClient = ModelToRepresentation.toRepresentation(currentClient, session);
for (Map.Entry<String, ClientTypeRepresentation.PropertyConfig> property : clientType.getConfig().entrySet()) {
String propertyName = property.getKey();
ClientTypeRepresentation.PropertyConfig propertyConfig = property.getValue();
Object oldVal = getClientProperty(oldClient, propertyName);
Object newVal = getClientProperty(newClient, propertyName);
// Validate that read-only client properties were not changed. Also validate that non-applicable properties were not changed.
if (!propertyConfig.getApplicable() || propertyConfig.getReadOnly()) {
if (!ObjectUtil.isEqualOrBothNull(oldVal, newVal)) {
logger.warnf("Cannot change property '%s' of client '%s' . Old value '%s', New value '%s'", propertyName, currentClient.getClientId(), oldVal, newVal);
throw new ClientTypeException("Cannot change property of client as it is not allowed");
}
}
}
}
private Object getClientProperty(ClientRepresentation client, String propertyName) {
PropertyDescriptor propertyDescriptor = clientRepresentationProperties.get(propertyName);
if (propertyDescriptor != null) {
// Java property
Method getter = propertyDescriptor.getReadMethod();
try {
return getter.invoke(client);
} catch (Exception e) {
logger.warnf("Cannot read property '%s' on client '%s'. Client type is '%s'", propertyName, client.getClientId(), clientType.getName());
throw new ClientTypeException("Cannot read property of client", e);
}
} else {
// Attribute
return client.getAttributes() == null ? null : client.getAttributes().get(propertyName);
}
}
}

View file

@ -0,0 +1,73 @@
/*
* 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.impl;
import java.beans.PropertyDescriptor;
import java.util.Map;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.representations.idm.ClientTypeRepresentation;
import org.keycloak.client.clienttype.ClientType;
import org.keycloak.client.clienttype.ClientTypeException;
import org.keycloak.client.clienttype.ClientTypeProvider;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class DefaultClientTypeProvider implements ClientTypeProvider {
private static final Logger logger = Logger.getLogger(DefaultClientTypeProvider.class);
private final KeycloakSession session;
private final Map<String, PropertyDescriptor> clientRepresentationProperties;
public DefaultClientTypeProvider(KeycloakSession session, Map<String, PropertyDescriptor> clientRepresentationProperties) {
this.session = session;
this.clientRepresentationProperties = clientRepresentationProperties;
}
@Override
public ClientType getClientType(ClientTypeRepresentation clientTypeRep) {
return new DefaultClientType(session, clientTypeRep, clientRepresentationProperties);
}
@Override
public ClientTypeRepresentation checkClientTypeConfig(ClientTypeRepresentation clientType) throws ClientTypeException {
Map<String, ClientTypeRepresentation.PropertyConfig> config = clientType.getConfig();
for (Map.Entry<String, ClientTypeRepresentation.PropertyConfig> entry : config.entrySet()) {
String propertyName = entry.getKey();
ClientTypeRepresentation.PropertyConfig propConfig = entry.getValue();
if (propConfig.getApplicable() == null) {
logger.errorf("Property '%s' does not have 'applicable' configured for client type '%s'", propertyName, clientType.getName());
throw new ClientTypeException("Invalid configuration of 'applicable' property on client type");
}
// Not supported to set read-only or default-value for properties, which are not applicable for the particular client
if (!propConfig.getApplicable() && (propConfig.getReadOnly() != null || propConfig.getDefaultValue() != null)) {
logger.errorf("Property '%s' is not applicable and so should not have read-only or default-value set for client type '%s'", propertyName, clientType.getName());
throw new ClientTypeException("Invalid configuration of property on client type");
}
}
// TODO:client-types retype configuration
return clientType;
}
}

View file

@ -0,0 +1,103 @@
/*
* 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.impl;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.keycloak.Config;
import org.keycloak.common.Profile;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.client.clienttype.ClientTypeProvider;
import org.keycloak.client.clienttype.ClientTypeProviderFactory;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class DefaultClientTypeProviderFactory implements ClientTypeProviderFactory, EnvironmentDependentProviderFactory {
public static final String PROVIDER_ID = "default";
private Map<String, PropertyDescriptor> clientRepresentationProperties;
@Override
public ClientTypeProvider create(KeycloakSession session) {
return new DefaultClientTypeProvider(session, clientRepresentationProperties);
}
@Override
public void init(Config.Scope config) {
Set<String> filtered = Arrays.stream(new String[] {"attributes", "type"}).collect(Collectors.toSet());
try {
BeanInfo bi = Introspector.getBeanInfo(ClientRepresentation.class);
PropertyDescriptor[] pd = bi.getPropertyDescriptors();
clientRepresentationProperties = Arrays.stream(pd)
.filter(desc -> !filtered.contains(desc.getName()))
.filter(desc -> desc.getWriteMethod() != null)
.map(desc -> {
// Take "is" methods into consideration
if (desc.getReadMethod() == null && Boolean.class.equals(desc.getPropertyType())) {
String methodName = "is" + desc.getName().substring(0, 1).toUpperCase() + desc.getName().substring(1);
try {
Method getter = ClientRepresentation.class.getDeclaredMethod(methodName);
desc.setReadMethod(getter);
} catch (Exception e) {
throw new IllegalStateException("Getter method for property " + desc.getName() + " cannot be found");
}
}
return desc;
})
.collect(Collectors.toMap(PropertyDescriptor::getName, Function.identity()));
} catch (IntrospectionException ie) {
throw new IllegalStateException("Introspection of Client representation failed", ie);
}
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public boolean isSupported() {
return Profile.isFeatureEnabled(Profile.Feature.CLIENT_TYPES);
}
}

View file

@ -0,0 +1,19 @@
#
# 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.
#
#
org.keycloak.services.clienttype.DefaultClientTypeManagerFactory

View file

@ -0,0 +1,19 @@
#
# 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.
#
#
org.keycloak.services.clienttype.impl.DefaultClientTypeProviderFactory

View file

@ -0,0 +1,123 @@
{
"client-types": [
{
"name": "sla",
"provider": "default",
"config": {
"standardFlowEnabled": {
"applicable": true,
"read-only": true,
"default-value": true
}
}
},
{
"name": "service-account",
"provider": "default",
"config": {
"alwaysDisplayInConsole": {
"applicable": false
},
"consentRequired": {
"applicable": true,
"read-only": true,
"default-value": false
},
"login_theme": {
"applicable": false
},
"protocol": {
"applicable": true,
"read-only": true,
"default-value": "openid-connect"
},
"publicClient": {
"applicable": true,
"read-only": true,
"default-value": false
},
"bearerOnly": {
"applicable": true,
"read-only": true,
"default-value": false
},
"standardFlowEnabled": {
"applicable": true,
"read-only": true,
"default-value": false
},
"implicitFlowEnabled": {
"applicable": true,
"read-only": true,
"default-value": false
},
"directAccessGrantsEnabled": {
"applicable": true,
"read-only": true,
"default-value": false
},
"serviceAccountsEnabled": {
"applicable": true,
"read-only": 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": {
"applicable": false
},
"policyUri": {
"applicable": false
},
"tosUri": {
"applicable": false
}
}
}
]
}