diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c5bac025d..573a514904 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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" diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java index 09af85a341..8e32eef256 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java @@ -126,7 +126,7 @@ public class ClientAdapter implements ClientModel, CachedObject { Map 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); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientScopeAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientScopeAdapter.java index 3a34abc888..f98c99af6d 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientScopeAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientScopeAdapter.java @@ -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 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 getRealmScopeMappingsStream() { return getScopeMappingsStream().filter(r -> RoleUtils.isRealmRole(r, cachedRealm)); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java index 65e8396a7a..9fe63af4cb 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java @@ -1457,7 +1457,7 @@ public class RealmAdapter implements CachedRealmModel { public Stream 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 clientScopeIds = defaultScope ? cached.getDefaultDefaultClientScopes() : cached.getOptionalDefaultClientScopes(); return clientScopeIds.stream() - .map(scope -> cacheSession.getClientScopeById(scope, this)) + .map(scope -> cacheSession.getClientScopeById(this, scope)) .filter(Objects::nonNull); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java index ed562ac790..0684c59c18 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheManager.java @@ -72,6 +72,19 @@ public class RealmCacheManager extends CacheManager { addInvalidations(HasRolePredicate.create().role(id), invalidations); } + public void clientScopeAdded(String realmId, Set invalidations) { + invalidations.add(RealmCacheSession.getClientScopesCacheKey(realmId)); + } + + public void clientScopeUpdated(String realmId, Set invalidations) { + invalidations.add(RealmCacheSession.getClientScopesCacheKey(realmId)); + } + + public void clientScopeRemoval(String realmId, Set invalidations) { + invalidations.add(RealmCacheSession.getClientScopesCacheKey(realmId)); + addInvalidations(InRealmPredicate.create().realm(realmId), invalidations); + } + public void groupQueriesInvalidations(String realmId, Set invalidations) { invalidations.add(RealmCacheSession.getGroupsQueryCacheKey(realmId)); invalidations.add(RealmCacheSession.getTopGroupsQueryCacheKey(realmId)); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java index 89c2c4e42d..c3c63c20a7 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java @@ -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 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 model = getClientScopeDelegate().getClientScopesStream(realm).collect(Collectors.toSet()); + if (model == null) return null; + Set 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 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) { diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/ClientScopeListQuery.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/ClientScopeListQuery.java new file mode 100644 index 0000000000..0a13b34bf2 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/ClientScopeListQuery.java @@ -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 clientScopes; + private final String realm; + private final String realmName; + + public ClientScopeListQuery(Long revisioned, String id, RealmModel realm, Set clientScopes) { + super(revisioned, id); + this.realm = realm.getId(); + this.realmName = realm.getName(); + this.clientScopes = clientScopes; + } + + @Override + public Set getClientScopes() { + return clientScopes; + } + + @Override + public String getRealm() { + return realm; + } + + @Override + public String toString() { + return "ClientScopeListQuery{" + + "id='" + getId() + "'" + + ", realmName='" + realmName + '\'' + + '}'; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/ClientScopeQuery.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/ClientScopeQuery.java new file mode 100644 index 0000000000..ef2258f8ac --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/ClientScopeQuery.java @@ -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 getClientScopes(); +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientScopeAddedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientScopeAddedEvent.java new file mode 100644 index 0000000000..c594bb4c7c --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientScopeAddedEvent.java @@ -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 invalidations) { + realmCache.clientScopeAdded(realmId, invalidations); + } + + public static class ExternalizerImpl implements Externalizer { + + 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; + } + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientScopeRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientScopeRemovedEvent.java new file mode 100644 index 0000000000..d23840a311 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/ClientScopeRemovedEvent.java @@ -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 invalidations) { + realmCache.clientScopeRemoval(realmId, invalidations); + } + + public static class ExternalizerImpl implements Externalizer { + + 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; + } + } +} diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java index 3737202679..9a4ee881ad 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java @@ -364,7 +364,7 @@ public class ClientAdapter implements ClientModel, JpaModel { 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 { @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(); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaClientScopeProviderFactory.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaClientScopeProviderFactory.java new file mode 100644 index 0000000000..22227f1f4a --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaClientScopeProviderFactory.java @@ -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() { + } + +} diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java index c95958ac63..495d3c92d1 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java @@ -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 Bill Burke * @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 getClientScopesStream(RealmModel realm) { + TypedQuery query = em.createNamedQuery("getClientScopeIds", String.class); + query.setParameter("realm", realm.getId()); + Stream 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 searchForGroupByNameStream(RealmModel realm, String search, Integer first, Integer max) { TypedQuery query = em.createNamedQuery("getGroupIdsByNameContaining", String.class) diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index 43837b55bc..4a3fac60fb 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -1958,72 +1958,33 @@ public class RealmAdapter implements RealmModel, JpaModel { @Override public Stream 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 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 { @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(); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientScopeClientMappingEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientScopeClientMappingEntity.java index c75b319412..6067fd8c89 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientScopeClientMappingEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientScopeClientMappingEntity.java @@ -36,8 +36,8 @@ import javax.persistence.Table; * @author Marek Posolda */ @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; } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientScopeEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientScopeEntity.java index 5cc1139d8d..554b8d7496 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientScopeEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientScopeEntity.java @@ -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 diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/DefaultClientScopeRealmMappingEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/DefaultClientScopeRealmMappingEntity.java index 334724a8f9..c667b10ddf 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/DefaultClientScopeRealmMappingEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/DefaultClientScopeRealmMappingEntity.java @@ -36,8 +36,8 @@ import javax.persistence.Table; * @author Marek Posolda */ @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; } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java index 70b0b854c3..c69160d80f 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java @@ -148,6 +148,7 @@ public class RealmEntity { @OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm") Collection userFederationMappers; + @Deprecated @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm") Collection clientScopes; @@ -813,6 +814,7 @@ public class RealmEntity { return this; } + @Deprecated public Collection getClientScopes() { if (clientScopes == null) { clientScopes = new LinkedList<>(); @@ -820,6 +822,7 @@ public class RealmEntity { return clientScopes; } + @Deprecated public void setClientScopes(Collection clientScopes) { this.clientScopes = clientScopes; } diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-13.0.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-13.0.0.xml index 23d6ad032e..3243afa857 100644 --- a/model/jpa/src/main/resources/META-INF/jpa-changelog-13.0.0.xml +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-13.0.0.xml @@ -38,4 +38,8 @@ + + + + diff --git a/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.ClientScopeProviderFactory b/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.ClientScopeProviderFactory new file mode 100644 index 0000000000..f5f1cbaf4b --- /dev/null +++ b/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.ClientScopeProviderFactory @@ -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 \ No newline at end of file diff --git a/model/map/src/main/java/org/keycloak/models/map/clientscope/AbstractClientScopeEntity.java b/model/map/src/main/java/org/keycloak/models/map/clientscope/AbstractClientScopeEntity.java new file mode 100644 index 0000000000..f4d8159591 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/clientscope/AbstractClientScopeEntity.java @@ -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 implements AbstractEntity { + + private final K id; + private final String realmId; + + private String name; + private String protocol; + private String description; + + private final Set scopeMappings = new LinkedHashSet<>(); + private final Map protocolMappers = new HashMap<>(); + private final Map 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 getAttributes() { + return attributes; + } + + public void setAttributes(Map 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 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 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 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); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/clientscope/AbstractClientScopeModel.java b/model/map/src/main/java/org/keycloak/models/map/clientscope/AbstractClientScopeModel.java new file mode 100644 index 0000000000..5e6fcb6df7 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/clientscope/AbstractClientScopeModel.java @@ -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 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(); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/clientscope/MapClientScopeAdapter.java b/model/map/src/main/java/org/keycloak/models/map/clientscope/MapClientScopeAdapter.java new file mode 100644 index 0000000000..eae5fd8cdd --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/clientscope/MapClientScopeAdapter.java @@ -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 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 getAttributes() { + return entity.getAttributes(); + } + + @Override + public RealmModel getRealm() { + return realm; + } + + @Override + public Stream 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 getScopeMappingsStream() { + return this.entity.getScopeMappings() + .map(realm::getRoleById) + .filter(Objects::nonNull); + } + + @Override + public Stream 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)); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/clientscope/MapClientScopeEntity.java b/model/map/src/main/java/org/keycloak/models/map/clientscope/MapClientScopeEntity.java new file mode 100644 index 0000000000..99dbb6ebf4 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/clientscope/MapClientScopeEntity.java @@ -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 { + + private MapClientScopeEntity() { + super(); + } + + public MapClientScopeEntity(UUID id, String realmId) { + super(id, realmId); + } + +} diff --git a/model/map/src/main/java/org/keycloak/models/map/clientscope/MapClientScopeProvider.java b/model/map/src/main/java/org/keycloak/models/map/clientscope/MapClientScopeProvider.java new file mode 100644 index 0000000000..ebf52c7f6b --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/clientscope/MapClientScopeProvider.java @@ -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 ALWAYS_FALSE = c -> { return false; }; + private final KeycloakSession session; + private final MapKeycloakTransaction tx; + private final MapStorage clientScopeStore; + + private static final Comparator COMPARE_BY_NAME = Comparator.comparing(MapClientScopeEntity::getName); + + public MapClientScopeProvider(KeycloakSession session, MapStorage 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 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 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 getClientScopesStream(RealmModel realm) { + ModelCriteriaBuilder 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 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() { + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/clientscope/MapClientScopeProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/clientscope/MapClientScopeProviderFactory.java new file mode 100644 index 0000000000..a5858cc8a1 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/clientscope/MapClientScopeProviderFactory.java @@ -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 implements ClientScopeProviderFactory { + + private MapStorage 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); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/MapFieldPredicates.java b/model/map/src/main/java/org/keycloak/models/map/storage/MapFieldPredicates.java index 9aaa64230b..ef8cc1448e 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/MapFieldPredicates.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/MapFieldPredicates.java @@ -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, UpdatePredicatesFunc, ClientModel>> CLIENT_PREDICATES = basePredicates(ClientModel.SearchableFields.ID); + public static final Map, UpdatePredicatesFunc, ClientScopeModel>> CLIENT_SCOPE_PREDICATES = basePredicates(ClientScopeModel.SearchableFields.ID); public static final Map, UpdatePredicatesFunc, GroupModel>> GROUP_PREDICATES = basePredicates(GroupModel.SearchableFields.ID); public static final Map, UpdatePredicatesFunc, RoleModel>> ROLE_PREDICATES = basePredicates(RoleModel.SearchableFields.ID); public static final Map, UpdatePredicatesFunc, 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); diff --git a/model/map/src/main/resources/META-INF/services/org.keycloak.models.ClientScopeProviderFactory b/model/map/src/main/resources/META-INF/services/org.keycloak.models.ClientScopeProviderFactory new file mode 100644 index 0000000000..af24ee869f --- /dev/null +++ b/model/map/src/main/resources/META-INF/services/org.keycloak.models.ClientScopeProviderFactory @@ -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 diff --git a/server-spi-private/src/main/java/org/keycloak/models/ClientScopeProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/models/ClientScopeProviderFactory.java new file mode 100644 index 0000000000..a1b0fca590 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/models/ClientScopeProviderFactory.java @@ -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 { +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/ClientScopeSpi.java b/server-spi-private/src/main/java/org/keycloak/models/ClientScopeSpi.java new file mode 100644 index 0000000000..56fe1a0fe9 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/models/ClientScopeSpi.java @@ -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 getProviderClass() { + return ClientScopeProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return ClientScopeProviderFactory.class; + } + +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java b/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java index 784338f140..6215cf67f8 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/models/cache/CacheRealmProvider.java @@ -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 Bill Burke * @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); diff --git a/server-spi-private/src/main/java/org/keycloak/storage/clientscope/ClientScopeStorageProvider.java b/server-spi-private/src/main/java/org/keycloak/storage/clientscope/ClientScopeStorageProvider.java new file mode 100644 index 0000000000..7a44d9dd0d --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/storage/clientscope/ClientScopeStorageProvider.java @@ -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 { +} diff --git a/server-spi-private/src/main/java/org/keycloak/storage/clientscope/ClientScopeStorageProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/storage/clientscope/ClientScopeStorageProviderFactory.java new file mode 100644 index 0000000000..e4381e4e48 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/storage/clientscope/ClientScopeStorageProviderFactory.java @@ -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 extends ComponentFactory { + + + /** + * 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 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 getCommonProviderConfigProperties() { + return ClientScopeStorageProviderSpi.commonConfig(); + } + + @Override + default + Map getTypeMetadata() { + return new HashMap<>(); + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/storage/clientscope/ClientScopeStorageProviderModel.java b/server-spi-private/src/main/java/org/keycloak/storage/clientscope/ClientScopeStorageProviderModel.java new file mode 100644 index 0000000000..4446dfda96 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/storage/clientscope/ClientScopeStorageProviderModel.java @@ -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; + + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/storage/clientscope/ClientScopeStorageProviderSpi.java b/server-spi-private/src/main/java/org/keycloak/storage/clientscope/ClientScopeStorageProviderSpi.java new file mode 100644 index 0000000000..5752f578fd --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/storage/clientscope/ClientScopeStorageProviderSpi.java @@ -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 getProviderClass() { + return ClientScopeStorageProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return ClientScopeStorageProviderFactory.class; + } + + private static final List commonConfig; + + static { + //corresponds to properties defined in CacheableStorageProviderModel and PrioritizedComponentModel + List 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 commonConfig() { + return commonConfig; + } + +} diff --git a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi index eb4a089f20..1f72a1485c 100755 --- a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -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 diff --git a/server-spi/src/main/java/org/keycloak/models/ClientScopeModel.java b/server-spi/src/main/java/org/keycloak/models/ClientScopeModel.java index 441b591b23..4ecc9e5b64 100755 --- a/server-spi/src/main/java/org/keycloak/models/ClientScopeModel.java +++ b/server-spi/src/main/java/org/keycloak/models/ClientScopeModel.java @@ -20,12 +20,20 @@ package org.keycloak.models; import java.util.Map; import org.keycloak.common.util.ObjectUtil; +import org.keycloak.storage.SearchableModelField; /** * @author Bill Burke * @version $Revision: 1 $ */ public interface ClientScopeModel extends ProtocolMapperContainerModel, ScopeContainerModel, OrderedModel { + + public static class SearchableFields { + public static final SearchableModelField ID = new SearchableModelField<>("id", String.class); + public static final SearchableModelField REALM_ID = new SearchableModelField<>("realmId", String.class); + public static final SearchableModelField NAME = new SearchableModelField<>("name", String.class); + } + String getId(); String getName(); diff --git a/server-spi/src/main/java/org/keycloak/models/ClientScopeProvider.java b/server-spi/src/main/java/org/keycloak/models/ClientScopeProvider.java new file mode 100644 index 0000000000..f147acd3f0 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/models/ClientScopeProvider.java @@ -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 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); +} diff --git a/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java b/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java index 42dc44871d..b63fcd9cb9 100755 --- a/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java +++ b/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java @@ -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 * diff --git a/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java index 035dbaecd3..b727b6e0ca 100755 --- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java +++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java @@ -885,15 +885,51 @@ public interface RealmModel extends RoleContainerModel { */ Stream 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); /** diff --git a/server-spi/src/main/java/org/keycloak/models/RealmProvider.java b/server-spi/src/main/java/org/keycloak/models/RealmProvider.java index c9c598f7cb..4d1fa4dcbc 100755 --- a/server-spi/src/main/java/org/keycloak/models/RealmProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/RealmProvider.java @@ -30,7 +30,7 @@ import java.util.stream.Stream; * @author Bill Burke * @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. diff --git a/server-spi/src/main/java/org/keycloak/storage/clientscope/ClientScopeLookupProvider.java b/server-spi/src/main/java/org/keycloak/storage/clientscope/ClientScopeLookupProvider.java new file mode 100644 index 0000000000..7ce88776ee --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/storage/clientscope/ClientScopeLookupProvider.java @@ -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); + +} diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java index 22627b2e3c..cf2efd1832 100644 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java @@ -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 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) { diff --git a/services/src/main/java/org/keycloak/storage/ClientScopeStorageManager.java b/services/src/main/java/org/keycloak/storage/ClientScopeStorageManager.java new file mode 100644 index 0000000000..6af7c32b9d --- /dev/null +++ b/services/src/main/java/org/keycloak/storage/ClientScopeStorageManager.java @@ -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 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 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() { + } +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientScopeStorageProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientScopeStorageProvider.java new file mode 100644 index 0000000000..6fbdf3774e --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientScopeStorageProvider.java @@ -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 getAttributes() { + return Collections.EMPTY_MAP; + } + + @Override + public Stream 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 getScopeMappingsStream() { + return Stream.empty(); + } + + @Override + public Stream 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; + } + } +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientScopeStorageProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientScopeStorageProviderFactory.java new file mode 100644 index 0000000000..be9f93b3df --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedClientScopeStorageProviderFactory.java @@ -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 { + + public static final String PROVIDER_ID = "hardcoded-clientscope"; + public static final String SCOPE_NAME = "scope_name"; + protected static final List 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 getConfigProperties() { + return CONFIG_PROPERTIES; + } +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.clientscope.ClientScopeStorageProviderFactory b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.clientscope.ClientScopeStorageProviderFactory new file mode 100644 index 0000000000..e38ebd4e23 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.storage.clientscope.ClientScopeStorageProviderFactory @@ -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 diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientScopeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientScopeTest.java index 6b9a7cce78..c0d5eeffb3 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientScopeTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientScopeTest.java @@ -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(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java index a293c19e60..91cee01d55 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java @@ -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); diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json index 5516bb5f7e..03352b610a 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json @@ -48,6 +48,10 @@ "provider": "${keycloak.client.provider:jpa}" }, + "clientScope": { + "provider": "${keycloak.clientScope.provider:jpa}" + }, + "group": { "provider": "${keycloak.group.provider:jpa}" }, diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/ClientScopeStorageTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/ClientScopeStorageTest.java new file mode 100644 index 0000000000..857924c4cf --- /dev/null +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/ClientScopeStorageTest.java @@ -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); + }); + } +} diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/KeycloakModelTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/KeycloakModelTest.java index df155e71f2..5508abbf0a 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/KeycloakModelTest.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/KeycloakModelTest.java @@ -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> ALLOWED_SPIS = ImmutableSet.>builder() .add(AuthorizationSpi.class) + .add(ClientScopeSpi.class) .add(ClientSpi.class) .add(ClusterSpi.class) .add(EventStoreSpi.class) diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Jpa.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Jpa.java index c23ae8ebea..e788d70320 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Jpa.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Jpa.java @@ -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) diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/JpaFederation.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/JpaFederation.java index ce893fcde6..62f6b21ccd 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/JpaFederation.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/JpaFederation.java @@ -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> ALLOWED_SPIS = ImmutableSet.>builder() .addAll(Jpa.ALLOWED_SPIS) .add(UserStorageProviderSpi.class) .add(UserFederatedStorageProviderSpi.class) + .add(ClientScopeStorageProviderSpi.class) .build(); static final Set> ALLOWED_FACTORIES = ImmutableSet.>builder() .addAll(Jpa.ALLOWED_FACTORIES) .add(JpaUserFederatedStorageProviderFactory.class) + .add(ClientScopeStorageProviderFactory.class) .build(); public JpaFederation() { super(ALLOWED_SPIS, ALLOWED_FACTORIES); } + + @Override + public Stream getParameters(Class 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); + } + } } diff --git a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json index ee2d9ea5d7..1f50efdab3 100755 --- a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json +++ b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json @@ -22,6 +22,10 @@ "provider": "${keycloak.client.provider:jpa}" }, + "clientScope": { + "provider": "${keycloak.clientScope.provider:jpa}" + }, + "group": { "provider": "${keycloak.group.provider:jpa}" },