Client type service account default type (#29037)
* Adding additional non-applicable client fields to the default service-account client type configuration. Signed-off-by: Patrick Jennings <pajennin@redhat.com> * Creating TypedClientAttribute which maps clientmodel fields to standard client type configurations. Adding overrides for fields in TypeAwareClientModelDelegate required for service-account client type. Signed-off-by: Patrick Jennings <pajennin@redhat.com> * Splitting client type attribute enum into 3 separate enums, representing the top level ClientModel fields, the extended attributes through the client_attributes table, and the composable fields on ClientRepresentation. Signed-off-by: Patrick Jennings <pajennin@redhat.com> * Removing reflection use for client types. Validation will be done in the RepresentationToModel methods that are responsible for the ClientRepresentation -> ClientModel create and update static methods. Signed-off-by: Patrick Jennings <pajennin@redhat.com> More updates Signed-off-by: Patrick Jennings <pajennin@redhat.com> * Update client utilzes type aware client property update method. Signed-off-by: Patrick Jennings <pajennin@redhat.com> * If user inputted representation object does not contain non-null value, try to get property value from the client. Type aware client model will return non-applicable or default value to keep fields consistent. Signed-off-by: Patrick Jennings <pajennin@redhat.com> * Cleaning up RepresentationToModel Signed-off-by: Patrick Jennings <pajennin@redhat.com> * Fixing issue when updating client secret. Signed-off-by: Patrick Jennings <pajennin@redhat.com> * Fixing issue where created clients would not have fullscope allowed, because getter is a boolean and so cannot be null. Signed-off-by: Patrick Jennings <pajennin@redhat.com> * Need to be able to clear out client attributes on update as was allowed before and causing failures in integration tests. Signed-off-by: Patrick Jennings <pajennin@redhat.com> * Fixing issues with redirectUri and weborigins defaults in type aware clients. Signed-off-by: Patrick Jennings <pajennin@redhat.com> * Need to allow client attributes the ability to clear out values during update. Signed-off-by: Patrick Jennings <pajennin@redhat.com> * Renaming interface based on PR feedback. Signed-off-by: Patrick Jennings <pajennin@redhat.com> * Shall be able to override URI sets with an empty set. Signed-off-by: Patrick Jennings <pajennin@redhat.com> * Comments around fields that are primitive and may cause problems determining whether to set sane default on create. Signed-off-by: Patrick Jennings <pajennin@redhat.com> --------- Signed-off-by: Patrick Jennings <pajennin@redhat.com>
This commit is contained in:
parent
65bdf1a604
commit
64824bb77f
12 changed files with 620 additions and 438 deletions
|
@ -19,7 +19,9 @@
|
||||||
package org.keycloak.client.clienttype;
|
package org.keycloak.client.clienttype;
|
||||||
|
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientTypeRepresentation;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO:client-types javadocs
|
* TODO:client-types javadocs
|
||||||
|
@ -41,10 +43,8 @@ public interface ClientType {
|
||||||
<T> T getDefaultValue(String optionName, Class<T> optionType);
|
<T> T getDefaultValue(String optionName, Class<T> optionType);
|
||||||
|
|
||||||
|
|
||||||
// Augment at the client type
|
Map<String, ClientTypeRepresentation.PropertyConfig> getConfiguration();
|
||||||
// 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?)
|
// Augment at the client type
|
||||||
void onUpdate(ClientModel currentClient, ClientRepresentation clientToUpdate) throws ClientTypeException;
|
ClientModel augment(ClientModel client);
|
||||||
}
|
}
|
|
@ -23,7 +23,7 @@ import org.keycloak.models.ModelIllegalStateException;
|
||||||
import org.keycloak.models.ProtocolMapperModel;
|
import org.keycloak.models.ProtocolMapperModel;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RoleModel;
|
import org.keycloak.models.RoleModel;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.atomic.AtomicMarkableReference;
|
import java.util.concurrent.atomic.AtomicMarkableReference;
|
||||||
|
|
|
@ -18,17 +18,23 @@
|
||||||
package org.keycloak.models.utils;
|
package org.keycloak.models.utils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ListIterator;
|
import java.util.ListIterator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
|
@ -50,8 +56,12 @@ import org.keycloak.authorization.store.StoreFactory;
|
||||||
import org.keycloak.broker.provider.IdentityProvider;
|
import org.keycloak.broker.provider.IdentityProvider;
|
||||||
import org.keycloak.broker.provider.IdentityProviderFactory;
|
import org.keycloak.broker.provider.IdentityProviderFactory;
|
||||||
import org.keycloak.broker.social.SocialIdentityProvider;
|
import org.keycloak.broker.social.SocialIdentityProvider;
|
||||||
|
import org.keycloak.client.clienttype.ClientType;
|
||||||
|
import org.keycloak.client.clienttype.ClientTypeException;
|
||||||
|
import org.keycloak.client.clienttype.ClientTypeManager;
|
||||||
import org.keycloak.common.Profile;
|
import org.keycloak.common.Profile;
|
||||||
import org.keycloak.common.util.MultivaluedHashMap;
|
import org.keycloak.common.util.MultivaluedHashMap;
|
||||||
|
import org.keycloak.common.util.ObjectUtil;
|
||||||
import org.keycloak.common.util.UriUtils;
|
import org.keycloak.common.util.UriUtils;
|
||||||
import org.keycloak.component.ComponentModel;
|
import org.keycloak.component.ComponentModel;
|
||||||
import org.keycloak.credential.CredentialModel;
|
import org.keycloak.credential.CredentialModel;
|
||||||
|
@ -317,18 +327,14 @@ public class RepresentationToModel {
|
||||||
logger.debugv("Create client: {0}", resourceRep.getClientId());
|
logger.debugv("Create client: {0}", resourceRep.getClientId());
|
||||||
|
|
||||||
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.getDescription() != null) client.setDescription(resourceRep.getDescription());
|
if (Profile.isFeatureEnabled(Profile.Feature.CLIENT_TYPES) && resourceRep.getType() != null) {
|
||||||
if (resourceRep.getType() != null) client.setType(resourceRep.getType());
|
ClientTypeManager mgr = session.getProvider(ClientTypeManager.class);
|
||||||
if (resourceRep.isEnabled() != null) client.setEnabled(resourceRep.isEnabled());
|
ClientType clientType = mgr.getClientType(realm, resourceRep.getType());
|
||||||
if (resourceRep.isAlwaysDisplayInConsole() != null) client.setAlwaysDisplayInConsole(resourceRep.isAlwaysDisplayInConsole());
|
client = clientType.augment(client);
|
||||||
client.setManagementUrl(resourceRep.getAdminUrl());
|
}
|
||||||
if (resourceRep.isSurrogateAuthRequired() != null)
|
|
||||||
client.setSurrogateAuthRequired(resourceRep.isSurrogateAuthRequired());
|
updateClientProperties(client, resourceRep, true);
|
||||||
if (resourceRep.getRootUrl() != null) client.setRootUrl(resourceRep.getRootUrl());
|
|
||||||
if (resourceRep.getBaseUrl() != null) client.setBaseUrl(resourceRep.getBaseUrl());
|
|
||||||
if (resourceRep.isBearerOnly() != null) client.setBearerOnly(resourceRep.isBearerOnly());
|
|
||||||
if (resourceRep.isConsentRequired() != null) client.setConsentRequired(resourceRep.isConsentRequired());
|
|
||||||
|
|
||||||
// Backwards compatibility only
|
// Backwards compatibility only
|
||||||
if (resourceRep.isDirectGrantsOnly() != null) {
|
if (resourceRep.isDirectGrantsOnly() != null) {
|
||||||
|
@ -337,58 +343,6 @@ public class RepresentationToModel {
|
||||||
client.setDirectAccessGrantsEnabled(resourceRep.isDirectGrantsOnly());
|
client.setDirectAccessGrantsEnabled(resourceRep.isDirectGrantsOnly());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resourceRep.isStandardFlowEnabled() != null)
|
|
||||||
client.setStandardFlowEnabled(resourceRep.isStandardFlowEnabled());
|
|
||||||
if (resourceRep.isImplicitFlowEnabled() != null)
|
|
||||||
client.setImplicitFlowEnabled(resourceRep.isImplicitFlowEnabled());
|
|
||||||
if (resourceRep.isDirectAccessGrantsEnabled() != null)
|
|
||||||
client.setDirectAccessGrantsEnabled(resourceRep.isDirectAccessGrantsEnabled());
|
|
||||||
if (resourceRep.isServiceAccountsEnabled() != null)
|
|
||||||
client.setServiceAccountsEnabled(resourceRep.isServiceAccountsEnabled());
|
|
||||||
|
|
||||||
if (resourceRep.isPublicClient() != null) client.setPublicClient(resourceRep.isPublicClient());
|
|
||||||
if (resourceRep.isFrontchannelLogout() != null)
|
|
||||||
client.setFrontchannelLogout(resourceRep.isFrontchannelLogout());
|
|
||||||
|
|
||||||
// set defaults to openid-connect if no protocol specified
|
|
||||||
if (resourceRep.getProtocol() != null) {
|
|
||||||
client.setProtocol(resourceRep.getProtocol());
|
|
||||||
} else {
|
|
||||||
client.setProtocol(OIDC);
|
|
||||||
}
|
|
||||||
if (resourceRep.getNodeReRegistrationTimeout() != null) {
|
|
||||||
client.setNodeReRegistrationTimeout(resourceRep.getNodeReRegistrationTimeout());
|
|
||||||
} else {
|
|
||||||
client.setNodeReRegistrationTimeout(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resourceRep.getNotBefore() != null) {
|
|
||||||
client.setNotBefore(resourceRep.getNotBefore());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resourceRep.getClientAuthenticatorType() != null) {
|
|
||||||
client.setClientAuthenticatorType(resourceRep.getClientAuthenticatorType());
|
|
||||||
} else {
|
|
||||||
client.setClientAuthenticatorType(KeycloakModelUtils.getDefaultClientAuthenticatorType());
|
|
||||||
}
|
|
||||||
|
|
||||||
// adding secret if the client isn't public nor bearer only
|
|
||||||
if (Objects.nonNull(resourceRep.getSecret())) {
|
|
||||||
client.setSecret(resourceRep.getSecret());
|
|
||||||
} else {
|
|
||||||
if (client.isPublicClient() || client.isBearerOnly()) {
|
|
||||||
client.setSecret(null);
|
|
||||||
} else {
|
|
||||||
KeycloakModelUtils.generateSecret(client);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resourceRep.getAttributes() != null) {
|
|
||||||
for (Map.Entry<String, String> entry : resourceRep.getAttributes().entrySet()) {
|
|
||||||
client.setAttribute(entry.getKey(), entry.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("saml".equals(resourceRep.getProtocol())
|
if ("saml".equals(resourceRep.getProtocol())
|
||||||
&& (resourceRep.getAttributes() == null
|
&& (resourceRep.getAttributes() == null
|
||||||
|| !resourceRep.getAttributes().containsKey("saml.artifact.binding.identifier"))) {
|
|| !resourceRep.getAttributes().containsKey("saml.artifact.binding.identifier"))) {
|
||||||
|
@ -413,35 +367,6 @@ public class RepresentationToModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (resourceRep.getRedirectUris() != null) {
|
|
||||||
for (String redirectUri : resourceRep.getRedirectUris()) {
|
|
||||||
client.addRedirectUri(redirectUri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (resourceRep.getWebOrigins() != null) {
|
|
||||||
for (String webOrigin : resourceRep.getWebOrigins()) {
|
|
||||||
logger.debugv("Client: {0} webOrigin: {1}", resourceRep.getClientId(), webOrigin);
|
|
||||||
client.addWebOrigin(webOrigin);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// add origins from redirect uris
|
|
||||||
if (resourceRep.getRedirectUris() != null) {
|
|
||||||
Set<String> origins = new HashSet<String>();
|
|
||||||
for (String redirectUri : resourceRep.getRedirectUris()) {
|
|
||||||
logger.debugv("add redirect-uri to origin: {0}", redirectUri);
|
|
||||||
if (redirectUri.startsWith("http")) {
|
|
||||||
String origin = UriUtils.getOrigin(redirectUri);
|
|
||||||
logger.debugv("adding default client origin: {0}", origin);
|
|
||||||
origins.add(origin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (origins.size() > 0) {
|
|
||||||
client.setWebOrigins(origins);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resourceRep.getRegisteredNodes() != null) {
|
if (resourceRep.getRegisteredNodes() != null) {
|
||||||
for (Map.Entry<String, Integer> entry : resourceRep.getRegisteredNodes().entrySet()) {
|
for (Map.Entry<String, Integer> entry : resourceRep.getRegisteredNodes().entrySet()) {
|
||||||
client.registerNode(entry.getKey(), entry.getValue());
|
client.registerNode(entry.getKey(), entry.getValue());
|
||||||
|
@ -457,7 +382,6 @@ public class RepresentationToModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
MigrationUtils.updateProtocolMappers(client);
|
MigrationUtils.updateProtocolMappers(client);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resourceRep.getClientTemplate() != null) {
|
if (resourceRep.getClientTemplate() != null) {
|
||||||
|
@ -467,12 +391,6 @@ public class RepresentationToModel {
|
||||||
|
|
||||||
updateClientScopes(resourceRep, client);
|
updateClientScopes(resourceRep, client);
|
||||||
|
|
||||||
if (resourceRep.isFullScopeAllowed() != null) {
|
|
||||||
client.setFullScopeAllowed(resourceRep.isFullScopeAllowed());
|
|
||||||
} else {
|
|
||||||
client.setFullScopeAllowed(!client.isConsentRequired());
|
|
||||||
}
|
|
||||||
|
|
||||||
client.updateClient();
|
client.updateClient();
|
||||||
resourceRep.setId(client.getId());
|
resourceRep.setId(client.getId());
|
||||||
|
|
||||||
|
@ -489,44 +407,25 @@ public class RepresentationToModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void updateClient(ClientRepresentation rep, ClientModel resource, KeycloakSession session) {
|
public static void updateClient(ClientRepresentation rep, ClientModel resource, KeycloakSession session) {
|
||||||
|
|
||||||
|
if (Profile.isFeatureEnabled(Profile.Feature.CLIENT_TYPES)) {
|
||||||
|
if (!ObjectUtil.isEqualOrBothNull(rep.getType(), rep.getType())) {
|
||||||
|
throw new ClientTypeException("Not supported to change client type");
|
||||||
|
}
|
||||||
|
if (rep.getType() != null) {
|
||||||
|
RealmModel realm = session.getContext().getRealm();
|
||||||
|
ClientTypeManager mgr = session.getProvider(ClientTypeManager.class);
|
||||||
|
ClientType clientType = mgr.getClientType(realm, rep.getType());
|
||||||
|
resource = clientType.augment(resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String newClientId = rep.getClientId();
|
String newClientId = rep.getClientId();
|
||||||
String previousClientId = resource.getClientId();
|
String previousClientId = resource.getClientId();
|
||||||
if (newClientId != null) resource.setClientId(newClientId);
|
|
||||||
if (rep.getName() != null) resource.setName(rep.getName());
|
|
||||||
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.isAlwaysDisplayInConsole() != null) resource.setAlwaysDisplayInConsole(rep.isAlwaysDisplayInConsole());
|
|
||||||
if (rep.isBearerOnly() != null) resource.setBearerOnly(rep.isBearerOnly());
|
|
||||||
if (rep.isConsentRequired() != null) resource.setConsentRequired(rep.isConsentRequired());
|
|
||||||
if (rep.isStandardFlowEnabled() != null) resource.setStandardFlowEnabled(rep.isStandardFlowEnabled());
|
|
||||||
if (rep.isImplicitFlowEnabled() != null) resource.setImplicitFlowEnabled(rep.isImplicitFlowEnabled());
|
|
||||||
if (rep.isDirectAccessGrantsEnabled() != null)
|
|
||||||
resource.setDirectAccessGrantsEnabled(rep.isDirectAccessGrantsEnabled());
|
|
||||||
if (rep.isServiceAccountsEnabled() != null) resource.setServiceAccountsEnabled(rep.isServiceAccountsEnabled());
|
|
||||||
if (rep.isPublicClient() != null) resource.setPublicClient(rep.isPublicClient());
|
|
||||||
if (rep.isFullScopeAllowed() != null) resource.setFullScopeAllowed(rep.isFullScopeAllowed());
|
|
||||||
if (rep.isFrontchannelLogout() != null) resource.setFrontchannelLogout(rep.isFrontchannelLogout());
|
|
||||||
if (rep.getRootUrl() != null) resource.setRootUrl(rep.getRootUrl());
|
|
||||||
if (rep.getAdminUrl() != null) resource.setManagementUrl(rep.getAdminUrl());
|
|
||||||
if (rep.getBaseUrl() != null) resource.setBaseUrl(rep.getBaseUrl());
|
|
||||||
if (rep.isSurrogateAuthRequired() != null) resource.setSurrogateAuthRequired(rep.isSurrogateAuthRequired());
|
|
||||||
if (rep.getNodeReRegistrationTimeout() != null)
|
|
||||||
resource.setNodeReRegistrationTimeout(rep.getNodeReRegistrationTimeout());
|
|
||||||
if (rep.getClientAuthenticatorType() != null)
|
|
||||||
resource.setClientAuthenticatorType(rep.getClientAuthenticatorType());
|
|
||||||
|
|
||||||
if (rep.getProtocol() != null) resource.setProtocol(rep.getProtocol());
|
if (newClientId != null) resource.setClientId(newClientId);
|
||||||
if (rep.getAttributes() != null) {
|
|
||||||
for (Map.Entry<String, String> entry : rep.getAttributes().entrySet()) {
|
updateClientProperties(resource, rep, false);
|
||||||
resource.setAttribute(entry.getKey(), entry.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (rep.getAttributes() != null) {
|
|
||||||
for (Map.Entry<String, String> entry : removeEmptyString(rep.getAttributes()).entrySet()) {
|
|
||||||
resource.setAttribute(entry.getKey(), entry.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("saml".equals(rep.getProtocol())
|
if ("saml".equals(rep.getProtocol())
|
||||||
&& (rep.getAttributes() == null
|
&& (rep.getAttributes() == null
|
||||||
|
@ -548,46 +447,20 @@ public class RepresentationToModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rep.getNotBefore() != null) {
|
|
||||||
resource.setNotBefore(rep.getNotBefore());
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> redirectUris = rep.getRedirectUris();
|
|
||||||
if (redirectUris != null) {
|
|
||||||
resource.setRedirectUris(new HashSet<>(redirectUris));
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> webOrigins = rep.getWebOrigins();
|
|
||||||
if (webOrigins != null) {
|
|
||||||
resource.setWebOrigins(new HashSet<>(webOrigins));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rep.getRegisteredNodes() != null) {
|
if (rep.getRegisteredNodes() != null) {
|
||||||
for (Map.Entry<String, Integer> entry : rep.getRegisteredNodes().entrySet()) {
|
for (Map.Entry<String, Integer> entry : rep.getRegisteredNodes().entrySet()) {
|
||||||
resource.registerNode(entry.getKey(), entry.getValue());
|
resource.registerNode(entry.getKey(), entry.getValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resource.isPublicClient() || resource.isBearerOnly()) {
|
|
||||||
resource.setSecret(null);
|
|
||||||
} else {
|
|
||||||
String currentSecret = resource.getSecret();
|
|
||||||
String newSecret = rep.getSecret();
|
|
||||||
|
|
||||||
if (newSecret == null && currentSecret == null) {
|
|
||||||
KeycloakModelUtils.generateSecret(resource);
|
|
||||||
} else if (newSecret != null) {
|
|
||||||
resource.setSecret(newSecret);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resource.updateClient();
|
resource.updateClient();
|
||||||
|
|
||||||
if (!Objects.equals(newClientId, previousClientId)) {
|
if (!Objects.equals(newClientId, previousClientId)) {
|
||||||
|
ClientModel finalResource = resource;
|
||||||
ClientModel.ClientIdChangeEvent event = new ClientModel.ClientIdChangeEvent() {
|
ClientModel.ClientIdChangeEvent event = new ClientModel.ClientIdChangeEvent() {
|
||||||
@Override
|
@Override
|
||||||
public ClientModel getUpdatedClient() {
|
public ClientModel getUpdatedClient() {
|
||||||
return resource;
|
return finalResource;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -609,6 +482,126 @@ public class RepresentationToModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update client properties and process any {@link ClientTypeException} validation errors combining into one to be thrown.
|
||||||
|
*
|
||||||
|
* @param client {@link ClientModel} to update
|
||||||
|
* @param rep {@link ClientRepresentation} to apply updates.
|
||||||
|
* @param isNew Whether the client is new or not (needed because some getters cannot return null).
|
||||||
|
*/
|
||||||
|
private static void updateClientProperties(ClientModel client, ClientRepresentation rep, boolean isNew) {
|
||||||
|
List<Supplier<ClientTypeException>> clientPropertyUpdates = new LinkedList<Supplier<ClientTypeException>>() {{
|
||||||
|
/**
|
||||||
|
* Values from the ClientRepresentation take precedence.
|
||||||
|
* Then values from the ClientModel (these may be augmented from the client type configuration).
|
||||||
|
* Otherwise, we can choose some sane default.
|
||||||
|
*/
|
||||||
|
add(updatePropertyAction(client::setName, rep::getName, client::getName));
|
||||||
|
add(updatePropertyAction(client::setDescription, rep::getDescription, client::getDescription));
|
||||||
|
add(updatePropertyAction(client::setType, rep::getType, client::getType));
|
||||||
|
add(updatePropertyAction(client::setEnabled, rep::isEnabled, client::isEnabled));
|
||||||
|
add(updatePropertyAction(client::setAlwaysDisplayInConsole, rep::isAlwaysDisplayInConsole, client::isAlwaysDisplayInConsole));
|
||||||
|
add(updatePropertyAction(client::setManagementUrl, rep::getAdminUrl, client::getManagementUrl));
|
||||||
|
add(updatePropertyAction(client::setSurrogateAuthRequired, rep::isSurrogateAuthRequired, client::isSurrogateAuthRequired));
|
||||||
|
add(updatePropertyAction(client::setRootUrl, rep::getRootUrl, client::getRootUrl));
|
||||||
|
add(updatePropertyAction(client::setBaseUrl, rep::getBaseUrl, client::getBaseUrl));
|
||||||
|
add(updatePropertyAction(client::setBearerOnly, rep::isBearerOnly, client::isBearerOnly));
|
||||||
|
add(updatePropertyAction(client::setConsentRequired, rep::isConsentRequired, client::isConsentRequired));
|
||||||
|
add(updatePropertyAction(client::setStandardFlowEnabled, rep::isStandardFlowEnabled, client::isStandardFlowEnabled));
|
||||||
|
add(updatePropertyAction(client::setImplicitFlowEnabled, rep::isImplicitFlowEnabled, client::isImplicitFlowEnabled));
|
||||||
|
add(updatePropertyAction(client::setDirectAccessGrantsEnabled, rep::isDirectAccessGrantsEnabled, client::isDirectAccessGrantsEnabled));
|
||||||
|
add(updatePropertyAction(client::setServiceAccountsEnabled, rep::isServiceAccountsEnabled, client::isServiceAccountsEnabled));
|
||||||
|
add(updatePropertyAction(client::setPublicClient, rep::isPublicClient, client::isPublicClient));
|
||||||
|
add(updatePropertyAction(client::setFrontchannelLogout, rep::isFrontchannelLogout, client::isFrontchannelLogout));
|
||||||
|
add(updatePropertyAction(client::setNotBefore, rep::getNotBefore, client::getNotBefore));
|
||||||
|
// Fields with defaults if not initially provided
|
||||||
|
add(updatePropertyAction(client::setProtocol, rep::getProtocol, client::getProtocol, () -> OIDC));
|
||||||
|
add(updatePropertyAction(client::setNodeReRegistrationTimeout, rep::getNodeReRegistrationTimeout, () -> defaultNodeReRegistrationTimeout(client, isNew)));
|
||||||
|
add(updatePropertyAction(client::setClientAuthenticatorType, rep::getClientAuthenticatorType, client::getClientAuthenticatorType, KeycloakModelUtils::getDefaultClientAuthenticatorType));
|
||||||
|
add(updatePropertyAction(client::setFullScopeAllowed, rep::isFullScopeAllowed, () -> defaultFullScopeAllowed(client, isNew)));
|
||||||
|
// Client Secret
|
||||||
|
add(updatePropertyAction(client::setSecret, () -> determineNewSecret(client, rep)));
|
||||||
|
// Redirect uris / Web origins
|
||||||
|
add(updatePropertyAction(client::setRedirectUris, () -> collectionToSet(rep.getRedirectUris()), client::getRedirectUris));
|
||||||
|
add(updatePropertyAction(client::setWebOrigins, () -> collectionToSet(rep.getWebOrigins()), () -> defaultWebOrigins(client)));
|
||||||
|
}};
|
||||||
|
|
||||||
|
// Extended client attributes
|
||||||
|
if (rep.getAttributes() != null) {
|
||||||
|
for (Map.Entry<String, String> entry : rep.getAttributes().entrySet()) {
|
||||||
|
clientPropertyUpdates.add(
|
||||||
|
updatePropertyAction(val -> client.setAttribute(entry.getKey(), val), entry::getValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ClientTypeException> propertyUpdateExceptions = clientPropertyUpdates
|
||||||
|
.stream()
|
||||||
|
.map(Supplier::get)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (propertyUpdateExceptions.size() > 0) {
|
||||||
|
throw new ClientTypeException(
|
||||||
|
"Cannot change property of client as it is not allowed by the specified client type.",
|
||||||
|
propertyUpdateExceptions.stream().map(ClientTypeException::getParameters).flatMap(Stream::of).toArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Boolean defaultFullScopeAllowed(ClientModel client, boolean isNew) {
|
||||||
|
// If the client is newly created and the value is set to true, the value must be augmented through a
|
||||||
|
// client type configuration, so do not update. Yet if new and false, the field MAY be controlled through a
|
||||||
|
// client type. We can only attempt to set the sane default based on consent required.
|
||||||
|
return isNew && !client.isFullScopeAllowed() ? !client.isConsentRequired() : client.isFullScopeAllowed();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Integer defaultNodeReRegistrationTimeout(ClientModel client, boolean isNew) {
|
||||||
|
// If the client is newly created and the value is 0, the client MAY not be augmented with a client type.
|
||||||
|
if (isNew && Objects.equals(client.getNodeReRegistrationTimeout(), 0)) {
|
||||||
|
// Return the sane default.
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// Update client request without an overriding value or is new and augmented with a client type,
|
||||||
|
// do not attempt to update.
|
||||||
|
return client.getNodeReRegistrationTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String determineNewSecret(ClientModel client, ClientRepresentation rep) {
|
||||||
|
if (Boolean.TRUE.equals(rep.isPublicClient()) || Boolean.TRUE.equals(rep.isBearerOnly())) {
|
||||||
|
// Clear out the secret with null
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// adding secret if the client isn't public nor bearer only
|
||||||
|
String currentSecret = client.getSecret();
|
||||||
|
String newSecret = rep.getSecret();
|
||||||
|
|
||||||
|
if (newSecret == null && currentSecret == null) {
|
||||||
|
return KeycloakModelUtils.generateSecret(client);
|
||||||
|
} else if (newSecret != null) {
|
||||||
|
return newSecret;
|
||||||
|
}
|
||||||
|
// Do not change current secret.
|
||||||
|
return currentSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<String> defaultWebOrigins(ClientModel client) {
|
||||||
|
Set<String> webOrigins = client.getWebOrigins();
|
||||||
|
if (webOrigins != null && !webOrigins.isEmpty()) {
|
||||||
|
return webOrigins;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> redirectUris = client.getRedirectUris();
|
||||||
|
if (redirectUris == null || redirectUris.isEmpty()) {
|
||||||
|
return new HashSet<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.getRedirectUris()
|
||||||
|
.stream()
|
||||||
|
.filter(uri -> uri.startsWith("http"))
|
||||||
|
.map(UriUtils::getOrigin)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
public static void updateClientProtocolMappers(ClientRepresentation rep, ClientModel resource) {
|
public static void updateClientProtocolMappers(ClientRepresentation rep, ClientModel resource) {
|
||||||
|
|
||||||
if (rep.getProtocolMappers() != null) {
|
if (rep.getProtocolMappers() != null) {
|
||||||
|
@ -663,6 +656,45 @@ public class RepresentationToModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Supplier to update property, if not null.
|
||||||
|
* Captures {@link ClientTypeException} if thrown by the setter.
|
||||||
|
*
|
||||||
|
* @param modelSetter setter to call.
|
||||||
|
* @param representationGetter getter supplying the property update.
|
||||||
|
* @return {@link Supplier<T>} resulting whether a {@link ClientTypeException} was thrown.
|
||||||
|
* @param <T> Type of property.
|
||||||
|
*/
|
||||||
|
private static <T> Supplier<ClientTypeException> updateProperty(Consumer<T> modelSetter, Supplier<T> representationGetter) {
|
||||||
|
return () -> {
|
||||||
|
try {
|
||||||
|
T value = representationGetter.get();
|
||||||
|
modelSetter.accept(value);
|
||||||
|
} catch (ClientTypeException cte) {
|
||||||
|
return cte;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an update property action, passing the first non-null value supplied to the setter, in argument order.
|
||||||
|
*
|
||||||
|
* @param modelSetter {@link Consumer<T>} The setter to call.
|
||||||
|
* @param getters {@link Supplier<T>} to query for the first non-null value.
|
||||||
|
* @return {@link Supplier} with results of operation.
|
||||||
|
* @param <T> Type of property.
|
||||||
|
*/
|
||||||
|
private static <T> Supplier<ClientTypeException> updatePropertyAction(Consumer<T> modelSetter, Supplier<T>... getters) {
|
||||||
|
Stream<T> firstNonNullSupplied = Stream.of(getters)
|
||||||
|
.map(Supplier::get)
|
||||||
|
.map(Optional::ofNullable)
|
||||||
|
.filter(Optional::isPresent)
|
||||||
|
.map(Optional::get);
|
||||||
|
return updateProperty(modelSetter, () -> firstNonNullSupplied.findFirst().orElse(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private static String generateProtocolNameKey(String protocol, String name) {
|
private static String generateProtocolNameKey(String protocol, String name) {
|
||||||
return String.format("%s%%%s", protocol, name);
|
return String.format("%s%%%s", protocol, name);
|
||||||
}
|
}
|
||||||
|
@ -1607,4 +1639,10 @@ public class RepresentationToModel {
|
||||||
|
|
||||||
return toModel(representation, authorization, client);
|
return toModel(representation, authorization, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static <T> Set<T> collectionToSet(Collection<T> collection) {
|
||||||
|
return Optional.ofNullable(collection)
|
||||||
|
.map(HashSet::new)
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,14 +18,15 @@
|
||||||
|
|
||||||
package org.keycloak.services.clienttype.client;
|
package org.keycloak.services.clienttype.client;
|
||||||
|
|
||||||
import java.util.function.Consumer;
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.keycloak.common.util.ObjectUtil;
|
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.delegate.ClientModelLazyDelegate;
|
import org.keycloak.models.delegate.ClientModelLazyDelegate;
|
||||||
import org.keycloak.client.clienttype.ClientType;
|
import org.keycloak.client.clienttype.ClientType;
|
||||||
import org.keycloak.client.clienttype.ClientTypeException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delegates to client-type and underlying delegate
|
* Delegates to client-type and underlying delegate
|
||||||
|
@ -47,45 +48,219 @@ public class TypeAwareClientModelDelegate extends ClientModelLazyDelegate {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isStandardFlowEnabled() {
|
public boolean isStandardFlowEnabled() {
|
||||||
return getBooleanProperty("standardFlowEnabled", super::isStandardFlowEnabled);
|
return TypedClientSimpleAttribute.STANDARD_FLOW_ENABLED
|
||||||
|
.getClientAttribute(clientType, super::isStandardFlowEnabled, Boolean.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setStandardFlowEnabled(boolean standardFlowEnabled) {
|
public void setStandardFlowEnabled(boolean standardFlowEnabled) {
|
||||||
setBooleanProperty("standardFlowEnabled", standardFlowEnabled, super::setStandardFlowEnabled);
|
TypedClientSimpleAttribute.STANDARD_FLOW_ENABLED
|
||||||
|
.setClientAttribute(clientType, standardFlowEnabled, super::setStandardFlowEnabled, Boolean.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
protected boolean getBooleanProperty(String propertyName, Supplier<Boolean> clientGetter) {
|
public boolean isBearerOnly() {
|
||||||
// Check if clientType supports the feature. If not, simply return false
|
return TypedClientSimpleAttribute.BEARER_ONLY
|
||||||
if (!clientType.isApplicable(propertyName)) {
|
.getClientAttribute(clientType, super::isBearerOnly, Boolean.class);
|
||||||
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
|
@Override
|
||||||
if (clientType.isReadOnly(propertyName)) {
|
public void setBearerOnly(boolean bearerOnly) {
|
||||||
return clientType.getDefaultValue(propertyName, Boolean.class);
|
TypedClientSimpleAttribute.BEARER_ONLY
|
||||||
|
.setClientAttribute(clientType, bearerOnly, super::setBearerOnly, Boolean.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delegate to clientGetter
|
@Override
|
||||||
return clientGetter.get();
|
public boolean isConsentRequired() {
|
||||||
|
return TypedClientSimpleAttribute.CONSENT_REQUIRED
|
||||||
|
.getClientAttribute(clientType, super::isConsentRequired, Boolean.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setBooleanProperty(String propertyName, Boolean newValue, Consumer<Boolean> clientSetter) {
|
@Override
|
||||||
// Check if clientType supports the feature. If not, return directly
|
public void setConsentRequired(boolean consentRequired) {
|
||||||
if (!clientType.isApplicable(propertyName)) {
|
TypedClientSimpleAttribute.CONSENT_REQUIRED
|
||||||
return;
|
.setClientAttribute(clientType, consentRequired, super::setConsentRequired, Boolean.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is read-only. If yes and there is an attempt to change some stuff, then throw an exception
|
@Override
|
||||||
if (clientType.isReadOnly(propertyName)) {
|
public boolean isDirectAccessGrantsEnabled() {
|
||||||
Boolean oldVal = clientType.getDefaultValue(propertyName, Boolean.class);
|
return TypedClientSimpleAttribute.DIRECT_ACCESS_GRANTS_ENABLED
|
||||||
if (!ObjectUtil.isEqualOrBothNull(oldVal, newValue)) {
|
.getClientAttribute(clientType, super::isDirectAccessGrantsEnabled, Boolean.class);
|
||||||
throw new ClientTypeException("Property " + propertyName + " of client " + getClientId() + " is read-only due to client type " + clientType.getName());
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setDirectAccessGrantsEnabled(boolean directAccessGrantsEnabled) {
|
||||||
|
TypedClientSimpleAttribute.DIRECT_ACCESS_GRANTS_ENABLED
|
||||||
|
.setClientAttribute(clientType, directAccessGrantsEnabled, super::setDirectAccessGrantsEnabled, Boolean.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAlwaysDisplayInConsole() {
|
||||||
|
return TypedClientSimpleAttribute.ALWAYS_DISPLAY_IN_CONSOLE
|
||||||
|
.getClientAttribute(clientType, super::isAlwaysDisplayInConsole, Boolean.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAlwaysDisplayInConsole(boolean alwaysDisplayInConsole) {
|
||||||
|
TypedClientSimpleAttribute.ALWAYS_DISPLAY_IN_CONSOLE
|
||||||
|
.setClientAttribute(clientType, alwaysDisplayInConsole, super::setAlwaysDisplayInConsole, Boolean.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isFrontchannelLogout() {
|
||||||
|
return TypedClientSimpleAttribute.FRONTCHANNEL_LOGOUT
|
||||||
|
.getClientAttribute(clientType, super::isFrontchannelLogout, Boolean.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setFrontchannelLogout(boolean frontchannelLogout) {
|
||||||
|
TypedClientSimpleAttribute.FRONTCHANNEL_LOGOUT
|
||||||
|
.setClientAttribute(clientType, frontchannelLogout, super::setFrontchannelLogout, Boolean.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isImplicitFlowEnabled() {
|
||||||
|
return TypedClientSimpleAttribute.IMPLICIT_FLOW_ENABLED
|
||||||
|
.getClientAttribute(clientType, super::isImplicitFlowEnabled, Boolean.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setImplicitFlowEnabled(boolean implicitFlowEnabled) {
|
||||||
|
TypedClientSimpleAttribute.IMPLICIT_FLOW_ENABLED
|
||||||
|
.setClientAttribute(clientType, implicitFlowEnabled, super::setImplicitFlowEnabled, Boolean.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isServiceAccountsEnabled() {
|
||||||
|
return TypedClientSimpleAttribute.SERVICE_ACCOUNTS_ENABLED
|
||||||
|
.getClientAttribute(clientType, super::isServiceAccountsEnabled, Boolean.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setServiceAccountsEnabled(boolean flag) {
|
||||||
|
TypedClientSimpleAttribute.SERVICE_ACCOUNTS_ENABLED
|
||||||
|
.setClientAttribute(clientType, flag, super::setServiceAccountsEnabled, Boolean.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getProtocol() {
|
||||||
|
return TypedClientSimpleAttribute.PROTOCOL
|
||||||
|
.getClientAttribute(clientType, super::getProtocol, String.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setProtocol(String protocol) {
|
||||||
|
TypedClientSimpleAttribute.PROTOCOL
|
||||||
|
.setClientAttribute(clientType, protocol, super::setProtocol, String.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPublicClient() {
|
||||||
|
return TypedClientSimpleAttribute.PUBLIC_CLIENT
|
||||||
|
.getClientAttribute(clientType, super::isPublicClient, Boolean.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPublicClient(boolean flag) {
|
||||||
|
TypedClientSimpleAttribute.PUBLIC_CLIENT
|
||||||
|
.setClientAttribute(clientType, flag, super::setPublicClient, Boolean.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> getWebOrigins() {
|
||||||
|
return TypedClientSimpleAttribute.WEB_ORIGINS
|
||||||
|
.getClientAttribute(clientType, super::getWebOrigins, Set.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setWebOrigins(Set<String> webOrigins) {
|
||||||
|
TypedClientSimpleAttribute.WEB_ORIGINS
|
||||||
|
.setClientAttribute(clientType, webOrigins, super::setWebOrigins, Set.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addWebOrigin(String webOrigin) {
|
||||||
|
TypedClientSimpleAttribute.WEB_ORIGINS
|
||||||
|
.setClientAttribute(clientType, webOrigin, super::addWebOrigin, String.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeWebOrigin(String webOrigin) {
|
||||||
|
TypedClientSimpleAttribute.WEB_ORIGINS
|
||||||
|
.setClientAttribute(clientType, null, (val) -> super.removeWebOrigin(webOrigin), String.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> getRedirectUris() {
|
||||||
|
return TypedClientSimpleAttribute.REDIRECT_URIS
|
||||||
|
.getClientAttribute(clientType, super::getRedirectUris, Set.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRedirectUris(Set<String> redirectUris) {
|
||||||
|
TypedClientSimpleAttribute.REDIRECT_URIS
|
||||||
|
.setClientAttribute(clientType, redirectUris, super::setRedirectUris, Set.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addRedirectUri(String redirectUri) {
|
||||||
|
TypedClientSimpleAttribute.REDIRECT_URIS
|
||||||
|
.setClientAttribute(clientType, redirectUri, super::addRedirectUri, String.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeRedirectUri(String redirectUri) {
|
||||||
|
TypedClientSimpleAttribute.REDIRECT_URIS
|
||||||
|
.setClientAttribute(clientType, null, (val) -> super.removeRedirectUri(redirectUri), String.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAttribute(String name, String value) {
|
||||||
|
TypedClientExtendedAttribute attribute = TypedClientExtendedAttribute.getAttributesByName().get(name);
|
||||||
|
if (attribute != null) {
|
||||||
|
attribute.setClientAttribute(clientType, value, (newValue) -> super.setAttribute(name, newValue), String.class);
|
||||||
|
} else {
|
||||||
|
super.setAttribute(name, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call clientSetter
|
@Override
|
||||||
clientSetter.accept(newValue);
|
public void removeAttribute(String name) {
|
||||||
|
TypedClientExtendedAttribute attribute = TypedClientExtendedAttribute.getAttributesByName().get(name);
|
||||||
|
if (attribute != null) {
|
||||||
|
attribute.setClientAttribute(clientType, null, (val) -> super.removeAttribute(name), String.class);
|
||||||
|
} else {
|
||||||
|
super.removeAttribute(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAttribute(String name) {
|
||||||
|
TypedClientExtendedAttribute attribute = TypedClientExtendedAttribute.getAttributesByName().get(name);
|
||||||
|
if (attribute != null) {
|
||||||
|
return attribute.getClientAttribute(clientType, () -> super.getAttribute(name), String.class);
|
||||||
|
} else {
|
||||||
|
return super.getAttribute(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, String> getAttributes() {
|
||||||
|
// Start with attributes set on the delegate.
|
||||||
|
Map<String, String> attributes = new HashMap<>(super.getAttributes());
|
||||||
|
|
||||||
|
// Get extended client type attributes and values from the client type configuration.
|
||||||
|
Set<String> extendedClientTypeAttributes =
|
||||||
|
clientType.getConfiguration().entrySet().stream()
|
||||||
|
.map(Map.Entry::getKey)
|
||||||
|
.filter(entry -> TypedClientExtendedAttribute.getAttributesByName().containsKey(entry))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
// Augment client type attributes on top of attributes on the delegate.
|
||||||
|
for (String entry : extendedClientTypeAttributes) {
|
||||||
|
attributes.put(entry, getAttribute(entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
package org.keycloak.services.clienttype.client;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.client.clienttype.ClientType;
|
||||||
|
import org.keycloak.client.clienttype.ClientTypeException;
|
||||||
|
import org.keycloak.common.util.ObjectUtil;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
enum TypedClientSimpleAttribute implements TypedClientAttribute {
|
||||||
|
// Top Level client attributes
|
||||||
|
STANDARD_FLOW_ENABLED("standardFlowEnabled", false),
|
||||||
|
BEARER_ONLY("bearerOnly", false),
|
||||||
|
CONSENT_REQUIRED("consentRequired", false),
|
||||||
|
DIRECT_ACCESS_GRANTS_ENABLED("directAccessGrantsEnabled", false),
|
||||||
|
ALWAYS_DISPLAY_IN_CONSOLE("alwaysDisplayInConsole", false),
|
||||||
|
FRONTCHANNEL_LOGOUT("frontchannelLogout", false),
|
||||||
|
IMPLICIT_FLOW_ENABLED("implicitFlowEnabled", false),
|
||||||
|
PROTOCOL("protocol", null),
|
||||||
|
PUBLIC_CLIENT("publicClient", false),
|
||||||
|
REDIRECT_URIS("redirectUris", Set.of()),
|
||||||
|
SERVICE_ACCOUNTS_ENABLED("serviceAccountsEnabled", false),
|
||||||
|
WEB_ORIGINS("webOrigins", Set.of()),
|
||||||
|
;
|
||||||
|
|
||||||
|
private final String propertyName;
|
||||||
|
private final Object nonApplicableValue;
|
||||||
|
|
||||||
|
TypedClientSimpleAttribute(String propertyName, Object nonApplicableValue) {
|
||||||
|
this.propertyName = propertyName;
|
||||||
|
this.nonApplicableValue = nonApplicableValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPropertyName() {
|
||||||
|
return propertyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getNonApplicableValue() {
|
||||||
|
return nonApplicableValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TypedClientExtendedAttribute implements TypedClientAttribute {
|
||||||
|
// Extended Client Type attributes defined as client attribute entities.
|
||||||
|
DEVICE_AUTHORIZATION_GRANT_ENABLED("oauth2.device.authorization.grant.enabled", "false"),
|
||||||
|
CIBA_GRANT_ENABLED("oidc.ciba.grant.enabled", "false"),
|
||||||
|
LOGIN_THEME("login_theme", null),
|
||||||
|
LOGO_URI("logoUri", null),
|
||||||
|
POLICY_URI("policyUri", null);
|
||||||
|
|
||||||
|
private static final Map<String, TypedClientExtendedAttribute> attributesByName = new HashMap<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
Arrays.stream(TypedClientExtendedAttribute.values())
|
||||||
|
.forEach(attribute -> attributesByName.put(attribute.getPropertyName(), attribute));
|
||||||
|
}
|
||||||
|
|
||||||
|
private final String propertyName;
|
||||||
|
private final Object nonApplicableValue;
|
||||||
|
|
||||||
|
TypedClientExtendedAttribute(String propertyName, Object nonApplicableValue) {
|
||||||
|
this.propertyName = propertyName;
|
||||||
|
this.nonApplicableValue = nonApplicableValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPropertyName() {
|
||||||
|
return propertyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getNonApplicableValue() {
|
||||||
|
return nonApplicableValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<String, TypedClientExtendedAttribute> getAttributesByName() {
|
||||||
|
return attributesByName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TypedClientAttribute {
|
||||||
|
Logger logger = Logger.getLogger(TypedClientAttribute.class);
|
||||||
|
|
||||||
|
default <T> T getClientAttribute(ClientType clientType, Supplier<T> clientGetter, Class<T> tClass) {
|
||||||
|
String propertyName = getPropertyName();
|
||||||
|
Object nonApplicableValue = getNonApplicableValue();
|
||||||
|
|
||||||
|
// Check if clientType supports the feature.
|
||||||
|
if (!clientType.isApplicable(propertyName)) {
|
||||||
|
try {
|
||||||
|
return tClass.cast(nonApplicableValue);
|
||||||
|
} catch (ClassCastException e) {
|
||||||
|
logger.error("Could not apply client type property %s: %s", propertyName, e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, tClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delegate to clientGetter
|
||||||
|
return clientGetter.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
default <T> void setClientAttribute(ClientType clientType, T newValue, Consumer<T> clientSetter, Class<T> tClass) {
|
||||||
|
String propertyName = getPropertyName();
|
||||||
|
Object nonApplicableValue = getNonApplicableValue();
|
||||||
|
// Check if clientType supports the feature. If not, return directly
|
||||||
|
if (!clientType.isApplicable(propertyName) && !Objects.equals(nonApplicableValue, newValue)) {
|
||||||
|
logger.warnf("Property %s is not-applicable to client type %s and can not be modified.", propertyName, clientType.getName());
|
||||||
|
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)) {
|
||||||
|
T oldVal = clientType.getDefaultValue(propertyName, tClass);
|
||||||
|
if (!ObjectUtil.isEqualOrBothNull(oldVal, newValue)) {
|
||||||
|
throw new ClientTypeException(
|
||||||
|
"Property " + propertyName + " is read-only due to client type " + clientType.getName(),
|
||||||
|
propertyName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delegate to clientSetter
|
||||||
|
clientSetter.accept(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
String getPropertyName();
|
||||||
|
Object getNonApplicableValue();
|
||||||
|
}
|
|
@ -18,40 +18,22 @@
|
||||||
|
|
||||||
package org.keycloak.services.clienttype.impl;
|
package org.keycloak.services.clienttype.impl;
|
||||||
|
|
||||||
import jakarta.ws.rs.core.Response;
|
|
||||||
import org.jboss.logging.Logger;
|
|
||||||
import org.keycloak.client.clienttype.ClientType;
|
import org.keycloak.client.clienttype.ClientType;
|
||||||
import org.keycloak.client.clienttype.ClientTypeException;
|
|
||||||
import org.keycloak.models.ClientModel;
|
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.representations.idm.ClientTypeRepresentation;
|
||||||
|
import org.keycloak.services.clienttype.client.TypeAwareClientModelDelegate;
|
||||||
|
|
||||||
import java.beans.PropertyDescriptor;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
public class DefaultClientType implements ClientType {
|
public class DefaultClientType implements ClientType {
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(DefaultClientType.class);
|
|
||||||
|
|
||||||
private final KeycloakSession session;
|
|
||||||
private final ClientTypeRepresentation clientType;
|
private final ClientTypeRepresentation clientType;
|
||||||
|
|
||||||
private final Map<String, PropertyDescriptor> clientRepresentationProperties;
|
public DefaultClientType(ClientTypeRepresentation clientType) {
|
||||||
|
|
||||||
public DefaultClientType(KeycloakSession session, ClientTypeRepresentation clientType, Map<String, PropertyDescriptor> clientRepresentationProperties) {
|
|
||||||
this.session = session;
|
|
||||||
this.clientType = clientType;
|
this.clientType = clientType;
|
||||||
this.clientRepresentationProperties = clientRepresentationProperties;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -83,103 +65,12 @@ public class DefaultClientType implements ClientType {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(ClientRepresentation createdClient) throws ClientTypeException {
|
public Map<String, ClientTypeRepresentation.PropertyConfig> getConfiguration() {
|
||||||
// Create empty client augmented with the applicable default client type values.
|
return clientType.getConfig();
|
||||||
ClientRepresentation defaultClientRep = augmentClient(new ClientRepresentation());
|
|
||||||
|
|
||||||
validateClientRequest(createdClient, defaultClientRep);
|
|
||||||
|
|
||||||
augmentClient(createdClient);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onUpdate(ClientModel currentClient, ClientRepresentation newClient) throws ClientTypeException {
|
public ClientModel augment(ClientModel client) {
|
||||||
ClientRepresentation currentRep = ModelToRepresentation.toRepresentation(currentClient, session);
|
return new TypeAwareClientModelDelegate(this, () -> client);
|
||||||
validateClientRequest(newClient, currentRep);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void validateClientRequest(ClientRepresentation newClient, ClientRepresentation currentClient) throws ClientTypeException {
|
|
||||||
List<String> validationErrors = clientType.getConfig().entrySet().stream()
|
|
||||||
.filter(property -> clientPropertyHasInvalidChangeRequested(currentClient, newClient, property.getKey(), property.getValue()))
|
|
||||||
.map(Map.Entry::getKey)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
if (validationErrors.size() > 0) {
|
|
||||||
throw new ClientTypeException(
|
|
||||||
"Cannot change property of client as it is not allowed by the specified client type.",
|
|
||||||
validationErrors.toArray());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected ClientRepresentation augmentClient(ClientRepresentation client) {
|
|
||||||
clientType.getConfig().entrySet()
|
|
||||||
.forEach(property -> setClientProperty(client, property.getKey(), property.getValue()));
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean clientPropertyHasInvalidChangeRequested(
|
|
||||||
ClientRepresentation oldClient,
|
|
||||||
ClientRepresentation newClient,
|
|
||||||
String propertyName,
|
|
||||||
ClientTypeRepresentation.PropertyConfig propertyConfig) {
|
|
||||||
Object newClientProperty = getClientProperty(newClient, propertyName);
|
|
||||||
Object oldClientProperty = getClientProperty(oldClient, propertyName);
|
|
||||||
|
|
||||||
return (
|
|
||||||
// Validate that non-applicable client properties were not changed.
|
|
||||||
!propertyConfig.getApplicable() &&
|
|
||||||
!Objects.isNull(newClientProperty) &&
|
|
||||||
!Objects.equals(oldClientProperty, newClientProperty)
|
|
||||||
) || (
|
|
||||||
// Validate that applicable read-only client properties were not changed.
|
|
||||||
propertyConfig.getApplicable() &&
|
|
||||||
propertyConfig.getReadOnly() &&
|
|
||||||
!Objects.isNull(newClientProperty) &&
|
|
||||||
!Objects.equals(oldClientProperty, newClientProperty)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setClientProperty(ClientRepresentation client,
|
|
||||||
String propertyName,
|
|
||||||
ClientTypeRepresentation.PropertyConfig propertyConfig) {
|
|
||||||
|
|
||||||
if (!propertyConfig.getApplicable() || propertyConfig.getDefaultValue() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clientRepresentationProperties.containsKey(propertyName)) {
|
|
||||||
// Java property on client representation
|
|
||||||
Method setter = clientRepresentationProperties.get(propertyName).getWriteMethod();
|
|
||||||
try {
|
|
||||||
setter.invoke(client, propertyConfig.getDefaultValue());
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.warnf("Cannot set property '%s' on client with value '%s'. Check configuration of the client type '%s'", propertyName, propertyConfig.getDefaultValue(), clientType.getName());
|
|
||||||
throw new ClientTypeException("Cannot set property on client", e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Client attribute
|
|
||||||
if (client.getAttributes() == null) {
|
|
||||||
client.setAttributes(new HashMap<>());
|
|
||||||
}
|
|
||||||
client.getAttributes().put(propertyName, propertyConfig.getDefaultValue().toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -18,11 +18,9 @@
|
||||||
|
|
||||||
package org.keycloak.services.clienttype.impl;
|
package org.keycloak.services.clienttype.impl;
|
||||||
|
|
||||||
import java.beans.PropertyDescriptor;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.models.KeycloakSession;
|
|
||||||
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;
|
||||||
|
@ -35,17 +33,9 @@ public class DefaultClientTypeProvider implements ClientTypeProvider {
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(DefaultClientTypeProvider.class);
|
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
|
@Override
|
||||||
public ClientType getClientType(ClientTypeRepresentation clientTypeRep) {
|
public ClientType getClientType(ClientTypeRepresentation clientTypeRep) {
|
||||||
return new DefaultClientType(session, clientTypeRep, clientRepresentationProperties);
|
return new DefaultClientType(clientTypeRep);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -18,23 +18,11 @@
|
||||||
|
|
||||||
package org.keycloak.services.clienttype.impl;
|
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.Config;
|
||||||
import org.keycloak.common.Profile;
|
import org.keycloak.common.Profile;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
|
||||||
import org.keycloak.client.clienttype.ClientTypeProvider;
|
import org.keycloak.client.clienttype.ClientTypeProvider;
|
||||||
import org.keycloak.client.clienttype.ClientTypeProviderFactory;
|
import org.keycloak.client.clienttype.ClientTypeProviderFactory;
|
||||||
|
|
||||||
|
@ -45,41 +33,13 @@ public class DefaultClientTypeProviderFactory implements ClientTypeProviderFacto
|
||||||
|
|
||||||
public static final String PROVIDER_ID = "default";
|
public static final String PROVIDER_ID = "default";
|
||||||
|
|
||||||
private Map<String, PropertyDescriptor> clientRepresentationProperties;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ClientTypeProvider create(KeycloakSession session) {
|
public ClientTypeProvider create(KeycloakSession session) {
|
||||||
return new DefaultClientTypeProvider(session, clientRepresentationProperties);
|
return new DefaultClientTypeProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(Config.Scope config) {
|
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
|
@Override
|
||||||
public void postInit(KeycloakSessionFactory factory) {
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
|
|
|
@ -42,8 +42,6 @@ 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;
|
||||||
|
@ -82,11 +80,6 @@ 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);
|
||||||
|
|
||||||
|
|
|
@ -152,17 +152,6 @@ 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 -> {
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
"config": {
|
"config": {
|
||||||
"standardFlowEnabled": {
|
"standardFlowEnabled": {
|
||||||
"applicable": true,
|
"applicable": true,
|
||||||
"read-only": true,
|
"default-value": true,
|
||||||
"default-value": true
|
"read-only": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -18,57 +18,67 @@
|
||||||
"alwaysDisplayInConsole": {
|
"alwaysDisplayInConsole": {
|
||||||
"applicable": false
|
"applicable": false
|
||||||
},
|
},
|
||||||
|
"authorizationServicesEnabled": {
|
||||||
|
"applicable": false
|
||||||
|
},
|
||||||
|
"bearerOnly": {
|
||||||
|
"applicable": false
|
||||||
|
},
|
||||||
"consentRequired": {
|
"consentRequired": {
|
||||||
"applicable": true,
|
"applicable": true,
|
||||||
"read-only": true,
|
"default-value": false,
|
||||||
"default-value": false
|
"read-only": true
|
||||||
|
},
|
||||||
|
"directAccessGrantsEnabled": {
|
||||||
|
"applicable": false
|
||||||
|
},
|
||||||
|
"frontchannelLogout": {
|
||||||
|
"applicable": false
|
||||||
|
},
|
||||||
|
"implicitFlowEnabled": {
|
||||||
|
"applicable": false
|
||||||
},
|
},
|
||||||
"login_theme": {
|
"login_theme": {
|
||||||
"applicable": false
|
"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
|
|
||||||
},
|
|
||||||
"logoUri": {
|
"logoUri": {
|
||||||
"applicable": false
|
"applicable": false
|
||||||
},
|
},
|
||||||
|
"oauth2.device.authorization.grant.enabled": {
|
||||||
|
"applicable": false
|
||||||
|
},
|
||||||
|
"oidc.ciba.grant.enabled": {
|
||||||
|
"applicable": false
|
||||||
|
},
|
||||||
"policyUri": {
|
"policyUri": {
|
||||||
"applicable": false
|
"applicable": false
|
||||||
},
|
},
|
||||||
|
"protocol": {
|
||||||
|
"applicable": true,
|
||||||
|
"default-value": "openid-connect",
|
||||||
|
"read-only": true
|
||||||
|
},
|
||||||
|
"publicClient": {
|
||||||
|
"applicable": true,
|
||||||
|
"default-value": false,
|
||||||
|
"read-only": true
|
||||||
|
},
|
||||||
|
"redirectUris": {
|
||||||
|
"applicable": false
|
||||||
|
},
|
||||||
|
"serviceAccountsEnabled": {
|
||||||
|
"applicable": true,
|
||||||
|
"default-value": true,
|
||||||
|
"read-only": true
|
||||||
|
},
|
||||||
|
"standardFlowEnabled": {
|
||||||
|
"applicable": false
|
||||||
|
},
|
||||||
"tosUri": {
|
"tosUri": {
|
||||||
"applicable": false
|
"applicable": false
|
||||||
|
},
|
||||||
|
"webOrigins": {
|
||||||
|
"applicable": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,14 +126,10 @@ public class ClientTypesTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
clientRep.setServiceAccountsEnabled(true);
|
clientRep.setServiceAccountsEnabled(true);
|
||||||
|
|
||||||
// Adding non-applicable attribute should not fail
|
// Adding non-applicable attribute should not fail but not update client attribute
|
||||||
clientRep.getAttributes().put(ClientModel.LOGO_URI, "https://foo");
|
clientRep.getAttributes().put(ClientModel.LOGO_URI, "https://foo");
|
||||||
try {
|
|
||||||
testRealm().clients().get(clientRep.getId()).update(clientRep);
|
testRealm().clients().get(clientRep.getId()).update(clientRep);
|
||||||
Assert.fail("Not expected to update client");
|
assertEquals(testRealm().clients().get(clientRep.getId()).toRepresentation().getAttributes().get(ClientModel.LOGO_URI), null);
|
||||||
} catch (BadRequestException bre) {
|
|
||||||
assertErrorResponseContainsParams(bre.getResponse(), "logoUri");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update of supported attribute should be successful
|
// Update of supported attribute should be successful
|
||||||
clientRep.getAttributes().remove(ClientModel.LOGO_URI);
|
clientRep.getAttributes().remove(ClientModel.LOGO_URI);
|
||||||
|
@ -180,7 +176,7 @@ public class ClientTypesTest extends AbstractTestRealmKeycloakTest {
|
||||||
assertEquals("default", serviceAccountType.getProvider());
|
assertEquals("default", serviceAccountType.getProvider());
|
||||||
|
|
||||||
ClientTypeRepresentation.PropertyConfig cfg = serviceAccountType.getConfig().get("standardFlowEnabled");
|
ClientTypeRepresentation.PropertyConfig cfg = serviceAccountType.getConfig().get("standardFlowEnabled");
|
||||||
assertPropertyConfig("standardFlowEnabled", cfg, true, true, false);
|
assertPropertyConfig("standardFlowEnabled", cfg, false, null, null);
|
||||||
|
|
||||||
cfg = serviceAccountType.getConfig().get("serviceAccountsEnabled");
|
cfg = serviceAccountType.getConfig().get("serviceAccountsEnabled");
|
||||||
assertPropertyConfig("serviceAccountsEnabled", cfg, true, true, true);
|
assertPropertyConfig("serviceAccountsEnabled", cfg, true, true, true);
|
||||||
|
|
Loading…
Reference in a new issue