KEYCLOAK-17021 Client Scope map store

This commit is contained in:
vramik 2021-01-26 00:42:19 +01:00 committed by Hynek Mlnařík
parent 2c64a56072
commit 6e501946b1
55 changed files with 2271 additions and 150 deletions

View file

@ -160,7 +160,7 @@ jobs:
run: |
declare -A PARAMS TESTGROUP
PARAMS["quarkus"]="-Pauth-server-quarkus"
PARAMS["undertow-map"]="-Pauth-server-undertow -Dkeycloak.client.provider=map -Dkeycloak.group.provider=map -Dkeycloak.role.provider=map -Dkeycloak.authSession.provider=map -Dkeycloak.user.provider=map"
PARAMS["undertow-map"]="-Pauth-server-undertow -Dkeycloak.client.provider=map -Dkeycloak.group.provider=map -Dkeycloak.role.provider=map -Dkeycloak.authSession.provider=map -Dkeycloak.user.provider=map -Dkeycloak.clientScope.provider=map"
PARAMS["wildfly"]="-Pauth-server-wildfly"
TESTGROUP["group1"]="-Dtest=!**.crossdc.**,!**.cluster.**,%regex[org.keycloak.testsuite.(a[abc]|ad[a-l]|[^a-q]).*]" # Tests alphabetically before admin tests and those after "r"
TESTGROUP["group2"]="-Dtest=!**.crossdc.**,!**.cluster.**,%regex[org.keycloak.testsuite.(ad[^a-l]|a[^a-d]|b).*]" # Admin tests and those starting with "b"

View file

@ -126,7 +126,7 @@ public class ClientAdapter implements ClientModel, CachedObject {
Map<String, ClientScopeModel> clientScopes = new HashMap<>();
for (String scopeId : clientScopeIds) {
ClientScopeModel clientScope = cacheSession.getClientScopeById(scopeId, cachedRealm);
ClientScopeModel clientScope = cacheSession.getClientScopeById(cachedRealm, scopeId);
if (clientScope != null) {
if (!filterByProtocol || clientScope.getProtocol().equals(clientProtocol)) {
clientScopes.put(clientScope.getName(), clientScope);

View file

@ -47,8 +47,8 @@ public class ClientScopeAdapter implements ClientScopeModel {
private void getDelegateForUpdate() {
if (updated == null) {
cacheSession.registerClientScopeInvalidation(cached.getId());
updated = cacheSession.getRealmDelegate().getClientScopeById(cached.getId(), cachedRealm);
cacheSession.registerClientScopeInvalidation(cached.getId(), cachedRealm.getId());
updated = cacheSession.getClientScopeDelegate().getClientScopeById(cachedRealm, cached.getId());
if (updated == null) throw new IllegalStateException("Not found in database");
}
}
@ -61,7 +61,7 @@ public class ClientScopeAdapter implements ClientScopeModel {
protected boolean isUpdated() {
if (updated != null) return true;
if (!invalidated) return false;
updated = cacheSession.getRealmDelegate().getClientScopeById(cached.getId(), cachedRealm);
updated = cacheSession.getClientScopeDelegate().getClientScopeById(cachedRealm, cached.getId());
if (updated == null) throw new IllegalStateException("Not found in database");
return true;
}
@ -73,6 +73,7 @@ public class ClientScopeAdapter implements ClientScopeModel {
return cached.getId();
}
@Override
public RealmModel getRealm() {
return cachedRealm;
}
@ -155,22 +156,26 @@ public class ClientScopeAdapter implements ClientScopeModel {
updated.setProtocol(protocol);
}
@Override
public Stream<RoleModel> getScopeMappingsStream() {
if (isUpdated()) return updated.getScopeMappingsStream();
return cached.getScope().stream()
.map(id -> cacheSession.getRoleById(cachedRealm, id));
}
@Override
public void addScopeMapping(RoleModel role) {
getDelegateForUpdate();
updated.addScopeMapping(role);
}
@Override
public void deleteScopeMapping(RoleModel role) {
getDelegateForUpdate();
updated.deleteScopeMapping(role);
}
@Override
public Stream<RoleModel> getRealmScopeMappingsStream() {
return getScopeMappingsStream().filter(r -> RoleUtils.isRealmRole(r, cachedRealm));
}

View file

@ -1457,7 +1457,7 @@ public class RealmAdapter implements CachedRealmModel {
public Stream<ClientScopeModel> getClientScopesStream() {
if (isUpdated()) return updated.getClientScopesStream();
return cached.getClientScopes().stream().map(scope -> {
ClientScopeModel model = cacheSession.getClientScopeById(scope, this);
ClientScopeModel model = cacheSession.getClientScopeById(this, scope);
if (model == null) {
throw new IllegalStateException("Cached clientScope not found: " + scope);
}
@ -1467,31 +1467,31 @@ public class RealmAdapter implements CachedRealmModel {
@Override
public ClientScopeModel addClientScope(String name) {
getDelegateForUpdate();
ClientScopeModel app = updated.addClientScope(name);
cacheSession.registerClientScopeInvalidation(app.getId());
return app;
RealmModel realm = getDelegateForUpdate();
ClientScopeModel clientScope = updated.addClientScope(name);
cacheSession.registerClientScopeInvalidation(clientScope.getId(), realm.getId());
return clientScope;
}
@Override
public ClientScopeModel addClientScope(String id, String name) {
getDelegateForUpdate();
ClientScopeModel app = updated.addClientScope(id, name);
cacheSession.registerClientScopeInvalidation(app.getId());
return app;
RealmModel realm = getDelegateForUpdate();
ClientScopeModel clientScope = updated.addClientScope(id, name);
cacheSession.registerClientScopeInvalidation(clientScope.getId(), realm.getId());
return clientScope;
}
@Override
public boolean removeClientScope(String id) {
cacheSession.registerClientScopeInvalidation(id);
getDelegateForUpdate();
RealmModel realm = getDelegateForUpdate();
cacheSession.registerClientScopeInvalidation(id, realm.getId());
return updated.removeClientScope(id);
}
@Override
public ClientScopeModel getClientScopeById(String id) {
if (isUpdated()) return updated.getClientScopeById(id);
return cacheSession.getClientScopeById(id, this);
return cacheSession.getClientScopeById(this, id);
}
@Override
@ -1511,7 +1511,7 @@ public class RealmAdapter implements CachedRealmModel {
if (isUpdated()) return updated.getDefaultClientScopesStream(defaultScope);
List<String> clientScopeIds = defaultScope ? cached.getDefaultDefaultClientScopes() : cached.getOptionalDefaultClientScopes();
return clientScopeIds.stream()
.map(scope -> cacheSession.getClientScopeById(scope, this))
.map(scope -> cacheSession.getClientScopeById(this, scope))
.filter(Objects::nonNull);
}

View file

@ -72,6 +72,19 @@ public class RealmCacheManager extends CacheManager {
addInvalidations(HasRolePredicate.create().role(id), invalidations);
}
public void clientScopeAdded(String realmId, Set<String> invalidations) {
invalidations.add(RealmCacheSession.getClientScopesCacheKey(realmId));
}
public void clientScopeUpdated(String realmId, Set<String> invalidations) {
invalidations.add(RealmCacheSession.getClientScopesCacheKey(realmId));
}
public void clientScopeRemoval(String realmId, Set<String> invalidations) {
invalidations.add(RealmCacheSession.getClientScopesCacheKey(realmId));
addInvalidations(InRealmPredicate.create().realm(realmId), invalidations);
}
public void groupQueriesInvalidations(String realmId, Set<String> invalidations) {
invalidations.add(RealmCacheSession.getGroupsQueryCacheKey(realmId));
invalidations.add(RealmCacheSession.getTopGroupsQueryCacheKey(realmId));

View file

@ -101,6 +101,7 @@ public class RealmCacheSession implements CacheRealmProvider {
protected KeycloakSession session;
protected RealmProvider realmDelegate;
protected ClientProvider clientDelegate;
protected ClientScopeProvider clientScopeDelegate;
protected GroupProvider groupDelegate;
protected RoleProvider roleDelegate;
protected boolean transactionActive;
@ -158,6 +159,12 @@ public class RealmCacheSession implements CacheRealmProvider {
clientDelegate = session.clientStorageManager();
return clientDelegate;
}
public ClientScopeProvider getClientScopeDelegate() {
if (!transactionActive) throw new IllegalStateException("Cannot access delegate without a transaction");
if (clientScopeDelegate != null) return clientScopeDelegate;
clientScopeDelegate = session.clientScopeStorageManager();
return clientScopeDelegate;
}
public RoleProvider getRoleDelegate() {
if (!transactionActive) throw new IllegalStateException("Cannot access delegate without a transaction");
if (roleDelegate != null) return roleDelegate;
@ -194,10 +201,9 @@ public class RealmCacheSession implements CacheRealmProvider {
}
@Override
public void registerClientScopeInvalidation(String id) {
public void registerClientScopeInvalidation(String id, String realmId) {
invalidateClientScope(id);
// Note: Adding/Removing client template is supposed to invalidate CachedRealm as well, so the list of clientScopes is invalidated.
// But separate RealmUpdatedEvent will be sent for it. So ClientTemplateEvent don't need to take care of it.
cache.clientScopeUpdated(realmId, invalidations);
invalidationEvents.add(ClientTemplateEvent.create(id));
}
@ -532,6 +538,10 @@ public class RealmCacheSession implements CacheRealmProvider {
return realm + ".groups";
}
static String getClientScopesCacheKey(String realm) {
return realm + ".clientscopes";
}
static String getTopGroupsQueryCacheKey(String realm) {
return realm + ".top.groups";
}
@ -595,6 +605,7 @@ public class RealmCacheSession implements CacheRealmProvider {
public void close() {
if (realmDelegate != null) realmDelegate.close();
if (clientDelegate != null) clientDelegate.close();
if (clientScopeDelegate != null) clientScopeDelegate.close();
if (roleDelegate != null) roleDelegate.close();
}
@ -1192,7 +1203,7 @@ public class RealmCacheSession implements CacheRealmProvider {
}
@Override
public ClientScopeModel getClientScopeById(String id, RealmModel realm) {
public ClientScopeModel getClientScopeById(RealmModel realm, String id) {
CachedClientScope cached = cache.get(id, CachedClientScope.class);
if (cached != null && !cached.getRealm().equals(realm.getId())) {
cached = null;
@ -1200,13 +1211,13 @@ public class RealmCacheSession implements CacheRealmProvider {
if (cached == null) {
Long loaded = cache.getCurrentRevision(id);
ClientScopeModel model = getRealmDelegate().getClientScopeById(id, realm);
ClientScopeModel model = getClientScopeDelegate().getClientScopeById(realm, id);
if (model == null) return null;
if (invalidations.contains(id)) return model;
cached = new CachedClientScope(loaded, realm, model);
cache.addRevisioned(cached, startupRevision);
} else if (invalidations.contains(id)) {
return getRealmDelegate().getClientScopeById(id, realm);
return getClientScopeDelegate().getClientScopeById(realm, id);
} else if (managedClientScopes.containsKey(id)) {
return managedClientScopes.get(id);
}
@ -1215,6 +1226,85 @@ public class RealmCacheSession implements CacheRealmProvider {
return adapter;
}
@Override
public Stream<ClientScopeModel> getClientScopesStream(RealmModel realm) {
String cacheKey = getClientScopesCacheKey(realm.getId());
boolean queryDB = invalidations.contains(cacheKey) || listInvalidations.contains(realm.getId());
if (queryDB) {
return getClientScopeDelegate().getClientScopesStream(realm);
}
ClientScopeListQuery query = cache.get(cacheKey, ClientScopeListQuery.class);
if (query != null) {
logger.tracev("getClientScopesStream cache hit: {0}", realm.getName());
}
if (query == null) {
Long loaded = cache.getCurrentRevision(cacheKey);
Set<ClientScopeModel> model = getClientScopeDelegate().getClientScopesStream(realm).collect(Collectors.toSet());
if (model == null) return null;
Set<String> ids = model.stream().map(ClientScopeModel::getId).collect(Collectors.toSet());
query = new ClientScopeListQuery(loaded, cacheKey, realm, ids);
logger.tracev("adding client scopes cache miss: realm {0} key {1}", realm.getName(), cacheKey);
cache.addRevisioned(query, startupRevision);
return model.stream();
}
Set<ClientScopeModel> list = new HashSet<>();
for (String id : query.getClientScopes()) {
ClientScopeModel clientScope = session.clientScopes().getClientScopeById(realm, id);
if (clientScope == null) {
invalidations.add(cacheKey);
return getClientScopeDelegate().getClientScopesStream(realm);
}
list.add(clientScope);
}
return list.stream();
}
@Override
public ClientScopeModel addClientScope(RealmModel realm, String name) {
ClientScopeModel clientScope = getClientScopeDelegate().addClientScope(realm, name);
return addedClientScope(realm, clientScope);
}
@Override
public ClientScopeModel addClientScope(RealmModel realm, String id, String name) {
ClientScopeModel clientScope = getClientScopeDelegate().addClientScope(realm, id, name);
return addedClientScope(realm, clientScope);
}
private ClientScopeModel addedClientScope(RealmModel realm, ClientScopeModel clientScope) {
logger.tracef("Added client scope %s", clientScope.getId());
invalidateClientScope(clientScope.getId());
// this is needed so that a client scope that hasn't been committed isn't cached in a query
listInvalidations.add(realm.getId());
invalidationEvents.add(ClientScopeAddedEvent.create(clientScope.getId(), realm.getId()));
cache.clientScopeAdded(realm.getId(), invalidations);
return clientScope;
}
@Override
public boolean removeClientScope(RealmModel realm, String id) {
//removeClientScope can throw ModelException in case the client scope us used so invalidate only if the removal is succesful
if (getClientScopeDelegate().removeClientScope(realm, id)) {
listInvalidations.add(realm.getId());
invalidateClientScope(id);
invalidationEvents.add(ClientScopeRemovedEvent.create(id, realm.getId()));
return true;
} else {
return false;
}
}
@Override
public void removeClientScopes(RealmModel realm) {
realm.getClientScopesStream().map(ClientScopeModel::getId).forEach(id -> removeClientScope(realm, id));
}
// Don't cache ClientInitialAccessModel for now
@Override
public ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count) {

View file

@ -0,0 +1,53 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.cache.infinispan.entities;
import org.keycloak.models.RealmModel;
import java.util.Set;
public class ClientScopeListQuery extends AbstractRevisioned implements ClientScopeQuery {
private final Set<String> clientScopes;
private final String realm;
private final String realmName;
public ClientScopeListQuery(Long revisioned, String id, RealmModel realm, Set<String> clientScopes) {
super(revisioned, id);
this.realm = realm.getId();
this.realmName = realm.getName();
this.clientScopes = clientScopes;
}
@Override
public Set<String> getClientScopes() {
return clientScopes;
}
@Override
public String getRealm() {
return realm;
}
@Override
public String toString() {
return "ClientScopeListQuery{" +
"id='" + getId() + "'" +
", realmName='" + realmName + '\'' +
'}';
}
}

View file

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

View file

@ -0,0 +1,88 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.cache.infinispan.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import org.infinispan.commons.marshall.Externalizer;
import org.infinispan.commons.marshall.MarshallUtil;
import org.infinispan.commons.marshall.SerializeWith;
@SerializeWith(ClientScopeAddedEvent.ExternalizerImpl.class)
public class ClientScopeAddedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String clientScopeId;
private String realmId;
public static ClientScopeAddedEvent create(String clientScopeId, String realmId) {
ClientScopeAddedEvent event = new ClientScopeAddedEvent();
event.clientScopeId = clientScopeId;
event.realmId = realmId;
return event;
}
@Override
public String getId() {
return clientScopeId;
}
@Override
public String toString() {
return String.format("ClientScopeAddedEvent [ clientScopeId=%s, realmId=%s ]", clientScopeId, realmId);
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.clientScopeAdded(realmId, invalidations);
}
public static class ExternalizerImpl implements Externalizer<ClientScopeAddedEvent> {
private static final int VERSION_1 = 1;
@Override
public void writeObject(ObjectOutput output, ClientScopeAddedEvent obj) throws IOException {
output.writeByte(VERSION_1);
MarshallUtil.marshallString(obj.clientScopeId, output);
MarshallUtil.marshallString(obj.realmId, output);
}
@Override
public ClientScopeAddedEvent readObject(ObjectInput input) throws IOException, ClassNotFoundException {
switch (input.readByte()) {
case VERSION_1:
return readObjectVersion1(input);
default:
throw new IOException("Unknown version");
}
}
public ClientScopeAddedEvent readObjectVersion1(ObjectInput input) throws IOException, ClassNotFoundException {
ClientScopeAddedEvent res = new ClientScopeAddedEvent();
res.clientScopeId = MarshallUtil.unmarshallString(input);
res.realmId = MarshallUtil.unmarshallString(input);
return res;
}
}
}

View file

@ -0,0 +1,89 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.cache.infinispan.events;
import java.util.Set;
import org.keycloak.models.cache.infinispan.RealmCacheManager;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import org.infinispan.commons.marshall.Externalizer;
import org.infinispan.commons.marshall.MarshallUtil;
import org.infinispan.commons.marshall.SerializeWith;
@SerializeWith(ClientScopeRemovedEvent.ExternalizerImpl.class)
public class ClientScopeRemovedEvent extends InvalidationEvent implements RealmCacheInvalidationEvent {
private String clientScopeId;
private String realmId;
public static ClientScopeRemovedEvent create(String clientScopeId, String realmId) {
ClientScopeRemovedEvent event = new ClientScopeRemovedEvent();
event.clientScopeId = clientScopeId;
event.realmId = realmId;
return event;
}
@Override
public String getId() {
return clientScopeId;
}
@Override
public String toString() {
return String.format("ClientScopeRemovedEvent [ clientScopeId=%s, realmId=%s ]", clientScopeId, realmId);
}
@Override
public void addInvalidations(RealmCacheManager realmCache, Set<String> invalidations) {
realmCache.clientScopeRemoval(realmId, invalidations);
}
public static class ExternalizerImpl implements Externalizer<ClientScopeRemovedEvent> {
private static final int VERSION_1 = 1;
@Override
public void writeObject(ObjectOutput output, ClientScopeRemovedEvent obj) throws IOException {
output.writeByte(VERSION_1);
MarshallUtil.marshallString(obj.clientScopeId, output);
MarshallUtil.marshallString(obj.realmId, output);
}
@Override
public ClientScopeRemovedEvent readObject(ObjectInput input) throws IOException, ClassNotFoundException {
switch (input.readByte()) {
case VERSION_1:
return readObjectVersion1(input);
default:
throw new IOException("Unknown version");
}
}
public ClientScopeRemovedEvent readObjectVersion1(ObjectInput input) throws IOException, ClassNotFoundException {
ClientScopeRemovedEvent res = new ClientScopeRemovedEvent();
res.clientScopeId = MarshallUtil.unmarshallString(input);
res.realmId = MarshallUtil.unmarshallString(input);
return res;
}
}
}

View file

@ -364,7 +364,7 @@ public class ClientAdapter implements ClientModel, JpaModel<ClientEntity> {
private void persist(ClientScopeModel clientScope, boolean defaultScope) {
ClientScopeClientMappingEntity entity = new ClientScopeClientMappingEntity();
entity.setClientScope(ClientScopeAdapter.toClientScopeEntity(clientScope, em));
entity.setClientScopeId(clientScope.getId());
entity.setClient(getEntity());
entity.setDefaultScope(defaultScope);
em.persist(entity);
@ -375,7 +375,7 @@ public class ClientAdapter implements ClientModel, JpaModel<ClientEntity> {
@Override
public void removeClientScope(ClientScopeModel clientScope) {
int numRemoved = em.createNamedQuery("deleteClientScopeClientMapping")
.setParameter("clientScope", ClientScopeAdapter.toClientScopeEntity(clientScope, em))
.setParameter("clientScopeId", clientScope.getId())
.setParameter("client", getEntity())
.executeUpdate();
em.flush();

View file

@ -0,0 +1,54 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.jpa;
import org.keycloak.Config;
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.models.ClientScopeProvider;
import org.keycloak.models.ClientScopeProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import javax.persistence.EntityManager;
public class JpaClientScopeProviderFactory implements ClientScopeProviderFactory {
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public String getId() {
return "jpa";
}
@Override
public ClientScopeProvider create(KeycloakSession session) {
EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
return new JpaRealmProvider(session, em);
}
@Override
public void close() {
}
}

View file

@ -17,6 +17,22 @@
package org.keycloak.models.jpa;
import static org.keycloak.common.util.StackUtil.getShortStackTrace;
import static org.keycloak.models.jpa.PaginationUtils.paginateQuery;
import static org.keycloak.utils.StreamsUtil.closing;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaDelete;
import javax.persistence.criteria.Root;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.connections.jpa.util.JpaUtils;
@ -25,10 +41,12 @@ import org.keycloak.models.ClientInitialAccessModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientProvider;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.ClientScopeProvider;
import org.keycloak.models.GroupModel;
import org.keycloak.models.GroupProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmProvider;
import org.keycloak.models.RoleContainerModel;
@ -43,33 +61,11 @@ import org.keycloak.models.jpa.entities.RealmLocalizationTextsEntity;
import org.keycloak.models.jpa.entities.RoleEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaDelete;
import javax.persistence.criteria.Root;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.keycloak.models.ModelException;
import static org.keycloak.common.util.StackUtil.getShortStackTrace;
import static org.keycloak.models.jpa.PaginationUtils.paginateQuery;
import static org.keycloak.utils.StreamsUtil.closing;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class JpaRealmProvider implements RealmProvider, ClientProvider, GroupProvider, RoleProvider {
public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientScopeProvider, GroupProvider, RoleProvider {
protected static final Logger logger = Logger.getLogger(JpaRealmProvider.class);
private final KeycloakSession session;
protected EntityManager em;
@ -168,10 +164,7 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, GroupPro
num = em.createNamedQuery("deleteDefaultClientScopeRealmMappingByRealm")
.setParameter("realm", realm).executeUpdate();
for (ClientScopeEntity a : new LinkedList<>(realm.getClientScopes())) {
adapter.removeClientScope(a.getId());
}
session.clientScopes().removeClientScopes(adapter);
session.roles().removeRoles(adapter);
adapter.getTopLevelGroupsStream().forEach(adapter::removeGroup);
@ -737,15 +730,66 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, GroupPro
}
@Override
public ClientScopeModel getClientScopeById(String id, RealmModel realm) {
ClientScopeEntity app = em.find(ClientScopeEntity.class, id);
public ClientScopeModel getClientScopeById(RealmModel realm, String id) {
ClientScopeEntity clientScope = em.find(ClientScopeEntity.class, id);
// Check if application belongs to this realm
if (app == null || !realm.getId().equals(app.getRealm().getId())) return null;
ClientScopeAdapter adapter = new ClientScopeAdapter(realm, em, session, app);
// Check if client scope belongs to this realm
if (clientScope == null || !realm.getId().equals(clientScope.getRealm().getId())) return null;
ClientScopeAdapter adapter = new ClientScopeAdapter(realm, em, session, clientScope);
return adapter;
}
@Override
public Stream<ClientScopeModel> getClientScopesStream(RealmModel realm) {
TypedQuery<String> query = em.createNamedQuery("getClientScopeIds", String.class);
query.setParameter("realm", realm.getId());
Stream<String> scopes = query.getResultStream();
return closing(scopes.map(realm::getClientScopeById));
}
@Override
public ClientScopeModel addClientScope(RealmModel realm, String id, String name) {
if (id == null) {
id = KeycloakModelUtils.generateId();
}
ClientScopeEntity entity = new ClientScopeEntity();
entity.setId(id);
name = KeycloakModelUtils.convertClientScopeName(name);
entity.setName(name);
RealmEntity ref = em.getReference(RealmEntity.class, realm.getId());
entity.setRealm(ref);
em.persist(entity);
em.flush();
return new ClientScopeAdapter(realm, em, session, entity);
}
@Override
public boolean removeClientScope(RealmModel realm, String id) {
if (id == null) return false;
ClientScopeModel clientScope = getClientScopeById(realm, id);
if (clientScope == null) return false;
if (KeycloakModelUtils.isClientScopeUsed(realm, clientScope)) {
throw new ModelException("Cannot remove client scope, it is currently in use");
}
session.users().preRemove(clientScope);
realm.removeDefaultClientScope(clientScope);
ClientScopeEntity clientScopeEntity = em.find(ClientScopeEntity.class, id, LockModeType.PESSIMISTIC_WRITE);
em.createNamedQuery("deleteClientScopeRoleMappingByClientScope").setParameter("clientScope", clientScopeEntity).executeUpdate();
em.remove(clientScopeEntity);
em.flush();
return true;
}
@Override
public void removeClientScopes(RealmModel realm) {
// No need to go through cache. Client scopes were already invalidated
realm.getClientScopesStream().map(ClientScopeModel::getId).forEach(id -> this.removeClientScope(realm, id));
}
@Override
public Stream<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Integer first, Integer max) {
TypedQuery<String> query = em.createNamedQuery("getGroupIdsByNameContaining", String.class)

View file

@ -1958,72 +1958,33 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
@Override
public Stream<ClientScopeModel> getClientScopesStream() {
return realm.getClientScopes().stream().map(ClientScopeEntity::getId).map(this::getClientScopeById);
return session.clientScopes().getClientScopesStream(this);
}
@Override
public ClientScopeModel addClientScope(String name) {
return this.addClientScope(KeycloakModelUtils.generateId(), name);
return session.clientScopes().addClientScope(this, name);
}
@Override
public ClientScopeModel addClientScope(String id, String name) {
ClientScopeEntity entity = new ClientScopeEntity();
entity.setId(id);
name = KeycloakModelUtils.convertClientScopeName(name);
entity.setName(name);
entity.setRealm(realm);
realm.getClientScopes().add(entity);
em.persist(entity);
em.flush();
final ClientScopeModel resource = new ClientScopeAdapter(this, em, session, entity);
em.flush();
return resource;
return session.clientScopes().addClientScope(this, id, name);
}
@Override
public boolean removeClientScope(String id) {
if (id == null) return false;
ClientScopeModel clientScope = getClientScopeById(id);
if (clientScope == null) return false;
if (KeycloakModelUtils.isClientScopeUsed(this, clientScope)) {
throw new ModelException("Cannot remove client scope, it is currently in use");
}
ClientScopeEntity clientScopeEntity = null;
Iterator<ClientScopeEntity> it = realm.getClientScopes().iterator();
while (it.hasNext()) {
ClientScopeEntity ae = it.next();
if (ae.getId().equals(id)) {
clientScopeEntity = ae;
it.remove();
break;
}
}
if (clientScope == null) {
return false;
}
session.users().preRemove(clientScope);
em.createNamedQuery("deleteClientScopeRoleMappingByClientScope").setParameter("clientScope", clientScopeEntity).executeUpdate();
em.flush();
em.remove(clientScopeEntity);
em.flush();
return true;
return session.clientScopes().removeClientScope(this, id);
}
@Override
public ClientScopeModel getClientScopeById(String id) {
return session.realms().getClientScopeById(id, this);
return session.clientScopes().getClientScopeById(this, id);
}
@Override
public void addDefaultClientScope(ClientScopeModel clientScope, boolean defaultScope) {
DefaultClientScopeRealmMappingEntity entity = new DefaultClientScopeRealmMappingEntity();
entity.setClientScope(ClientScopeAdapter.toClientScopeEntity(clientScope, em));
entity.setClientScopeId(clientScope.getId());
entity.setRealm(getEntity());
entity.setDefaultScope(defaultScope);
em.persist(entity);
@ -2034,7 +1995,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
@Override
public void removeDefaultClientScope(ClientScopeModel clientScope) {
int numRemoved = em.createNamedQuery("deleteDefaultClientScopeRealmMapping")
.setParameter("clientScope", ClientScopeAdapter.toClientScopeEntity(clientScope, em))
.setParameter("clientScopeId", clientScope.getId())
.setParameter("realm", getEntity())
.executeUpdate();
em.flush();

View file

@ -36,8 +36,8 @@ import javax.persistence.Table;
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@NamedQueries({
@NamedQuery(name="clientScopeClientMappingIdsByClient", query="select m.clientScope.id from ClientScopeClientMappingEntity m where m.client = :client and m.defaultScope = :defaultScope"),
@NamedQuery(name="deleteClientScopeClientMapping", query="delete from ClientScopeClientMappingEntity where client = :client and clientScope = :clientScope"),
@NamedQuery(name="clientScopeClientMappingIdsByClient", query="select m.clientScopeId from ClientScopeClientMappingEntity m where m.client = :client and m.defaultScope = :defaultScope"),
@NamedQuery(name="deleteClientScopeClientMapping", query="delete from ClientScopeClientMappingEntity where client = :client and clientScopeId = :clientScopeId"),
@NamedQuery(name="deleteClientScopeClientMappingByClient", query="delete from ClientScopeClientMappingEntity where client = :client")
})
@Entity
@ -46,9 +46,8 @@ import javax.persistence.Table;
public class ClientScopeClientMappingEntity {
@Id
@ManyToOne(fetch= FetchType.LAZY)
@JoinColumn(name = "SCOPE_ID")
protected ClientScopeEntity clientScope;
@Column(name = "SCOPE_ID")
protected String clientScopeId;
@Id
@ManyToOne(fetch= FetchType.LAZY)
@ -58,12 +57,12 @@ public class ClientScopeClientMappingEntity {
@Column(name="DEFAULT_SCOPE")
protected boolean defaultScope;
public ClientScopeEntity getClientScope() {
return clientScope;
public String getClientScopeId() {
return clientScopeId;
}
public void setClientScope(ClientScopeEntity clientScope) {
this.clientScope = clientScope;
public void setClientScopeId(String clientScopeId) {
this.clientScopeId = clientScopeId;
}
public ClientEntity getClient() {
@ -84,20 +83,20 @@ public class ClientScopeClientMappingEntity {
public static class Key implements Serializable {
protected ClientScopeEntity clientScope;
protected String clientScopeId;
protected ClientEntity client;
public Key() {
}
public Key(ClientScopeEntity clientScope, ClientEntity client) {
this.clientScope = clientScope;
public Key(String clientScopeId, ClientEntity client) {
this.clientScopeId = clientScopeId;
this.client = client;
}
public ClientScopeEntity getClientScope() {
return clientScope;
public String getClientScopeId() {
return clientScopeId;
}
public ClientEntity getClient() {
@ -111,7 +110,7 @@ public class ClientScopeClientMappingEntity {
ClientScopeClientMappingEntity.Key key = (ClientScopeClientMappingEntity.Key) o;
if (clientScope != null ? !clientScope.getId().equals(key.clientScope != null ? key.clientScope.getId() : null) : key.clientScope != null) return false;
if (clientScopeId != null ? !clientScopeId.equals(key.getClientScopeId() != null ? key.getClientScopeId() : null) : key.getClientScopeId() != null) return false;
if (client != null ? !client.getId().equals(key.client != null ? key.client.getId() : null) : key.client != null) return false;
return true;
@ -119,7 +118,7 @@ public class ClientScopeClientMappingEntity {
@Override
public int hashCode() {
int result = clientScope != null ? clientScope.getId().hashCode() : 0;
int result = clientScopeId != null ? clientScopeId.hashCode() : 0;
result = 31 * result + (client != null ? client.getId().hashCode() : 0);
return result;
}
@ -133,7 +132,7 @@ public class ClientScopeClientMappingEntity {
ClientScopeClientMappingEntity key = (ClientScopeClientMappingEntity) o;
if (clientScope != null ? !clientScope.getId().equals(key.clientScope != null ? key.clientScope.getId() : null) : key.clientScope != null) return false;
if (clientScopeId != null ? !clientScopeId.equals(key.getClientScopeId() != null ? key.getClientScopeId() : null) : key.getClientScopeId() != null) return false;
if (client != null ? !client.getId().equals(key.client != null ? key.client.getId() : null) : key.client != null) return false;
return true;
@ -141,7 +140,7 @@ public class ClientScopeClientMappingEntity {
@Override
public int hashCode() {
int result = clientScope != null ? clientScope.getId().hashCode() : 0;
int result = clientScopeId != null ? clientScopeId.hashCode() : 0;
result = 31 * result + (client != null ? client.getId().hashCode() : 0);
return result;
}

View file

@ -17,7 +17,6 @@
package org.keycloak.models.jpa.entities;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
@ -33,8 +32,9 @@ import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
@ -47,6 +47,9 @@ import org.hibernate.annotations.Nationalized;
*/
@Entity
@Table(name="CLIENT_SCOPE", uniqueConstraints = {@UniqueConstraint(columnNames = {"REALM_ID", "NAME"})})
@NamedQueries({
@NamedQuery(name="getClientScopeIds", query="select scope.id from ClientScopeEntity scope where scope.realm.id = :realm")
})
public class ClientScopeEntity {
@Id

View file

@ -36,8 +36,8 @@ import javax.persistence.Table;
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@NamedQueries({
@NamedQuery(name="defaultClientScopeRealmMappingIdsByRealm", query="select m.clientScope.id from DefaultClientScopeRealmMappingEntity m where m.realm = :realm and m.defaultScope = :defaultScope"),
@NamedQuery(name="deleteDefaultClientScopeRealmMapping", query="delete from DefaultClientScopeRealmMappingEntity where realm = :realm and clientScope = :clientScope"),
@NamedQuery(name="defaultClientScopeRealmMappingIdsByRealm", query="select m.clientScopeId from DefaultClientScopeRealmMappingEntity m where m.realm = :realm and m.defaultScope = :defaultScope"),
@NamedQuery(name="deleteDefaultClientScopeRealmMapping", query="delete from DefaultClientScopeRealmMappingEntity where realm = :realm and clientScopeId = :clientScopeId"),
@NamedQuery(name="deleteDefaultClientScopeRealmMappingByRealm", query="delete from DefaultClientScopeRealmMappingEntity where realm = :realm")
})
@Entity
@ -46,9 +46,8 @@ import javax.persistence.Table;
public class DefaultClientScopeRealmMappingEntity {
@Id
@ManyToOne(fetch= FetchType.LAZY)
@JoinColumn(name = "SCOPE_ID")
protected ClientScopeEntity clientScope;
@Column(name = "SCOPE_ID")
protected String clientScopeId;
@Id
@ManyToOne(fetch= FetchType.LAZY)
@ -58,12 +57,12 @@ public class DefaultClientScopeRealmMappingEntity {
@Column(name="DEFAULT_SCOPE")
protected boolean defaultScope;
public ClientScopeEntity getClientScope() {
return clientScope;
public String getClientScopeId() {
return clientScopeId;
}
public void setClientScope(ClientScopeEntity clientScope) {
this.clientScope = clientScope;
public void setClientScopeId(String clientScopeId) {
this.clientScopeId = clientScopeId;
}
public RealmEntity getRealm() {
@ -84,20 +83,20 @@ public class DefaultClientScopeRealmMappingEntity {
public static class Key implements Serializable {
protected ClientScopeEntity clientScope;
protected String clientScopeId;
protected RealmEntity realm;
public Key() {
}
public Key(ClientScopeEntity clientScope, RealmEntity realm) {
this.clientScope = clientScope;
public Key(String clientScopeId, RealmEntity realm) {
this.clientScopeId = clientScopeId;
this.realm = realm;
}
public ClientScopeEntity getClientScope() {
return clientScope;
public String getClientScopeId() {
return clientScopeId;
}
public RealmEntity getRealm() {
@ -111,7 +110,7 @@ public class DefaultClientScopeRealmMappingEntity {
DefaultClientScopeRealmMappingEntity.Key key = (DefaultClientScopeRealmMappingEntity.Key) o;
if (clientScope != null ? !clientScope.getId().equals(key.clientScope != null ? key.clientScope.getId() : null) : key.clientScope != null) return false;
if (clientScopeId != null ? !clientScopeId.equals(key.getClientScopeId() != null ? key.getClientScopeId() : null) : key.getClientScopeId() != null) return false;
if (realm != null ? !realm.getId().equals(key.realm != null ? key.realm.getId() : null) : key.realm != null) return false;
return true;
@ -119,7 +118,7 @@ public class DefaultClientScopeRealmMappingEntity {
@Override
public int hashCode() {
int result = clientScope != null ? clientScope.getId().hashCode() : 0;
int result = clientScopeId != null ? clientScopeId.hashCode() : 0;
result = 31 * result + (realm != null ? realm.getId().hashCode() : 0);
return result;
}
@ -133,7 +132,7 @@ public class DefaultClientScopeRealmMappingEntity {
DefaultClientScopeRealmMappingEntity key = (DefaultClientScopeRealmMappingEntity) o;
if (clientScope != null ? !clientScope.getId().equals(key.clientScope != null ? key.clientScope.getId() : null) : key.clientScope != null) return false;
if (clientScopeId != null ? !clientScopeId.equals(key.getClientScopeId() != null ? key.getClientScopeId() : null) : key.getClientScopeId() != null) return false;
if (realm != null ? !realm.getId().equals(key.realm != null ? key.realm.getId() : null) : key.realm != null) return false;
return true;
@ -141,7 +140,7 @@ public class DefaultClientScopeRealmMappingEntity {
@Override
public int hashCode() {
int result = clientScope != null ? clientScope.getId().hashCode() : 0;
int result = clientScopeId != null ? clientScopeId.hashCode() : 0;
result = 31 * result + (realm != null ? realm.getId().hashCode() : 0);
return result;
}

View file

@ -148,6 +148,7 @@ public class RealmEntity {
@OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm")
Collection<UserFederationMapperEntity> userFederationMappers;
@Deprecated
@OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm")
Collection<ClientScopeEntity> clientScopes;
@ -813,6 +814,7 @@ public class RealmEntity {
return this;
}
@Deprecated
public Collection<ClientScopeEntity> getClientScopes() {
if (clientScopes == null) {
clientScopes = new LinkedList<>();
@ -820,6 +822,7 @@ public class RealmEntity {
return clientScopes;
}
@Deprecated
public void setClientScopes(Collection<ClientScopeEntity> clientScopes) {
this.clientScopes = clientScopes;
}

View file

@ -38,4 +38,8 @@
</createIndex>
</changeSet>
<changeSet author="keycloak" id="map-remove-ri-13.0.0">
<dropForeignKeyConstraint baseTableName="DEFAULT_CLIENT_SCOPE" constraintName="FK_R_DEF_CLI_SCOPE_SCOPE"/>
<dropForeignKeyConstraint baseTableName="CLIENT_SCOPE_CLIENT" constraintName="FK_C_CLI_SCOPE_SCOPE"/>
</changeSet>
</databaseChangeLog>

View file

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

View file

@ -0,0 +1,170 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.clientscope;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.map.common.AbstractEntity;
public abstract class AbstractClientScopeEntity<K> implements AbstractEntity<K> {
private final K id;
private final String realmId;
private String name;
private String protocol;
private String description;
private final Set<String> scopeMappings = new LinkedHashSet<>();
private final Map<String, ProtocolMapperModel> protocolMappers = new HashMap<>();
private final Map<String, String> attributes = new HashMap<>();
/**
* Flag signalizing that any of the setters has been meaningfully used.
*/
protected boolean updated;
protected AbstractClientScopeEntity() {
this.id = null;
this.realmId = null;
}
public AbstractClientScopeEntity(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 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 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.clear();
this.attributes.putAll(attributes);
}
public ProtocolMapperModel addProtocolMapper(ProtocolMapperModel model) {
Objects.requireNonNull(model.getId(), "protocolMapper.id");
updated = true;
this.protocolMappers.put(model.getId(), model);
return model;
}
public Stream<ProtocolMapperModel> getProtocolMappers() {
return protocolMappers.values().stream();
}
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 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 getRealmId() {
return this.realmId;
}
public Stream<String> getScopeMappings() {
return scopeMappings.stream();
}
public void addScopeMapping(String id) {
if (id != null) {
updated = true;
scopeMappings.add(id);
}
}
public void deleteScopeMapping(String id) {
updated |= scopeMappings.remove(id);
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.clientscope;
import java.util.Objects;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.map.common.AbstractEntity;
public abstract class AbstractClientScopeModel<E extends AbstractEntity> implements ClientScopeModel {
protected final KeycloakSession session;
protected final RealmModel realm;
protected final E entity;
public AbstractClientScopeModel(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 boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ClientScopeModel)) return false;
ClientScopeModel that = (ClientScopeModel) o;
return Objects.equals(that.getId(), getId());
}
@Override
public int hashCode() {
return getId().hashCode();
}
}

View file

@ -0,0 +1,189 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.clientscope;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
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.models.utils.RoleUtils;
public class MapClientScopeAdapter extends AbstractClientScopeModel<MapClientScopeEntity> implements ClientScopeModel {
public MapClientScopeAdapter(KeycloakSession session, RealmModel realm, MapClientScopeEntity entity) {
super(session, realm, entity);
}
@Override
public String getId() {
return entity.getId().toString();
}
@Override
public String getName() {
return entity.getName();
}
@Override
public void setName(String name) {
entity.setName(KeycloakModelUtils.convertClientScopeName(name));
}
@Override
public String getDescription() {
return entity.getDescription();
}
@Override
public void setDescription(String description) {
entity.setDescription(description);
}
@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 RealmModel getRealm() {
return realm;
}
@Override
public Stream<ProtocolMapperModel> getProtocolMappersStream() {
return entity.getProtocolMappers().distinct();
}
@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()
.filter(pm -> Objects.equals(pm.getProtocol(), protocol) && Objects.equals(pm.getName(), name))
.findAny()
.orElse(null);
}
@Override
public Stream<RoleModel> getScopeMappingsStream() {
return this.entity.getScopeMappings()
.map(realm::getRoleById)
.filter(Objects::nonNull);
}
@Override
public Stream<RoleModel> getRealmScopeMappingsStream() {
return getScopeMappingsStream().filter(r -> RoleUtils.isRealmRole(r, realm));
}
@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) {
return RoleUtils.hasRole(getScopeMappingsStream(), role);
}
@Override
public String toString() {
return String.format("%s@%08x", getId(), System.identityHashCode(this));
}
}

View file

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

View file

@ -0,0 +1,165 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.clientscope;
import java.util.Comparator;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jboss.logging.Logger;
import static org.keycloak.common.util.StackUtil.getShortStackTrace;
import org.keycloak.models.ClientScopeModel.SearchableFields;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.ClientScopeProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.map.common.Serialization;
import org.keycloak.models.map.storage.MapKeycloakTransaction;
import org.keycloak.models.map.storage.MapStorage;
import org.keycloak.models.map.storage.ModelCriteriaBuilder;
import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator;
import org.keycloak.models.utils.KeycloakModelUtils;
public class MapClientScopeProvider implements ClientScopeProvider {
private static final Logger LOG = Logger.getLogger(MapClientScopeProvider.class);
private static final Predicate<MapClientScopeEntity> ALWAYS_FALSE = c -> { return false; };
private final KeycloakSession session;
private final MapKeycloakTransaction<UUID, MapClientScopeEntity, ClientScopeModel> tx;
private final MapStorage<UUID, MapClientScopeEntity, ClientScopeModel> clientScopeStore;
private static final Comparator<MapClientScopeEntity> COMPARE_BY_NAME = Comparator.comparing(MapClientScopeEntity::getName);
public MapClientScopeProvider(KeycloakSession session, MapStorage<UUID, MapClientScopeEntity, ClientScopeModel> clientScopeStore) {
this.session = session;
this.clientScopeStore = clientScopeStore;
this.tx = clientScopeStore.createTransaction();
session.getTransactionManager().enlist(tx);
}
private MapClientScopeEntity registerEntityForChanges(MapClientScopeEntity origEntity) {
final MapClientScopeEntity res = tx.read(origEntity.getId(), id -> Serialization.from(origEntity));
tx.updateIfChanged(origEntity.getId(), res, MapClientScopeEntity::isUpdated);
return res;
}
private Function<MapClientScopeEntity, ClientScopeModel> entityToAdapterFunc(RealmModel realm) {
// Clone entity before returning back, to avoid giving away a reference to the live object to the caller
return origEntity -> new MapClientScopeAdapter(session, realm, registerEntityForChanges(origEntity));
}
private Predicate<MapClientScopeEntity> entityRealmFilter(RealmModel realm) {
if (realm == null || realm.getId() == null) {
return MapClientScopeProvider.ALWAYS_FALSE;
}
String realmId = realm.getId();
return entity -> Objects.equals(realmId, entity.getRealmId());
}
@Override
public Stream<ClientScopeModel> getClientScopesStream(RealmModel realm) {
ModelCriteriaBuilder<ClientScopeModel> mcb = clientScopeStore.createCriteriaBuilder()
.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId());
return tx.getUpdatedNotRemoved(mcb)
.sorted(COMPARE_BY_NAME)
.map(entityToAdapterFunc(realm));
}
@Override
public ClientScopeModel addClientScope(RealmModel realm, String id, String name) {
// Check Db constraint: @UniqueConstraint(columnNames = {"REALM_ID", "NAME"})
ModelCriteriaBuilder<ClientScopeModel> mcb = clientScopeStore.createCriteriaBuilder()
.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId())
.compare(SearchableFields.NAME, Operator.EQ, name);
if (tx.getCount(mcb) > 0) {
throw new ModelDuplicateException("Client scope with name '" + name + "' in realm " + realm.getName());
}
final UUID entityId = id == null ? UUID.randomUUID() : UUID.fromString(id);
LOG.tracef("addClientScope(%s, %s, %s)%s", realm, id, name, getShortStackTrace());
MapClientScopeEntity entity = new MapClientScopeEntity(entityId, realm.getId());
entity.setName(KeycloakModelUtils.convertClientScopeName(name));
if (tx.read(entity.getId()) != null) {
throw new ModelDuplicateException("Client scope exists: " + id);
}
tx.create(entity.getId(), entity);
return entityToAdapterFunc(realm).apply(entity);
}
@Override
public boolean removeClientScope(RealmModel realm, String id) {
if (id == null) return false;
ClientScopeModel clientScope = getClientScopeById(realm, id);
if (clientScope == null) return false;
if (KeycloakModelUtils.isClientScopeUsed(realm, clientScope)) {
throw new ModelException("Cannot remove client scope, it is currently in use");
}
session.users().preRemove(clientScope);
realm.removeDefaultClientScope(clientScope);
tx.delete(UUID.fromString(id));
return true;
}
@Override
public void removeClientScopes(RealmModel realm) {
LOG.tracef("removeClients(%s)%s", realm, getShortStackTrace());
getClientScopesStream(realm)
.map(ClientScopeModel::getId)
.collect(Collectors.toSet()) // This is necessary to read out all the client IDs before removing the clients
.forEach(id -> removeClientScope(realm, id));
}
@Override
public ClientScopeModel getClientScopeById(RealmModel realm, String id) {
if (id == null) {
return null;
}
LOG.tracef("getClientScopeById(%s, %s)%s", realm, id, getShortStackTrace());
UUID uuid;
try {
uuid = UUID.fromString(id);
} catch (IllegalArgumentException ex) {
return null;
}
MapClientScopeEntity entity = tx.read(uuid);
return (entity == null || ! entityRealmFilter(realm).test(entity))
? null
: entityToAdapterFunc(realm).apply(entity);
}
@Override
public void close() {
}
}

View file

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

View file

@ -17,11 +17,13 @@
package org.keycloak.models.map.storage;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.map.authSession.AbstractRootAuthenticationSessionEntity;
import org.keycloak.models.map.client.AbstractClientEntity;
import org.keycloak.models.map.clientscope.AbstractClientScopeEntity;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.group.AbstractGroupEntity;
import org.keycloak.models.map.role.AbstractRoleEntity;
@ -48,6 +50,7 @@ import java.util.function.Predicate;
public class MapFieldPredicates {
public static final Map<SearchableModelField<ClientModel>, UpdatePredicatesFunc<Object, AbstractClientEntity<Object>, ClientModel>> CLIENT_PREDICATES = basePredicates(ClientModel.SearchableFields.ID);
public static final Map<SearchableModelField<ClientScopeModel>, UpdatePredicatesFunc<Object, AbstractClientScopeEntity<Object>, ClientScopeModel>> CLIENT_SCOPE_PREDICATES = basePredicates(ClientScopeModel.SearchableFields.ID);
public static final Map<SearchableModelField<GroupModel>, UpdatePredicatesFunc<Object, AbstractGroupEntity<Object>, GroupModel>> GROUP_PREDICATES = basePredicates(GroupModel.SearchableFields.ID);
public static final Map<SearchableModelField<RoleModel>, UpdatePredicatesFunc<Object, AbstractRoleEntity<Object>, RoleModel>> ROLE_PREDICATES = basePredicates(RoleModel.SearchableFields.ID);
public static final Map<SearchableModelField<UserModel>, UpdatePredicatesFunc<Object, AbstractUserEntity<Object>, UserModel>> USER_PREDICATES = basePredicates(UserModel.SearchableFields.ID);
@ -60,6 +63,9 @@ public class MapFieldPredicates {
put(CLIENT_PREDICATES, ClientModel.SearchableFields.REALM_ID, AbstractClientEntity::getRealmId);
put(CLIENT_PREDICATES, ClientModel.SearchableFields.CLIENT_ID, AbstractClientEntity::getClientId);
put(CLIENT_SCOPE_PREDICATES, ClientScopeModel.SearchableFields.REALM_ID, AbstractClientScopeEntity::getRealmId);
put(CLIENT_SCOPE_PREDICATES, ClientScopeModel.SearchableFields.NAME, AbstractClientScopeEntity::getName);
put(GROUP_PREDICATES, GroupModel.SearchableFields.REALM_ID, AbstractGroupEntity::getRealmId);
put(GROUP_PREDICATES, GroupModel.SearchableFields.NAME, AbstractGroupEntity::getName);
put(GROUP_PREDICATES, GroupModel.SearchableFields.PARENT_ID, AbstractGroupEntity::getParentId);
@ -95,6 +101,7 @@ public class MapFieldPredicates {
static {
PREDICATES.put(ClientModel.class, CLIENT_PREDICATES);
PREDICATES.put(ClientScopeModel.class, CLIENT_SCOPE_PREDICATES);
PREDICATES.put(RoleModel.class, ROLE_PREDICATES);
PREDICATES.put(GroupModel.class, GROUP_PREDICATES);
PREDICATES.put(UserModel.class, USER_PREDICATES);

View file

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

View file

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

View file

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

View file

@ -18,6 +18,7 @@
package org.keycloak.models.cache;
import org.keycloak.models.ClientProvider;
import org.keycloak.models.ClientScopeProvider;
import org.keycloak.models.GroupProvider;
import org.keycloak.models.RealmProvider;
import org.keycloak.models.RoleProvider;
@ -26,14 +27,14 @@ import org.keycloak.models.RoleProvider;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface CacheRealmProvider extends RealmProvider, ClientProvider, GroupProvider, RoleProvider {
public interface CacheRealmProvider extends RealmProvider, ClientProvider, ClientScopeProvider, GroupProvider, RoleProvider {
void clear();
RealmProvider getRealmDelegate();
void registerRealmInvalidation(String id, String name);
void registerClientInvalidation(String id, String clientId, String realmId);
void registerClientScopeInvalidation(String id);
void registerClientScopeInvalidation(String id, String realmId);
void registerRoleInvalidation(String id, String roleName, String roleContainerId);

View file

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

View file

@ -0,0 +1,107 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.clientscope;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.keycloak.Config;
import org.keycloak.component.ComponentFactory;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ProviderConfigProperty;
public interface ClientScopeStorageProviderFactory<T extends ClientScopeStorageProvider> extends ComponentFactory<T, ClientScopeStorageProvider> {
/**
* called per Keycloak transaction.
*
* @param session
* @param model
* @return
*/
@Override
T create(KeycloakSession session, ComponentModel model);
/**
* This is the name of the provider.
*
* @return
*/
@Override
String getId();
@Override
default void init(Config.Scope config) {
}
@Override
default void postInit(KeycloakSessionFactory factory) {
}
@Override
default void close() {
}
@Override
default String getHelpText() {
return "";
}
@Override
default List<ProviderConfigProperty> getConfigProperties() {
return Collections.EMPTY_LIST;
}
@Override
default void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
}
/**
* Called when ClientScopeStorageProviderFactory is created. This allows you to do initialization of any additional configuration
* you need to add.
*
* @param session
* @param realm
* @param model
*/
@Override
default void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {
}
/**
* configuration properties that are common across all ClientScopeStorageProvider implementations
*
* @return
*/
@Override
default List<ProviderConfigProperty> getCommonProviderConfigProperties() {
return ClientScopeStorageProviderSpi.commonConfig();
}
@Override
default
Map<String, Object> getTypeMetadata() {
return new HashMap<>();
}
}

View file

@ -0,0 +1,56 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.clientscope;
import org.keycloak.component.ComponentModel;
import org.keycloak.storage.CacheableStorageProviderModel;
/**
* Stored configuration of a Client scope Storage provider instance.
*/
public class ClientScopeStorageProviderModel extends CacheableStorageProviderModel {
public ClientScopeStorageProviderModel() {
setProviderType(ClientScopeStorageProvider.class.getName());
}
public ClientScopeStorageProviderModel(ComponentModel copy) {
super(copy);
}
private transient Boolean enabled;
@Override
public void setEnabled(boolean flag) {
enabled = flag;
getConfig().putSingle(ENABLED, Boolean.toString(flag));
}
@Override
public boolean isEnabled() {
if (enabled == null) {
String val = getConfig().getFirst(ENABLED);
if (val == null) {
enabled = true;
} else {
enabled = Boolean.valueOf(val);
}
}
return enabled;
}
}

View file

@ -0,0 +1,79 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.clientscope;
import java.util.Collections;
import java.util.List;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
public class ClientScopeStorageProviderSpi implements Spi {
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return "clientscope-storage";
}
@Override
public Class<? extends Provider> getProviderClass() {
return ClientScopeStorageProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return ClientScopeStorageProviderFactory.class;
}
private static final List<ProviderConfigProperty> commonConfig;
static {
//corresponds to properties defined in CacheableStorageProviderModel and PrioritizedComponentModel
List<ProviderConfigProperty> config = ProviderConfigurationBuilder.create()
.property()
.name("enabled").type(ProviderConfigProperty.BOOLEAN_TYPE).add()
.property()
.name("priority").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("cachePolicy").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("maxLifespan").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("evictionHour").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("evictionMinute").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("evictionDay").type(ProviderConfigProperty.STRING_TYPE).add()
.property()
.name("cacheInvalidBefore").type(ProviderConfigProperty.STRING_TYPE).add()
.build();
commonConfig = Collections.unmodifiableList(config);
}
public static List<ProviderConfigProperty> commonConfig() {
return commonConfig;
}
}

View file

@ -19,6 +19,7 @@ org.keycloak.provider.ExceptionConverterSpi
org.keycloak.storage.UserStorageProviderSpi
org.keycloak.storage.federated.UserFederatedStorageProviderSpi
org.keycloak.models.ClientSpi
org.keycloak.models.ClientScopeSpi
org.keycloak.models.GroupSpi
org.keycloak.models.RealmSpi
org.keycloak.models.RoleSpi
@ -76,6 +77,7 @@ org.keycloak.credential.CredentialSpi
org.keycloak.keys.PublicKeyStorageSpi
org.keycloak.keys.KeySpi
org.keycloak.storage.client.ClientStorageProviderSpi
org.keycloak.storage.clientscope.ClientScopeStorageProviderSpi
org.keycloak.storage.role.RoleStorageProviderSpi
org.keycloak.storage.group.GroupStorageProviderSpi
org.keycloak.crypto.SignatureSpi

View file

@ -20,12 +20,20 @@ package org.keycloak.models;
import java.util.Map;
import org.keycloak.common.util.ObjectUtil;
import org.keycloak.storage.SearchableModelField;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface ClientScopeModel extends ProtocolMapperContainerModel, ScopeContainerModel, OrderedModel {
public static class SearchableFields {
public static final SearchableModelField<ClientScopeModel> ID = new SearchableModelField<>("id", String.class);
public static final SearchableModelField<ClientScopeModel> REALM_ID = new SearchableModelField<>("realmId", String.class);
public static final SearchableModelField<ClientScopeModel> NAME = new SearchableModelField<>("name", String.class);
}
String getId();
String getName();

View file

@ -0,0 +1,76 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models;
import java.util.stream.Stream;
import org.keycloak.provider.Provider;
import org.keycloak.storage.clientscope.ClientScopeLookupProvider;
/**
* Provider of the client scopes records.
*/
public interface ClientScopeProvider extends Provider, ClientScopeLookupProvider {
/**
* Returns all the client scopes of the given realm as a stream.
* @param realm Realm.
* @return Stream of the client scopes. Never returns {@code null}.
*/
Stream<ClientScopeModel> getClientScopesStream(RealmModel realm);
/**
* Creates new client scope with given {@code name} to the given realm.
* Spaces in {@code name} will be replaced by underscore so that scope name
* can be used as value of scope parameter. The internal ID will be created automatically.
* @param realm Realm owning this client scope.
* @param name String name of the client scope.
* @return Model of the created client scope.
* @throws ModelDuplicateException if client scope with given name already exists
*/
default ClientScopeModel addClientScope(RealmModel realm, String name) {
return ClientScopeProvider.this.addClientScope(realm, null, name);
}
/**
* Creates new client scope with given internal ID and {@code name} to the given realm.
* Spaces in {@code name} will be replaced by underscore so that scope name
* can be used as value of scope parameter.
* @param realm Realm owning this client scope.
* @param id Internal ID of the client scope or {@code null} if one is to be created by the underlying store
* @param name String name of the client scope.
* @return Model of the created client scope.
* @throws IllegalArgumentException If {@code id} does not conform
* the format understood by the underlying store.
* @throws ModelDuplicateException if client scope with given name already exists
*/
ClientScopeModel addClientScope(RealmModel realm, String id, String name);
/**
* Removes client scope from the given realm.
* @param realm Realm.
* @param id Internal ID of the client scope
* @return {@code true} if the client scope existed and has been removed, {@code false} otherwise.
* @throws ModelException if client scope is in use.
*/
boolean removeClientScope(RealmModel realm, String id);
/**
* Removes all client scopes from the given realm.
* @param realm Realm.
*/
void removeClientScopes(RealmModel realm);
}

View file

@ -115,6 +115,15 @@ public interface KeycloakSession {
*/
ClientProvider clients();
/**
* Returns a managed provider instance. Will start a provider transaction. This transaction is managed by the KeycloakSession
* transaction.
*
* @return Currently used ClientScopeProvider instance.
* @throws IllegalStateException if transaction is not active
*/
ClientScopeProvider clientScopes();
/**
* Returns a managed group provider instance.
*
@ -162,9 +171,16 @@ public interface KeycloakSession {
*/
UserProvider users();
/**
* @return ClientStorageManager instance
*/
ClientProvider clientStorageManager();
/**
* @return ClientScopeStorageManager instance
*/
ClientScopeProvider clientScopeStorageManager();
/**
* @return RoleStorageManager instance
*/
@ -205,6 +221,13 @@ public interface KeycloakSession {
*/
ClientProvider clientLocalStorage();
/**
* Keycloak specific local storage for client scopes. No cache in front, this api talks directly to database configured for Keycloak
*
* @return
*/
ClientScopeProvider clientScopeLocalStorage();
/**
* Keycloak specific local storage for groups. No cache in front, this api talks directly to storage configured for Keycloak
*

View file

@ -885,15 +885,51 @@ public interface RealmModel extends RoleContainerModel {
*/
Stream<ClientScopeModel> getClientScopesStream();
/**
* Creates new client scope with the given name. Internal ID is created automatically.
* If given name contains spaces, those are replaced by underscores.
* @param name {@code String} name of the client scope.
* @return Model of the created client scope.
* @throws ModelDuplicateException if client scope with same id or name already exists.
*/
ClientScopeModel addClientScope(String name);
/**
* Creates new client scope with the given internal ID and name.
* If given name contains spaces, those are replaced by underscores.
* @param id {@code String} id of the client scope.
* @param name {@code String} name of the client scope.
* @return Model of the created client scope.
* @throws ModelDuplicateException if client scope with same id or name already exists.
*/
ClientScopeModel addClientScope(String id, String name);
/**
* Removes client scope with given {@code id} from this realm.
* @param id of the client scope
* @return true if the realm contained the scope and the removal was successful, false otherwise
*/
boolean removeClientScope(String id);
/**
* @param id of the client scope
* @return Client scope with the given {@code id}, or {@code null} when the scope does not exist.
*/
ClientScopeModel getClientScopeById(String id);
/**
* Adds given client scopes among default/optional client scopes of this realm.
* The scope will be assigned to each new client.
* @param clientScope to be added
* @param defaultScope if {@code true} the scope will be added among default client scopes,
* if {@code false} it will be added among optional client scopes
*/
void addDefaultClientScope(ClientScopeModel clientScope, boolean defaultScope);
/**
* Removes given client scope from default or optional client scopes of this realm.
* @param clientScope to be removed
*/
void removeDefaultClientScope(ClientScopeModel clientScope);
/**

View file

@ -30,7 +30,7 @@ import java.util.stream.Stream;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface RealmProvider extends Provider /* TODO: Remove in future version */, ClientProvider, GroupProvider, RoleProvider /* up to here */ {
public interface RealmProvider extends Provider /* TODO: Remove in future version */, ClientProvider, ClientScopeProvider, GroupProvider, RoleProvider /* up to here */ {
// Note: The reason there are so many query methods here is for layering a cache on top of an persistent KeycloakSession
MigrationModel getMigrationModel();
@ -39,7 +39,16 @@ public interface RealmProvider extends Provider /* TODO: Remove in future versio
RealmModel getRealm(String id);
RealmModel getRealmByName(String name);
ClientScopeModel getClientScopeById(String id, RealmModel realm);
/**
* @deprecated Use the corresponding method from {@link ClientScopeProvider}. */
default ClientScopeModel getClientScopeById(String id, RealmModel realm) {
return getClientScopeById(realm, id);
}
/**
* @deprecated Use the corresponding method from {@link ClientScopeProvider}. */
@Override
ClientScopeModel getClientScopeById(RealmModel realm, String id);
/**
* @deprecated Use {@link #getRealmsStream() getRealmsStream} instead.

View file

@ -0,0 +1,32 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.clientscope;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.RealmModel;
public interface ClientScopeLookupProvider {
/**
* Exact search for a client scope by its internal ID..
* @param realm Realm.
* @param id Internal ID of the role.
* @return Model of the client scope.
*/
ClientScopeModel getClientScopeById(RealmModel realm, String id);
}

View file

@ -22,6 +22,7 @@ import org.keycloak.credential.UserCredentialStoreManager;
import org.keycloak.jose.jws.DefaultTokenManager;
import org.keycloak.keys.DefaultKeyManager;
import org.keycloak.models.ClientProvider;
import org.keycloak.models.ClientScopeProvider;
import org.keycloak.models.GroupProvider;
import org.keycloak.models.TokenManager;
import org.keycloak.models.KeycloakContext;
@ -43,6 +44,7 @@ import org.keycloak.services.clientpolicy.ClientPolicyManager;
import org.keycloak.services.clientpolicy.DefaultClientPolicyManager;
import org.keycloak.sessions.AuthenticationSessionProvider;
import org.keycloak.storage.ClientStorageManager;
import org.keycloak.storage.ClientScopeStorageManager;
import org.keycloak.storage.GroupStorageManager;
import org.keycloak.storage.RoleStorageManager;
import org.keycloak.storage.UserStorageManager;
@ -71,10 +73,12 @@ public class DefaultKeycloakSession implements KeycloakSession {
private final Map<String, Object> attributes = new HashMap<>();
private RealmProvider model;
private ClientProvider clientProvider;
private ClientScopeProvider clientScopeProvider;
private GroupProvider groupProvider;
private RoleProvider roleProvider;
private UserStorageManager userStorageManager;
private ClientStorageManager clientStorageManager;
private ClientScopeStorageManager clientScopeStorageManager;
private RoleStorageManager roleStorageManager;
private GroupStorageManager groupStorageManager;
private UserCredentialStoreManager userCredentialStorageManager;
@ -118,6 +122,16 @@ public class DefaultKeycloakSession implements KeycloakSession {
}
}
private ClientScopeProvider getClientScopeProvider() {
// TODO: Extract ClientScopeProvider from CacheRealmProvider and use that instead
ClientScopeProvider cache = getProvider(CacheRealmProvider.class);
if (cache != null) {
return cache;
} else {
return clientScopeStorageManager();
}
}
private GroupProvider getGroupProvider() {
// TODO: Extract GroupProvider from CacheRealmProvider and use that instead
GroupProvider cache = getProvider(CacheRealmProvider.class);
@ -204,6 +218,11 @@ public class DefaultKeycloakSession implements KeycloakSession {
return getProvider(ClientProvider.class);
}
@Override
public ClientScopeProvider clientScopeLocalStorage() {
return getProvider(ClientScopeProvider.class);
}
@Override
public GroupProvider groupLocalStorage() {
return getProvider(GroupProvider.class);
@ -217,6 +236,14 @@ public class DefaultKeycloakSession implements KeycloakSession {
return clientStorageManager;
}
@Override
public ClientScopeProvider clientScopeStorageManager() {
if (clientScopeStorageManager == null) {
clientScopeStorageManager = new ClientScopeStorageManager(this);
}
return clientScopeStorageManager;
}
@Override
public RoleProvider roleLocalStorage() {
return getProvider(RoleProvider.class);
@ -350,6 +377,14 @@ public class DefaultKeycloakSession implements KeycloakSession {
return clientProvider;
}
@Override
public ClientScopeProvider clientScopes() {
if (clientScopeProvider == null) {
clientScopeProvider = getClientScopeProvider();
}
return clientScopeProvider;
}
@Override
public GroupProvider groups() {
if (groupProvider == null) {

View file

@ -0,0 +1,76 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage;
import java.util.stream.Stream;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.ClientScopeProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.storage.clientscope.ClientScopeLookupProvider;
import org.keycloak.storage.clientscope.ClientScopeStorageProvider;
import org.keycloak.storage.clientscope.ClientScopeStorageProviderFactory;
import org.keycloak.storage.clientscope.ClientScopeStorageProviderModel;
public class ClientScopeStorageManager extends AbstractStorageManager<ClientScopeStorageProvider, ClientScopeStorageProviderModel> implements ClientScopeProvider {
public ClientScopeStorageManager(KeycloakSession session) {
super(session, ClientScopeStorageProviderFactory.class, ClientScopeStorageProvider.class,
ClientScopeStorageProviderModel::new, "clientscope");
}
/* CLIENT SCOPE PROVIDER LOOKUP METHODS - implemented by client scope storage providers */
@Override
public ClientScopeModel getClientScopeById(RealmModel realm, String id) {
StorageId storageId = new StorageId(id);
if (storageId.getProviderId() == null) {
return session.clientScopeLocalStorage().getClientScopeById(realm, id);
}
ClientScopeLookupProvider provider = getStorageProviderInstance(realm, storageId.getProviderId(), ClientScopeLookupProvider.class);
if (provider == null) return null;
return provider.getClientScopeById(realm, id);
}
/* CLIENT SCOPE PROVIDER METHODS - provided only by local storage (e.g. not supported by storage providers) */
@Override
public Stream<ClientScopeModel> getClientScopesStream(RealmModel realm) {
return session.clientScopeLocalStorage().getClientScopesStream(realm);
}
@Override
public ClientScopeModel addClientScope(RealmModel realm, String id, String name) {
return session.clientScopeLocalStorage().addClientScope(realm, id, name);
}
@Override
public boolean removeClientScope(RealmModel realm, String id) {
return session.clientScopeLocalStorage().removeClientScope(realm, id);
}
@Override
public void removeClientScopes(RealmModel realm) {
session.clientScopeLocalStorage().removeClientScopes(realm);
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,182 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.federation;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Stream;
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.storage.StorageId;
import org.keycloak.storage.clientscope.ClientScopeLookupProvider;
import org.keycloak.storage.clientscope.ClientScopeStorageProvider;
import org.keycloak.storage.clientscope.ClientScopeStorageProviderModel;
public class HardcodedClientScopeStorageProvider implements ClientScopeStorageProvider, ClientScopeLookupProvider {
private final ClientScopeStorageProviderModel component;
private final String clientScopeName;
public HardcodedClientScopeStorageProvider(KeycloakSession session, ClientScopeStorageProviderModel component) {
this.component = component;
this.clientScopeName = component.getConfig().getFirst(HardcodedClientScopeStorageProviderFactory.SCOPE_NAME);
}
@Override
public ClientScopeModel getClientScopeById(RealmModel realm, String id) {
StorageId storageId = new StorageId(id);
final String scopeName = storageId.getExternalId();
if (this.clientScopeName.equals(scopeName)) return new HardcodedClientScopeAdapter(realm);
return null;
}
@Override
public void close() {
}
public class HardcodedClientScopeAdapter implements ClientScopeModel {
private final RealmModel realm;
private StorageId storageId;
public HardcodedClientScopeAdapter(RealmModel realm) {
this.realm = realm;
}
@Override
public String getId() {
if (storageId == null) {
storageId = new StorageId(component.getId(), getName());
}
return storageId.getId();
}
@Override
public String getName() {
return clientScopeName;
}
@Override
public RealmModel getRealm() {
return realm;
}
@Override
public void setName(String name) {
throw new UnsupportedOperationException("Not supported.");
}
@Override
public String getDescription() {
return "Federated client scope";
}
@Override
public void setDescription(String description) {
throw new UnsupportedOperationException("Not supported.");
}
@Override
public String getProtocol() {
return "openid-connect";
}
@Override
public void setProtocol(String protocol) {
throw new UnsupportedOperationException("Not supported.");
}
@Override
public void setAttribute(String name, String value) {
throw new UnsupportedOperationException("Not supported.");
}
@Override
public void removeAttribute(String name) {
throw new UnsupportedOperationException("Not supported.");
}
@Override
public String getAttribute(String name) {
return null;
}
@Override
public Map<String, String> getAttributes() {
return Collections.EMPTY_MAP;
}
@Override
public Stream<ProtocolMapperModel> getProtocolMappersStream() {
return Stream.empty();
}
@Override
public ProtocolMapperModel addProtocolMapper(ProtocolMapperModel model) {
throw new UnsupportedOperationException("Not supported.");
}
@Override
public void removeProtocolMapper(ProtocolMapperModel mapping) {
throw new UnsupportedOperationException("Not supported.");
}
@Override
public void updateProtocolMapper(ProtocolMapperModel mapping) {
throw new UnsupportedOperationException("Not supported.");
}
@Override
public ProtocolMapperModel getProtocolMapperById(String id) {
return null;
}
@Override
public ProtocolMapperModel getProtocolMapperByName(String protocol, String name) {
return null;
}
@Override
public Stream<RoleModel> getScopeMappingsStream() {
return Stream.empty();
}
@Override
public Stream<RoleModel> getRealmScopeMappingsStream() {
return Stream.empty();
}
@Override
public void addScopeMapping(RoleModel role) {
throw new UnsupportedOperationException("Not supported.");
}
@Override
public void deleteScopeMapping(RoleModel role) {
throw new UnsupportedOperationException("Not supported.");
}
@Override
public boolean hasScope(RoleModel role) {
return false;
}
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.federation;
import java.util.List;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.storage.clientscope.ClientScopeStorageProviderFactory;
import org.keycloak.storage.clientscope.ClientScopeStorageProviderModel;
public class HardcodedClientScopeStorageProviderFactory implements ClientScopeStorageProviderFactory<HardcodedClientScopeStorageProvider> {
public static final String PROVIDER_ID = "hardcoded-clientscope";
public static final String SCOPE_NAME = "scope_name";
protected static final List<ProviderConfigProperty> CONFIG_PROPERTIES;
@Override
public HardcodedClientScopeStorageProvider create(KeycloakSession session, ComponentModel model) {
return new HardcodedClientScopeStorageProvider(session, new ClientScopeStorageProviderModel(model));
}
@Override
public String getId() {
return PROVIDER_ID;
}
static {
CONFIG_PROPERTIES = ProviderConfigurationBuilder.create()
.property().name(SCOPE_NAME)
.type(ProviderConfigProperty.STRING_TYPE)
.label("Hardcoded Scope Name")
.helpText("Only this scope name is available for lookup")
.defaultValue("hardcoded-clientscope")
.add()
.build();
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return CONFIG_PROPERTIES;
}
}

View file

@ -0,0 +1,17 @@
#
# Copyright 2021 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
org.keycloak.testsuite.federation.HardcodedClientScopeStorageProviderFactory

View file

@ -51,6 +51,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.junit.Assert.assertEquals;
@ -82,7 +83,8 @@ public class ClientScopeTest extends AbstractClientTest {
@Test (expected = NotFoundException.class)
public void testGetUnknownScope() {
clientScopes().get("unknown-id").toRepresentation();
String unknownId = UUID.randomUUID().toString();
clientScopes().get(unknownId).toRepresentation();
}

View file

@ -257,7 +257,7 @@ public class ExportImportTest extends AbstractKeycloakTest {
File testRealm = new File(url.getFile());
assertThat(testRealm, Matchers.notNullValue());
File newFile = new File("test-new-realm.json");
File newFile = new File("target", "test-new-realm.json");
try {
FileUtils.copyFile(testRealm, newFile);

View file

@ -48,6 +48,10 @@
"provider": "${keycloak.client.provider:jpa}"
},
"clientScope": {
"provider": "${keycloak.clientScope.provider:jpa}"
},
"group": {
"provider": "${keycloak.group.provider:jpa}"
},

View file

@ -0,0 +1,73 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.model;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Test;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmProvider;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.clientscope.ClientScopeStorageProvider;
import org.keycloak.storage.clientscope.ClientScopeStorageProviderModel;
import org.keycloak.testsuite.federation.HardcodedClientScopeStorageProviderFactory;
@RequireProvider(RealmProvider.class)
@RequireProvider(ClientScopeStorageProvider.class)
public class ClientScopeStorageTest extends KeycloakModelTest {
private String realmId;
private String clientScopeFederationId;
@Override
public void createEnvironment(KeycloakSession s) {
RealmModel realm = s.realms().createRealm("realm");
realm.setDefaultRole(s.roles().addRealmRole(realm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + realm.getName()));
this.realmId = realm.getId();
}
@Override
public void cleanEnvironment(KeycloakSession s) {
s.realms().removeRealm(realmId);
}
@Test
public void testGetClientScopeById() {
getParameters(ClientScopeStorageProviderModel.class).forEach(fs -> inComittedTransaction(fs, (session, federatedStorage) -> {
Assume.assumeThat("Cannot handle more than 1 client scope federation provider", clientScopeFederationId, Matchers.nullValue());
RealmModel realm = session.realms().getRealm(realmId);
federatedStorage.setParentId(realmId);
federatedStorage.setEnabled(true);
federatedStorage.getConfig().putSingle(HardcodedClientScopeStorageProviderFactory.SCOPE_NAME, HardcodedClientScopeStorageProviderFactory.SCOPE_NAME);
ComponentModel res = realm.addComponentModel(federatedStorage);
clientScopeFederationId = res.getId();
log.infof("Added %s client scope federation provider: %s", federatedStorage.getName(), clientScopeFederationId);
}));
inComittedTransaction(1, (session, i) -> {
final RealmModel realm = session.realms().getRealm(realmId);
StorageId storageId = new StorageId(clientScopeFederationId, "scope_name");
ClientScopeModel hardcoded = session.clientScopes().getClientScopeById(realm, storageId.getId());
Assert.assertNotNull(hardcoded);
});
}
}

View file

@ -25,6 +25,7 @@ import org.keycloak.events.EventStoreSpi;
import org.keycloak.executors.DefaultExecutorsProviderFactory;
import org.keycloak.executors.ExecutorsSpi;
import org.keycloak.models.AbstractKeycloakTransaction;
import org.keycloak.models.ClientScopeSpi;
import org.keycloak.models.ClientSpi;
import org.keycloak.models.GroupSpi;
import org.keycloak.models.KeycloakSession;
@ -141,6 +142,7 @@ public abstract class KeycloakModelTest {
private static final Set<Class<? extends Spi>> ALLOWED_SPIS = ImmutableSet.<Class<? extends Spi>>builder()
.add(AuthorizationSpi.class)
.add(ClientScopeSpi.class)
.add(ClientSpi.class)
.add(ClusterSpi.class)
.add(EventStoreSpi.class)

View file

@ -28,6 +28,7 @@ import org.keycloak.events.jpa.JpaEventStoreProviderFactory;
import org.keycloak.testsuite.model.KeycloakModelParameters;
import org.keycloak.models.dblock.DBLockSpi;
import org.keycloak.models.jpa.JpaClientProviderFactory;
import org.keycloak.models.jpa.JpaClientScopeProviderFactory;
import org.keycloak.models.jpa.JpaGroupProviderFactory;
import org.keycloak.models.jpa.JpaRealmProviderFactory;
import org.keycloak.models.jpa.JpaRoleProviderFactory;
@ -57,6 +58,7 @@ public class Jpa extends KeycloakModelParameters {
.add(DefaultJpaConnectionProviderFactory.class)
.add(JPAAuthorizationStoreFactory.class)
.add(JpaClientProviderFactory.class)
.add(JpaClientScopeProviderFactory.class)
.add(JpaEventStoreProviderFactory.class)
.add(JpaGroupProviderFactory.class)
.add(JpaRealmProviderFactory.class)

View file

@ -22,9 +22,15 @@ import org.keycloak.provider.Spi;
import org.keycloak.storage.UserStorageProviderSpi;
import org.keycloak.storage.federated.UserFederatedStorageProviderSpi;
import org.keycloak.storage.jpa.JpaUserFederatedStorageProviderFactory;
import org.keycloak.testsuite.federation.BackwardsCompatibilityUserStorageFactory;
import com.google.common.collect.ImmutableSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import org.keycloak.storage.clientscope.ClientScopeStorageProvider;
import org.keycloak.storage.clientscope.ClientScopeStorageProviderFactory;
import org.keycloak.storage.clientscope.ClientScopeStorageProviderModel;
import org.keycloak.storage.clientscope.ClientScopeStorageProviderSpi;
import org.keycloak.testsuite.federation.HardcodedClientScopeStorageProviderFactory;
/**
*
@ -32,19 +38,36 @@ import java.util.Set;
*/
public class JpaFederation extends KeycloakModelParameters {
private final AtomicInteger counter = new AtomicInteger();
static final Set<Class<? extends Spi>> ALLOWED_SPIS = ImmutableSet.<Class<? extends Spi>>builder()
.addAll(Jpa.ALLOWED_SPIS)
.add(UserStorageProviderSpi.class)
.add(UserFederatedStorageProviderSpi.class)
.add(ClientScopeStorageProviderSpi.class)
.build();
static final Set<Class<? extends ProviderFactory>> ALLOWED_FACTORIES = ImmutableSet.<Class<? extends ProviderFactory>>builder()
.addAll(Jpa.ALLOWED_FACTORIES)
.add(JpaUserFederatedStorageProviderFactory.class)
.add(ClientScopeStorageProviderFactory.class)
.build();
public JpaFederation() {
super(ALLOWED_SPIS, ALLOWED_FACTORIES);
}
@Override
public <T> Stream<T> getParameters(Class<T> clazz) {
if (ClientScopeStorageProviderModel.class.isAssignableFrom(clazz)) {
ClientScopeStorageProviderModel federatedStorage = new ClientScopeStorageProviderModel();
federatedStorage.setName(HardcodedClientScopeStorageProviderFactory.PROVIDER_ID + ":" + counter.getAndIncrement());
federatedStorage.setProviderId(HardcodedClientScopeStorageProviderFactory.PROVIDER_ID);
federatedStorage.setProviderType(ClientScopeStorageProvider.class.getName());
return Stream.of((T) federatedStorage);
} else {
return super.getParameters(clazz);
}
}
}

View file

@ -22,6 +22,10 @@
"provider": "${keycloak.client.provider:jpa}"
},
"clientScope": {
"provider": "${keycloak.clientScope.provider:jpa}"
},
"group": {
"provider": "${keycloak.group.provider:jpa}"
},