Introduce per-field delegation of entities

This commit is contained in:
Hynek Mlnařík 2022-01-05 14:06:45 +01:00 committed by GitHub
parent 80873ea4b9
commit d39eb95705
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1818 additions and 42 deletions

View file

@ -17,17 +17,20 @@
package org.keycloak.models.map.storage.jpa.client.delegate; package org.keycloak.models.map.storage.jpa.client.delegate;
import java.util.UUID; import java.util.UUID;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.JoinType; import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Root; import javax.persistence.criteria.Root;
import org.keycloak.models.map.client.MapClientEntity; import org.keycloak.models.map.client.MapClientEntity;
import org.keycloak.models.map.client.MapClientEntityFields; import org.keycloak.models.map.client.MapClientEntityFields;
import org.keycloak.models.map.common.EntityField;
import org.keycloak.models.map.common.delegate.DelegateProvider; import org.keycloak.models.map.common.delegate.DelegateProvider;
import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity; import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity;
public class JpaClientDelegateProvider<T extends MapClientEntity> implements DelegateProvider { public class JpaClientDelegateProvider implements DelegateProvider<MapClientEntity> {
private JpaClientEntity delegate; private JpaClientEntity delegate;
private final EntityManager em; private final EntityManager em;
@ -38,7 +41,7 @@ public class JpaClientDelegateProvider<T extends MapClientEntity> implements Del
} }
@Override @Override
public JpaClientEntity getDelegate(boolean isRead, Object field, Object... parameters) { public MapClientEntity getDelegate(boolean isRead, Enum<? extends EntityField<MapClientEntity>> field, Object... parameters) {
if (delegate.isMetadataInitialized()) return delegate; if (delegate.isMetadataInitialized()) return delegate;
if (isRead) { if (isRead) {
if (field instanceof MapClientEntityFields) { if (field instanceof MapClientEntityFields) {

View file

@ -16,6 +16,8 @@
*/ */
package org.keycloak.models.map.common.delegate; package org.keycloak.models.map.common.delegate;
import org.keycloak.models.map.common.EntityField;
/** /**
* Interface for a provider of a delegate of type {@code T}, optionally * Interface for a provider of a delegate of type {@code T}, optionally
* providing the flag on the object been updated. * providing the flag on the object been updated.
@ -25,10 +27,12 @@ public interface DelegateProvider<T> {
/** /**
* Returns a delegate for and entity for an operation on a field. * Returns a delegate for and entity for an operation on a field.
* @param isRead {@code true} when the delegate requested for a read operation, false otherwise * @param isRead {@code true} when the delegate requested for a read operation, false otherwise
* @param field Identification of the field this delegates operates on. * @param field Identification of the field this delegates operates on. While this parameter
* can be any object including {@code null}, if it is a known field, then it is guaranteed to be
* one of the {@code EntityField}s enumerated in one of the {@code Map*EntityFields} enum.
* @return * @return
*/ */
T getDelegate(boolean isRead, Object field, Object... parameters); T getDelegate(boolean isRead, Enum<? extends EntityField<T>> field, Object... parameters);
default boolean isUpdated() { return false; } default boolean isUpdated() { return false; }
} }

View file

@ -8,17 +8,17 @@ import org.keycloak.models.map.common.UpdatableEntity;
public interface EntityFieldDelegate<E> extends UpdatableEntity { public interface EntityFieldDelegate<E> extends UpdatableEntity {
// Non-collection values // Non-collection values
Object get(EntityField<E> field); <EF extends Enum<? extends EntityField<E>> & EntityField<E>> Object get(EF field);
default <T> void set(EntityField<E> field, T value) {} default <T, EF extends Enum<? extends EntityField<E>> & EntityField<E>> void set(EF field, T value) {}
default <T> void collectionAdd(EntityField<E> field, T value) { default <T, EF extends Enum<? extends EntityField<E>> & EntityField<E>> void collectionAdd(EF field, T value) {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Collection<T> c = (Collection<T>) get(field); Collection<T> c = (Collection<T>) get(field);
if (c != null) { if (c != null) {
c.add(value); c.add(value);
} }
} }
default <T> Object collectionRemove(EntityField<E> field, T value) { default <T, EF extends Enum<? extends EntityField<E>> & EntityField<E>> Object collectionRemove(EF field, T value) {
Collection<?> c = (Collection<?>) get(field); Collection<?> c = (Collection<?>) get(field);
return c == null ? null : c.remove(value); return c == null ? null : c.remove(value);
} }
@ -32,19 +32,19 @@ public interface EntityFieldDelegate<E> extends UpdatableEntity {
* @param valueClass class of the value * @param valueClass class of the value
* @return * @return
*/ */
default <K> Object mapGet(EntityField<E> field, K key) { default <K, EF extends Enum<? extends EntityField<E>> & EntityField<E>> Object mapGet(EF field, K key) {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Map<K, ?> m = (Map<K, ?>) get(field); Map<K, ?> m = (Map<K, ?>) get(field);
return m == null ? null : m.get(key); return m == null ? null : m.get(key);
} }
default <K, T> void mapPut(EntityField<E> field, K key, T value) { default <K, T, EF extends Enum<? extends EntityField<E>> & EntityField<E>> void mapPut(EF field, K key, T value) {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Map<K, T> m = (Map<K, T>) get(field); Map<K, T> m = (Map<K, T>) get(field);
if (m != null) { if (m != null) {
m.put(key, value); m.put(key, value);
} }
} }
default <K> Object mapRemove(EntityField<E> field, K key) { default <K, EF extends Enum<? extends EntityField<E>> & EntityField<E>> Object mapRemove(EF field, K key) {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Map<K, ?> m = (Map<K, ?>) get(field); Map<K, ?> m = (Map<K, ?>) get(field);
if (m != null) { if (m != null) {

View file

@ -0,0 +1,51 @@
/*
* 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.common.delegate;
import java.util.concurrent.atomic.AtomicMarkableReference;
import java.util.function.Supplier;
/**
*
* @author hmlnarik
*/
public class LazilyInitialized<T> {
private final Supplier<T> supplier;
private final AtomicMarkableReference<T> supplierRef = new AtomicMarkableReference<>(null, false);
public LazilyInitialized(Supplier<T> supplier) {
this.supplier = supplier;
}
public T get() {
if (! isInitialized()) {
supplierRef.compareAndSet(null, supplier == null ? null : supplier.get(), false, true);
}
return supplierRef.getReference();
}
/**
* Returns {@code true} if the reference to the object has been initialized
* @return
*/
public boolean isInitialized() {
return supplierRef.isMarked();
}
}

View file

@ -16,6 +16,8 @@
*/ */
package org.keycloak.models.map.common.delegate; package org.keycloak.models.map.common.delegate;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.common.EntityField;
import org.keycloak.models.map.common.UpdatableEntity; import org.keycloak.models.map.common.UpdatableEntity;
import java.util.concurrent.atomic.AtomicMarkableReference; import java.util.concurrent.atomic.AtomicMarkableReference;
import java.util.function.Supplier; import java.util.function.Supplier;
@ -24,37 +26,28 @@ import java.util.function.Supplier;
* *
* @author hmlnarik * @author hmlnarik
*/ */
public class LazyDelegateProvider<T extends UpdatableEntity> implements DelegateProvider { public class LazyDelegateProvider<T extends AbstractEntity> implements DelegateProvider<T> {
private final Supplier<T> delegateSupplier; protected final LazilyInitialized<T> delegateSupplier;
private final AtomicMarkableReference<T> delegate = new AtomicMarkableReference<>(null, false);
public LazyDelegateProvider(Supplier<T> delegateSupplier) { public LazyDelegateProvider(Supplier<T> delegateSupplier) {
this.delegateSupplier = delegateSupplier; this.delegateSupplier = new LazilyInitialized<>(delegateSupplier);
} }
@Override @Override
public T getDelegate(boolean isRead, Object field, Object... parameters) { public T getDelegate(boolean isRead, Enum<? extends EntityField<T>> field, Object... parameters) {
if (! isDelegateInitialized()) { T ref = delegateSupplier.get();
delegate.compareAndSet(null, delegateSupplier == null ? null : delegateSupplier.get(), false, true);
}
T ref = delegate.getReference();
if (ref == null) { if (ref == null) {
throw new IllegalStateException("Invalid delegate obtained"); throw new IllegalStateException("Invalid delegate obtained");
} }
return ref; return ref;
} }
protected boolean isDelegateInitialized() {
return delegate.isMarked();
}
@Override @Override
public boolean isUpdated() { public boolean isUpdated() {
if (isDelegateInitialized()) { if (delegateSupplier.isInitialized()) {
T d = getDelegate(true, this); T d = delegateSupplier.get();
return d.isUpdated(); return d instanceof UpdatableEntity ? ((UpdatableEntity) d).isUpdated() : false;
} }
return false; return false;
} }

View file

@ -0,0 +1,297 @@
/*
* 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.common.delegate;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.common.EntityField;
import org.keycloak.models.map.common.UpdatableEntity;
import org.keycloak.models.map.storage.tree.TreeStorageNodeInstance;
import org.keycloak.models.map.storage.tree.TreeStorageNodePrescription.FieldContainedStatus;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
*
* @author hmlnarik
*/
public class PerFieldDelegateProvider<V extends AbstractEntity> implements EntityFieldDelegate<V> {
private final TreeStorageNodeInstance<V> node;
private final V entity;
private final LazilyInitialized<V> lowerEntity;
public PerFieldDelegateProvider(TreeStorageNodeInstance<V> node, V entity, Supplier<V> fallbackProvider) {
this.node = node;
this.entity = entity;
this.lowerEntity = new LazilyInitialized<>(fallbackProvider);
}
public PerFieldDelegateProvider(TreeStorageNodeInstance<V>.WithEntity nodeWithEntity, Supplier<V> fallbackProvider) {
this(nodeWithEntity.getNode(), nodeWithEntity.getEntity(), fallbackProvider);
}
private V getEntityFromDescendantNode() {
final V res = lowerEntity.get();
Objects.requireNonNull(res, () -> "Descendant entity not found for node " + node);
return res;
}
@Override
public <K, EF extends Enum<? extends EntityField<V>> & EntityField<V>> Object mapRemove(EF field, K key) {
Objects.requireNonNull(key, "Key must not be null");
boolean needsSetEntity = false;
boolean needsSetLowerEntity = false;
switch (node.isCacheFor(field, key)) {
case FULLY:
needsSetEntity = true;
break;
case NOT_CONTAINED:
needsSetLowerEntity = true;
break;
}
switch (node.isPrimarySourceFor(field, key)) {
case FULLY:
needsSetEntity = true;
needsSetLowerEntity = false;
break;
case NOT_CONTAINED:
needsSetLowerEntity = true;
break;
}
Object res = null;
if (needsSetEntity) {
res = field.mapRemove(entity, key);
}
if (needsSetLowerEntity) {
res = field.mapRemove(getEntityFromDescendantNode(), key);
}
return res;
}
@Override
public <K, T, EF extends Enum<? extends EntityField<V>> & EntityField<V>> void mapPut(EF field, K key, T value) {
Objects.requireNonNull(key, "Key must not be null");
boolean needsSetEntity = false;
boolean needsSetLowerEntity = false;
switch (node.isCacheFor(field, key)) {
case FULLY:
needsSetEntity = true;
break;
case NOT_CONTAINED:
needsSetLowerEntity = true;
break;
}
switch (node.isPrimarySourceFor(field, key)) {
case FULLY:
needsSetEntity = true;
needsSetLowerEntity = false;
break;
case NOT_CONTAINED:
needsSetLowerEntity = true;
break;
}
if (needsSetEntity) {
field.mapPut(entity, key, value);
}
if (needsSetLowerEntity) {
field.mapPut(getEntityFromDescendantNode(), key, value);
}
}
@Override
public <K, EF extends Enum<? extends EntityField<V>> & EntityField<V>> Object mapGet(EF field, K key) {
Objects.requireNonNull(key, "Key must not be null");
switch (node.isCacheFor(field, key).max(() -> node.isPrimarySourceFor(field, key))) {
case FULLY:
return field.mapGet(entity, key);
case NOT_CONTAINED:
return field.mapGet(getEntityFromDescendantNode(), key);
}
throw new IllegalStateException("Field is not determined: " + field);
}
@Override
public <T, EF extends Enum<? extends EntityField<V>> & EntityField<V>> Object collectionRemove(EF field, T value) {
boolean needsSetEntity = false;
boolean needsSetLowerEntity = false;
switch (node.isCacheFor(field, value)) {
case FULLY:
needsSetEntity = true;
break;
case NOT_CONTAINED:
needsSetLowerEntity = true;
break;
}
switch (node.isPrimarySourceFor(field, value)) {
case FULLY:
needsSetEntity = true;
needsSetLowerEntity = false;
break;
case NOT_CONTAINED:
needsSetLowerEntity = true;
break;
}
Object res = null;
if (needsSetEntity) {
res = field.collectionRemove(entity, value);
}
if (needsSetLowerEntity) {
res = field.collectionRemove(getEntityFromDescendantNode(), value);
}
return res;
}
@Override
public <T, EF extends Enum<? extends EntityField<V>> & EntityField<V>> void collectionAdd(EF field, T value) {
boolean needsSetEntity = false;
boolean needsSetLowerEntity = false;
switch (node.isCacheFor(field, null)) {
case FULLY:
needsSetEntity = true;
break;
case NOT_CONTAINED:
needsSetLowerEntity = true;
break;
}
switch (node.isPrimarySourceFor(field, null)) {
case FULLY:
needsSetEntity = true;
needsSetLowerEntity = false;
break;
case NOT_CONTAINED:
needsSetLowerEntity = true;
break;
}
if (needsSetEntity) {
field.collectionAdd(entity, value);
}
if (needsSetLowerEntity) {
field.collectionAdd(getEntityFromDescendantNode(), value);
}
}
private final Collector<Map.Entry, ?, Map<Object, Object>> ENTRY_TO_HASH_MAP_OVERRIDING_KEYS_COLLECTOR = Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (Object a, Object b) -> b, HashMap::new);
@Override
@SuppressWarnings("unchecked")
public <EF extends Enum<? extends EntityField<V>> & EntityField<V>> Object get(EF field) {
switch (node.isCacheFor(field, null).max(() -> node.isPrimarySourceFor(field, null))) {
case FULLY:
return field.get(entity);
case NOT_CONTAINED:
return field.get(getEntityFromDescendantNode());
}
// It has to be partial field. The only supported partial field is a Map
if (field.getMapKeyClass() == Void.class) {
throw new IllegalStateException("Field " + field + " expected to be a map but is " + field.getFieldClass());
}
Map<Object, Object> m1 = (Map<Object, Object>) field.get(entity);
Map m2 = (Map) field.get(getEntityFromDescendantNode());
if (m1 == null) {
return m2 == null ? null : new HashMap<>(m2);
}
Predicate<Map.Entry<Object, Object>> isInNode = me -> node.isCacheFor(field, me.getKey())
.max(() -> node.isPrimarySourceFor(field, me.getKey())) == FieldContainedStatus.FULLY;
Stream<Map.Entry<Object, Object>> s = m1.entrySet().stream()
.filter(isInNode);
if (m2 == null) {
return s.collect(ENTRY_TO_HASH_MAP_OVERRIDING_KEYS_COLLECTOR);
}
return Stream.concat(s, m2.entrySet().stream().filter(isInNode.negate()))
.collect(ENTRY_TO_HASH_MAP_OVERRIDING_KEYS_COLLECTOR);
}
@Override
public <T, EF extends Enum<? extends EntityField<V>> & EntityField<V>> void set(EF field, T value) {
boolean needsSetEntity = false;
boolean needsSetLowerEntity = false;
switch (node.isCacheFor(field, null)) {
case FULLY:
needsSetEntity = true;
break;
case PARTIALLY:
needsSetEntity = true;
needsSetLowerEntity = true;
break;
}
switch (node.isPrimarySourceFor(field, null)) {
case FULLY:
needsSetEntity = true;
needsSetLowerEntity = false;
break;
case PARTIALLY:
needsSetEntity = true;
needsSetLowerEntity = true;
break;
case NOT_CONTAINED:
needsSetLowerEntity = true;
break;
}
if (needsSetEntity) {
field.set(entity, value);
}
if (needsSetLowerEntity) {
field.set(getEntityFromDescendantNode(), value);
}
}
@Override
public boolean isUpdated() {
return entity instanceof UpdatableEntity ? ((UpdatableEntity) entity).isUpdated() : false;
}
}

View file

@ -16,13 +16,14 @@
*/ */
package org.keycloak.models.map.common.delegate; package org.keycloak.models.map.common.delegate;
import org.keycloak.models.map.common.EntityField;
import org.keycloak.models.map.common.UpdatableEntity; import org.keycloak.models.map.common.UpdatableEntity;
/** /**
* *
* @author hmlnarik * @author hmlnarik
*/ */
public class SimpleDelegateProvider<T extends UpdatableEntity> implements DelegateProvider { public class SimpleDelegateProvider<T extends UpdatableEntity> implements DelegateProvider<T> {
private final T delegate; private final T delegate;
@ -31,7 +32,7 @@ public class SimpleDelegateProvider<T extends UpdatableEntity> implements Delega
} }
@Override @Override
public T getDelegate(boolean isRead, Object field, Object... parameters) { public T getDelegate(boolean isRead, Enum<? extends EntityField<T>> field, Object... parameters) {
return this.delegate; return this.delegate;
} }

View file

@ -45,9 +45,9 @@ public class DefaultTreeNode<Self extends DefaultTreeNode<Self>> implements Tree
private static final AtomicInteger COUNTER = new AtomicInteger(); private static final AtomicInteger COUNTER = new AtomicInteger();
private final Map<String, Object> nodeProperties; protected final Map<String, Object> nodeProperties;
private final Map<String, Object> edgeProperties; protected final Map<String, Object> edgeProperties;
private final Map<String, Object> treeProperties; protected final Map<String, Object> treeProperties;
private final LinkedList<Self> children = new LinkedList<>(); private final LinkedList<Self> children = new LinkedList<>();
private String id; private String id;
private Self parent; private Self parent;
@ -224,6 +224,14 @@ public class DefaultTreeNode<Self extends DefaultTreeNode<Self>> implements Tree
return Collections.unmodifiableList(this.children); return Collections.unmodifiableList(this.children);
} }
public boolean hasChildren() {
return ! this.children.isEmpty();
}
public boolean hasNoChildren() {
return this.children.isEmpty();
}
@Override @Override
public void addChild(Self node) { public void addChild(Self node) {
if (node == null) { if (node == null) {

View file

@ -0,0 +1,90 @@
/*
* 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.storage.tree;
import org.keycloak.models.map.common.EntityField;
import org.keycloak.models.map.storage.criteria.DefaultModelCriteria;
/**
*
* @author hmlnarik
*/
public final class NodeProperties {
/**
* Defines the filter that must be satisfied for every entity within this store.
* Type: {@link DefaultModelCriteria}
*/
public static final String ENTITY_RESTRICTION = "entity-restriction";
public static final String AUTHORITATIVE_DECIDER = "authoritative-decider";
public static final String STRONGLY_AUTHORITATIVE = "strongly-authoritative";
public static final String READ_ONLY = "read-only";
public static final String REVALIDATE = "revalidate";
public static final String AUTHORITATIVE_NODES = "___authoritative-nodes___";
public static final String STORAGE_PROVIDER = "___storage-provider___";
public static final String STORAGE_SUPPLIER = "___storage-supplier___";
/**
* Map of pairs ({@code k}: {@link EntityField}, {@code v}: {@link Collection}) of fields that the node is primary source for.
* <p>
* For example, the following statements are expressed:
* <ul>
* <li>{@code (name -> null)}: This node is primary source for the value of the field {@code name}.
* <li>{@code (attributes -> null)}: This node is primary source for the values of all attributes.
* <li>{@code (attributes -> {"address", "logo"})}: This node is primary source only for the values of attributes "address" and "logo".
* </ul>
*/
public static final String PRIMARY_SOURCE_FOR = "___primary-source-for___";
/**
* Map of pairs ({@code k}: {@link EntityField}, {@code v}: {@link Collection}) of fields that the node is not primary source for.
* <p>
* For example, the following statements are expressed:
* <ul>
* <li>{@code (name -> null)}: This node is not primary source for the value of the field {@code name}.
* <li>{@code (attributes -> null)}: This node is not primary source for the values of any attributes.
* <li>{@code (attributes -> {"address", "logo"})}: This node is primary source only for attributes apart from "address" and "logo" attributes.
* </ul>
*/
public static final String PRIMARY_SOURCE_FOR_EXCLUDED = "___primary-source-for-excluded___";
/**
* Map of pairs ({@code k}: {@link EntityField}, {@code v}: {@link Collection}) of fields that the node is primary source for.
* <p>
* For example, the following statements are expressed:
* <ul>
* <li>{@code (name -> null)}: This node is primary source for the value of the field {@code name}.
* <li>{@code (attributes -> null)}: This node is primary source for the values of all attributes.
* <li>{@code (attributes -> {"address", "logo"})}: This node is primary source only for the values of attributes "address" and "logo".
* </ul>
*/
public static final String CACHE_FOR = "___cache-for___";
/**
* Map of pairs ({@code k}: {@link EntityField}, {@code v}: {@link Collection}) of fields that the node is not primary source for.
* <p>
* For example, the following statements are expressed:
* <ul>
* <li>{@code (name -> null)}: This node is not primary source for the value of the field {@code name}.
* <li>{@code (attributes -> null)}: This node is not primary source for the values of any attributes.
* <li>{@code (attributes -> {"address", "logo"})}: This node is primary source only for attributes apart from "address" and "logo" attributes.
* </ul>
*/
public static final String CACHE_FOR_EXCLUDED = "___cache-for-excluded___";
}

View file

@ -0,0 +1,29 @@
/*
* 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.storage.tree;
/**
*
* @author hmlnarik
*/
public final class TreeProperties {
public static final String MODEL_CLASS = "model-class";
public static final String DEFAULT_STORE_CREATE = "default-create";
public static final String DEFAULT_STORE_READ = "default-read";
}

View file

@ -0,0 +1,98 @@
/*
* 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.storage.tree;
import java.util.HashMap;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.common.EntityField;
import org.keycloak.models.map.storage.tree.TreeStorageNodePrescription.FieldContainedStatus;
/**
* Instance of the tree storage that is based on a prescription ({@link TreeStorageNodePrescription}),
* i.e. it provides a map storage instance that can be used for accessing data.
*
* @author hmlnarik
*/
public class TreeStorageNodeInstance<V extends AbstractEntity>
extends DefaultTreeNode<TreeStorageNodeInstance<V>> {
private final KeycloakSession session;
private final TreeStorageNodePrescription prescription;
private final TreeStorageNodeInstance<V> original; // If this node is a subtree, keep reference to the node in the original tree here
public class WithEntity {
private final V entity;
public WithEntity(V entity) {
this.entity = entity;
}
public V getEntity() {
return entity;
}
public TreeStorageNodeInstance<V> getNode() {
return TreeStorageNodeInstance.this;
}
}
public TreeStorageNodeInstance(KeycloakSession session, TreeStorageNodeInstance<V> original) {
super(original.getNodeProperties(), original.getEdgeProperties(), original.getTreeProperties());
this.original = original;
this.prescription = original.prescription;
this.session = session;
}
public TreeStorageNodeInstance(KeycloakSession session, TreeStorageNodePrescription prescription) {
super(
prescription.getNodeProperties() == null ? null : new HashMap<>(prescription.getNodeProperties()),
prescription.getEdgeProperties() == null ? null : new HashMap<>(prescription.getEdgeProperties()),
prescription.getTreeProperties()
);
this.prescription = prescription;
this.session = session;
this.original = null;
}
public TreeStorageNodeInstance<V> cloneNodeOnly() {
return new TreeStorageNodeInstance<>(session, this.original == null ? this : this.original);
}
@Override
public String getId() {
return prescription.getId();
}
public boolean isReadOnly() {
return prescription.isReadOnly();
}
public FieldContainedStatus isCacheFor(EntityField<V> field, Object parameter) {
return prescription.isCacheFor(field, parameter);
}
public FieldContainedStatus isPrimarySourceFor(Enum<? extends EntityField<V>> field, Object parameter) {
return prescription.isPrimarySourceFor((EntityField<?>) field, parameter);
}
}

View file

@ -0,0 +1,280 @@
/*
* 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.storage.tree;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.common.EntityField;
import org.keycloak.models.map.storage.ModelEntityUtil;
import org.keycloak.models.map.storage.criteria.DefaultModelCriteria;
/**
* Prescription of the tree storage. This prescription can
* be externalized and contains e.g. details on the particular storage type
* represented by this node, or properties of the node and edge between this
* and the parent storage.
* <p>
* Realization of this prescription is in {@link TreeStorageNodeInstance}, namely it does not
* contain a map storage instance that can be directly used
* for accessing data.
*
* @author hmlnarik
*/
public class TreeStorageNodePrescription extends DefaultTreeNode<TreeStorageNodePrescription> {
private static final Logger LOG = Logger.getLogger(TreeStorageNodePrescription.class);
private final boolean isPrimarySourceForAnything;
private final boolean isPrimarySourceForEverything;
private final boolean isCacheForAnything;
public static enum FieldContainedStatus {
/**
* Field is fully contained in the storage in this node.
* For example, attribute {@code foo} or field {@code NAME} stored in this node would be {@code FULLY} contained field.
* @see #PARTIALLY
*/
FULLY {
@Override
public FieldContainedStatus minus(FieldContainedStatus s) {
switch (s) {
case FULLY:
return FieldContainedStatus.NOT_CONTAINED;
case PARTIALLY:
return FieldContainedStatus.PARTIALLY;
default:
return FieldContainedStatus.FULLY;
}
}
},
/**
* Field is contained in the storage in this node but parts of it might be contained in some child node as well.
* For example, a few attributes can be partially supplied from an LDAP, and the rest could be supplied from the
* supplementing JPA node.
* <p>
* This status is never used in the case of a fully specified field access but can be used for map-like attributes
* where the key is not specified.
*/
PARTIALLY {
@Override
public FieldContainedStatus minus(FieldContainedStatus s) {
switch (s) {
case FULLY:
return FieldContainedStatus.NOT_CONTAINED;
default:
return FieldContainedStatus.PARTIALLY;
}
}
},
/** Field is not contained in the storage in this node but parts of it might be contained in some child node as well */
NOT_CONTAINED {
@Override
public FieldContainedStatus minus(FieldContainedStatus s) {
return FieldContainedStatus.NOT_CONTAINED;
}
};
/**
* Returns the status of the field if this field status in this node was stripped off the field status {@code s}.
* Specifically, for two nodes in parent/child relationship, {@code parent.minus(child)} answers the question:
* "How much of the field does parent contain that is not contained in the child?"
* <ul>
* <li>If the field in this node is contained {@link #FULLY} or {@link #PARTIALLY}, and the
* field in the other node is contained {@link #FULLY}, then
* there is no need to store any part of the field in this node,
* i.e. the result is {@link #NOT_CONTAINED}</li>
* <li>If the field in this node is contained {@link #PARTIALLY} and the field in the other node is also only contained {@link #PARTIALLY}, then
* the portions might be disjunct, so it is still necessary to store a part of the field in this node, i.e. the result is {@link #PARTIALLY}</li>
* <li>If the field in this node is {@link #NOT_CONTAINED} at all, then the result remains {@link #NOT_CONTAINED} regardless of the other node status</li>
* </ul>
*/
public abstract FieldContainedStatus minus(FieldContainedStatus s);
/**
* Returns higher of this and the {@code other} field status: {@link #FULLY} &gt; {@link #PARTIALLY} &gt; {@link #NOT_CONTAINED}
*/
public FieldContainedStatus max(FieldContainedStatus other) {
return (other == null || ordinal() < other.ordinal()) ? this : other;
}
public FieldContainedStatus max(Supplier<FieldContainedStatus> otherFunc) {
if (ordinal() == 0) {
return this;
}
FieldContainedStatus other = otherFunc.get();
return other == null || ordinal() < other.ordinal() ? this : other;
}
}
/**
* Map of prescriptions restricted per entity class derived from this prescription.
*/
private final Map<Class<? extends AbstractEntity>, TreeStorageNodePrescription> restricted = new ConcurrentHashMap<>();
public TreeStorageNodePrescription(Map<String, Object> treeProperties) {
this(treeProperties, null, null);
}
public TreeStorageNodePrescription(Map<String, Object> nodeProperties, Map<String, Object> edgeProperties, Map<String, Object> treeProperties) {
super(nodeProperties, edgeProperties, treeProperties);
Map<?, ?> psf = (Map<?, ?>) this.nodeProperties.get(NodeProperties.PRIMARY_SOURCE_FOR);
Map<?, ?> psfe = (Map<?, ?>) this.nodeProperties.get(NodeProperties.PRIMARY_SOURCE_FOR_EXCLUDED);
isPrimarySourceForAnything = (psf != null && ! psf.isEmpty()) || (psfe != null && ! psfe.isEmpty());
isPrimarySourceForEverything = (psf == null) && (psfe == null || psfe.isEmpty()); // This could be restricted further
Map<?, ?> cf = (Map<?, ?>) this.nodeProperties.get(NodeProperties.CACHE_FOR);
Map<?, ?> cfe = (Map<?, ?>) this.nodeProperties.get(NodeProperties.CACHE_FOR_EXCLUDED);
isCacheForAnything = (cf != null && ! cf.isEmpty()) || (cfe != null && ! cfe.isEmpty());
}
public <V extends AbstractEntity> TreeStorageNodePrescription forEntityClass(Class<V> targetEntityClass) {
return restricted.computeIfAbsent(targetEntityClass, c -> {
HashMap<String, Object> treeProperties = new HashMap<>(restrictConfigMap(targetEntityClass, getTreeProperties()));
treeProperties.put(TreeProperties.MODEL_CLASS, ModelEntityUtil.getModelType(targetEntityClass));
return cloneTree(n -> n.forEntityClass(targetEntityClass, treeProperties));
});
}
public <V extends AbstractEntity> TreeStorageNodeInstance<V> instantiate(KeycloakSession session) {
return cloneTree(n -> new TreeStorageNodeInstance<>(session, n));
}
private <V extends AbstractEntity> TreeStorageNodePrescription forEntityClass(Class<V> targetEntityClass,
Map<String, Object> treeProperties) {
Map<String, Object> nodeProperties = restrictConfigMap(targetEntityClass, getNodeProperties());
Map<String, Object> edgeProperties = restrictConfigMap(targetEntityClass, getEdgeProperties());
if (nodeProperties == null || edgeProperties == null) {
LOG.debugf("Cannot restrict storage for %s from node: %s", targetEntityClass, this);
return null;
}
return new TreeStorageNodePrescription(nodeProperties, edgeProperties, treeProperties);
}
/**
* Restricts configuration map to options that are either applicable everywhere (have no '[' in their name)
* or apply to a selected entity class (e.g. ends in "[clients]" for {@code MapClientEntity}).
* @return A new configuration map crafted for this particular entity class
*/
private static <V extends AbstractEntity> Map<String, Object> restrictConfigMap(Class<V> targetEntityClass, Map<String, Object> np) {
final Class<Object> modelType = ModelEntityUtil.getModelType(targetEntityClass, null);
String name = Optional.ofNullable(modelType)
.map(ModelEntityUtil::getModelName)
.orElse(null);
if (name == null) {
return null;
}
// Start with all properties not specific for any domain
Map<String, Object> res = np.entrySet().stream()
.filter(me -> me.getKey().indexOf('[') == -1)
.filter(me -> me.getValue() != null)
.collect(Collectors.toMap(Entry::getKey, Entry::getValue, (s, a) -> s, HashMap::new));
// Now add and/or replace properties specific for the target domain
Pattern specificPropertyPattern = Pattern.compile("(.*?)\\[.*\\b" + Pattern.quote(name) + "\\b.*\\]");
np.keySet().stream()
.map(specificPropertyPattern::matcher)
.filter(Matcher::matches)
.forEach(m -> res.put(m.group(1), np.get(m.group(0))));
return res;
}
public boolean isReadOnly() {
return getNodeProperty(NodeProperties.READ_ONLY, Boolean.class).orElse(false);
}
/**
* Returns if the given field is primary source for the field, potentially specified further by a parameter.
*
* @param field Field
* @param parameter First parameter, which in case of maps is the key to that map, e.g. attribute name.
* @return For a fully specified field and a parameter (e.g. "attribute" and "attr1"), or a parameterless field (e.g. "client_id"),
* returns either {@code FULLY} or {@code NOT_CONTAINED}. May also return {@code PARTIAL} for a field that requires
* a parameter but the parameter is not specified.
*/
public FieldContainedStatus isPrimarySourceFor(EntityField<?> field, Object parameter) {
if (isPrimarySourceForEverything) {
return FieldContainedStatus.FULLY;
}
if (! isPrimarySourceForAnything) {
return FieldContainedStatus.NOT_CONTAINED;
}
FieldContainedStatus isPrimarySourceFor = getNodeProperty(NodeProperties.PRIMARY_SOURCE_FOR, Map.class)
.map(m -> isFieldWithParameterIncludedInMap(m, field, parameter))
.orElse(FieldContainedStatus.FULLY);
FieldContainedStatus isExcludedPrimarySourceFor = getNodeProperty(NodeProperties.PRIMARY_SOURCE_FOR_EXCLUDED, Map.class)
.map(m -> isFieldWithParameterIncludedInMap(m, field, parameter))
.orElse(FieldContainedStatus.NOT_CONTAINED);
return isPrimarySourceFor.minus(isExcludedPrimarySourceFor);
}
public FieldContainedStatus isCacheFor(EntityField<?> field, Object parameter) {
if (! isCacheForAnything) {
return FieldContainedStatus.NOT_CONTAINED;
}
// If there is CACHE_FOR_EXCLUDED then this node is a cache for all fields, and should be treated as such even if there is no CACHE_FOR
// This is analogous to PRIMARY_SOURCE_FOR(_EXCLUDED).
// However if there is neither CACHE_FOR_EXCLUDED nor CACHE_FOR, then this node is not a cache
final Optional<Map> cacheForExcluded = getNodeProperty(NodeProperties.CACHE_FOR_EXCLUDED, Map.class);
FieldContainedStatus isCacheFor = getNodeProperty(NodeProperties.CACHE_FOR, Map.class)
.map(m -> isFieldWithParameterIncludedInMap(m, field, parameter))
.orElse(cacheForExcluded.isPresent() ? FieldContainedStatus.FULLY : FieldContainedStatus.NOT_CONTAINED);
FieldContainedStatus isExcludedCacheFor = cacheForExcluded
.map(m -> isFieldWithParameterIncludedInMap(m, field, parameter))
.orElse(FieldContainedStatus.NOT_CONTAINED);
return isCacheFor.minus(isExcludedCacheFor);
}
private FieldContainedStatus isFieldWithParameterIncludedInMap(Map<?, ?> field2possibleParameters, EntityField<?> field, Object parameter) {
Collection<?> specificCases = (Collection<?>) field2possibleParameters.get(field);
if (specificCases == null) {
return field2possibleParameters.containsKey(field)
? FieldContainedStatus.FULLY
: FieldContainedStatus.NOT_CONTAINED;
} else {
return parameter == null
? FieldContainedStatus.PARTIALLY
: specificCases.contains(parameter)
? FieldContainedStatus.FULLY
: FieldContainedStatus.NOT_CONTAINED;
}
}
@Override
protected String getLabel() {
return getId() + getNodeProperty(NodeProperties.STORAGE_PROVIDER, String.class).map(s -> " [" + s + "]").orElse("");
}
}

View file

@ -0,0 +1,325 @@
/*
* 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.common.delegate;
import org.keycloak.models.map.client.MapClientEntity;
import org.keycloak.models.map.client.MapClientEntityFields;
import org.keycloak.models.map.client.MapClientEntityImpl;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.storage.tree.NodeProperties;
import org.keycloak.models.map.storage.tree.TreeStorageNodeInstance;
import org.keycloak.models.map.storage.tree.TreeStorageNodePrescription;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Before;
import org.junit.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
/**
*
* @author hmlnarik
*/
public class PerFieldDelegateProviderCacheTest {
private MapClientEntity upperEnt;
private MapClientEntity lowerEnt;
private HashMap<String, Object> upperNodeProperties;
private EnumMap<MapClientEntityFields, Object> upperCacheFor;
private EnumMap<MapClientEntityFields, Object> upperCacheForExcluded;
private HashMap<String, Object> lowerNodeProperties;
private EnumMap<MapClientEntityFields, Object> lowerCacheFor;
private EnumMap<MapClientEntityFields, Object> lowerCacheForExcluded;
private TreeStorageNodeInstance<MapClientEntity> upperTsni;
private TreeStorageNodeInstance<MapClientEntity> lowerTsni;
AtomicInteger lowerEntSupplierCallCount = new AtomicInteger();
@Before
public void initEntities() {
upperEnt = new MapClientEntityImpl();
lowerEnt = new MapClientEntityImpl();
upperEnt.setProtocol("upper-protocol");
upperEnt.addRedirectUri("upper-redirectUri-1");
upperEnt.addRedirectUri("upper-redirectUri-2");
upperEnt.setClientId("upper-clientId-1");
upperEnt.setAttribute("attr1", Arrays.asList("upper-value-1"));
upperEnt.setAttribute("attr2", Arrays.asList("upper-value-2"));
upperEnt.setAttribute("attr3", Arrays.asList("upper-value-3"));
lowerEnt.setProtocol("lower-protocol");
lowerEnt.addRedirectUri("lower-redirectUri-1");
lowerEnt.addRedirectUri("lower-redirectUri-2");
lowerEnt.setClientId("lower-clientId-1");
lowerEnt.setAttribute("attr1", Arrays.asList("lower-value-1"));
lowerEnt.setAttribute("attr3", Arrays.asList("lower-value-3"));
lowerEnt.setAttribute("attr4", Arrays.asList("lower-value-4"));
upperNodeProperties = new HashMap<>();
upperCacheFor = new EnumMap<>(MapClientEntityFields.class);
upperCacheForExcluded = new EnumMap<>(MapClientEntityFields.class);
lowerNodeProperties = new HashMap<>();
lowerCacheFor = new EnumMap<>(MapClientEntityFields.class);
lowerCacheForExcluded = new EnumMap<>(MapClientEntityFields.class);
lowerEntSupplierCallCount.set(0);
}
private MapClientEntity prepareEntityAndTreeNodeInstances() {
TreeStorageNodePrescription upperTsnp = new TreeStorageNodePrescription(upperNodeProperties, null, null);
TreeStorageNodePrescription lowerTsnp = new TreeStorageNodePrescription(lowerNodeProperties, null, null);
upperTsni = new TreeStorageNodeInstance<>(null, upperTsnp);
lowerTsni = new TreeStorageNodeInstance<>(null, lowerTsnp);
PerFieldDelegateProvider<MapClientEntity> fieldProvider = new PerFieldDelegateProvider<>(upperTsni.new WithEntity(upperEnt), () -> {
lowerEntSupplierCallCount.incrementAndGet();
return lowerEnt;
});
return DeepCloner.DUMB_CLONER.entityFieldDelegate(MapClientEntity.class, fieldProvider);
}
@Test
public void testGet_CacheFor() {
//
// High-level perspective: cache for listed fields, the primary source for values is in the lower entity.
//
// Upper node is not a primary source for any fields, and caches only the enumerated fields
// Lower node is a primary source for all fields
// There is an (intentional) discrepancy between the field values in lowerEnt and upperEnt to be able to distinguish
// which entity the return value was obtained from.
upperCacheFor.put(MapClientEntityFields.CLIENT_ID, null);
upperCacheFor.put(MapClientEntityFields.ATTRIBUTES, Arrays.asList("attr2", "attr3", "attr4"));
upperNodeProperties.put(NodeProperties.CACHE_FOR, upperCacheFor);
upperNodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR, Collections.emptyMap()); // set the upper object to be exclusively a cache - none of the fields is primary source
MapClientEntity ent = prepareEntityAndTreeNodeInstances();
assertThat(lowerEntSupplierCallCount.get(), is(0));
assertThat(ent.getClientId(), is("upper-clientId-1"));
assertThat(ent.getAttribute("attr2"), contains("upper-value-2"));
assertThat(ent.getAttribute("attr3"), contains("upper-value-3"));
assertThat(ent.getAttribute("attr4"), nullValue());
assertThat(lowerEntSupplierCallCount.get(), is(0));
assertThat(ent.getProtocol(), is("lower-protocol"));
assertThat(ent.getAttribute("attr1"), contains("lower-value-1"));
assertThat(lowerEntSupplierCallCount.get(), is(1));
assertThat(ent.getAttributes().keySet(), containsInAnyOrder("attr1", "attr2", "attr3"));
assertThat(ent.getAttributes(), hasEntry("attr1", Arrays.asList("lower-value-1")));
assertThat(ent.getAttributes(), hasEntry("attr2", Arrays.asList("upper-value-2")));
assertThat(ent.getAttributes(), hasEntry("attr3", Arrays.asList("upper-value-3")));
assertThat(lowerEntSupplierCallCount.get(), is(1));
}
@Test
public void testSet_CacheFor() {
//
// High-level perspective: fields available in the lower layer (e.g. LDAP) cached in a faster layer (e.g. database)
//
// Upper node is a primary source for all fields apart from cached ones. It only caches only the enumerated fields
// Lower node is a primary source for all fields
// There is an (intentional) discrepancy between the field values in lowerEnt and upperEnt to be able to distinguish
// which entity the return value was obtained from.
upperCacheFor.put(MapClientEntityFields.CLIENT_ID, null);
upperCacheFor.put(MapClientEntityFields.PROTOCOL, null);
upperCacheFor.put(MapClientEntityFields.ATTRIBUTES, Arrays.asList("attr2", "attr3", "attr4"));
upperNodeProperties.put(NodeProperties.CACHE_FOR, upperCacheFor);
upperNodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR_EXCLUDED, upperCacheFor);
// When there is primary source in the node properties, named properties are considered as owned by this entity, and
// all other are considered as owned by child entity.
// Thus there is no call to the child entity creator.
MapClientEntity ent = prepareEntityAndTreeNodeInstances();
// When
ent.setProtocol("modified-protocol"); // modification in: upper and lower
ent.setAttribute("attr1", Arrays.asList("modified-value-1")); // modification in: upper
ent.setAttribute("attrX", Arrays.asList("modified-value-X")); // modification in: upper
ent.addRedirectUri("added-redirectUri"); // modification in: upper
ent.removeRedirectUri("upper-redirectUri-2"); // modification in: upper
ent.removeRedirectUri("lower-redirectUri-2"); // modification in: upper
// Then
assertThat(lowerEntSupplierCallCount.get(), is(1));
assertThat(ent.getClientId(), is("upper-clientId-1"));
assertThat(upperEnt.getClientId(), is("upper-clientId-1"));
assertThat(lowerEnt.getClientId(), is("lower-clientId-1"));
assertThat(ent.getRedirectUris(), containsInAnyOrder("upper-redirectUri-1", "added-redirectUri"));
assertThat(upperEnt.getRedirectUris(), containsInAnyOrder("upper-redirectUri-1", "added-redirectUri"));
assertThat(lowerEnt.getRedirectUris(), containsInAnyOrder("lower-redirectUri-1", "lower-redirectUri-2"));
assertThat(ent.getProtocol(), is("modified-protocol"));
assertThat(upperEnt.getProtocol(), is("modified-protocol"));
assertThat(lowerEnt.getProtocol(), is("modified-protocol"));
assertThat(ent.getAttribute("attr1"), contains("modified-value-1"));
assertThat(upperEnt.getAttribute("attr1"), contains("modified-value-1"));
assertThat(lowerEnt.getAttribute("attr1"), contains("lower-value-1"));
assertThat(ent.getAttribute("attr3"), contains("upper-value-3"));
assertThat(upperEnt.getAttribute("attr3"), contains("upper-value-3"));
assertThat(lowerEnt.getAttribute("attr3"), contains("lower-value-3"));
assertThat(ent.getAttribute("attrX"), contains("modified-value-X"));
assertThat(upperEnt.getAttribute("attrX"), contains("modified-value-X"));
assertThat(lowerEnt.getAttribute("attrX"), nullValue());
assertThat(ent.getAttributes().keySet(), containsInAnyOrder(
"attr1", // From upper
"attr2", // From upper, since it is a "cached" value (deliberately a stale cache here, it is not in lower)
"attr3", // From upper
// "attr4", // From upper where it has no value (deliberately a stale cache here, it has a value in lower)
"attrX" // From upper
));
assertThat(upperEnt.getAttributes().keySet(), containsInAnyOrder("attr1", "attr2", "attr3", "attrX"));
assertThat(lowerEnt.getAttributes().keySet(), containsInAnyOrder("attr1", "attr3", "attr4"));
}
@Test
public void testGet_CacheForExcluded() {
//
// High-level perspective: listed fields exclusively available in the lower layer (e.g. LDAP),
// never stored nor cached in the top layer (e.g. database).
// All other fields are stored in the top layer.
//
// Upper node is a primary source for all fields apart from cached ones. It only caches only the enumerated fields
// Lower node is a primary source for all fields
// There is an (intentional) discrepancy between the field values in lowerEnt and upperEnt to be able to distinguish
// which entity the return value was obtained from.
upperCacheForExcluded.put(MapClientEntityFields.CLIENT_ID, null);
upperCacheForExcluded.put(MapClientEntityFields.ATTRIBUTES, Arrays.asList("attr2", "attr3", "attr4"));
upperNodeProperties.put(NodeProperties.CACHE_FOR_EXCLUDED, upperCacheForExcluded);
upperNodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR_EXCLUDED, upperCacheForExcluded);
MapClientEntity ent = prepareEntityAndTreeNodeInstances();
assertThat(lowerEntSupplierCallCount.get(), is(0));
assertThat(ent.getProtocol(), is("upper-protocol"));
assertThat(ent.getAttribute("attr1"), contains("upper-value-1"));
assertThat(lowerEntSupplierCallCount.get(), is(0));
assertThat(ent.getAttribute("attr2"), nullValue());
assertThat(lowerEntSupplierCallCount.get(), is(1));
assertThat(ent.getAttribute("attr3"), contains("lower-value-3"));
assertThat(ent.getAttribute("attr4"), contains("lower-value-4"));
assertThat(ent.getClientId(), is("lower-clientId-1"));
assertThat(ent.getAttributes().keySet(), containsInAnyOrder("attr1", "attr3", "attr4"));
assertThat(ent.getAttributes(), hasEntry("attr1", Arrays.asList("upper-value-1")));
assertThat(ent.getAttributes(), hasEntry("attr3", Arrays.asList("lower-value-3")));
assertThat(ent.getAttributes(), hasEntry("attr4", Arrays.asList("lower-value-4")));
assertThat(lowerEntSupplierCallCount.get(), is(1));
}
@Test
public void testSet_CacheForExcluded() {
//
// High-level perspective: listed fields exclusively available in the lower layer (e.g. LDAP),
// never stored nor cached in the top layer (e.g. database).
// All other fields are stored in the top layer.
//
// When there is are primary source exclusion in the node properties, all properties apart from those enumerated
// are considered as owned by this entity. Those enumerated are obtained from the child entity.
// Thus there must be a single call to the child entity creator.
upperCacheForExcluded.put(MapClientEntityFields.CLIENT_ID, null);
upperCacheForExcluded.put(MapClientEntityFields.ATTRIBUTES, Arrays.asList("attr2", "attr3", "attr4"));
upperNodeProperties.put(NodeProperties.CACHE_FOR_EXCLUDED, upperCacheForExcluded);
upperNodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR_EXCLUDED, upperCacheForExcluded);
// When there is primary source exclusion in the node properties, listed properties are considered as owned by the child
// entity, and all other are considered as owned by this entity.
// Thus there is no call to the child entity creator.
MapClientEntity ent = prepareEntityAndTreeNodeInstances();
// When
ent.setClientId("modified-client-id-1"); // modification in: lower
ent.setProtocol("modified-protocol"); // modification in: upper
ent.setAttribute("attr4", Arrays.asList("modified-value-4")); // modification in: lower
ent.setAttribute("attrX", Arrays.asList("modified-value-X")); // modification in: upper
ent.addRedirectUri("added-redirectUri"); // modification in: upper
ent.removeRedirectUri("upper-redirectUri-2"); // modification in: upper
ent.removeRedirectUri("lower-redirectUri-2"); // modification in: upper
// Then
assertThat(lowerEntSupplierCallCount.get(), is(1));
assertThat(ent.getClientId(), is("modified-client-id-1"));
assertThat(upperEnt.getClientId(), is("upper-clientId-1"));
assertThat(lowerEnt.getClientId(), is("modified-client-id-1"));
assertThat(ent.getRedirectUris(), containsInAnyOrder("upper-redirectUri-1", "added-redirectUri"));
assertThat(upperEnt.getRedirectUris(), containsInAnyOrder("upper-redirectUri-1", "added-redirectUri"));
assertThat(lowerEnt.getRedirectUris(), containsInAnyOrder("lower-redirectUri-1", "lower-redirectUri-2"));
assertThat(ent.getProtocol(), is("modified-protocol"));
assertThat(upperEnt.getProtocol(), is("modified-protocol"));
assertThat(lowerEnt.getProtocol(), is("lower-protocol"));
assertThat(ent.getAttribute("attr1"), contains("upper-value-1"));
assertThat(upperEnt.getAttribute("attr1"), contains("upper-value-1"));
assertThat(lowerEnt.getAttribute("attr1"), contains("lower-value-1"));
assertThat(ent.getAttribute("attr2"), nullValue());
assertThat(upperEnt.getAttribute("attr2"), contains("upper-value-2"));
assertThat(lowerEnt.getAttribute("attr2"), nullValue());
assertThat(ent.getAttribute("attr3"), contains("lower-value-3"));
assertThat(upperEnt.getAttribute("attr3"), contains("upper-value-3"));
assertThat(lowerEnt.getAttribute("attr3"), contains("lower-value-3"));
assertThat(ent.getAttribute("attr4"), contains("modified-value-4"));
assertThat(upperEnt.getAttribute("attr4"), nullValue());
assertThat(lowerEnt.getAttribute("attr4"), contains("modified-value-4"));
assertThat(ent.getAttribute("attrX"), contains("modified-value-X"));
assertThat(upperEnt.getAttribute("attrX"), contains("modified-value-X"));
assertThat(lowerEnt.getAttribute("attrX"), nullValue());
assertThat(ent.getAttributes().keySet(), containsInAnyOrder(
"attr1", // From upper
// "attr2", // From lower where it has no value
"attr3", // From lower
"attr4", // From lower
"attrX" // From upper
));
assertThat(upperEnt.getAttributes().keySet(), containsInAnyOrder("attr1", "attr2", "attr3", "attrX"));
assertThat(lowerEnt.getAttributes().keySet(), containsInAnyOrder("attr1", "attr3", "attr4"));
}
}

View file

@ -0,0 +1,388 @@
/*
* 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.common.delegate;
import org.keycloak.models.map.client.MapClientEntity;
import org.keycloak.models.map.client.MapClientEntityFields;
import org.keycloak.models.map.client.MapClientEntityImpl;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.storage.tree.NodeProperties;
import org.keycloak.models.map.storage.tree.TreeStorageNodeInstance;
import org.keycloak.models.map.storage.tree.TreeStorageNodePrescription;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Before;
import org.junit.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
/**
*
* @author hmlnarik
*/
public class PerFieldDelegateProviderPrimarySourceTest {
private MapClientEntity upperEnt;
private MapClientEntity lowerEnt;
private HashMap<String, Object> upperNodeProperties;
private EnumMap<MapClientEntityFields, Object> upperPrimarySourceFor;
private EnumMap<MapClientEntityFields, Object> upperPrimarySourceForExcluded;
private HashMap<String, Object> lowerNodeProperties;
private EnumMap<MapClientEntityFields, Object> lowerPrimarySourceFor;
private EnumMap<MapClientEntityFields, Object> lowerPrimarySourceForExcluded;
private TreeStorageNodeInstance<MapClientEntity> upperTsni;
private TreeStorageNodeInstance<MapClientEntity> lowerTsni;
AtomicInteger lowerEntSupplierCallCount = new AtomicInteger();
@Before
public void initEntities() {
upperEnt = new MapClientEntityImpl();
lowerEnt = new MapClientEntityImpl();
upperEnt.setProtocol("upper-protocol");
upperEnt.addRedirectUri("upper-redirectUri-1");
upperEnt.addRedirectUri("upper-redirectUri-2");
upperEnt.setClientId("upper-clientId-1");
upperEnt.setAttribute("attr1", Arrays.asList("upper-value-1"));
upperEnt.setAttribute("attr2", Arrays.asList("upper-value-2"));
upperEnt.setAttribute("attr3", Arrays.asList("upper-value-3"));
lowerEnt.setProtocol("lower-protocol");
lowerEnt.addRedirectUri("lower-redirectUri-1");
lowerEnt.addRedirectUri("lower-redirectUri-2");
lowerEnt.setClientId("lower-clientId-1");
lowerEnt.setAttribute("attr1", Arrays.asList("lower-value-1"));
lowerEnt.setAttribute("attr3", Arrays.asList("lower-value-3"));
lowerEnt.setAttribute("attr4", Arrays.asList("lower-value-4"));
upperNodeProperties = new HashMap<>();
upperPrimarySourceFor = new EnumMap<>(MapClientEntityFields.class);
upperPrimarySourceForExcluded = new EnumMap<>(MapClientEntityFields.class);
lowerNodeProperties = new HashMap<>();
lowerPrimarySourceFor = new EnumMap<>(MapClientEntityFields.class);
lowerPrimarySourceForExcluded = new EnumMap<>(MapClientEntityFields.class);
lowerEntSupplierCallCount.set(0);
}
private MapClientEntity prepareEntityAndTreeNodeInstances() {
TreeStorageNodePrescription upperTsnp = new TreeStorageNodePrescription(upperNodeProperties, null, null);
TreeStorageNodePrescription lowerTsnp = new TreeStorageNodePrescription(lowerNodeProperties, null, null);
upperTsni = new TreeStorageNodeInstance<>(null, upperTsnp);
lowerTsni = new TreeStorageNodeInstance<>(null, lowerTsnp);
PerFieldDelegateProvider<MapClientEntity> fieldProvider = new PerFieldDelegateProvider<>(upperTsni.new WithEntity(upperEnt), () -> {
lowerEntSupplierCallCount.incrementAndGet();
return lowerEnt;
});
return DeepCloner.DUMB_CLONER.entityFieldDelegate(MapClientEntity.class, fieldProvider);
}
@Test
public void testGet_NoPrimarySource() {
// When there is no primary source (exclusion) in the node properties, all properties are considered as owned by this entity.
// Thus there is no call to the child entity creator.
MapClientEntity ent = prepareEntityAndTreeNodeInstances();
assertThat(lowerEntSupplierCallCount.get(), is(0));
assertThat(ent.getProtocol(), is("upper-protocol"));
assertThat(ent.getAttribute("attr1"), contains("upper-value-1"));
assertThat(lowerEntSupplierCallCount.get(), is(0));
assertThat(ent.getAttribute("attr2"), contains("upper-value-2"));
assertThat(ent.getAttribute("attr3"), contains("upper-value-3"));
assertThat(ent.getAttribute("attr4"), nullValue());
assertThat(lowerEntSupplierCallCount.get(), is(0));
assertThat(ent.getClientId(), is("upper-clientId-1"));
assertThat(ent.getAttributes().keySet(), containsInAnyOrder("attr1", "attr2", "attr3"));
assertThat(ent.getAttributes(), hasEntry("attr1", Arrays.asList("upper-value-1")));
assertThat(ent.getAttributes(), hasEntry("attr2", Arrays.asList("upper-value-2")));
assertThat(ent.getAttributes(), hasEntry("attr3", Arrays.asList("upper-value-3")));
assertThat(lowerEntSupplierCallCount.get(), is(0));
}
@Test
public void testSet_NoPrimarySource() {
// When there is no primary source (exclusion) in the node properties, all properties are considered as owned by this entity.
// Thus there is no call to the child entity creator.
MapClientEntity ent = prepareEntityAndTreeNodeInstances();
// When
ent.setProtocol("modified-protocol"); // modification in: upper
ent.setAttribute("attr1", Arrays.asList("modified-value-1")); // modification in: upper
ent.setAttribute("attrX", Arrays.asList("modified-value-X")); // modification in: upper
ent.addRedirectUri("added-redirectUri"); // modification in: upper
ent.removeRedirectUri("upper-redirectUri-2"); // modification in: upper
ent.removeRedirectUri("lower-redirectUri-2"); // modification in: upper
// Then
assertThat(lowerEntSupplierCallCount.get(), is(0));
assertThat(ent.getClientId(), is("upper-clientId-1"));
assertThat(upperEnt.getClientId(), is("upper-clientId-1"));
assertThat(lowerEnt.getClientId(), is("lower-clientId-1"));
assertThat(ent.getProtocol(), is("modified-protocol"));
assertThat(upperEnt.getProtocol(), is("modified-protocol"));
assertThat(lowerEnt.getProtocol(), is("lower-protocol"));
assertThat(ent.getRedirectUris(), containsInAnyOrder("upper-redirectUri-1", "added-redirectUri"));
assertThat(upperEnt.getRedirectUris(), containsInAnyOrder("upper-redirectUri-1", "added-redirectUri"));
assertThat(lowerEnt.getRedirectUris(), containsInAnyOrder("lower-redirectUri-1", "lower-redirectUri-2"));
assertThat(ent.getAttribute("attr1"), contains("modified-value-1"));
assertThat(upperEnt.getAttribute("attr1"), contains("modified-value-1"));
assertThat(lowerEnt.getAttribute("attr1"), contains("lower-value-1"));
assertThat(ent.getAttribute("attr3"), contains("upper-value-3"));
assertThat(upperEnt.getAttribute("attr3"), contains("upper-value-3"));
assertThat(lowerEnt.getAttribute("attr3"), contains("lower-value-3"));
assertThat(ent.getAttribute("attrX"), contains("modified-value-X"));
assertThat(upperEnt.getAttribute("attrX"), contains("modified-value-X"));
assertThat(lowerEnt.getAttribute("attrX"), nullValue());
assertThat(ent.getAttributes().keySet(), containsInAnyOrder("attr1", "attr2", "attr3", "attrX"));
assertThat(upperEnt.getAttributes().keySet(), containsInAnyOrder("attr1", "attr2", "attr3", "attrX"));
assertThat(lowerEnt.getAttributes().keySet(), containsInAnyOrder("attr1", "attr3", "attr4"));
}
@Test
public void testGet_PrimarySourceFor() {
//
// High-level perspective: only listed fields are available in upper entity, the rest is in lower one
//
// When there is are primary source in the node properties, only properties from those listed
// are considered as owned by this entity. Those not listed are obtained from the child entity.
// Thus there must be a single call to the child entity creator.
upperPrimarySourceFor.put(MapClientEntityFields.CLIENT_ID, null);
upperPrimarySourceFor.put(MapClientEntityFields.ATTRIBUTES, Arrays.asList("attr2", "attr3", "attr4"));
upperNodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR, upperPrimarySourceFor);
MapClientEntity ent = prepareEntityAndTreeNodeInstances();
assertThat(lowerEntSupplierCallCount.get(), is(0));
assertThat(ent.getClientId(), is("upper-clientId-1"));
assertThat(ent.getAttribute("attr2"), contains("upper-value-2"));
assertThat(ent.getAttribute("attr3"), contains("upper-value-3"));
assertThat(ent.getAttribute("attr4"), nullValue());
assertThat(lowerEntSupplierCallCount.get(), is(0));
assertThat(ent.getProtocol(), is("lower-protocol"));
assertThat(ent.getAttribute("attr1"), contains("lower-value-1"));
assertThat(lowerEntSupplierCallCount.get(), is(1));
assertThat(ent.getAttributes().keySet(), containsInAnyOrder("attr1", "attr2", "attr3"));
assertThat(ent.getAttributes(), hasEntry("attr1", Arrays.asList("lower-value-1")));
assertThat(ent.getAttributes(), hasEntry("attr2", Arrays.asList("upper-value-2")));
assertThat(ent.getAttributes(), hasEntry("attr3", Arrays.asList("upper-value-3")));
assertThat(lowerEntSupplierCallCount.get(), is(1));
}
@Test
public void testSet_PrimarySourceFor() {
//
// High-level perspective: only listed fields are available in upper entity, the rest is in lower one
//
// When there is are primary source in the node properties, only properties from those enumerated
// are considered as owned by this entity. Those not listed are obtained from the child entity.
// Thus there must be a single call to the child entity creator.
upperPrimarySourceFor.put(MapClientEntityFields.CLIENT_ID, null);
upperPrimarySourceFor.put(MapClientEntityFields.ATTRIBUTES, Arrays.asList("attr2", "attr3", "attr4"));
upperNodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR, upperPrimarySourceFor);
// When there is primary source in the node properties, named properties are considered as owned by this entity, and
// all other are considered as owned by child entity.
// Thus there is no call to the child entity creator.
MapClientEntity ent = prepareEntityAndTreeNodeInstances();
// When
ent.setClientId("modified-client-id"); // modification in: upper
ent.setProtocol("modified-protocol"); // modification in: lower
ent.setAttribute("attr1", Arrays.asList("modified-value-1")); // modification in: lower
ent.setAttribute("attrX", Arrays.asList("modified-value-X")); // modification in: lower
ent.addRedirectUri("added-redirectUri"); // modification in: lower
ent.removeRedirectUri("upper-redirectUri-2"); // modification in: lower
ent.removeRedirectUri("lower-redirectUri-2"); // modification in: lower
// Then
assertThat(lowerEntSupplierCallCount.get(), is(1));
assertThat(ent.getClientId(), is("modified-client-id"));
assertThat(upperEnt.getClientId(), is("modified-client-id"));
assertThat(lowerEnt.getClientId(), is("lower-clientId-1"));
assertThat(ent.getRedirectUris(), containsInAnyOrder("lower-redirectUri-1", "added-redirectUri"));
assertThat(upperEnt.getRedirectUris(), containsInAnyOrder("upper-redirectUri-1", "upper-redirectUri-2"));
assertThat(lowerEnt.getRedirectUris(), containsInAnyOrder("lower-redirectUri-1", "added-redirectUri"));
assertThat(ent.getProtocol(), is("modified-protocol"));
assertThat(upperEnt.getProtocol(), is("upper-protocol"));
assertThat(lowerEnt.getProtocol(), is("modified-protocol"));
assertThat(ent.getAttribute("attr1"), contains("modified-value-1"));
assertThat(upperEnt.getAttribute("attr1"), contains("upper-value-1"));
assertThat(lowerEnt.getAttribute("attr1"), contains("modified-value-1"));
assertThat(ent.getAttribute("attr3"), contains("upper-value-3"));
assertThat(upperEnt.getAttribute("attr3"), contains("upper-value-3"));
assertThat(lowerEnt.getAttribute("attr3"), contains("lower-value-3"));
assertThat(ent.getAttribute("attrX"), contains("modified-value-X"));
assertThat(upperEnt.getAttribute("attrX"), nullValue());
assertThat(lowerEnt.getAttribute("attrX"), contains("modified-value-X"));
assertThat(ent.getAttributes().keySet(), containsInAnyOrder(
"attr1", // From lower
"attr2", // From upper
"attr3", // From upper
// "attr4", // From upper where it has no value
"attrX" // From lower
));
assertThat(upperEnt.getAttributes().keySet(), containsInAnyOrder("attr1", "attr2", "attr3"));
assertThat(lowerEnt.getAttributes().keySet(), containsInAnyOrder("attr1", "attr3", "attr4", "attrX"));
}
@Test
public void testGet_PrimarySourceForExcluded() {
//
// High-level perspective: all but the listed fields are available in upper entity (e.g. JPA),
// the listed ones are stored in the lower entity (e.g. LDAP)
//
// When there is are primary source exclusion in the node properties, all properties apart from those enumerated
// are considered as owned by this entity. Those enumerated are obtained from the child entity.
// Thus there must be a single call to the child entity creator.
upperPrimarySourceForExcluded.put(MapClientEntityFields.CLIENT_ID, null);
upperPrimarySourceForExcluded.put(MapClientEntityFields.ATTRIBUTES, Arrays.asList("attr2", "attr3", "attr4"));
upperNodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR_EXCLUDED, upperPrimarySourceForExcluded);
MapClientEntity ent = prepareEntityAndTreeNodeInstances();
assertThat(lowerEntSupplierCallCount.get(), is(0));
assertThat(ent.getProtocol(), is("upper-protocol"));
assertThat(ent.getAttribute("attr1"), contains("upper-value-1"));
assertThat(lowerEntSupplierCallCount.get(), is(0));
assertThat(ent.getAttribute("attr2"), nullValue());
assertThat(lowerEntSupplierCallCount.get(), is(1));
assertThat(ent.getAttribute("attr3"), contains("lower-value-3"));
assertThat(ent.getAttribute("attr4"), contains("lower-value-4"));
assertThat(ent.getClientId(), is("lower-clientId-1"));
assertThat(ent.getAttributes().keySet(), containsInAnyOrder("attr1", "attr3", "attr4"));
assertThat(ent.getAttributes(), hasEntry("attr1", Arrays.asList("upper-value-1")));
assertThat(ent.getAttributes(), hasEntry("attr3", Arrays.asList("lower-value-3")));
assertThat(ent.getAttributes(), hasEntry("attr4", Arrays.asList("lower-value-4")));
assertThat(lowerEntSupplierCallCount.get(), is(1));
}
@Test
public void testSet_PrimarySourceForExcluded() {
//
// High-level perspective: all but the listed fields are available in upper entity (e.g. JPA),
// the listed ones are stored in the lower entity (e.g. LDAP)
//
// When there is are primary source exclusion in the node properties, all properties apart from those enumerated
// are considered as owned by this entity. Those enumerated are obtained from the child entity.
// Thus there must be a single call to the child entity creator.
upperPrimarySourceForExcluded.put(MapClientEntityFields.CLIENT_ID, null);
upperPrimarySourceForExcluded.put(MapClientEntityFields.ATTRIBUTES, Arrays.asList("attr2", "attr3", "attr4"));
upperNodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR_EXCLUDED, upperPrimarySourceForExcluded);
// When there is primary source exclusion in the node properties, listed properties are considered as owned by the child
// entity, and all other are considered as owned by this entity.
// Thus there is no call to the child entity creator.
MapClientEntity ent = prepareEntityAndTreeNodeInstances();
// When
ent.setClientId("modified-client-id-1"); // modification in: lower
ent.setProtocol("modified-protocol"); // modification in: upper
ent.setAttribute("attr4", Arrays.asList("modified-value-4")); // modification in: lower
ent.setAttribute("attrX", Arrays.asList("modified-value-X")); // modification in: upper
ent.addRedirectUri("added-redirectUri"); // modification in: upper
ent.removeRedirectUri("upper-redirectUri-2"); // modification in: upper
ent.removeRedirectUri("lower-redirectUri-2"); // modification in: upper
// Then
assertThat(lowerEntSupplierCallCount.get(), is(1));
assertThat(ent.getClientId(), is("modified-client-id-1"));
assertThat(upperEnt.getClientId(), is("upper-clientId-1"));
assertThat(lowerEnt.getClientId(), is("modified-client-id-1"));
assertThat(ent.getRedirectUris(), containsInAnyOrder("upper-redirectUri-1", "added-redirectUri"));
assertThat(upperEnt.getRedirectUris(), containsInAnyOrder("upper-redirectUri-1", "added-redirectUri"));
assertThat(lowerEnt.getRedirectUris(), containsInAnyOrder("lower-redirectUri-1", "lower-redirectUri-2"));
assertThat(ent.getProtocol(), is("modified-protocol"));
assertThat(upperEnt.getProtocol(), is("modified-protocol"));
assertThat(lowerEnt.getProtocol(), is("lower-protocol"));
assertThat(ent.getAttribute("attr1"), contains("upper-value-1"));
assertThat(upperEnt.getAttribute("attr1"), contains("upper-value-1"));
assertThat(lowerEnt.getAttribute("attr1"), contains("lower-value-1"));
assertThat(ent.getAttribute("attr2"), nullValue());
assertThat(upperEnt.getAttribute("attr2"), contains("upper-value-2"));
assertThat(lowerEnt.getAttribute("attr2"), nullValue());
assertThat(ent.getAttribute("attr3"), contains("lower-value-3"));
assertThat(upperEnt.getAttribute("attr3"), contains("upper-value-3"));
assertThat(lowerEnt.getAttribute("attr3"), contains("lower-value-3"));
assertThat(ent.getAttribute("attr4"), contains("modified-value-4"));
assertThat(upperEnt.getAttribute("attr4"), nullValue());
assertThat(lowerEnt.getAttribute("attr4"), contains("modified-value-4"));
assertThat(ent.getAttribute("attrX"), contains("modified-value-X"));
assertThat(upperEnt.getAttribute("attrX"), contains("modified-value-X"));
assertThat(lowerEnt.getAttribute("attrX"), nullValue());
assertThat(ent.getAttributes().keySet(), containsInAnyOrder(
"attr1", // From upper
// "attr2", // From lower where it has no value
"attr3", // From lower
"attr4", // From lower
"attrX" // From lower
));
assertThat(upperEnt.getAttributes().keySet(), containsInAnyOrder("attr1", "attr2", "attr3", "attrX"));
assertThat(lowerEnt.getAttributes().keySet(), containsInAnyOrder("attr1", "attr3", "attr4"));
}
}

View file

@ -44,16 +44,16 @@ public class DefaultTreeNodeTest {
private class Node extends DefaultTreeNode<Node> { private class Node extends DefaultTreeNode<Node> {
public Node() { public Node() {
super(treeProperties); super(DefaultTreeNodeTest.this.treeProperties);
} }
public Node(String id) { public Node(String id) {
super(treeProperties); super(DefaultTreeNodeTest.this.treeProperties);
setId(id); setId(id);
} }
public Node(Node parent, String id) { public Node(Node parent, String id) {
super(treeProperties); super(DefaultTreeNodeTest.this.treeProperties);
setId(id); setId(id);
setParent(parent); setParent(parent);
} }

View file

@ -0,0 +1,210 @@
/*
* 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.storage.tree;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.map.client.MapClientEntity;
import org.keycloak.models.map.client.MapClientEntityFields;
import org.keycloak.models.map.client.MapClientEntityFields;
import org.keycloak.models.map.realm.MapRealmEntity;
import org.keycloak.models.map.storage.ModelEntityUtil;
import org.keycloak.models.map.storage.tree.TreeStorageNodePrescription.FieldContainedStatus;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.junit.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.sameInstance;
/**
*
* @author hmlnarik
*/
public class TreeStorageNodePrescriptionTest {
@Test
public void testEmpty() {
TreeStorageNodePrescription n = new TreeStorageNodePrescription(null);
TreeStorageNodePrescription c1 = n.forEntityClass(MapClientEntity.class);
TreeStorageNodePrescription c2 = n.forEntityClass(MapClientEntity.class);
TreeStorageNodePrescription r1 = n.forEntityClass(MapRealmEntity.class);
assertThat(c1, sameInstance(c2));
assertThat(c1, not(sameInstance(r1)));
assertThat(c1.getNodeProperties().entrySet(), empty());
assertThat(c1.getEdgeProperties().entrySet(), empty());
assertThat(c1.getTreeProperties().size(), is(1));
assertThat(c1.getTreeProperties(), hasEntry(TreeProperties.MODEL_CLASS, ClientModel.class));
assertThat(r1.getTreeProperties(), hasEntry(TreeProperties.MODEL_CLASS, RealmModel.class));
}
@Test
public void testTreePropertyProjection() {
Map<String, Object> treeProperties = new HashMap<>();
treeProperties.put("prop[" + ModelEntityUtil.getModelName(ClientModel.class) + "]", "propClientValue");
treeProperties.put("prop[" + ModelEntityUtil.getModelName(RealmModel.class) + "]", "propRealmValue");
treeProperties.put("propRealmOnly[" + ModelEntityUtil.getModelName(RealmModel.class) + "]", "propRealmOnlyValue");
treeProperties.put("propBoth", "propBothValue");
Map<String, Object> nodeProperties = new HashMap<>();
nodeProperties.put("nprop[" + ModelEntityUtil.getModelName(ClientModel.class) + "]", "propClientValue");
nodeProperties.put("nprop[" + ModelEntityUtil.getModelName(RealmModel.class) + "]", "propRealmValue");
nodeProperties.put("npropRealmOnly[" + ModelEntityUtil.getModelName(RealmModel.class) + "]", "propRealmOnlyValue");
nodeProperties.put("npropBoth", "propBothValue");
Map<String, Object> edgeProperties = new HashMap<>();
edgeProperties.put("eprop[" + ModelEntityUtil.getModelName(ClientModel.class) + "]", "propClientValue");
edgeProperties.put("eprop[" + ModelEntityUtil.getModelName(RealmModel.class) + "]", "propRealmValue");
edgeProperties.put("epropRealmOnly[" + ModelEntityUtil.getModelName(RealmModel.class) + "]", "propRealmOnlyValue");
edgeProperties.put("epropBoth", "propBothValue");
TreeStorageNodePrescription n = new TreeStorageNodePrescription(nodeProperties, edgeProperties, treeProperties);
TreeStorageNodePrescription c1 = n.forEntityClass(MapClientEntity.class);
TreeStorageNodePrescription r1 = n.forEntityClass(MapRealmEntity.class);
assertThat(c1.getTreeProperties(), hasEntry(TreeProperties.MODEL_CLASS, ClientModel.class));
assertThat(c1.getTreeProperties(), hasEntry("prop", "propClientValue"));
assertThat(c1.getTreeProperties(), hasEntry("propBoth", "propBothValue"));
assertThat(c1.getTreeProperties().size(), is(3));
assertThat(c1.getNodeProperties(), hasEntry("nprop", "propClientValue"));
assertThat(c1.getNodeProperties(), hasEntry("npropBoth", "propBothValue"));
assertThat(c1.getNodeProperties().size(), is(2));
assertThat(c1.getEdgeProperties(), hasEntry("eprop", "propClientValue"));
assertThat(c1.getEdgeProperties(), hasEntry("epropBoth", "propBothValue"));
assertThat(c1.getEdgeProperties().size(), is(2));
assertThat(r1.getTreeProperties(), hasEntry(TreeProperties.MODEL_CLASS, RealmModel.class));
assertThat(r1.getTreeProperties(), hasEntry("prop", "propRealmValue"));
assertThat(r1.getTreeProperties(), hasEntry("propRealmOnly", "propRealmOnlyValue"));
assertThat(r1.getTreeProperties(), hasEntry("propBoth", "propBothValue"));
assertThat(r1.getTreeProperties().size(), is(4));
}
/**
* Test a node that has neither PRIMARY_SOURCE_FOR nor PRIMARY_SOURCE_FOR_EXCLUDED set.
* <p>
* Represents e.g. a node in the tree that stores all fields.
*/
@Test
public void testPrimarySourceForBasicUnset() {
Map<String, Object> nodeProperties = new HashMap<>();
TreeStorageNodePrescription n = new TreeStorageNodePrescription(nodeProperties, null, null);
for (MapClientEntityFields field : MapClientEntityFields.values()) {
assertThat("Field " + field + " has primary source in this node", n.isPrimarySourceFor(field, null), is(FieldContainedStatus.FULLY));
}
}
/**
* Test a node that has PRIMARY_SOURCE_FOR set to all fields with no specialization (i.e. {@code null}),
* and no PRIMARY_SOURCE_FOR_EXCLUDED is set.
* <p>
* Represents e.g. a node in the tree that stores all fields.
*/
@Test
public void testPrimarySourceForBasicSet() {
Map<String, Object> nodeProperties = new HashMap<>();
EnumMap<MapClientEntityFields, Collection<String>> primarySourceFor = new EnumMap<>(MapClientEntityFields.class);
for (MapClientEntityFields field : MapClientEntityFields.values()) {
primarySourceFor.put(field, null);
}
nodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR, primarySourceFor);
TreeStorageNodePrescription n = new TreeStorageNodePrescription(nodeProperties, null, null);
for (MapClientEntityFields field : MapClientEntityFields.values()) {
assertThat("Field " + field + " has primary source in this node", n.isPrimarySourceFor(field, null), is(FieldContainedStatus.FULLY));
}
}
/**
* Test a node that has PRIMARY_SOURCE_FOR set to only ID field with no specialization (i.e. {@code null}),
* and no PRIMARY_SOURCE_FOR_EXCLUDED is set.
* <p>
* Represents e.g. a node in the tree that stores only ID (maintains existence check of an object)
*/
@Test
public void testPrimarySourceForBasicSetId() {
Map<String, Object> nodeProperties = new HashMap<>();
EnumMap<MapClientEntityFields, Collection<String>> primarySourceFor = new EnumMap<>(MapClientEntityFields.class);
nodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR, primarySourceFor);
primarySourceFor.put(MapClientEntityFields.ID, null);
TreeStorageNodePrescription n = new TreeStorageNodePrescription(nodeProperties, null, null);
for (MapClientEntityFields field : MapClientEntityFields.values()) {
assertThat(n.isPrimarySourceFor(field, null),
is(field == MapClientEntityFields.ID ? FieldContainedStatus.FULLY : FieldContainedStatus.NOT_CONTAINED));
}
}
/**
* Test a node that has no PRIMARY_SOURCE_FOR set,
* and PRIMARY_SOURCE_FOR_EXCLUDED is set to ATTRIBUTES field with no specialization (i.e. {@code null}).
* <p>
* Represents e.g. a node in the tree that stores all attributes apart from attributes
*/
@Test
public void testPrimarySourceForWithExcluded() {
Map<String, Object> nodeProperties = new HashMap<>();
EnumMap<MapClientEntityFields, Collection<String>> primarySourceForExcluded = new EnumMap<>(MapClientEntityFields.class);
nodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR_EXCLUDED, primarySourceForExcluded);
primarySourceForExcluded.put(MapClientEntityFields.ATTRIBUTES, null);
// node is primary for all fields apart from all attributes
TreeStorageNodePrescription n = new TreeStorageNodePrescription(nodeProperties, null, null);
for (MapClientEntityFields field : MapClientEntityFields.values()) {
assertThat(n.isPrimarySourceFor(field, null),
is(field == MapClientEntityFields.ATTRIBUTES ? FieldContainedStatus.NOT_CONTAINED: FieldContainedStatus.FULLY));
}
}
/**
* Test a node that has PRIMARY_SOURCE_FOR set to ATTRIBUTES field with no specialization (i.e. {@code null}),
* and PRIMARY_SOURCE_FOR_EXCLUDED is set to ATTRIBUTES field specialization to "attr1" and "attr2".
* <p>
* Represents e.g. a node in the tree that acts as a supplementary store for all attributes apart from "attr1" and "attr2"
*/
@Test
public void testPrimarySourceForWithExcludedTwoAttributes() {
Map<String, Object> nodeProperties = new HashMap<>();
EnumMap<MapClientEntityFields, Collection<String>> primarySourceFor = new EnumMap<>(MapClientEntityFields.class);
nodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR, primarySourceFor);
primarySourceFor.put(MapClientEntityFields.ATTRIBUTES, null);
EnumMap<MapClientEntityFields, Collection<String>> primarySourceForExcluded = new EnumMap<>(MapClientEntityFields.class);
nodeProperties.put(NodeProperties.PRIMARY_SOURCE_FOR_EXCLUDED, primarySourceForExcluded);
primarySourceForExcluded.put(MapClientEntityFields.ATTRIBUTES, Arrays.asList("attr1", "attr2"));
// node is primary for all attributes apart from "attr1" and "attr2"
TreeStorageNodePrescription n = new TreeStorageNodePrescription(nodeProperties, null, null);
assertThat("Field ID has NOT primary source in this node", n.isPrimarySourceFor(MapClientEntityFields.ID, null), is(FieldContainedStatus.NOT_CONTAINED));
assertThat("Attribute attr1 has NOT primary source in this node", n.isPrimarySourceFor(MapClientEntityFields.ATTRIBUTES, "attr1"), is(FieldContainedStatus.NOT_CONTAINED));
assertThat("Attribute attr2 has NOT primary source in this node", n.isPrimarySourceFor(MapClientEntityFields.ATTRIBUTES, "attr2"), is(FieldContainedStatus.NOT_CONTAINED));
assertThat("Attribute attr3 has primary source in this node", n.isPrimarySourceFor(MapClientEntityFields.ATTRIBUTES, "attr3"), is(FieldContainedStatus.FULLY));
assertThat("Attributes have primary source in this node and other source in some other nodes", n.isPrimarySourceFor(MapClientEntityFields.ATTRIBUTES, null), is(FieldContainedStatus.PARTIALLY));
}
}

View file

@ -27,7 +27,6 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.keycloak.models.map.client.MapClientEntity; import org.keycloak.models.map.client.MapClientEntity;
import org.keycloak.models.map.client.MapClientEntityFieldDelegate;
import org.keycloak.models.map.client.MapClientEntityFields; import org.keycloak.models.map.client.MapClientEntityFields;
import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.common.EntityField; import org.keycloak.models.map.common.EntityField;
@ -80,7 +79,7 @@ public class Dict<E> extends UpdatableEntity.Impl implements EntityFieldDelegate
} }
@Override @Override
public Object get(EntityField<E> field) { public <EF extends Enum<? extends EntityField<E>> & EntityField<E>> Object get(EF field) {
if ("Attributes".equals(field.getName())) { if ("Attributes".equals(field.getName())) {
return attributeName2key.entrySet().stream() return attributeName2key.entrySet().stream()
.filter(me -> get(me.getValue()) != null) .filter(me -> get(me.getValue()) != null)
@ -94,7 +93,7 @@ public class Dict<E> extends UpdatableEntity.Impl implements EntityFieldDelegate
} }
@Override @Override
public <T> void set(EntityField<E> field, T value) { public <T, EF extends Enum<? extends EntityField<E>> & EntityField<E>> void set(EF field, T value) {
String key = fieldName2key.get(field.getName()); String key = fieldName2key.get(field.getName());
if (key != null) { if (key != null) {
put(key, value); put(key, value);
@ -102,7 +101,7 @@ public class Dict<E> extends UpdatableEntity.Impl implements EntityFieldDelegate
} }
@Override @Override
public <K> Object mapGet(EntityField<E> field, K key) { public <K, EF extends Enum<? extends EntityField<E>> & EntityField<E>> Object mapGet(EF field, K key) {
if ("Attributes".equals(field.getName()) && attributeName2key.containsKey(key)) { if ("Attributes".equals(field.getName()) && attributeName2key.containsKey(key)) {
Object v = get(attributeName2key.get(key)); Object v = get(attributeName2key.get(key));
return v == null ? null : Collections.singletonList(get(attributeName2key.get(key))); return v == null ? null : Collections.singletonList(get(attributeName2key.get(key)));
@ -111,7 +110,7 @@ public class Dict<E> extends UpdatableEntity.Impl implements EntityFieldDelegate
} }
@Override @Override
public <K, T> void mapPut(EntityField<E> field, K key, T value) { public <K, T, EF extends Enum<? extends EntityField<E>> & EntityField<E>> void mapPut(EF field, K key, T value) {
if ("Attributes".equals(field.getName()) && attributeName2key.containsKey(key) && (value instanceof List)) { if ("Attributes".equals(field.getName()) && attributeName2key.containsKey(key) && (value instanceof List)) {
List<?> l = (List<?>) value; List<?> l = (List<?>) value;
if (l.isEmpty()) { if (l.isEmpty()) {
@ -123,7 +122,7 @@ public class Dict<E> extends UpdatableEntity.Impl implements EntityFieldDelegate
} }
@Override @Override
public <K> Object mapRemove(EntityField<E> field, K key) { public <K, EF extends Enum<? extends EntityField<E>> & EntityField<E>> Object mapRemove(EF field, K key) {
if ("Attributes".equals(field.getName()) && attributeName2key.containsKey(key)) { if ("Attributes".equals(field.getName()) && attributeName2key.containsKey(key)) {
Object o = remove(attributeName2key.get(key)); Object o = remove(attributeName2key.get(key));
return o == null ? null : Collections.singletonList(o); return o == null ? null : Collections.singletonList(o);