KEYCLOAK-14553 Client map store

Co-Authored-By: vramik <vramik@redhat.com>
This commit is contained in:
Hynek Mlnarik 2020-06-22 22:20:19 +02:00 committed by Hynek Mlnařík
parent 2c29c58af1
commit ac0011ab6f
25 changed files with 2145 additions and 7 deletions

View file

@ -0,0 +1,508 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.client;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.map.common.AbstractEntity;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
*
* @author hmlnarik
*/
public abstract class AbstractClientEntity<K> implements AbstractEntity<K> {
private K id;
private String realmId;
private String clientId;
private String name;
private String description;
private Set<String> redirectUris = new HashSet<>();
private boolean enabled;
private boolean alwaysDisplayInConsole;
private String clientAuthenticatorType;
private String secret;
private String registrationToken;
private String protocol;
private Map<String, String> attributes = new HashMap<>();
private Map<String, String> authFlowBindings = new HashMap<>();
private boolean publicClient;
private boolean fullScopeAllowed;
private boolean frontchannelLogout;
private int notBefore;
private Set<String> scope = new HashSet<>();
private Set<String> webOrigins = new HashSet<>();
private Map<String, ProtocolMapperModel> protocolMappers = new HashMap<>();
private Map<String, Boolean> clientScopes = new HashMap<>();
private Set<String> scopeMappings = new LinkedHashSet<>();
private List<String> defaultRoles = new LinkedList<>();
private boolean surrogateAuthRequired;
private String managementUrl;
private String rootUrl;
private String baseUrl;
private boolean bearerOnly;
private boolean consentRequired;
private boolean standardFlowEnabled;
private boolean implicitFlowEnabled;
private boolean directAccessGrantsEnabled;
private boolean serviceAccountsEnabled;
private int nodeReRegistrationTimeout;
/**
* Flag signalizing that any of the setters has been meaningfully used.
*/
protected boolean updated;
protected AbstractClientEntity() {
this.id = null;
this.realmId = null;
}
public AbstractClientEntity(K id, String realmId) {
Objects.requireNonNull(id, "id");
Objects.requireNonNull(realmId, "realmId");
this.id = id;
this.realmId = realmId;
}
@Override
public K getId() {
return this.id;
}
@Override
public boolean isUpdated() {
return this.updated;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.updated |= ! Objects.equals(this.clientId, clientId);
this.clientId = clientId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.updated |= ! Objects.equals(this.name, name);
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.updated |= ! Objects.equals(this.description, description);
this.description = description;
}
public Set<String> getRedirectUris() {
return redirectUris;
}
public void setRedirectUris(Set<String> redirectUris) {
this.updated |= ! Objects.equals(this.redirectUris, redirectUris);
this.redirectUris = redirectUris;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.updated |= ! Objects.equals(this.enabled, enabled);
this.enabled = enabled;
}
public boolean isAlwaysDisplayInConsole() {
return alwaysDisplayInConsole;
}
public void setAlwaysDisplayInConsole(boolean alwaysDisplayInConsole) {
this.updated |= ! Objects.equals(this.alwaysDisplayInConsole, alwaysDisplayInConsole);
this.alwaysDisplayInConsole = alwaysDisplayInConsole;
}
public String getClientAuthenticatorType() {
return clientAuthenticatorType;
}
public void setClientAuthenticatorType(String clientAuthenticatorType) {
this.updated |= ! Objects.equals(this.clientAuthenticatorType, clientAuthenticatorType);
this.clientAuthenticatorType = clientAuthenticatorType;
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.updated |= ! Objects.equals(this.secret, secret);
this.secret = secret;
}
public String getRegistrationToken() {
return registrationToken;
}
public void setRegistrationToken(String registrationToken) {
this.updated |= ! Objects.equals(this.registrationToken, registrationToken);
this.registrationToken = registrationToken;
}
public String getProtocol() {
return protocol;
}
public void setProtocol(String protocol) {
this.updated |= ! Objects.equals(this.protocol, protocol);
this.protocol = protocol;
}
public Map<String, String> getAttributes() {
return attributes;
}
public void setAttributes(Map<String, String> attributes) {
this.updated |= ! Objects.equals(this.attributes, attributes);
this.attributes = attributes;
}
public Map<String, String> getAuthFlowBindings() {
return authFlowBindings;
}
public void setAuthFlowBindings(Map<String, String> authFlowBindings) {
this.updated |= ! Objects.equals(this.authFlowBindings, authFlowBindings);
this.authFlowBindings = authFlowBindings;
}
public boolean isPublicClient() {
return publicClient;
}
public void setPublicClient(boolean publicClient) {
this.updated |= ! Objects.equals(this.publicClient, publicClient);
this.publicClient = publicClient;
}
public boolean isFullScopeAllowed() {
return fullScopeAllowed;
}
public void setFullScopeAllowed(boolean fullScopeAllowed) {
this.updated |= ! Objects.equals(this.fullScopeAllowed, fullScopeAllowed);
this.fullScopeAllowed = fullScopeAllowed;
}
public boolean isFrontchannelLogout() {
return frontchannelLogout;
}
public void setFrontchannelLogout(boolean frontchannelLogout) {
this.updated |= ! Objects.equals(this.frontchannelLogout, frontchannelLogout);
this.frontchannelLogout = frontchannelLogout;
}
public int getNotBefore() {
return notBefore;
}
public void setNotBefore(int notBefore) {
this.updated |= ! Objects.equals(this.notBefore, notBefore);
this.notBefore = notBefore;
}
public Set<String> getScope() {
return scope;
}
public void setScope(Set<String> scope) {
this.updated |= ! Objects.equals(this.scope, scope);
this.scope.clear();
this.scope.addAll(scope);
}
public Set<String> getWebOrigins() {
return webOrigins;
}
public void setWebOrigins(Set<String> webOrigins) {
this.updated |= ! Objects.equals(this.webOrigins, webOrigins);
this.webOrigins.clear();
this.webOrigins.addAll(webOrigins);
}
public ProtocolMapperModel addProtocolMapper(ProtocolMapperModel model) {
Objects.requireNonNull(model.getId(), "protocolMapper.id");
updated = true;
this.protocolMappers.put(model.getId(), model);
return model;
}
public Collection<ProtocolMapperModel> getProtocolMappers() {
return protocolMappers.values();
}
public void updateProtocolMapper(String id, ProtocolMapperModel mapping) {
updated = true;
protocolMappers.put(id, mapping);
}
public void removeProtocolMapper(String id) {
updated |= protocolMappers.remove(id) != null;
}
public void setProtocolMappers(Collection<ProtocolMapperModel> protocolMappers) {
this.updated |= ! Objects.equals(this.protocolMappers, protocolMappers);
this.protocolMappers.clear();
this.protocolMappers.putAll(protocolMappers.stream().collect(Collectors.toMap(ProtocolMapperModel::getId, Function.identity())));
}
public ProtocolMapperModel getProtocolMapperById(String id) {
return id == null ? null : protocolMappers.get(id);
}
public boolean isSurrogateAuthRequired() {
return surrogateAuthRequired;
}
public void setSurrogateAuthRequired(boolean surrogateAuthRequired) {
this.updated |= ! Objects.equals(this.surrogateAuthRequired, surrogateAuthRequired);
this.surrogateAuthRequired = surrogateAuthRequired;
}
public String getManagementUrl() {
return managementUrl;
}
public void setManagementUrl(String managementUrl) {
this.updated |= ! Objects.equals(this.managementUrl, managementUrl);
this.managementUrl = managementUrl;
}
public String getRootUrl() {
return rootUrl;
}
public void setRootUrl(String rootUrl) {
this.updated |= ! Objects.equals(this.rootUrl, rootUrl);
this.rootUrl = rootUrl;
}
public String getBaseUrl() {
return baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.updated |= ! Objects.equals(this.baseUrl, baseUrl);
this.baseUrl = baseUrl;
}
public List<String> getDefaultRoles() {
return defaultRoles;
}
public void setDefaultRoles(Collection<String> defaultRoles) {
this.updated |= ! Objects.equals(this.defaultRoles, defaultRoles);
this.defaultRoles.clear();
this.defaultRoles.addAll(defaultRoles);
}
public void addDefaultRole(String name) {
updated = true;
if (name != null) {
defaultRoles.add(name);
}
}
public void removeDefaultRoles(String... defaultRoles) {
for (String defaultRole : defaultRoles) {
updated |= this.defaultRoles.remove(defaultRole);
}
}
public boolean isBearerOnly() {
return bearerOnly;
}
public void setBearerOnly(boolean bearerOnly) {
this.updated |= ! Objects.equals(this.bearerOnly, bearerOnly);
this.bearerOnly = bearerOnly;
}
public boolean isConsentRequired() {
return consentRequired;
}
public void setConsentRequired(boolean consentRequired) {
this.updated |= ! Objects.equals(this.consentRequired, consentRequired);
this.consentRequired = consentRequired;
}
public boolean isStandardFlowEnabled() {
return standardFlowEnabled;
}
public void setStandardFlowEnabled(boolean standardFlowEnabled) {
this.updated |= ! Objects.equals(this.standardFlowEnabled, standardFlowEnabled);
this.standardFlowEnabled = standardFlowEnabled;
}
public boolean isImplicitFlowEnabled() {
return implicitFlowEnabled;
}
public void setImplicitFlowEnabled(boolean implicitFlowEnabled) {
this.updated |= ! Objects.equals(this.implicitFlowEnabled, implicitFlowEnabled);
this.implicitFlowEnabled = implicitFlowEnabled;
}
public boolean isDirectAccessGrantsEnabled() {
return directAccessGrantsEnabled;
}
public void setDirectAccessGrantsEnabled(boolean directAccessGrantsEnabled) {
this.updated |= ! Objects.equals(this.directAccessGrantsEnabled, directAccessGrantsEnabled);
this.directAccessGrantsEnabled = directAccessGrantsEnabled;
}
public boolean isServiceAccountsEnabled() {
return serviceAccountsEnabled;
}
public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) {
this.updated |= ! Objects.equals(this.serviceAccountsEnabled, serviceAccountsEnabled);
this.serviceAccountsEnabled = serviceAccountsEnabled;
}
public int getNodeReRegistrationTimeout() {
return nodeReRegistrationTimeout;
}
public void setNodeReRegistrationTimeout(int nodeReRegistrationTimeout) {
this.updated |= ! Objects.equals(this.nodeReRegistrationTimeout, nodeReRegistrationTimeout);
this.nodeReRegistrationTimeout = nodeReRegistrationTimeout;
}
public void addWebOrigin(String webOrigin) {
updated = true;
this.webOrigins.add(webOrigin);
}
public void removeWebOrigin(String webOrigin) {
updated |= this.webOrigins.remove(webOrigin);
}
public void addRedirectUri(String redirectUri) {
this.updated |= ! this.redirectUris.contains(redirectUri);
this.redirectUris.add(redirectUri);
}
public void removeRedirectUri(String redirectUri) {
updated |= this.redirectUris.remove(redirectUri);
}
public void setAttribute(String name, String value) {
this.updated = true;
this.attributes.put(name, value);
}
public void removeAttribute(String name) {
this.updated |= this.attributes.remove(name) != null;
}
public String getAttribute(String name) {
return this.attributes.get(name);
}
public String getAuthenticationFlowBindingOverride(String binding) {
return this.authFlowBindings.get(binding);
}
public Map<String, String> getAuthenticationFlowBindingOverrides() {
return this.authFlowBindings;
}
public void removeAuthenticationFlowBindingOverride(String binding) {
updated |= this.authFlowBindings.remove(binding) != null;
}
public void setAuthenticationFlowBindingOverride(String binding, String flowId) {
this.updated = true;
this.authFlowBindings.put(binding, flowId);
}
public Collection<String> getScopeMappings() {
return scopeMappings;
}
public void addScopeMapping(String id) {
if (id != null) {
updated = true;
scopeMappings.add(id);
}
}
public void deleteScopeMapping(String id) {
updated |= scopeMappings.remove(id);
}
public void addClientScope(String id, boolean defaultScope) {
if (id != null) {
updated = true;
this.clientScopes.put(id, defaultScope);
}
}
public void removeClientScope(String id) {
if (id != null) {
updated |= clientScopes.remove(id) != null;
}
}
public Stream<String> getClientScopes(boolean defaultScope) {
return this.clientScopes.entrySet().stream()
.filter(me -> Objects.equals(me.getValue(), defaultScope))
.map(Entry::getKey);
}
public String getRealmId() {
return this.realmId;
}
}

View file

@ -0,0 +1,111 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.client;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.map.common.AbstractEntity;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
*
* @author hmlnarik
*/
public abstract class AbstractClientModel<E extends AbstractEntity> implements ClientModel {
protected final KeycloakSession session;
protected final RealmModel realm;
protected final E entity;
public AbstractClientModel(KeycloakSession session, RealmModel realm, E entity) {
Objects.requireNonNull(entity, "entity");
Objects.requireNonNull(realm, "realm");
this.session = session;
this.realm = realm;
this.entity = entity;
}
@Override
public void addClientScopes(Set<ClientScopeModel> clientScopes, boolean defaultScope) {
for (ClientScopeModel cs : clientScopes) {
addClientScope(cs, defaultScope);
}
}
@Override
public Set<RoleModel> getRealmScopeMappings() {
String realmId = realm.getId();
return getScopeMappingsStream()
.filter(rm -> Objects.equals(rm.getContainerId(), realmId))
.collect(Collectors.toSet());
}
@Override
public RoleModel getRole(String name) {
return session.realms().getClientRole(realm, this, name);
}
@Override
public RoleModel addRole(String name) {
return session.realms().addClientRole(realm, this, name);
}
@Override
public RoleModel addRole(String id, String name) {
return session.realms().addClientRole(realm, this, id, name);
}
@Override
public boolean removeRole(RoleModel role) {
return session.realms().removeRole(realm, role);
}
@Override
public Set<RoleModel> getRoles() {
return session.realms().getClientRoles(realm, this);
}
@Override
public Set<RoleModel> getRoles(Integer firstResult, Integer maxResults) {
return session.realms().getClientRoles(realm, this, firstResult, maxResults);
}
@Override
public Set<RoleModel> searchForRoles(String search, Integer first, Integer max) {
return session.realms().searchForClientRoles(realm, this, search, first, max);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ClientModel)) return false;
ClientModel that = (ClientModel) o;
return Objects.equals(that.getId(), getId());
}
@Override
public int hashCode() {
return getId().hashCode();
}
}

View file

@ -0,0 +1,540 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.client;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import com.google.common.base.Functions;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
*
* @author hmlnarik
*/
public abstract class MapClientAdapter extends AbstractClientModel<MapClientEntity> implements ClientModel {
public MapClientAdapter(KeycloakSession session, RealmModel realm, MapClientEntity entity) {
super(session, realm, entity);
}
@Override
public String getId() {
return entity.getId().toString();
}
@Override
public String getClientId() {
return entity.getClientId();
}
@Override
public void setClientId(String clientId) {
entity.setClientId(clientId);
}
@Override
public String getName() {
return entity.getName();
}
@Override
public void setName(String name) {
entity.setName(name);
}
@Override
public String getDescription() {
return entity.getDescription();
}
@Override
public void setDescription(String description) {
entity.setDescription(description);
}
@Override
public boolean isEnabled() {
return entity.isEnabled();
}
@Override
public void setEnabled(boolean enabled) {
entity.setEnabled(enabled);
}
@Override
public boolean isAlwaysDisplayInConsole() {
return entity.isAlwaysDisplayInConsole();
}
@Override
public void setAlwaysDisplayInConsole(boolean alwaysDisplayInConsole) {
entity.setAlwaysDisplayInConsole(alwaysDisplayInConsole);
}
@Override
public boolean isSurrogateAuthRequired() {
return entity.isSurrogateAuthRequired();
}
@Override
public void setSurrogateAuthRequired(boolean surrogateAuthRequired) {
entity.setSurrogateAuthRequired(surrogateAuthRequired);
}
@Override
public Set<String> getWebOrigins() {
return entity.getWebOrigins();
}
@Override
public void setWebOrigins(Set<String> webOrigins) {
entity.setWebOrigins(webOrigins);
}
@Override
public void addWebOrigin(String webOrigin) {
entity.addWebOrigin(webOrigin);
}
@Override
public void removeWebOrigin(String webOrigin) {
entity.removeWebOrigin(webOrigin);
}
@Override
public Set<String> getRedirectUris() {
return entity.getRedirectUris();
}
@Override
public void setRedirectUris(Set<String> redirectUris) {
entity.setRedirectUris(redirectUris);
}
@Override
public void addRedirectUri(String redirectUri) {
entity.addRedirectUri(redirectUri);
}
@Override
public void removeRedirectUri(String redirectUri) {
entity.removeRedirectUri(redirectUri);
}
@Override
public String getManagementUrl() {
return entity.getManagementUrl();
}
@Override
public void setManagementUrl(String url) {
entity.setManagementUrl(url);
}
@Override
public String getRootUrl() {
return entity.getRootUrl();
}
@Override
public void setRootUrl(String url) {
entity.setRootUrl(url);
}
@Override
public String getBaseUrl() {
return entity.getBaseUrl();
}
@Override
public void setBaseUrl(String url) {
entity.setBaseUrl(url);
}
@Override
public boolean isBearerOnly() {
return entity.isBearerOnly();
}
@Override
public void setBearerOnly(boolean only) {
entity.setBearerOnly(only);
}
@Override
public String getClientAuthenticatorType() {
return entity.getClientAuthenticatorType();
}
@Override
public void setClientAuthenticatorType(String clientAuthenticatorType) {
entity.setClientAuthenticatorType(clientAuthenticatorType);
}
@Override
public boolean validateSecret(String secret) {
return MessageDigest.isEqual(secret.getBytes(), entity.getSecret().getBytes());
}
@Override
public String getSecret() {
return entity.getSecret();
}
@Override
public void setSecret(String secret) {
entity.setSecret(secret);
}
@Override
public int getNodeReRegistrationTimeout() {
return entity.getNodeReRegistrationTimeout();
}
@Override
public void setNodeReRegistrationTimeout(int timeout) {
entity.setNodeReRegistrationTimeout(timeout);
}
@Override
public String getRegistrationToken() {
return entity.getRegistrationToken();
}
@Override
public void setRegistrationToken(String registrationToken) {
entity.setRegistrationToken(registrationToken);
}
@Override
public String getProtocol() {
return entity.getProtocol();
}
@Override
public void setProtocol(String protocol) {
entity.setProtocol(protocol);
}
@Override
public void setAttribute(String name, String value) {
entity.setAttribute(name, value);
}
@Override
public void removeAttribute(String name) {
entity.removeAttribute(name);
}
@Override
public String getAttribute(String name) {
return entity.getAttribute(name);
}
@Override
public Map<String, String> getAttributes() {
return entity.getAttributes();
}
@Override
public String getAuthenticationFlowBindingOverride(String binding) {
return entity.getAuthenticationFlowBindingOverride(binding);
}
@Override
public Map<String, String> getAuthenticationFlowBindingOverrides() {
return entity.getAuthenticationFlowBindingOverrides();
}
@Override
public void removeAuthenticationFlowBindingOverride(String binding) {
entity.removeAuthenticationFlowBindingOverride(binding);
}
@Override
public void setAuthenticationFlowBindingOverride(String binding, String flowId) {
entity.setAuthenticationFlowBindingOverride(binding, flowId);
}
@Override
public boolean isFrontchannelLogout() {
return entity.isFrontchannelLogout();
}
@Override
public void setFrontchannelLogout(boolean flag) {
entity.setFrontchannelLogout(flag);
}
@Override
public boolean isFullScopeAllowed() {
return entity.isFullScopeAllowed();
}
@Override
public void setFullScopeAllowed(boolean value) {
entity.setFullScopeAllowed(value);
}
@Override
public boolean isPublicClient() {
return entity.isPublicClient();
}
@Override
public void setPublicClient(boolean flag) {
entity.setPublicClient(flag);
}
@Override
public boolean isConsentRequired() {
return entity.isConsentRequired();
}
@Override
public void setConsentRequired(boolean consentRequired) {
entity.setConsentRequired(consentRequired);
}
@Override
public boolean isStandardFlowEnabled() {
return entity.isStandardFlowEnabled();
}
@Override
public void setStandardFlowEnabled(boolean standardFlowEnabled) {
entity.setStandardFlowEnabled(standardFlowEnabled);
}
@Override
public boolean isImplicitFlowEnabled() {
return entity.isImplicitFlowEnabled();
}
@Override
public void setImplicitFlowEnabled(boolean implicitFlowEnabled) {
entity.setImplicitFlowEnabled(implicitFlowEnabled);
}
@Override
public boolean isDirectAccessGrantsEnabled() {
return entity.isDirectAccessGrantsEnabled();
}
@Override
public void setDirectAccessGrantsEnabled(boolean directAccessGrantsEnabled) {
entity.setDirectAccessGrantsEnabled(directAccessGrantsEnabled);
}
@Override
public boolean isServiceAccountsEnabled() {
return entity.isServiceAccountsEnabled();
}
@Override
public void setServiceAccountsEnabled(boolean serviceAccountsEnabled) {
entity.setServiceAccountsEnabled(serviceAccountsEnabled);
}
@Override
public RealmModel getRealm() {
return realm;
}
@Override
public int getNotBefore() {
return entity.getNotBefore();
}
@Override
public void setNotBefore(int notBefore) {
entity.setNotBefore(notBefore);
}
/*************** Client scopes ****************/
@Override
public void addClientScope(ClientScopeModel clientScope, boolean defaultScope) {
final String id = clientScope == null ? null : clientScope.getId();
if (id != null) {
entity.addClientScope(id, defaultScope);
}
}
@Override
public void removeClientScope(ClientScopeModel clientScope) {
final String id = clientScope == null ? null : clientScope.getId();
if (id != null) {
entity.removeClientScope(id);
}
}
@Override
public Map<String, ClientScopeModel> getClientScopes(boolean defaultScope, boolean filterByProtocol) {
Stream<ClientScopeModel> res = this.entity.getClientScopes(defaultScope)
.map(realm::getClientScopeById)
.filter(Objects::nonNull);
if (filterByProtocol) {
String clientProtocol = getProtocol() == null ? OIDCLoginProtocol.LOGIN_PROTOCOL : getProtocol();
res = res.filter(cs -> Objects.equals(cs.getProtocol(), clientProtocol));
}
return res.collect(Collectors.toMap(ClientScopeModel::getName, Functions.identity()));
}
/*************** Scopes mappings ****************/
@Override
public Stream<RoleModel> getScopeMappingsStream() {
return this.entity.getScopeMappings().stream()
.map(realm::getRoleById)
.filter(Objects::nonNull);
}
@Override
public void addScopeMapping(RoleModel role) {
final String id = role == null ? null : role.getId();
if (id != null) {
this.entity.addScopeMapping(id);
}
}
@Override
public void deleteScopeMapping(RoleModel role) {
final String id = role == null ? null : role.getId();
if (id != null) {
this.entity.deleteScopeMapping(id);
}
}
@Override
public boolean hasScope(RoleModel role) {
if (isFullScopeAllowed()) return true;
final String id = role == null ? null : role.getId();
if (id != null && this.entity.getScopeMappings().contains(id)) {
return true;
}
if (getScopeMappingsStream().anyMatch(r -> r.hasRole(role))) {
return true;
}
Set<RoleModel> roles = getRoles();
if (roles.contains(role)) return true;
return roles.stream().anyMatch(r -> r.hasRole(role));
}
/*************** Default roles ****************/
@Override
public List<String> getDefaultRoles() {
return entity.getDefaultRoles();
}
@Override
public void addDefaultRole(String name) {
RoleModel role = getRole(name);
if (role == null) {
addRole(name);
}
this.entity.addDefaultRole(name);
}
@Override
public void removeDefaultRoles(String... defaultRoles) {
this.entity.removeDefaultRoles(defaultRoles);
}
/*************** Protocol mappers ****************/
@Override
public Set<ProtocolMapperModel> getProtocolMappers() {
return Collections.unmodifiableSet(new HashSet<>(entity.getProtocolMappers()));
}
@Override
public ProtocolMapperModel addProtocolMapper(ProtocolMapperModel model) {
if (model == null) {
return null;
}
ProtocolMapperModel pm = new ProtocolMapperModel();
pm.setId(KeycloakModelUtils.generateId());
pm.setName(model.getName());
pm.setProtocol(model.getProtocol());
pm.setProtocolMapper(model.getProtocolMapper());
if (model.getConfig() != null) {
pm.setConfig(new HashMap<>(model.getConfig()));
} else {
pm.setConfig(new HashMap<>());
}
return entity.addProtocolMapper(pm);
}
@Override
public void removeProtocolMapper(ProtocolMapperModel mapping) {
final String id = mapping == null ? null : mapping.getId();
if (id != null) {
entity.removeProtocolMapper(id);
}
}
@Override
public void updateProtocolMapper(ProtocolMapperModel mapping) {
final String id = mapping == null ? null : mapping.getId();
if (id != null) {
entity.updateProtocolMapper(id, mapping);
}
}
@Override
public ProtocolMapperModel getProtocolMapperById(String id) {
return entity.getProtocolMapperById(id);
}
@Override
public ProtocolMapperModel getProtocolMapperByName(String protocol, String name) {
return entity.getProtocolMappers().stream()
.filter(pm -> Objects.equals(pm.getProtocol(), protocol) && Objects.equals(pm.getName(), name))
.findAny()
.orElse(null);
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.client;
import java.util.UUID;
/**
*
* @author hmlnarik
*/
public class MapClientEntity extends AbstractClientEntity<UUID> {
protected MapClientEntity() {
super();
}
public MapClientEntity(UUID id, String realmId) {
super(id, realmId);
}
}

View file

@ -0,0 +1,310 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.client;
import org.jboss.logging.Logger;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmModel.ClientUpdatedEvent;
import org.keycloak.models.RealmProvider;
import org.keycloak.models.RoleModel;
import org.keycloak.models.map.storage.MapKeycloakTransaction;
import org.keycloak.models.map.common.Serialization;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.keycloak.models.map.storage.MapStorage;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class MapClientProvider implements ClientProvider {
protected static final Logger logger = Logger.getLogger(MapClientProvider.class);
private static final Predicate<MapClientEntity> ALWAYS_FALSE = c -> { return false; };
private final KeycloakSession session;
final MapKeycloakTransaction<UUID, MapClientEntity> tx;
private final MapStorage<UUID, MapClientEntity> clientStore;
private final ConcurrentMap<UUID, ConcurrentMap<String, Integer>> clientRegisteredNodesStore;
private static final Comparator<MapClientEntity> COMPARE_BY_CLIENT_ID = new Comparator<MapClientEntity>() {
@Override
public int compare(MapClientEntity o1, MapClientEntity o2) {
String c1 = o1 == null ? null : o1.getClientId();
String c2 = o2 == null ? null : o2.getClientId();
return c1 == c2 ? 0
: c1 == null ? -1
: c2 == null ? 1
: c1.compareTo(c2);
}
};
public MapClientProvider(KeycloakSession session, MapStorage<UUID, MapClientEntity> clientStore, ConcurrentMap<UUID, ConcurrentMap<String, Integer>> clientRegisteredNodesStore) {
this.session = session;
this.clientStore = clientStore;
this.clientRegisteredNodesStore = clientRegisteredNodesStore;
this.tx = new MapKeycloakTransaction<>(clientStore);
session.getTransactionManager().enlistAfterCompletion(tx);
}
private ClientUpdatedEvent clientUpdatedEvent(ClientModel c) {
return new RealmModel.ClientUpdatedEvent() {
@Override
public ClientModel getUpdatedClient() {
return c;
}
@Override
public KeycloakSession getKeycloakSession() {
return session;
}
};
}
private MapClientEntity registerEntityForChanges(MapClientEntity origEntity) {
final MapClientEntity res = Serialization.from(origEntity);
tx.putIfChanged(origEntity.getId(), res, MapClientEntity::isUpdated);
return res;
}
private Function<MapClientEntity, ClientModel> entityToAdapterFunc(RealmModel realm) {
// Clone entity before returning back, to avoid giving away a reference to the live object to the caller
return origEntity -> new MapClientAdapter(session, realm, registerEntityForChanges(origEntity)) {
@Override
public void updateClient() {
// commit
MapClientProvider.this.tx.replace(entity.getId(), this.entity);
session.getKeycloakSessionFactory().publish(clientUpdatedEvent(this));
}
/** This is runtime information and should have never been part of the adapter */
@Override
public Map<String, Integer> getRegisteredNodes() {
return clientRegisteredNodesStore.computeIfAbsent(entity.getId(), k -> new ConcurrentHashMap<>());
}
@Override
public void registerNode(String nodeHost, int registrationTime) {
Map<String, Integer> value = getRegisteredNodes();
value.put(nodeHost, registrationTime);
}
@Override
public void unregisterNode(String nodeHost) {
getRegisteredNodes().remove(nodeHost);
}
};
}
private Predicate<MapClientEntity> entityRealmFilter(RealmModel realm) {
if (realm == null || realm.getId() == null) {
return MapClientProvider.ALWAYS_FALSE;
}
String realmId = realm.getId();
return entity -> Objects.equals(realmId, entity.getRealmId());
}
@Override
public List<ClientModel> getClients(RealmModel realm, Integer firstResult, Integer maxResults) {
Stream<ClientModel> s = getClientsStream(realm);
if (firstResult >= 0) {
s = s.skip(firstResult);
}
if (maxResults >= 0) {
s = s.limit(maxResults);
}
return s.collect(Collectors.toList());
}
private Stream<MapClientEntity> getNotRemovedUpdatedClientsStream() {
Stream<MapClientEntity> updatedAndNotRemovedClientsStream = clientStore.entrySet().stream()
.map(tx::getUpdated) // If the client has been removed, tx.get will return null, otherwise it will return me.getValue()
.filter(Objects::nonNull);
return Stream.concat(tx.createdValuesStream(clientStore.keySet()), updatedAndNotRemovedClientsStream);
}
// @Override
public Stream<ClientModel> getClientsStream(RealmModel realm) {
return getNotRemovedUpdatedClientsStream()
.filter(entityRealmFilter(realm))
.sorted(COMPARE_BY_CLIENT_ID)
.map(entityToAdapterFunc(realm))
;
}
@Override
public List<ClientModel> getClients(RealmModel realm) {
return getClientsStream(realm).collect(Collectors.toList());
}
@Override
public ClientModel addClient(RealmModel realm, String id, String clientId) {
final UUID entityId = id == null ? UUID.randomUUID() : UUID.fromString(id);
if (clientId == null) {
clientId = entityId.toString();
}
MapClientEntity entity = new MapClientEntity(entityId, realm.getId());
entity.setClientId(clientId);
entity.setEnabled(true);
entity.setStandardFlowEnabled(true);
if (tx.get(entity.getId(), clientStore::get) != null) {
throw new ModelDuplicateException("Client exists: " + id);
}
tx.putIfAbsent(entity.getId(), entity);
final ClientModel resource = entityToAdapterFunc(realm).apply(entity);
// TODO: Sending an event should be extracted to store layer
session.getKeycloakSessionFactory().publish((RealmModel.ClientCreationEvent) () -> resource);
resource.updateClient(); // This is actualy strange contract - it should be the store code to call updateClient
return resource;
}
@Override
public List<ClientModel> getAlwaysDisplayInConsoleClients(RealmModel realm) {
return getClientsStream(realm)
.filter(ClientModel::isAlwaysDisplayInConsole)
.collect(Collectors.toList());
}
@Override
public void removeClients(RealmModel realm) {
LOG.tracef("removeClients(%s)%s", realm, getShortStackTrace());
getClientsStream(realm)
.map(ClientModel::getId)
.collect(Collectors.toSet()) // This is necessary to read out all the client IDs before removing the clients
.forEach(cid -> removeClient(realm, cid));
}
@Override
public boolean removeClient(RealmModel realm, String id) {
if (id == null) {
return false;
}
// TODO: Sending an event (and client role removal) should be extracted to store layer
final ClientModel client = getClientById(realm, id);
if (client == null) return false;
session.users().preRemove(realm, client);
final RealmProvider realms = session.realms();
for (RoleModel role : client.getRoles()) {
realms.removeRole(realm, role);
}
session.getKeycloakSessionFactory().publish(new RealmModel.ClientRemovedEvent() {
@Override
public ClientModel getClient() {
return client;
}
@Override
public KeycloakSession getKeycloakSession() {
return session;
}
});
// TODO: ^^^^^^^ Up to here
tx.remove(UUID.fromString(id));
return true;
}
@Override
public long getClientsCount(RealmModel realm) {
return this.getNotRemovedUpdatedClientsStream()
.filter(entityRealmFilter(realm))
.count();
}
@Override
public ClientModel getClientById(RealmModel realm, String id) {
if (id == null) {
return null;
}
MapClientEntity entity = tx.get(UUID.fromString(id), clientStore::get);
return (entity == null || ! entityRealmFilter(realm).test(entity))
? null
: entityToAdapterFunc(realm).apply(entity);
}
@Override
public ClientModel getClientByClientId(RealmModel realm, String clientId) {
if (clientId == null) {
return null;
}
String clientIdLower = clientId.toLowerCase();
return getNotRemovedUpdatedClientsStream()
.filter(entityRealmFilter(realm))
.filter(entity -> entity.getClientId() != null && Objects.equals(entity.getClientId().toLowerCase(), clientIdLower))
.map(entityToAdapterFunc(realm))
.findFirst()
.orElse(null)
;
}
@Override
public List<ClientModel> searchClientsByClientId(RealmModel realm, String clientId, Integer firstResult, Integer maxResults) {
if (clientId == null) {
return Collections.EMPTY_LIST;
}
String clientIdLower = clientId.toLowerCase();
Stream<MapClientEntity> s = getNotRemovedUpdatedClientsStream()
.filter(entityRealmFilter(realm))
.filter(entity -> entity.getClientId() != null && entity.getClientId().toLowerCase().contains(clientIdLower))
.sorted(COMPARE_BY_CLIENT_ID);
if (firstResult >= 0) {
s = s.skip(firstResult);
}
if (maxResults >= 0) {
s = s.limit(maxResults);
}
return s
.map(entityToAdapterFunc(realm))
.collect(Collectors.toList())
;
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,51 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.client;
import org.keycloak.models.map.common.AbstractMapProviderFactory;
import org.keycloak.models.ClientProvider;
import org.keycloak.models.ClientProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.keycloak.models.map.storage.MapStorageProvider;
import org.keycloak.models.map.storage.MapStorage;
/**
*
* @author hmlnarik
*/
public class MapClientProviderFactory extends AbstractMapProviderFactory<ClientProvider> implements ClientProviderFactory {
private final ConcurrentHashMap<UUID, ConcurrentMap<String, Integer>> REGISTERED_NODES_STORE = new ConcurrentHashMap<>();
private MapStorage<UUID, MapClientEntity> store;
@Override
public void postInit(KeycloakSessionFactory factory) {
MapStorageProvider sp = (MapStorageProvider) factory.getProviderFactory(MapStorageProvider.class);
this.store = sp.getStorage("clients", UUID.class, MapClientEntity.class);
}
@Override
public ClientProvider create(KeycloakSession session) {
return new MapClientProvider(session, store, REGISTERED_NODES_STORE);
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.common;
/**
*
* @author hmlnarik
*/
public interface AbstractEntity<K> {
K getId();
/**
* Flag signalizing that any of the setters has been meaningfully used.
* @return
*/
boolean isUpdated();
}

View file

@ -0,0 +1,51 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.common;
import org.keycloak.Config.Scope;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.jboss.logging.Logger;
/**
*
* @author hmlnarik
*/
public abstract class AbstractMapProviderFactory<T extends Provider> implements ProviderFactory<T> {
public static final String PROVIDER_ID = "map";
protected final Logger LOG = Logger.getLogger(getClass());
@Override
public void init(Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
}

View file

@ -0,0 +1,61 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.common;
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.io.IOException;
/**
*
* @author hmlnarik
*/
public class Serialization {
public static final ObjectMapper MAPPER = new ObjectMapper();
abstract class IgnoreUpdatedMixIn { @JsonIgnore public abstract boolean isUpdated(); }
static {
MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
MAPPER.enable(SerializationFeature.INDENT_OUTPUT);
MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
MAPPER.setVisibility(PropertyAccessor.ALL, Visibility.NONE);
MAPPER.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
MAPPER.addMixIn(AbstractEntity.class, IgnoreUpdatedMixIn.class);
}
public static <T extends AbstractEntity> T from(T orig) {
if (orig == null) {
return null;
}
try {
// Naive solution but will do.
final T res = MAPPER.readValue(MAPPER.writeValueAsBytes(orig), (Class<T>) orig.getClass());
return res;
} catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
}

View file

@ -0,0 +1,132 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.storage;
import org.keycloak.Config.Scope;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.common.Serialization;
import com.fasterxml.jackson.databind.JavaType;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.EnumSet;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import org.jboss.logging.Logger;
/**
*
* @author hmlnarik
*/
public class ConcurrentHashMapStorageProvider implements MapStorageProvider {
private static class ConcurrentHashMapStorage<K, V> extends ConcurrentHashMap<K, V> implements MapStorage<K, V> {
}
private static final String PROVIDER_ID = "concurrenthashmap";
private static final Logger LOG = Logger.getLogger(ConcurrentHashMapStorageProvider.class);
private final ConcurrentHashMap<String, ConcurrentHashMap<?,?>> storages = new ConcurrentHashMap<>();
private File storageDirectory;
@Override
public MapStorageProvider create(KeycloakSession session) {
return this;
}
@Override
public void init(Scope config) {
File f = new File(config.get("dir"));
try {
this.storageDirectory = f.exists()
? f
: Files.createTempDirectory("storage-map-chm-").toFile();
} catch (IOException ex) {
this.storageDirectory = null;
}
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
storages.forEach(this::storeMap);
}
private void storeMap(String fileName, ConcurrentHashMap<?, ?> store) {
if (fileName != null) {
File f = getFile(fileName);
try {
if (storageDirectory != null && storageDirectory.exists()) {
LOG.debugf("Storing contents to %s", f.getCanonicalPath());
Serialization.MAPPER.writeValue(f, store.values());
} else {
LOG.debugf("Not storing contents of %s because directory %s does not exist", fileName, this.storageDirectory);
}
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}
private <K, V extends AbstractEntity<K>> ConcurrentHashMapStorage<?, V> loadMap(String fileName, Class<V> valueType, EnumSet<Flag> flags) {
ConcurrentHashMapStorage<K, V> store = new ConcurrentHashMapStorage<>();
if (! flags.contains(Flag.INITIALIZE_EMPTY)) {
final File f = getFile(fileName);
if (f != null && f.exists()) {
try {
LOG.debugf("Restoring contents from %s", f.getCanonicalPath());
JavaType type = Serialization.MAPPER.getTypeFactory().constructCollectionType(List.class, valueType);
List<V> values = Serialization.MAPPER.readValue(f, type);
values.forEach((V mce) -> store.put(mce.getId(), mce));
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}
return store;
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
@SuppressWarnings("unchecked")
public <K, V extends AbstractEntity<K>> MapStorage<K, V> getStorage(String name, Class<K> keyType, Class<V> valueType, Flag... flags) {
EnumSet<Flag> f = flags == null || flags.length == 0 ? EnumSet.noneOf(Flag.class) : EnumSet.of(flags[0], flags);
return (MapStorage<K, V>) storages.computeIfAbsent(name, n -> loadMap(name, valueType, f));
}
private File getFile(String fileName) {
return storageDirectory == null
? null
: new File(storageDirectory, "map-" + fileName + ".json");
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.storage;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
/**
*
* @author hmlnarik
*/
public interface MapStorageProvider extends Provider, ProviderFactory<MapStorageProvider> {
public enum Flag {
INITIALIZE_EMPTY,
LOCAL
}
/**
* Returns a key-value storage
* @param <K> type of the primary key
* @param <V> type of the value
* @param name Name of the storage
* @param flags
* @return
*/
<K, V extends AbstractEntity<K>> MapStorage<K, V> getStorage(String name, Class<K> keyType, Class<V> valueType, Flag... flags);
}

View file

@ -0,0 +1,51 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.storage;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
*
* @author hmlnarik
*/
public class MapStorageSpi implements Spi {
public static final String NAME = "mapStorage";
@Override
public boolean isInternal() {
return false;
}
@Override
public String getName() {
return NAME;
}
@Override
public Class<? extends Provider> getProviderClass() {
return MapStorageProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return MapStorageProvider.class;
}
}

View file

@ -0,0 +1,18 @@
#
# Copyright 2020 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.models.map.client.MapClientProviderFactory

View file

@ -0,0 +1,18 @@
#
# Copyright 2020 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.models.map.storage.ConcurrentHashMapStorageProvider

View file

@ -0,0 +1,18 @@
#
# Copyright 2020 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.models.map.storage.MapStorageSpi

View file

@ -0,0 +1,23 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models;
import org.keycloak.provider.ProviderFactory;
public interface ClientProviderFactory extends ProviderFactory<ClientProvider> {
}

View file

@ -0,0 +1,46 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
public class ClientSpi implements Spi {
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return "client";
}
@Override
public Class<? extends Provider> getProviderClass() {
return ClientProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return ClientProviderFactory.class;
}
}

View file

@ -18,6 +18,7 @@
org.keycloak.provider.ExceptionConverterSpi org.keycloak.provider.ExceptionConverterSpi
org.keycloak.storage.UserStorageProviderSpi org.keycloak.storage.UserStorageProviderSpi
org.keycloak.storage.federated.UserFederatedStorageProviderSpi org.keycloak.storage.federated.UserFederatedStorageProviderSpi
org.keycloak.models.ClientSpi
org.keycloak.models.RealmSpi org.keycloak.models.RealmSpi
org.keycloak.models.ActionTokenStoreSpi org.keycloak.models.ActionTokenStoreSpi
org.keycloak.models.CodeToTokenStoreSpi org.keycloak.models.CodeToTokenStoreSpi

View file

@ -35,6 +35,11 @@ public interface ClientModel extends ClientScopeModel, RoleContainerModel, Prot
String PUBLIC_KEY = "publicKey"; String PUBLIC_KEY = "publicKey";
String X509CERTIFICATE = "X509Certificate"; String X509CERTIFICATE = "X509Certificate";
/**
* Stores the current state of the client immediately to the underlying store, similarly to a commit.
*
* @deprecated Do not use, to be removed
*/
void updateClient(); void updateClient();
/** /**

View file

@ -19,6 +19,10 @@ package org.keycloak.models;
import org.keycloak.provider.ProviderEvent; import org.keycloak.provider.ProviderEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -53,7 +57,26 @@ public interface RoleContainerModel {
void addDefaultRole(String name); void addDefaultRole(String name);
void updateDefaultRoles(String... defaultRoles); default void updateDefaultRoles(String... defaultRoles) {
List<String> defaultRolesArray = Arrays.asList(defaultRoles);
Collection<String> entities = getDefaultRoles();
Set<String> already = new HashSet<>();
ArrayList<String> remove = new ArrayList<>();
for (String rel : entities) {
if (! defaultRolesArray.contains(rel)) {
remove.add(rel);
} else {
already.add(rel);
}
}
removeDefaultRoles(remove.toArray(new String[] {}));
for (String roleName : defaultRoles) {
if (!already.contains(roleName)) {
addDefaultRole(roleName);
}
}
}
void removeDefaultRoles(String... defaultRoles); void removeDefaultRoles(String... defaultRoles);

View file

@ -18,6 +18,8 @@
package org.keycloak.models; package org.keycloak.models;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -25,14 +27,26 @@ import java.util.Set;
*/ */
public interface ScopeContainerModel { public interface ScopeContainerModel {
Set<RoleModel> getScopeMappings(); @Deprecated
default Set<RoleModel> getScopeMappings() {
return getScopeMappingsStream().collect(Collectors.toSet());
}
default Stream<RoleModel> getScopeMappingsStream() {
return getScopeMappings().stream();
};
/**
* From the scope mappings returned by {@link #getScopeMappings()} returns only those
* that belong to the realm that owns this scope container.
* @return
*/
Set<RoleModel> getRealmScopeMappings();
void addScopeMapping(RoleModel role); void addScopeMapping(RoleModel role);
void deleteScopeMapping(RoleModel role); void deleteScopeMapping(RoleModel role);
Set<RoleModel> getRealmScopeMappings();
boolean hasScope(RoleModel role); boolean hasScope(RoleModel role);
} }

View file

@ -100,6 +100,10 @@
<groupId>com.google.guava</groupId> <groupId>com.google.guava</groupId>
<artifactId>guava</artifactId> <artifactId>guava</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-map</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.keycloak.testsuite</groupId> <groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-app-server-spi</artifactId> <artifactId>integration-arquillian-servers-app-server-spi</artifactId>
@ -159,7 +163,27 @@
</execution> </execution>
</executions> </executions>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<phase>process-test-resources</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<delete>
<fileset dir="${project.build.directory}" includes="map-*.json"/>
</delete>
</target>
</configuration>
</execution>
</executions>
</plugin>
<plugin> <plugin>
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
<configuration> <configuration>

View file

@ -71,6 +71,7 @@ import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
@ -317,12 +318,12 @@ public class RealmTest extends AbstractAdminTest {
realm1 = adminClient.realms().realm("test-immutable").toRepresentation(); realm1 = adminClient.realms().realm("test-immutable").toRepresentation();
realm1.setRealm("test-immutable-old"); realm1.setRealm("test-immutable-old");
adminClient.realms().realm("test-immutable").update(realm1); adminClient.realms().realm("test-immutable").update(realm1);
realm1 = adminClient.realms().realm("test-immutable-old").toRepresentation(); assertThat(adminClient.realms().realm("test-immutable-old").toRepresentation(), notNullValue());
RealmRepresentation realm2 = new RealmRepresentation(); RealmRepresentation realm2 = new RealmRepresentation();
realm2.setRealm("test-immutable"); realm2.setRealm("test-immutable");
adminClient.realms().create(realm2); adminClient.realms().create(realm2);
realm2 = adminClient.realms().realm("test-immutable").toRepresentation(); assertThat(adminClient.realms().realm("test-immutable").toRepresentation(), notNullValue());
adminClient.realms().realm("test-immutable-old").remove(); adminClient.realms().realm("test-immutable-old").remove();
adminClient.realms().realm("test-immutable").remove(); adminClient.realms().realm("test-immutable").remove();

View file

@ -44,6 +44,17 @@
"provider": "${keycloak.user.provider:jpa}" "provider": "${keycloak.user.provider:jpa}"
}, },
"client": {
"provider": "${keycloak.client.provider:jpa}"
},
"mapStorage": {
"provider": "${keycloak.mapStorage.provider:concurrenthashmap}",
"concurrenthashmap": {
"dir": "${project.build.directory:target}"
}
},
"userFederatedStorage": { "userFederatedStorage": {
"provider": "${keycloak.userFederatedStorage.provider:jpa}" "provider": "${keycloak.userFederatedStorage.provider:jpa}"
}, },

View file

@ -18,6 +18,17 @@
"provider": "${keycloak.realm.provider:}" "provider": "${keycloak.realm.provider:}"
}, },
"client": {
"provider": "${keycloak.client.provider:jpa}"
},
"mapStorage": {
"provider": "${keycloak.mapStorage.provider:concurrenthashmap}",
"concurrenthashmap": {
"dir": "${project.build.directory:target}"
}
},
"user": { "user": {
"provider": "${keycloak.user.provider:}" "provider": "${keycloak.user.provider:}"
}, },