diff --git a/api/src/main/java/org/pentaho/platform/api/engine/security/authorization/IAuthorizationActionService.java b/api/src/main/java/org/pentaho/platform/api/engine/security/authorization/IAuthorizationActionService.java index 7346a966f9..c8c7b90a84 100644 --- a/api/src/main/java/org/pentaho/platform/api/engine/security/authorization/IAuthorizationActionService.java +++ b/api/src/main/java/org/pentaho/platform/api/engine/security/authorization/IAuthorizationActionService.java @@ -62,7 +62,6 @@ default Stream getActions( @Nullable String actionNamespac .filter( action -> action.getName().startsWith( actionNamespacePrefix ) ); } - // TODO: tests /** * Gets an authorization action given its name. *

diff --git a/api/src/main/java/org/pentaho/platform/api/engine/security/authorization/caching/IAuthorizationDecisionCache.java b/api/src/main/java/org/pentaho/platform/api/engine/security/authorization/caching/IAuthorizationDecisionCache.java new file mode 100644 index 0000000000..4e0941b397 --- /dev/null +++ b/api/src/main/java/org/pentaho/platform/api/engine/security/authorization/caching/IAuthorizationDecisionCache.java @@ -0,0 +1,88 @@ +/*! ****************************************************************************** + * + * Pentaho + * + * Copyright (C) 2024 by Hitachi Vantara, LLC : http://www.pentaho.com + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file. + * + * Change Date: 2029-07-20 + ******************************************************************************/ + +package org.pentaho.platform.api.engine.security.authorization.caching; + +import edu.umd.cs.findbugs.annotations.NonNull; +import org.pentaho.platform.api.engine.security.authorization.IAuthorizationOptions; +import org.pentaho.platform.api.engine.security.authorization.IAuthorizationRequest; +import org.pentaho.platform.api.engine.security.authorization.decisions.IAuthorizationDecision; + +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * The {@code IAuthorizationDecisionCache} interface represents a cache for authorization decisions, + * and which follows the "loading cache" pattern. + */ +public interface IAuthorizationDecisionCache { + /** + * Gets a cached authorization decision for a specific authorization request and options, if available. + * + * @param request The authorization request. + * @param options The authorization options. + * @return An optional with the cached decision, if found; an empty optional, if not found. + */ + @NonNull + Optional get( @NonNull IAuthorizationRequest request, + @NonNull IAuthorizationOptions options ); + + /** + * Gets a cached authorization decision for a specific authorization request and options, + * loading it from the given loader function and storing it in the cache, if not available. + * + * @param request The authorization request. + * @param options The authorization options. + * @param loader A function that computes the authorization decision if it is not found in the cache. + * @return The authorization decision. + */ + @NonNull + IAuthorizationDecision get( @NonNull IAuthorizationRequest request, + @NonNull IAuthorizationOptions options, + @NonNull Function loader ); + + /** + * Caches an authorization decision for a specific authorization request and options. + *

+ * This operation is a no-op if the cache is disabled. + * + * @param request The authorization request. + * @param options The authorization options. + * @param decision The authorization decision to cache. + */ + void put( @NonNull IAuthorizationRequest request, + @NonNull IAuthorizationOptions options, + @NonNull IAuthorizationDecision decision ); + + /** + * Clears the cached authorization decision for a specific authorization request and options, if any. + * + * @param request The authorization request. + * @param options The authorization options. + */ + void invalidate( @NonNull IAuthorizationRequest request, @NonNull IAuthorizationOptions options ); + + /** + * Clears all cached authorization decisions for requests and options that match the given predicate. + * + * @param predicate A predicate that matches authorization requests and options to clear from the cache. + */ + void invalidateAll( @NonNull Predicate predicate ); + + /** + * Clears all cached authorization decisions. + *

+ * This operation is a no-op if the cache is disabled. + */ + void invalidateAll(); +} diff --git a/api/src/main/java/org/pentaho/platform/api/engine/security/authorization/caching/IAuthorizationDecisionCacheKey.java b/api/src/main/java/org/pentaho/platform/api/engine/security/authorization/caching/IAuthorizationDecisionCacheKey.java new file mode 100644 index 0000000000..36cb8979ab --- /dev/null +++ b/api/src/main/java/org/pentaho/platform/api/engine/security/authorization/caching/IAuthorizationDecisionCacheKey.java @@ -0,0 +1,31 @@ +/*! ****************************************************************************** + * + * Pentaho + * + * Copyright (C) 2024 by Hitachi Vantara, LLC : http://www.pentaho.com + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file. + * + * Change Date: 2029-07-20 + ******************************************************************************/ + +package org.pentaho.platform.api.engine.security.authorization.caching; + +import edu.umd.cs.findbugs.annotations.NonNull; +import org.pentaho.platform.api.engine.security.authorization.IAuthorizationOptions; +import org.pentaho.platform.api.engine.security.authorization.IAuthorizationRequest; + +/** + * The {@code IAuthorizationDecisionCacheKey} interface represents the key for cached authorization decision. + * It is constituted by an authorization request and options. + *

+ * Implementations must implement proper {@code equals()} and {@code hashCode()} methods. + */ +public interface IAuthorizationDecisionCacheKey { + @NonNull + IAuthorizationRequest getRequest(); + + @NonNull + IAuthorizationOptions getOptions(); +} diff --git a/assemblies/pentaho-solutions/src/main/resources/pentaho-solutions/system/pentahoObjects.spring.xml b/assemblies/pentaho-solutions/src/main/resources/pentaho-solutions/system/pentahoObjects.spring.xml index 76b20ff3c3..f49a7ec826 100644 --- a/assemblies/pentaho-solutions/src/main/resources/pentaho-solutions/system/pentahoObjects.spring.xml +++ b/assemblies/pentaho-solutions/src/main/resources/pentaho-solutions/system/pentahoObjects.spring.xml @@ -326,13 +326,27 @@ not directly from the PentahoObjectFactory. --> + + + + + + + + + + + + + class="org.pentaho.platform.engine.security.authorization.core.CachingAuthorizationService"> + diff --git a/core/src/main/java/org/pentaho/platform/engine/security/authorization/MemoryAuthorizationActionService.java b/core/src/main/java/org/pentaho/platform/engine/security/authorization/MemoryAuthorizationActionService.java new file mode 100644 index 0000000000..16a3faa161 --- /dev/null +++ b/core/src/main/java/org/pentaho/platform/engine/security/authorization/MemoryAuthorizationActionService.java @@ -0,0 +1,147 @@ +/*! ****************************************************************************** + * + * Pentaho + * + * Copyright (C) 2024 by Hitachi Vantara, LLC : http://www.pentaho.com + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file. + * + * Change Date: 2029-07-20 + ******************************************************************************/ + +package org.pentaho.platform.engine.security.authorization; + +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import org.apache.commons.lang.StringUtils; +import org.pentaho.platform.api.engine.IAuthorizationAction; +import org.pentaho.platform.api.engine.IPentahoObjectReference; +import org.pentaho.platform.api.engine.security.authorization.IAuthorizationActionService; +import org.pentaho.platform.engine.core.system.objfac.spring.Const; +import org.springframework.util.Assert; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; + +/** + * The {@code MemoryAuthorizationActionService} provides access to authorization actions stored in memory. + * The actions are received as Pentaho System object references, at construction. + */ +public class MemoryAuthorizationActionService implements IAuthorizationActionService { + + @NonNull + private final List actions; + + @NonNull + private final Map actionsByName; + + @NonNull + private final List systemActions; + + @NonNull + private final List pluginActions; + + @NonNull + private final Map> actionsByPlugin; + + public MemoryAuthorizationActionService( + @NonNull Supplier>> authorizationActionsSupplier ) { + + Assert.notNull( authorizationActionsSupplier, "Argument 'authorizationActionsSupplier' is required" ); + + actions = new ArrayList<>(); + actionsByName = new HashMap<>(); + systemActions = new ArrayList<>(); + pluginActions = new ArrayList<>(); + actionsByPlugin = new HashMap<>(); + + authorizationActionsSupplier + .get() + .filter( distinctByKey( IPentahoObjectReference::getObject ) ) + .filter( ref -> ref.getObject() != null && StringUtils.isNotEmpty( ref.getObject().getName() ) ) + .forEach( ref -> { + var action = ref.getObject(); + + actions.add( action ); + actionsByName.put( action.getName(), action ); + + String pluginId = getPluginId( ref ); + if ( StringUtils.isNotEmpty( pluginId ) ) { + pluginActions.add( action ); + actionsByPlugin + .computeIfAbsent( pluginId, k -> new ArrayList<>() ) + .add( action ); + } else { + systemActions.add( action ); + } + } ); + } + + /** + * Creates a predicate that filters out duplicate elements based on a key extracted from each element. + *

+ * This method uses a concurrent set to track seen keys, ensuring that only the first occurrence of each key is kept. + *

+ * Any elements with {@code null} keys are filtered out. + * + * @param keyExtractor A function to extract the key from each element. + * @param The type of the elements in the stream. + * @return A predicate that returns true for the first occurrence of each key and false for duplicates. + */ + private static Predicate distinctByKey( Function keyExtractor ) { + Set seen = ConcurrentHashMap.newKeySet(); + return t -> { + Object key = keyExtractor.apply( t ); + return key != null && seen.add( key ); + }; + } + + @Nullable + private static String getPluginId( @NonNull IPentahoObjectReference actionReference ) { + return (String) actionReference.getAttributes().get( Const.PUBLISHER_PLUGIN_ID_ATTRIBUTE ); + } + + @NonNull + @Override + public Stream getActions() { + return actions.stream(); + } + + @NonNull + @Override + public Optional getAction( @NonNull String actionName ) { + Assert.hasLength( actionName, "Argument 'actionName' is required" ); + + return Optional.ofNullable( actionsByName.get( actionName ) ); + } + + @NonNull + @Override + public Stream getSystemActions() { + return systemActions.stream(); + } + + @NonNull + @Override + public Stream getPluginActions() { + return pluginActions.stream(); + } + + @NonNull + @Override + public Stream getPluginActions( @NonNull String pluginId ) { + Assert.hasLength( pluginId, "Argument 'pluginId' is required" ); + + return actionsByPlugin.getOrDefault( pluginId, List.of() ).stream(); + } +} diff --git a/core/src/main/java/org/pentaho/platform/engine/security/authorization/PentahoSystemAuthorizationActionService.java b/core/src/main/java/org/pentaho/platform/engine/security/authorization/PentahoSystemAuthorizationActionService.java index 203b781917..613c66b38b 100644 --- a/core/src/main/java/org/pentaho/platform/engine/security/authorization/PentahoSystemAuthorizationActionService.java +++ b/core/src/main/java/org/pentaho/platform/engine/security/authorization/PentahoSystemAuthorizationActionService.java @@ -13,177 +13,105 @@ package org.pentaho.platform.engine.security.authorization; import edu.umd.cs.findbugs.annotations.NonNull; -import org.apache.commons.lang.StringUtils; +import edu.umd.cs.findbugs.annotations.Nullable; import org.pentaho.platform.api.engine.IAuthorizationAction; import org.pentaho.platform.api.engine.IPentahoObjectReference; +import org.pentaho.platform.api.engine.IPluginManager; import org.pentaho.platform.api.engine.security.authorization.IAuthorizationActionService; import org.pentaho.platform.engine.core.system.PentahoSystem; -import org.pentaho.platform.engine.core.system.objfac.spring.Const; import org.springframework.util.Assert; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; -import java.util.function.Predicate; +import java.util.Optional; import java.util.function.Supplier; import java.util.stream.Stream; /** * The {@code PentahoSystemAuthorizationActionService} provides access to authorization actions registered in the - * Pentaho System at any given time. + * Pentaho System. It listens to plugin changes, and updates its internal action list accordingly. */ public class PentahoSystemAuthorizationActionService implements IAuthorizationActionService { + + private static class PentahoSystemAuthorizationActionSupplier + implements Supplier>> { + + @Override + public Stream> get() { + return PentahoSystem.getObjectReferences( IAuthorizationAction.class, null ).stream(); + } + } + @NonNull - private final Supplier>> authorizationActionsSupplier; + private MemoryAuthorizationActionService memoryService; - public PentahoSystemAuthorizationActionService() { - this( PentahoSystemAuthorizationActionService::getPentahoSystemActionObjectReferences ); + public PentahoSystemAuthorizationActionService( @NonNull IPluginManager pluginManager ) { + this( pluginManager, new PentahoSystemAuthorizationActionSupplier() ); } public PentahoSystemAuthorizationActionService( + @NonNull IPluginManager pluginManager, @NonNull Supplier>> authorizationActionsSupplier ) { Assert.notNull( authorizationActionsSupplier, "Argument 'authorizationActionsSupplier' is required" ); - this.authorizationActionsSupplier = authorizationActionsSupplier; - } + this.memoryService = new MemoryAuthorizationActionService( authorizationActionsSupplier ); - // region Helper methods - - /** - * Gets all authorization action object references registered in the Pentaho System. - *

- * This method is used to retrieve all actions, including duplicates, as registered in the system. - * - * @return A stream of all authorization action object references. - */ - private static Stream> getPentahoSystemActionObjectReferences() { - return PentahoSystem.getObjectReferences( IAuthorizationAction.class, null ).stream(); + // Update the in-memory actions whenever plugins are changed. + pluginManager.addPluginManagerListener( () -> + this.memoryService = new MemoryAuthorizationActionService( authorizationActionsSupplier ) ); } - /** - * Gets all authorization actions registered in the Pentaho System, while making sure that no duplicate actions - * are returned. - *

- * An action is a duplicate action if it has the same {@link IAuthorizationAction#getName() name} as another action, - * as defined by the {@link IAuthorizationAction} interface's equality semantics. - * The action registered with the highest {@link IPentahoObjectReference#getRanking() ranking} is considered the - * original one, and any others duplicates. - *

- * This implementation performs direct action name comparison, to guard against potential issues with - * {@link Object#equals(Object)} and {@link Object#hashCode()} implementations of action instances. - * - * @return A stream of authorization action object references ensured to have no duplicates. - */ - protected Stream> getActionObjectReferences() { - return Objects.requireNonNull( authorizationActionsSupplier.get() ) - .filter( distinctByKey( PentahoSystemAuthorizationActionService::getActionObjectReferenceKey ) ); - } - /** - * Gets the key to use to compare two action object references. - *

- * Returns the name of the action, which is should be unique for each action. - * - * @param actionReference The action object reference to get the key for. - */ - private static String getActionObjectReferenceKey( - @NonNull IPentahoObjectReference actionReference ) { - return actionReference.getObject().getName(); + @NonNull + @Override + public Stream getActions() { + return memoryService.getActions(); } - /** - * Creates a predicate that filters out duplicate elements based on a key extracted from each element. - *

- * This method uses a concurrent set to track seen keys, ensuring that only the first occurrence of each key is kept. - *

- * Any elements with {@code null} keys are filtered out. - * - * @param keyExtractor A function to extract the key from each element. - * @param The type of the elements in the stream. - * @return A predicate that returns true for the first occurrence of each key and false for duplicates. - */ - private static Predicate distinctByKey( Function keyExtractor ) { - Set seen = ConcurrentHashMap.newKeySet(); - return t -> { - Object key = keyExtractor.apply( t ); - return key != null && seen.add( key ); - }; + @NonNull + @Override + public Stream getActions( @Nullable String actionNamespace ) { + return memoryService.getActions( actionNamespace ); } - /** - * Determines is a given action object reference is of a plugin action. - * - * @param actionReference The action object reference to check. - * @return {@code true} if the action reference is a plugin action; {@code false} otherwise. - */ - protected boolean isPluginActionObjectReference( - @NonNull IPentahoObjectReference actionReference ) { - return actionReference.getAttributes().containsKey( Const.PUBLISHER_PLUGIN_ID_ATTRIBUTE ); + @NonNull + @Override + public Optional getAction( @NonNull String actionName ) { + return memoryService.getAction( actionName ); } - /** - * Determines if a given action object reference is of a plugin action with the specified plugin ID. - * - * @param actionReference The action object reference to check. - * @param pluginId The ID of the plugin to check against. - * @return {@code true} if the action reference is a plugin action for the specified plugin ID; {@code false} - * otherwise. - */ - protected boolean isPluginActionObjectReference( - @NonNull IPentahoObjectReference actionReference, - @NonNull String pluginId ) { - return pluginId.equals( actionReference.getAttributes().get( Const.PUBLISHER_PLUGIN_ID_ATTRIBUTE ) ); + @NonNull + @Override + public Stream getSystemActions() { + return memoryService.getSystemActions(); } - /** - * Determines if a given action object reference is of a system action. - *

- * A system action is one that is not associated with any plugin, meaning it does not have the - * {@link Const#PUBLISHER_PLUGIN_ID_ATTRIBUTE} attribute set. - * - * @param actionReference The action object reference to check. - * @return {@code true} if the action reference is a system action; {@code false} otherwise. - */ - protected boolean isSystemActionObjectReference( - @NonNull IPentahoObjectReference actionReference ) { - return !isPluginActionObjectReference( actionReference ); + @NonNull + @Override + public Stream getPluginActions() { + return memoryService.getPluginActions(); } - // endregion @NonNull @Override - public Stream getActions() { - return getActionObjectReferences() - .map( IPentahoObjectReference::getObject ); + public Stream getPluginActions( @NonNull String pluginId ) { + return memoryService.getPluginActions( pluginId ); } @NonNull @Override - public Stream getSystemActions() { - return getActionObjectReferences() - .filter( this::isSystemActionObjectReference ) - .map( IPentahoObjectReference::getObject ); + public Stream getSelfActions() { + return memoryService.getSelfActions(); } @NonNull @Override - public Stream getPluginActions() { - return getActionObjectReferences() - .filter( this::isPluginActionObjectReference ) - .map( IPentahoObjectReference::getObject ); + public Stream getResourceActions() { + return memoryService.getResourceActions(); } @NonNull @Override - public Stream getPluginActions( @NonNull String pluginId ) { - if ( StringUtils.isEmpty( pluginId ) ) { - throw new IllegalArgumentException( "Argument `pluginId` cannot be null or empty." ); - } - - return getActionObjectReferences() - .filter( actionReference -> isPluginActionObjectReference( actionReference, pluginId ) ) - .map( IPentahoObjectReference::getObject ); + public Stream getResourceActions( @NonNull String resourceType ) { + return memoryService.getResourceActions( resourceType ); } } diff --git a/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/AuthorizationService.java b/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/AuthorizationService.java index 9a1a50eb3c..b228f15558 100644 --- a/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/AuthorizationService.java +++ b/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/AuthorizationService.java @@ -110,7 +110,7 @@ public IAuthorizationDecision authorize( @NonNull IAuthorizationRequest request } @NonNull - protected IAuthorizationDecision authorizeTracked( @NonNull IAuthorizationRequest request ) + private IAuthorizationDecision authorizeTracked( @NonNull IAuthorizationRequest request ) throws AuthorizationRequestCycleException { if ( pendingRequests.contains( request ) ) { @@ -119,14 +119,14 @@ protected IAuthorizationDecision authorizeTracked( @NonNull IAuthorizationReques pendingRequests.push( request ); try { - return authorizeRootRule( request ); + return authorizeCore( request ); } finally { pendingRequests.pop(); } } @NonNull - private IAuthorizationDecision authorizeRootRule( @NonNull IAuthorizationRequest request ) { + protected IAuthorizationDecision authorizeCore( @NonNull IAuthorizationRequest request ) { return authorizeRule( request, getRootRule() ) .orElseGet( () -> getDefaultDecision( request ) ); } diff --git a/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/CachingAuthorizationService.java b/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/CachingAuthorizationService.java new file mode 100644 index 0000000000..22f11a556f --- /dev/null +++ b/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/CachingAuthorizationService.java @@ -0,0 +1,79 @@ +/*! ****************************************************************************** + * + * Pentaho + * + * Copyright (C) 2024 by Hitachi Vantara, LLC : http://www.pentaho.com + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file. + * + * Change Date: 2029-07-20 + ******************************************************************************/ + +package org.pentaho.platform.engine.security.authorization.core; + +import edu.umd.cs.findbugs.annotations.NonNull; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.pentaho.platform.api.engine.security.authorization.IAuthorizationActionService; +import org.pentaho.platform.api.engine.security.authorization.IAuthorizationOptions; +import org.pentaho.platform.api.engine.security.authorization.IAuthorizationRequest; +import org.pentaho.platform.api.engine.security.authorization.IAuthorizationRule; +import org.pentaho.platform.api.engine.security.authorization.caching.IAuthorizationDecisionCache; +import org.pentaho.platform.api.engine.security.authorization.decisions.IAuthorizationDecision; +import org.springframework.util.Assert; + +public class CachingAuthorizationService extends AuthorizationService { + private static final Log logger = LogFactory.getLog( CachingAuthorizationService.class ); + + @NonNull + private final IAuthorizationDecisionCache decisionCache; + + /** + * Constructs an instance of the authorization service with a given root rule. + * + * @param actionService The service providing access to authorization actions. + * @param rootRule The root authorization rule. + */ + public CachingAuthorizationService( @NonNull IAuthorizationActionService actionService, + @NonNull IAuthorizationRule rootRule, + @NonNull IAuthorizationDecisionCache decisionCache ) { + super( actionService, rootRule ); + + Assert.notNull( decisionCache, "Argument 'decisionCache' is required" ); + + this.decisionCache = decisionCache; + } + + private class CachingAuthorizationContext extends AuthorizationContext { + public CachingAuthorizationContext( @NonNull IAuthorizationOptions options ) { + super( options ); + } + + @NonNull + @Override + protected IAuthorizationDecision authorizeCore( @NonNull IAuthorizationRequest request ) { + return decisionCache.get( request, getOptions(), + key -> super.authorizeCore( request ) ); + } + } + + @NonNull + @Override + public IAuthorizationDecision authorize( @NonNull IAuthorizationRequest request, + @NonNull IAuthorizationOptions options ) { + IAuthorizationDecision decision = super.authorize( request, options ); + + if ( logger.isDebugEnabled() ) { + logger.debug( decisionCache ); + } + + return decision; + } + + @NonNull + @Override + protected AuthorizationContext createContext( @NonNull IAuthorizationOptions options ) { + return new CachingAuthorizationContext( options ); + } +} diff --git a/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/caching/MemoryAuthorizationDecisionCache.java b/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/caching/MemoryAuthorizationDecisionCache.java new file mode 100644 index 0000000000..992b263623 --- /dev/null +++ b/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/caching/MemoryAuthorizationDecisionCache.java @@ -0,0 +1,161 @@ +/*! ****************************************************************************** + * + * Pentaho + * + * Copyright (C) 2024 by Hitachi Vantara, LLC : http://www.pentaho.com + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file. + * + * Change Date: 2029-07-20 + ******************************************************************************/ + +package org.pentaho.platform.engine.security.authorization.core.caching; + +import com.google.common.base.Objects; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.util.concurrent.UncheckedExecutionException; +import edu.umd.cs.findbugs.annotations.NonNull; +import org.pentaho.platform.api.engine.security.authorization.IAuthorizationOptions; +import org.pentaho.platform.api.engine.security.authorization.IAuthorizationRequest; +import org.pentaho.platform.api.engine.security.authorization.caching.IAuthorizationDecisionCache; +import org.pentaho.platform.api.engine.security.authorization.caching.IAuthorizationDecisionCacheKey; +import org.pentaho.platform.api.engine.security.authorization.decisions.IAuthorizationDecision; + +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Predicate; + +public class MemoryAuthorizationDecisionCache implements IAuthorizationDecisionCache { + @NonNull + private final Cache cache; + + public MemoryAuthorizationDecisionCache( + long expireAfterWrite, + long maximumSize, + boolean recordStats ) { + + final var cacheBuilder = CacheBuilder.newBuilder() + .expireAfterWrite( expireAfterWrite, TimeUnit.SECONDS ) + .maximumSize( maximumSize ); + + if ( recordStats ) { + cacheBuilder.recordStats(); + } + + cache = cacheBuilder.build(); + } + + @NonNull + protected IAuthorizationDecisionCacheKey createKey( @NonNull IAuthorizationRequest request, + @NonNull IAuthorizationOptions options ) { + return new AuthorizationDecisionCacheKey( request, options ); + } + + @NonNull + @Override + public Optional get( @NonNull IAuthorizationRequest request, + @NonNull IAuthorizationOptions options ) { + return Optional.ofNullable( cache.getIfPresent( createKey( request, options ) ) ); + } + + @NonNull + @Override + public IAuthorizationDecision get( @NonNull IAuthorizationRequest request, + @NonNull IAuthorizationOptions options, + @NonNull + Function loader ) { + try { + final var key = createKey( request, options ); + return cache.get( key, () -> loader.apply( key ) ); + } catch ( ExecutionException e ) { + throw new UncheckedExecutionException( e ); + } + } + + @Override + public void put( @NonNull IAuthorizationRequest request, + @NonNull IAuthorizationOptions options, + @NonNull IAuthorizationDecision decision ) { + cache.put( createKey( request, options ), decision ); + } + + @Override + public void invalidate( @NonNull IAuthorizationRequest request, @NonNull IAuthorizationOptions options ) { + cache.invalidate( createKey( request, options ) ); + } + + @Override + public void invalidateAll( @NonNull Predicate predicate ) { + + var invalidateRequests = cache + .asMap() + .keySet() + .stream() + .filter( predicate ) + .toList(); + + cache.invalidateAll( invalidateRequests ); + } + + @Override + public void invalidateAll() { + cache.invalidateAll(); + } + + @Override + public String toString() { + return String.format( "MemoryAuthorizationDecisionCache[stats=%s]", cache.stats() ); + } + + private static class AuthorizationDecisionCacheKey implements IAuthorizationDecisionCacheKey { + @NonNull + private final IAuthorizationRequest request; + @NonNull + private final IAuthorizationOptions options; + + public AuthorizationDecisionCacheKey( @NonNull IAuthorizationRequest request, + @NonNull IAuthorizationOptions options ) { + this.request = request; + this.options = options; + } + + @NonNull + @Override + public IAuthorizationRequest getRequest() { + return request; + } + + @NonNull + @Override + public IAuthorizationOptions getOptions() { + return options; + } + + @Override + public boolean equals( Object o ) { + if ( this == o ) { + return true; + } + + if ( !( o instanceof IAuthorizationDecisionCacheKey that ) ) { + return false; + } + + return request.equals( that.getRequest() ) && options.equals( that.getOptions() ); + } + + @Override + public int hashCode() { + return Objects.hashCode( request, options ); + } + + @Override + public String toString() { + return String.format( "AuthorizationDecisionCacheKey[request=%s, options=%s]", request, options ); + } + } +} diff --git a/core/src/test/java/org/pentaho/platform/engine/security/authorization/PentahoSystemAuthorizationActionServiceTest.java b/core/src/test/java/org/pentaho/platform/engine/security/authorization/PentahoSystemAuthorizationActionServiceTest.java index 49cb33dcd3..12ab10187f 100644 --- a/core/src/test/java/org/pentaho/platform/engine/security/authorization/PentahoSystemAuthorizationActionServiceTest.java +++ b/core/src/test/java/org/pentaho/platform/engine/security/authorization/PentahoSystemAuthorizationActionServiceTest.java @@ -17,6 +17,8 @@ import org.junit.Test; import org.pentaho.platform.api.engine.IAuthorizationAction; import org.pentaho.platform.api.engine.IPentahoObjectReference; +import org.pentaho.platform.api.engine.IPluginManager; +import org.pentaho.platform.api.engine.IPluginManagerListener; import org.pentaho.platform.engine.core.system.objfac.spring.Const; import java.util.ArrayList; @@ -27,13 +29,19 @@ import java.util.stream.Stream; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.pentaho.platform.engine.security.authorization.core.AuthorizationTestHelpers.createTestAction; public class PentahoSystemAuthorizationActionServiceTest { private PentahoSystemAuthorizationActionService service; + private IPluginManager pluginManager; @NonNull private IPentahoObjectReference createActionObjectReference( @@ -56,34 +64,24 @@ private IPentahoObjectReference createActionObjectReferenc @Before public void setUp() { - service = new PentahoSystemAuthorizationActionService( () -> createSampleActionsStream() ); + pluginManager = mock( IPluginManager.class ); + + service = new PentahoSystemAuthorizationActionService( + pluginManager, + this::createSampleActionsStream ); } @NonNull private Stream> createSampleActionsStream() { - var systemAction1 = mock( IAuthorizationAction.class ); - when( systemAction1.getName() ).thenReturn( "systemAction1" ); - - var systemAction2 = mock( IAuthorizationAction.class ); - when( systemAction2.getName() ).thenReturn( "systemAction2" ); - - var systemAction2Dup = mock( IAuthorizationAction.class ); - when( systemAction2Dup.getName() ).thenReturn( "systemAction2" ); - - // An action with a null name. Should be excluded from the results. - var systemAction3 = mock( IAuthorizationAction.class ); - - var pluginAction1 = mock( IAuthorizationAction.class ); - when( pluginAction1.getName() ).thenReturn( "pluginAction1" ); - - var pluginAction2 = mock( IAuthorizationAction.class ); - when( pluginAction2.getName() ).thenReturn( "pluginAction2" ); - - var pluginAction2Dup = mock( IAuthorizationAction.class ); - when( pluginAction2Dup.getName() ).thenReturn( "pluginAction2" ); - - var pluginAction3 = mock( IAuthorizationAction.class ); - when( pluginAction3.getName() ).thenReturn( "pluginAction3" ); + var systemAction1 = createTestAction( "systemAction1" ); + var systemAction2 = createTestAction( "systemAction2" ); + var systemAction2Dup = createTestAction( "systemAction2" ); + // An action with an empty name. Should be excluded from the results. + var systemAction3 = createTestAction( "" ); + var pluginAction1 = createTestAction( "pluginAction1" ); + var pluginAction2 = createTestAction( "pluginAction2" ); + var pluginAction2Dup = createTestAction( "pluginAction2" ); + var pluginAction3 = createTestAction( "pluginAction3" ); Map plugin1Attrs = new HashMap<>(); plugin1Attrs.put( Const.PUBLISHER_PLUGIN_ID_ATTRIBUTE, "plugin1" ); @@ -165,4 +163,41 @@ public void testGetPluginActionsWithNullPluginIdThrows() { public void testGetPluginActionsWithEmptyPluginIdThrows() { service.getPluginActions( "" ); } + + @Test + public void testPluginManagerRefreshesActionsOnPluginManagerReload() { + // Capture the plugin manager listener registered in the service constructor. + var listenerAtomicReference = new java.util.concurrent.atomic.AtomicReference(); + doAnswer( invocation -> { + listenerAtomicReference.set( invocation.getArgument( 0 ) ); + return null; + } ).when( pluginManager ).addPluginManagerListener( any() ); + + // Create a new service, which should register a listener with the mock plugin manager. + service = new PentahoSystemAuthorizationActionService( + pluginManager, + this::createSampleActionsStream ); + + // Verify that a listener was registered. + IPluginManagerListener listener = listenerAtomicReference.get(); + assertNotNull( listener ); + + // Capture actions and their names before listener is called + List actionsBefore = service.getActions().toList(); + List namesBefore = actionsBefore.stream().map( IAuthorizationAction::getName ).toList(); + + listener.onReload(); + + // Capture actions and their names after listener is called + List actionsAfter = service.getActions().toList(); + List namesAfter = actionsAfter.stream().map( IAuthorizationAction::getName ).toList(); + + // Assert that the names are the same + assertEquals( namesBefore, namesAfter ); + + // Assert that the instances are different for each name (by index) + for ( int i = 0; i < actionsBefore.size(); i++ ) { + assertNotSame( actionsBefore.get( i ), actionsAfter.get( i ) ); + } + } } diff --git a/core/src/test/java/org/pentaho/platform/engine/security/authorization/core/AuthorizationTestHelpers.java b/core/src/test/java/org/pentaho/platform/engine/security/authorization/core/AuthorizationTestHelpers.java index ded34a14fc..e10761fb17 100644 --- a/core/src/test/java/org/pentaho/platform/engine/security/authorization/core/AuthorizationTestHelpers.java +++ b/core/src/test/java/org/pentaho/platform/engine/security/authorization/core/AuthorizationTestHelpers.java @@ -74,8 +74,9 @@ public TestAuthorizationAction( @NonNull String actionName ) { this.actionName = actionName; } + @NonNull @Override - public @NonNull String getName() { + public String getName() { return actionName; } @@ -118,7 +119,7 @@ public static IAuthorizationAction createTestAction( @NonNull String actionName * Creates a basic authorization resource action with the specified name and resource type. * Includes implementation of equals and hashCode to ensure proper comparison in tests. * - * @param actionName The name of the action. + * @param actionName The name of the action. * @param resourceType The resource type this action applies to. * @return An IAuthorizationAction with the specified name and resource type. */ @@ -129,6 +130,7 @@ public static IAuthorizationAction createResourceAction( @NonNull String actionN // endregion // region Rule Mock Creation Helpers + /** * Creates a basic mock authorization rule with getRequestType() configured. * @@ -143,12 +145,13 @@ public static IAuthorizationRule createMockRule() { * Creates a basic mock authorization rule with getRequestType() configured for a specific request type. * * @param requestType The type of request this rule handles. - * @param The type of authorization request this rule handles. + * @param The type of authorization request this rule handles. * @return A mock IAuthorizationRule with getRequestType() returning the specified request type. */ @SuppressWarnings( "unchecked" ) @NonNull - public static IAuthorizationRule createMockRule( @NonNull Class requestType ) { + public static IAuthorizationRule createMockRule( + @NonNull Class requestType ) { IAuthorizationRule rule = mock( IAuthorizationRule.class ); when( rule.getRequestType() ).thenReturn( requestType ); return rule; @@ -156,6 +159,7 @@ public static IAuthorizationRule createMock // endregion // region Composite Decision Assertion Helpers + /** * Asserts that the given composite decision contains exactly the expected decisions, in the same order and with the * same references. @@ -196,6 +200,7 @@ public static void assertCompositeDecisionContainsExactly( @NonNull IAuthorizati // endregion // region Decision Mock Creation Helpers + /** * Creates a mock IAuthorizationDecision for the given request and granted status. *