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 7346a966f9c..c8c7b90a840 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/IAuthorizationOptions.java b/api/src/main/java/org/pentaho/platform/api/engine/security/authorization/IAuthorizationOptions.java index 11e41857c42..50d8e289920 100644 --- a/api/src/main/java/org/pentaho/platform/api/engine/security/authorization/IAuthorizationOptions.java +++ b/api/src/main/java/org/pentaho/platform/api/engine/security/authorization/IAuthorizationOptions.java @@ -13,6 +13,7 @@ package org.pentaho.platform.api.engine.security.authorization; import edu.umd.cs.findbugs.annotations.NonNull; +import org.pentaho.platform.api.engine.security.authorization.impl.DefaultAuthorizationOptions; /** * The {@code IAuthorizationOptions} interface encapsulates options that control the authorization evaluation process. @@ -34,39 +35,7 @@ public interface IAuthorizationOptions { * @return An instance of {@link IAuthorizationOptions}. */ static IAuthorizationOptions getDefault() { - return new IAuthorizationOptions() { - @NonNull - @Override - public AuthorizationDecisionReportingMode getDecisionReportingMode() { - return AuthorizationDecisionReportingMode.SETTLED; - } - - @Override - public boolean equals( Object obj ) { - if ( this == obj ) { - return true; - } - - if ( !( obj instanceof IAuthorizationOptions ) ) { - return false; - } - - IAuthorizationOptions other = (IAuthorizationOptions) obj; - return getDecisionReportingMode() == other.getDecisionReportingMode(); - } - - @Override - public int hashCode() { - return getDecisionReportingMode().hashCode(); - } - - @Override - public String toString() { - return String.format( - "IAuthorizationOptions{decisionReportingMode=%s}", - getDecisionReportingMode() ); - } - }; + return DefaultAuthorizationOptions.INSTANCE; } /** 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 00000000000..4e0941b3971 --- /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 00000000000..36cb8979ab5 --- /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/api/src/main/java/org/pentaho/platform/api/engine/security/authorization/impl/DefaultAuthorizationOptions.java b/api/src/main/java/org/pentaho/platform/api/engine/security/authorization/impl/DefaultAuthorizationOptions.java new file mode 100644 index 00000000000..556899a15e8 --- /dev/null +++ b/api/src/main/java/org/pentaho/platform/api/engine/security/authorization/impl/DefaultAuthorizationOptions.java @@ -0,0 +1,64 @@ +/*! ****************************************************************************** + * + * 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.impl; + +import edu.umd.cs.findbugs.annotations.NonNull; +import org.pentaho.platform.api.engine.security.authorization.AuthorizationDecisionReportingMode; +import org.pentaho.platform.api.engine.security.authorization.IAuthorizationOptions; + +/** + * The {@code DefaultAuthorizationOptions} class provides an implementation of {@link IAuthorizationOptions} with the + * default option values. + *

+ * This class is intended for internal use. + * It supports the implementation of the {@link IAuthorizationOptions#getDefault()} method. + */ +public class DefaultAuthorizationOptions implements IAuthorizationOptions { + + public static final DefaultAuthorizationOptions INSTANCE = new DefaultAuthorizationOptions(); + + private DefaultAuthorizationOptions() { + // Prevent external instantiation. + } + + @NonNull + @Override + public AuthorizationDecisionReportingMode getDecisionReportingMode() { + return AuthorizationDecisionReportingMode.SETTLED; + } + + @Override + public boolean equals( Object obj ) { + if ( this == obj ) { + return true; + } + + if ( !( obj instanceof IAuthorizationOptions other ) ) { + return false; + } + + return getDecisionReportingMode() == other.getDecisionReportingMode(); + } + + @Override + public int hashCode() { + return getDecisionReportingMode().hashCode(); + } + + @Override + public String toString() { + return String.format( + "IAuthorizationOptions{decisionReportingMode=%s}", + getDecisionReportingMode() ); + } +} diff --git a/assemblies/pentaho-solutions/src/main/resources/pentaho-solutions/system/applicationContext-spring-security-csrf.xml b/assemblies/pentaho-solutions/src/main/resources/pentaho-solutions/system/applicationContext-spring-security-csrf.xml index 1ac28296fa7..1a6e2e3eb1a 100644 --- a/assemblies/pentaho-solutions/src/main/resources/pentaho-solutions/system/applicationContext-spring-security-csrf.xml +++ b/assemblies/pentaho-solutions/src/main/resources/pentaho-solutions/system/applicationContext-spring-security-csrf.xml @@ -56,8 +56,9 @@ GET /api/system/refresh/reportingDataCache GET /api/system/refresh/mondrianSingleSchemaCache GET /api/system/refresh/mondrianSchemaCache + GET /api/system/refresh/authorizationDecisionCache --> - + + + + + + + + @@ -326,13 +334,45 @@ not directly from the PentahoObjectFactory. --> + + + + + + + + + + + + + + + + + + class="org.pentaho.platform.engine.security.authorization.core.CachingAuthorizationService"> - + + + diff --git a/assemblies/pentaho-solutions/src/main/resources/pentaho-solutions/system/repository.spring.xml b/assemblies/pentaho-solutions/src/main/resources/pentaho-solutions/system/repository.spring.xml index 795085aec92..2ef06ec8e02 100644 --- a/assemblies/pentaho-solutions/src/main/resources/pentaho-solutions/system/repository.spring.xml +++ b/assemblies/pentaho-solutions/src/main/resources/pentaho-solutions/system/repository.spring.xml @@ -317,6 +317,7 @@ + - - - - - - @@ -769,6 +754,7 @@ + diff --git a/assemblies/pentaho-solutions/src/main/resources/pentaho-solutions/system/systemListeners.xml b/assemblies/pentaho-solutions/src/main/resources/pentaho-solutions/system/systemListeners.xml index de6b26101c7..3a772f8cd2b 100644 --- a/assemblies/pentaho-solutions/src/main/resources/pentaho-solutions/system/systemListeners.xml +++ b/assemblies/pentaho-solutions/src/main/resources/pentaho-solutions/system/systemListeners.xml @@ -34,6 +34,9 @@ + + + diff --git a/assemblies/pentaho-war/src/main/webapp/WEB-INF/classes/log4j2.xml b/assemblies/pentaho-war/src/main/webapp/WEB-INF/classes/log4j2.xml index 32f81a8538b..c2aea5405c1 100644 --- a/assemblies/pentaho-war/src/main/webapp/WEB-INF/classes/log4j2.xml +++ b/assemblies/pentaho-war/src/main/webapp/WEB-INF/classes/log4j2.xml @@ -126,6 +126,7 @@ + diff --git a/core/src/main/java/org/pentaho/platform/engine/security/SecurityHelper.java b/core/src/main/java/org/pentaho/platform/engine/security/SecurityHelper.java index 123bbc894c7..fbec040ff21 100644 --- a/core/src/main/java/org/pentaho/platform/engine/security/SecurityHelper.java +++ b/core/src/main/java/org/pentaho/platform/engine/security/SecurityHelper.java @@ -13,10 +13,6 @@ package org.pentaho.platform.engine.security; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.pentaho.platform.api.engine.IAclHolder; @@ -33,15 +29,18 @@ import org.pentaho.platform.engine.core.system.PentahoSystem; import org.pentaho.platform.engine.core.system.StandaloneSession; import org.pentaho.platform.engine.core.system.UserSession; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; import java.util.concurrent.Callable; /** @@ -161,7 +160,7 @@ public T runAsUser( final String principalName, final Callable callable ) @Override public T - runAsUser( final String principalName, final IParameterProvider paramProvider, final Callable callable ) + runAsUser( final String principalName, final IParameterProvider paramProvider, final Callable callable ) throws Exception { IPentahoSession origSession = PentahoSessionHolder.getSession(); Authentication origAuth = SecurityContextHolder.getContext().getAuthentication(); @@ -198,8 +197,10 @@ public T runAsUser( final String principalName, final Callable callable ) public T runAsAnonymous( final Callable callable ) throws Exception { IPentahoSession origSession = PentahoSessionHolder.getSession(); Authentication origAuth = SecurityContextHolder.getContext().getAuthentication(); + IPentahoSession anonymousSession = null; try { - PentahoSessionHolder.setSession( new StandaloneSession() ); + anonymousSession = new StandaloneSession(); + PentahoSessionHolder.setSession( anonymousSession ); // get anonymous username/role defined in pentaho.xml String user = PentahoSystem @@ -220,6 +221,14 @@ public T runAsAnonymous( final Callable callable ) throws Exception { SecurityContextHolder.getContext().setAuthentication( auth ); return callable.call(); } finally { + if ( anonymousSession != null ) { + try { + anonymousSession.destroy(); + } catch ( Exception e ) { + e.printStackTrace(); + } + } + PentahoSessionHolder.setSession( origSession ); SecurityContextHolder.getContext().setAuthentication( origAuth ); } 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 00000000000..16a3faa161a --- /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 203b7819179..613c66b38b8 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/AbstractAuthorizationUser.java b/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/AbstractAuthorizationUser.java index 423cf6758b6..c93e1a604e8 100644 --- a/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/AbstractAuthorizationUser.java +++ b/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/AbstractAuthorizationUser.java @@ -33,6 +33,8 @@ public boolean equals( Object o ) { // possible to put principals of various types into a map. @Override public int hashCode() { - return Objects.hash( IAuthorizationUser.class, getName() ); + int result = IAuthorizationUser.class.hashCode(); + result = 31 * result + Objects.hashCode( getName() ); + return result; } } diff --git a/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/AuthorizationRequest.java b/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/AuthorizationRequest.java index 1ba1f78be89..81e5be91845 100644 --- a/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/AuthorizationRequest.java +++ b/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/AuthorizationRequest.java @@ -77,7 +77,9 @@ public boolean equals( Object o ) { @Override public int hashCode() { - return Objects.hash( principal, action ); + int result = principal.hashCode(); + result = 31 * result + action.hashCode(); + return result; } @Override diff --git a/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/AuthorizationRole.java b/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/AuthorizationRole.java index 169638c3e7e..70c526effd4 100644 --- a/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/AuthorizationRole.java +++ b/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/AuthorizationRole.java @@ -49,6 +49,8 @@ public boolean equals( Object o ) { // possible to put principals of various types into a map. @Override public int hashCode() { - return Objects.hash( IAuthorizationRole.class, getName() ); + int result = IAuthorizationRole.class.hashCode(); + result = 31 * result + name.hashCode(); + return result; } } 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 9a1a50eb3ca..2c07a62c24b 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 @@ -26,6 +26,7 @@ import org.pentaho.platform.engine.security.authorization.core.decisions.DefaultAuthorizationDecision; import org.pentaho.platform.engine.security.authorization.core.exceptions.AuthorizationRequestCycleException; import org.pentaho.platform.engine.security.authorization.core.exceptions.AuthorizationRequestUndefinedActionException; +import org.pentaho.platform.engine.security.authorization.core.rules.AbstractAuthorizationRule; import org.springframework.util.Assert; import java.util.ArrayDeque; @@ -33,6 +34,21 @@ import java.util.Objects; import java.util.Optional; +/** + * The {@code AuthorizationService} class is the default implementation of the {@link IAuthorizationService} interface. + *

+ * It performs authorization evaluations based on a root authorization rule, which may delegate to other rules as + * needed. + *

+ * The implementation is thread-safe, as each authorization evaluation is performed within its own + * {@link AuthorizationContext}, which tracks the evaluation state for that specific request. + * Of course, the thread-safety of the overall service also depends on the thread-safety of the provided authorization + * rules. + *

+ * One exception is setting the root rule, using {@link #setRootRule(IAuthorizationRule)} which is not thread-safe. Care + * must be taken to avoid inconsistent decisions in concurrent evaluations. This is intended to be used during + * application initialization. + */ public class AuthorizationService implements IAuthorizationService { private static final Log logger = LogFactory.getLog( AuthorizationService.class ); @@ -110,7 +126,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 +135,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 ) ); } @@ -207,17 +223,45 @@ protected IAuthorizationDecision getDefaultDecision( @NonNull IAuthorizationRequ } } + /** + * An authorization rule that always abstains. Used as a default root rule if none is provided. + */ + private static class AbstainAuthorizationRule extends AbstractAuthorizationRule { + + @NonNull + @Override + public Class getRequestType() { + return IAuthorizationRequest.class; + } + + @NonNull + @Override + public Optional authorize( @NonNull IAuthorizationRequest request, + @NonNull IAuthorizationContext context ) { + return abstain(); + } + } + @NonNull private final IAuthorizationActionService actionService; @NonNull - private final IAuthorizationRule rootRule; + private IAuthorizationRule rootRule; + + /** + * Constructs an instance of the authorization service with a default root rule that always abstains. + * + * @param actionService The service providing access to authorization actions. + */ + public AuthorizationService( @NonNull IAuthorizationActionService actionService ) { + this( actionService, new AbstainAuthorizationRule() ); + } /** * 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. + * @param rootRule The root authorization rule. */ public AuthorizationService( @NonNull IAuthorizationActionService actionService, @NonNull IAuthorizationRule rootRule ) { @@ -263,17 +307,30 @@ protected AuthorizationContext createContext( @NonNull IAuthorizationOptions opt * @return The root rule. */ @NonNull - protected final IAuthorizationRule getRootRule() { + public final IAuthorizationRule getRootRule() { return rootRule; } + /** + * Sets the root authorization rule. + *

+ * Warning: changing the root rule at runtime may lead to inconsistent authorization decisions if there are + * concurrent authorization evaluations. This method should only be used during application initialization. + * + * @param rootRule The root rule. + */ + public final void setRootRule( @NonNull IAuthorizationRule rootRule ) { + Assert.notNull( rootRule, "Argument 'rootRule' is required" ); + this.rootRule = rootRule; + } + /** * Gets the authorization action service. * * @return The action service. */ @NonNull - protected IAuthorizationActionService getActionService() { + protected final IAuthorizationActionService getActionService() { return actionService; } } 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 00000000000..5eca5dede9c --- /dev/null +++ b/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/CachingAuthorizationService.java @@ -0,0 +1,98 @@ +/*! ****************************************************************************** + * + * 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 default root rule that always abstains. + * + * @param actionService The service providing access to authorization actions. + * @param decisionCache The cache for authorization decisions. + */ + public CachingAuthorizationService( @NonNull IAuthorizationActionService actionService, + @NonNull IAuthorizationDecisionCache decisionCache ) { + super( actionService ); + + Assert.notNull( decisionCache, "Argument 'decisionCache' is required" ); + + this.decisionCache = 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. + * @param decisionCache The cache for authorization decisions. + */ + 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 ); + + // Don't really know at this point if recordStats is on. Logging only for trace level makes + // it less bad if disabled. When disabled, will just log 0s as stats. + // Also, this method is just one of the top-level authorization methods (authorizeRule is not logging stats). + if ( logger.isTraceEnabled() ) { + logger.trace( 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 00000000000..da4ccfdbf78 --- /dev/null +++ b/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/caching/MemoryAuthorizationDecisionCache.java @@ -0,0 +1,785 @@ +/*! ****************************************************************************** + * + * 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.annotations.VisibleForTesting; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheStats; +import com.google.common.util.concurrent.UncheckedExecutionException; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.pentaho.platform.api.engine.ILogoutListener; +import org.pentaho.platform.api.engine.IPentahoSession; +import org.pentaho.platform.api.engine.ISessionContainer; +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 org.pentaho.platform.engine.core.system.PentahoSessionHolder; +import org.pentaho.platform.engine.core.system.PentahoSystem; +import org.pentaho.platform.engine.core.system.StandaloneSession; +import org.springframework.util.Assert; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * An in-memory implementation of {@link IAuthorizationDecisionCache}, that associates cache entries to the current + * Pentaho session. + */ +public class MemoryAuthorizationDecisionCache implements + IAuthorizationDecisionCache, + ILogoutListener, + AutoCloseable { + + private static final Log logger = LogFactory.getLog( MemoryAuthorizationDecisionCache.class ); + + // region Helper classes + + /** + * Holds a session cache and manages the set of Pentaho sessions associated with it. + *

+ * The sessions associated with this session cache data are expected to share the same session key, as defined by + * {@link MemoryAuthorizationDecisionCache#getSessionKey(IPentahoSession)}. + */ + private class SessionCacheData { + + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + + @NonNull + private final String sessionKey; + + /** + * Stores the set of sessions associated with this session cache data. + *

+ * A weak set is used, so that sessions can be garbage collected if there are no other strong references to them. + * This is only a fallback mechanism, for badly behaved used of sessions, such as direct uses of + * {@link org.pentaho.platform.api.engine.ISecurityHelper#becomeUser(String)} without a corresponding destruction of + * the created session. Sessions are expected to be explicitly disassociated from this session cache data, either + * via a logout listener (for {@code PentahoHttpSession} sessions), or via explicit destruction + * (for {@link StandaloneSession} sessions). + *

+ * Hiding and cleaning up stale (GC'd) sessions is handled by the {@link WeakHashMap} implementation. + */ + private final Set sessions = Collections.newSetFromMap( new WeakHashMap<>() ); + + @NonNull + private final Cache cache; + + public SessionCacheData( @NonNull String sessionKey, + @NonNull Cache cache, + @NonNull IPentahoSession session ) { + this.sessionKey = sessionKey; + this.cache = cache; + + addSessionCore( session ); + } + + @NonNull + public Cache getCache( + @NonNull IPentahoSession forSession ) { + lock.readLock().lock(); + try { + if ( sessions.contains( forSession ) ) { + return cache; + } + } finally { + lock.readLock().unlock(); + } + + lock.writeLock().lock(); + try { + addSessionCore( forSession ); + } finally { + lock.writeLock().unlock(); + } + + return cache; + } + + /** + * Associates a session with this session cache data. + *

+ * If the session is already associated, does nothing. + *

+ * For {@link StandaloneSession} sessions, adds a {@link ISessionContainer session container} to the session, so + * that, when the session is destroyed, the session is disassociated from this session cache data. + *

+ * Standalone sessions must be explicitly destroyed, by, calling its destroy() method. + * To listen to a session's destroy "event", an {@link ISessionContainer} valued attribute is set on the session. + * Upon destroy(), any associated container's {@link ISessionContainer#setSession(IPentahoSession)} method is called + * with a null value, to disassociate the session from the container. + *

+ * This contrasts with {@code PentahoHttpSession} sessions, which are "destroyed" via a {@link ILogoutListener + * logout listener}. + * + * @param session The session to associate. + */ + private void addSessionCore( @NonNull IPentahoSession session ) { + if ( sessions.add( session ) + && ( session instanceof StandaloneSession standaloneSession ) ) { + + session.setAttribute( + StandaloneSessionContainer.class.getName(), + new StandaloneSessionContainer( standaloneSession ) ); + } + } + + /** + * Disassociates a session from this session cache data. + * + * @param session The session to disassociate. + * @return {@code true} if the session was disassociated and there are no more associated sessions; + * {@code false} otherwise. + */ + public boolean removeSession( @NonNull IPentahoSession session ) { + lock.writeLock().lock(); + try { + return sessions.remove( session ) && isStale(); + } finally { + lock.writeLock().unlock(); + } + } + + /** + * Indicates whether this session cache data is stale does not contain any associated sessions. + * + * @return {@code true} if there are no associated sessions; {@code false} otherwise. + */ + public boolean isStale() { + return sessions.isEmpty(); + } + + public void invalidate( @NonNull IAuthorizationDecisionCacheKey key ) { + if ( logger.isTraceEnabled() ) { + logger.trace( + String.format( + "Invalidating cache entry for key '%s' in session cache for '%s'", + key, sessionKey ) ); + } + + cache.invalidate( key ); + } + + public void invalidateAll( @NonNull Predicate predicate ) { + // There doesn't seem to be a "safer" way to iterate the cache keys using the Guava Cache API. + // The asMap() and its keySet() are both views, backed by the real objects, so they "suffer" from concurrent + // modification issues. The `asMap()` documentation says: + // > Iterators from the returned map are at least weakly consistent: they are safe for + // > concurrent use, but if the cache is modified (including by eviction) after the iterator is + // > created, it is undefined which of the changes (if any) will be reflected in that iterator. + // So, it seems that only _changes_ to the cache would possibly not be visible, not the original values at the + // time + // when the iterator was created. This is consistent with the general goals described for the main class's + // invalidate* methods. + var invalidateRequests = cache + .asMap() + .keySet() + .stream() + .filter( predicate ) + .toList(); + + if ( logger.isTraceEnabled() ) { + for ( var key : invalidateRequests ) { + logger.trace( + String.format( + "Invalidating cache entry for key '%s' in session cache for '%s'", + key, sessionKey ) ); + } + + logger.trace( String.format( "Session cache disposed for '%s'", sessionKey ) ); + } + + cache.invalidateAll( invalidateRequests ); + } + + /** + * Disposes this session cache data, by clearing all associated sessions and disposing the shared cache. + */ + public void dispose() { + lock.writeLock().lock(); + try { + sessions.clear(); + cache.invalidateAll(); + cache.cleanUp(); + } finally { + lock.writeLock().unlock(); + } + + if ( logger.isTraceEnabled() ) { + logger.trace( String.format( "Session cache disposed for '%s'", sessionKey ) ); + } + } + } + + private class StandaloneSessionContainer implements ISessionContainer { + @NonNull + private final StandaloneSession session; + + public StandaloneSessionContainer( @NonNull StandaloneSession session ) { + this.session = session; + } + + @Override + public void setSession( IPentahoSession cleanupSession ) { + invalidateSession( session ); + } + } + + 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() { + int result = request.hashCode(); + result = 31 * result + options.hashCode(); + return result; + } + + @Override + public String toString() { + return String.format( "AuthorizationDecisionCacheKey[request=%s, options=%s]", request, options ); + } + } + + /** + * Helper class of {@link MemoryAuthorizationDecisionCache} that aggregates code that manages the periodic sweeping of + * stale session caches. A stale session cache is one that has no associated sessions, presumably because they were + * garbage collected, without being properly destroyed. This can happen in case of misbehaved code that creates + * {@link StandaloneSession}s without destroying them, by calling {@link StandaloneSession#destroy()}. + *

+ * This feature is provided as a fallback / memory-leak protection mechanism, J.I.C. + */ + private class SessionCacheSweeper implements AutoCloseable { + private final ScheduledExecutorService sessionCacheSweeperExecutor; + private final ScheduledFuture sessionCacheSweeperHandle; + + public SessionCacheSweeper( long staleSessionsSweepInterval ) { + Assert.isTrue( + staleSessionsSweepInterval > 0, + "Argument 'staleSessionsSweepInterval' must be greater than zero." ); + + this.sessionCacheSweeperExecutor = Executors.newSingleThreadScheduledExecutor( runnable -> { + // Must be a daemon thread, to not block VM shutdown. + Thread t = new Thread( + runnable, + String.format( "%s-session-cleanup", MemoryAuthorizationDecisionCache.class.getSimpleName() ) ); + t.setDaemon( true ); + return t; + } ); + + this.sessionCacheSweeperHandle = sessionCacheSweeperExecutor.scheduleAtFixedRate( + this::doSweep, + staleSessionsSweepInterval, + staleSessionsSweepInterval, + TimeUnit.SECONDS ); + } + + protected void doSweep() { + if ( logger.isTraceEnabled() ) { + logger.trace( "Sweeping for stale session caches..." ); + } + + // Happy path: no dead session caches. + boolean hasStaleSessionCaches; + sessionsLock.readLock().lock(); + try { + hasStaleSessionCaches = cacheBySessionKey + .values() + .stream() + .anyMatch( SessionCacheData::isStale ); + } finally { + sessionsLock.readLock().unlock(); + } + + if ( hasStaleSessionCaches ) { + // Slow path: remove dead session caches. + List staleSessionKeys; + sessionsLock.writeLock().lock(); + try { + // Collect keys of stale sessions, to avoid copying the whole map to be able to remove during loop. + // Then invalidate those. + staleSessionKeys = cacheBySessionKey + .entrySet() + .stream() + .filter( entry -> entry.getValue().isStale() ) + .map( Map.Entry::getKey ) + .toList(); + + for ( String staleSessionKey : staleSessionKeys ) { + invalidateSessionCache( staleSessionKey, cacheBySessionKey.get( staleSessionKey ) ); + } + } finally { + sessionsLock.writeLock().unlock(); + } + + hasStaleSessionCaches = !staleSessionKeys.isEmpty(); + + if ( hasStaleSessionCaches && logger.isWarnEnabled() ) { + // Log, for monitoring purposes. + // This is expected to be a rare occurrence, so logging at warning level should be acceptable. + // If it happens often, it may indicate a problem in the application code, such as + // StandaloneSessions not being properly destroyed. + // Note that this log may be noisy in testing environments, where StandaloneSessions are more common. + // In production, PentahoHttpSessions are more common, which are properly handled via logout listeners. + logger.warn( + String.format( "Cleaned up %d stale session caches: %s", staleSessionKeys.size(), staleSessionKeys ) + ); + } + + } + + if ( logger.isTraceEnabled() ) { + if ( !hasStaleSessionCaches ) { + logger.trace( "No stale session caches found." ); + } + + // Print stats in the end in any case. + logger.trace( MemoryAuthorizationDecisionCache.this.toString() ); + } + } + + @Override + public void close() throws Exception { + sessionCacheSweeperHandle.cancel( true ); + sessionCacheSweeperExecutor.shutdown(); + } + } + // endregion Helper classes + + // Settings for each session's internal authorization cache, a Guava cache. + // Used by createSessionCache(). + private final long expireAfterWrite; + private final long maximumSize; + private final boolean recordStats; + + // Global lock for the sessions map, cacheBySessionKey. + // Also guards the pastCacheStats field. + @NonNull + private final ReentrantReadWriteLock sessionsLock; + + @NonNull + private final Map cacheBySessionKey; + + // Accumulates stats of removed session caches. + private CacheStats pastCacheStats = new CacheStats( 0, 0, 0, 0, 0, 0 ); + + @Nullable + private final AutoCloseable sessionCacheSweeper; + + /** + * Creates a new memory-based authorization decision cache. + * + * @param expireAfterWrite The number of seconds after which an authorization entry should be automatically + * removed from the cache. + * @param maximumSize The maximum number of entries that the cache may contain per-session. When the + * size is exceeded, the cache will evict entries that are less likely to be used + * again. + * @param recordStats Whether to record cache statistics, which may be retrieved informally via + * {@link #toString()}. + * @param staleSessionsSweepInterval The number of seconds between sweeps to remove stale session caches. + * A value of 0 or less disables this feature. + */ + public MemoryAuthorizationDecisionCache( + long expireAfterWrite, + long maximumSize, + boolean recordStats, + long staleSessionsSweepInterval ) { + + this.expireAfterWrite = expireAfterWrite; + this.maximumSize = maximumSize; + this.recordStats = recordStats; + + this.sessionsLock = new ReentrantReadWriteLock(); + this.cacheBySessionKey = new HashMap<>(); + + this.sessionCacheSweeper = createSessionCacheSweeper( staleSessionsSweepInterval ); + + registerLogoutListener(); + } + + @VisibleForTesting + @Nullable + protected AutoCloseable createSessionCacheSweeper( long staleSessionsSweepInterval ) { + return staleSessionsSweepInterval > 0 + ? new SessionCacheSweeper( staleSessionsSweepInterval ) + : null; + } + + // region Per-session cache management + @NonNull + protected Cache createSessionCache() { + final var cacheBuilder = CacheBuilder.newBuilder() + .expireAfterWrite( expireAfterWrite, TimeUnit.SECONDS ) + .maximumSize( maximumSize ); + + if ( recordStats ) { + cacheBuilder.recordStats(); + } + + return cacheBuilder.build(); + } + + @NonNull + protected String getSessionKey( @NonNull IPentahoSession session ) { + // Using session name so that different sessions of same user (e.g. one StandaloneSession and one + // PentahoHttpSession) use the same cache. It is common for the current PentahoHttpSession to create several + // StandaloneSessions, typically via runAsUser, or runAsSystem. Using session ID would associate different caches to + // the same user. + String sessionKey = session.getName(); + if ( sessionKey == null ) { + throw new IllegalStateException( "Pentaho session without name" ); + } + + return sessionKey; + } + + @NonNull + protected Optional> getSessionCacheOptional() { + var session = getSession(); + var sessionKey = getSessionKey( session ); + + sessionsLock.readLock().lock(); + try { + return Optional + .ofNullable( cacheBySessionKey.get( sessionKey ) ) + .map( cacheData -> cacheData.getCache( session ) ); + } finally { + sessionsLock.readLock().unlock(); + } + } + + @NonNull + protected Cache getSessionCache() { + var session = getSession(); + var sessionKey = getSessionKey( session ); + + SessionCacheData cacheData; + + // Happy path: a cache already exists for this session (key). + sessionsLock.readLock().lock(); + try { + cacheData = cacheBySessionKey.get( sessionKey ); + if ( cacheData != null ) { + return cacheData.getCache( session ); + } + } finally { + sessionsLock.readLock().unlock(); + } + + // Slow path: create a cache for this session (key), if it's not there yet. + sessionsLock.writeLock().lock(); + try { + // Recheck, after acquiring write lock. + cacheData = cacheBySessionKey.get( sessionKey ); + if ( cacheData != null ) { + return cacheData.getCache( session ); + } + + var cache = createSessionCache(); + cacheData = new SessionCacheData( sessionKey, cache, session ); + cacheBySessionKey.put( sessionKey, cacheData ); + return cache; + } finally { + sessionsLock.writeLock().unlock(); + } + } + + protected void invalidateSession( @NonNull IPentahoSession session ) { + var sessionKey = getSessionKey( session ); + + // Assume it likely has a corresponding cache, and enter write lock directly. + sessionsLock.writeLock().lock(); + try { + // Recheck, after acquiring write lock. + var cacheData = cacheBySessionKey.get( sessionKey ); + if ( cacheData != null && cacheData.removeSession( session ) ) { + // Last session, so dispose and remove session cache data from the map. + invalidateSessionCache( sessionKey, cacheData ); + } + } finally { + sessionsLock.writeLock().unlock(); + } + } + + private void invalidateSessionCache( @NonNull String sessionKey, @NonNull SessionCacheData cacheData ) { + sessionsLock.writeLock().lock(); + try { + // Store stats of removed cache. + if ( recordStats ) { + updatePastCacheStats( cacheData ); + } + + cacheData.dispose(); + cacheBySessionKey.remove( sessionKey ); + } finally { + sessionsLock.writeLock().unlock(); + } + } + + private void updatePastCacheStats( @NonNull SessionCacheData expiredCacheData ) { + // Must adjust the eviction count, given that all items in the expired cache can now be considered evicted. + + var expiredCacheStats = expiredCacheData.cache.stats(); + var additionalEvictionCount = expiredCacheStats.loadSuccessCount(); + + pastCacheStats = pastCacheStats + .plus( expiredCacheStats ) + .plus( new CacheStats( 0, 0, 0, 0, 0, additionalEvictionCount ) ); + } + // endregion Global and Per-session cache management + + // region Pentaho Integration + // Associate cache entries to the current session. + // Facilitates invalidation of all entries for a given session on logout, + // which is important for, for example, testing environments, to avoid cross test contamination. + @VisibleForTesting + protected void registerLogoutListener() { + PentahoSystem.addLogoutListener( this ); + } + + @VisibleForTesting + protected void unregisterLogoutListener() { + PentahoSystem.remove( this ); + } + + @VisibleForTesting + @NonNull + protected IPentahoSession getSession() { + IPentahoSession session = PentahoSessionHolder.getSession(); + Assert.notNull( session, "No current Pentaho session" ); + return session; + } + + // Called for PentahoHttpSession. Not for others: StandaloneSession. + @Override + public void onLogout( IPentahoSession session ) { + invalidateSession( Objects.requireNonNull( session ) ); + } + + @Override + public void close() throws Exception { + unregisterLogoutListener(); + invalidateAll(); + if ( sessionCacheSweeper != null ) { + sessionCacheSweeper.close(); + } + } + // endregion Pentaho Integration + + // region Main get, put methods + @NonNull + @Override + public Optional get( @NonNull IAuthorizationRequest request, + @NonNull IAuthorizationOptions options ) { + + var key = createAuthorizationKey( request, options ); + + return getSessionCacheOptional() + .map( cache -> cache.getIfPresent( key ) ); + } + + @NonNull + @Override + public IAuthorizationDecision get( + @NonNull IAuthorizationRequest request, + @NonNull IAuthorizationOptions options, + @NonNull Function loader ) { + + var key = createAuthorizationKey( request, options ); + try { + return Objects.requireNonNull( getSessionCache().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 ) { + var key = createAuthorizationKey( request, options ); + getSessionCache().put( key, decision ); + } + + @NonNull + protected IAuthorizationDecisionCacheKey createAuthorizationKey( @NonNull IAuthorizationRequest request, + @NonNull IAuthorizationOptions options ) { + return new AuthorizationDecisionCacheKey( request, options ); + } + // endregion Main get, put methods + + // region Authorization Request Invalidation + + // NOTE: Regarding all the invalidate* methods below: + // + // It is possible that, after the session map is copied and the end of the call, a new session cache is created and + // added to the original session map. + // This is acceptable, as the new cache will be empty, so there is nothing to invalidate. + // + // It is also possible that a session cache is removed from the original map, remaining present in the copied map. + // This is also acceptable, as the session cache will simply be invalidated again. + // + // Finally, it is also possible that authorization decision(s) are added to the captured session caches, during the + // per-session invalidation process. For example: + // 1. invalidate session A + // 2. new auth added to session C cache (not yet invalidated) + // 3. invalidate session B + // 4. invalidate session C + // 5. new auth added to session A cache (already invalidated) + // + // In case 2, above, the new auth will be invalidated in step 4, even though it was likely added to the cache after + // the invalidation process started. If a write lock were used, the entry would end up in the cache, which could be + // worse. + // In case 5, above, the new auth will not be invalidated, but this is acceptable, as the invalidation process + // started before the new auth was added to the cache. + // + // All this appears to be a good compromise between consistency, performance and implementation complexity, especially + // given the main use cases for these methods, of being able to invalidate authorization decisions when domain objects + // change, such as when a user's roles change, or a role's permissions change. + // One point in favor is that these changes are expected to be relatively infrequent, compared to authorization + // checks. + // + // If stronger consistency is required in the future, a more complex approach would be needed, such as read-locking + // the _entire_ cache (including contained per-session caches) when doing these operations. + + @Override + public void invalidate( @NonNull IAuthorizationRequest request, @NonNull IAuthorizationOptions options ) { + var key = createAuthorizationKey( request, options ); + copyCacheBySessionKey() + .forEach( ( sessionKey, cacheData ) -> cacheData.invalidate( key ) ); + } + + @Override + public void invalidateAll( @NonNull Predicate predicate ) { + copyCacheBySessionKey() + .forEach( ( sessionKey, cacheData ) -> cacheData.invalidateAll( predicate ) ); + } + + @Override + public void invalidateAll() { + sessionsLock.writeLock().lock(); + try { + cacheBySessionKey.forEach( ( sessionId, cacheData ) -> cacheData.dispose() ); + cacheBySessionKey.clear(); + } finally { + sessionsLock.writeLock().unlock(); + } + } + // endregion Authorization Request Invalidation + + /** + * Creates a copy of the sessions cache. + *

+ * The main use case is to avoid concurrent modification of the cache while iterating it. + * + * @return A copy of the sessions cache. + */ + @NonNull + private Map copyCacheBySessionKey() { + sessionsLock.readLock().lock(); + try { + return new HashMap<>( cacheBySessionKey ); + } finally { + sessionsLock.readLock().unlock(); + } + } + + /** + * Builds a consolidated {@link CacheStats} reflecting all session caches. + *

+ * This builds collects stats of each session in a way that is safe for concurrent access, however, the stats may be + * inconsistent with respect to each other, as they may be collected at different times without locking each session's + * internal workings. This appears to be a good compromise given the rough requirement of being able to log stats for + * monitoring and configuration fine-tuning purposes. + * + * @return The consolidated stats. + */ + protected CacheStats getStats() { + // Create safe copies of the session cache map and past session stats. + Map sessionCacheMapCopy; + CacheStats pastCacheStatsCopy; + + sessionsLock.readLock().lock(); + try { + sessionCacheMapCopy = new HashMap<>( cacheBySessionKey ); + pastCacheStatsCopy = pastCacheStats; + } finally { + sessionsLock.readLock().unlock(); + } + + return sessionCacheMapCopy + .values() + .stream() + .map( cacheData -> cacheData.cache.stats() ) + .reduce( pastCacheStatsCopy, CacheStats::plus ); + } + + @Override + public String toString() { + return String.format( "MemoryAuthorizationDecisionCache[stats=%s]", getStats() ); + } +} diff --git a/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/caching/MemoryAuthorizationDecisionCacheSystemListener.java b/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/caching/MemoryAuthorizationDecisionCacheSystemListener.java new file mode 100644 index 00000000000..304e0e7a287 --- /dev/null +++ b/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/caching/MemoryAuthorizationDecisionCacheSystemListener.java @@ -0,0 +1,61 @@ +/*! ****************************************************************************** + * + * 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 edu.umd.cs.findbugs.annotations.NonNull; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.pentaho.platform.api.engine.IPentahoSession; +import org.pentaho.platform.api.engine.IPentahoSystemListener; + +import java.util.Objects; + +/** + * System listener that closes a {@link MemoryAuthorizationDecisionCache} when the Pentaho system is shutting down. + */ +public class MemoryAuthorizationDecisionCacheSystemListener implements IPentahoSystemListener { + private static final Log logger = LogFactory.getLog( MemoryAuthorizationDecisionCacheSystemListener.class ); + + @NonNull + private final MemoryAuthorizationDecisionCache cache; + + public MemoryAuthorizationDecisionCacheSystemListener( @NonNull MemoryAuthorizationDecisionCache cache ) { + this.cache = Objects.requireNonNull( cache ); + } + + @Override + public boolean startup( IPentahoSession session ) { + if ( logger.isTraceEnabled() ) { + logger.trace( "Started authorization decision cache" ); + } + + return true; + } + + @Override + public void shutdown() { + if ( logger.isTraceEnabled() ) { + logger.trace( "Shutting down authorization decision cache..." ); + } + + try { + this.cache.close(); + } catch ( Exception e ) { + logger.error( "Error closing authorization decision cache", e ); + } + + if ( logger.isTraceEnabled() ) { + logger.trace( "Shut down authorization decision cache successfully" ); + } + } +} diff --git a/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/resources/GenericAuthorizationResource.java b/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/resources/GenericAuthorizationResource.java index dfa712b5b97..c82a22038fe 100644 --- a/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/resources/GenericAuthorizationResource.java +++ b/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/resources/GenericAuthorizationResource.java @@ -58,7 +58,9 @@ public boolean equals( Object o ) { @Override public int hashCode() { - return Objects.hash( typeId, id ); + int result = typeId.hashCode(); + result = 31 * result + id.hashCode(); + return result; } @Override diff --git a/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/resources/ResourceAuthorizationRequest.java b/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/resources/ResourceAuthorizationRequest.java index 788a26af707..cd0f22b36f2 100644 --- a/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/resources/ResourceAuthorizationRequest.java +++ b/core/src/main/java/org/pentaho/platform/engine/security/authorization/core/resources/ResourceAuthorizationRequest.java @@ -21,8 +21,6 @@ import org.pentaho.platform.engine.security.authorization.core.AuthorizationRequest; import org.springframework.util.Assert; -import java.util.Objects; - /** * The {@code ResourceAuthorizationRequest} class represents an authorization request for a specific resource. * It extends the basic authorization request to include resource-specific authorization context. @@ -89,12 +87,14 @@ public boolean equals( Object o ) { } IResourceAuthorizationRequest that = (IResourceAuthorizationRequest) o; - return Objects.equals( resource, that.getResource() ); + return resource.equals( that.getResource() ); } @Override public int hashCode() { - return Objects.hash( super.hashCode(), resource ); + int result = super.hashCode(); + result = 31 * result + resource.hashCode(); + return result; } @Override 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 49cb33dcd33..12ab10187fa 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 ded34a14fcb..e10761fb17e 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. * diff --git a/extensions/src/main/java/org/pentaho/platform/plugin/services/cache/CacheManager.java b/extensions/src/main/java/org/pentaho/platform/plugin/services/cache/CacheManager.java index 3c38b72d08a..a861e04323a 100644 --- a/extensions/src/main/java/org/pentaho/platform/plugin/services/cache/CacheManager.java +++ b/extensions/src/main/java/org/pentaho/platform/plugin/services/cache/CacheManager.java @@ -246,7 +246,7 @@ public boolean cacheEnabled( String region ) { } public void onLogout( final IPentahoSession session ) { - removeRegionCache( session.getName() ); + killSessionCache( session ); } public boolean addCacheRegion( String region, Properties cacheProperties ) { diff --git a/extensions/src/main/java/org/pentaho/platform/web/http/api/resources/SystemRefreshResource.java b/extensions/src/main/java/org/pentaho/platform/web/http/api/resources/SystemRefreshResource.java index 138a157b080..0b13ad18a14 100644 --- a/extensions/src/main/java/org/pentaho/platform/web/http/api/resources/SystemRefreshResource.java +++ b/extensions/src/main/java/org/pentaho/platform/web/http/api/resources/SystemRefreshResource.java @@ -16,6 +16,7 @@ import org.codehaus.enunciate.Facet; import org.pentaho.platform.api.engine.ICacheManager; import org.pentaho.platform.api.engine.IPentahoSession; +import org.pentaho.platform.api.engine.security.authorization.caching.IAuthorizationDecisionCache; import org.pentaho.platform.engine.core.system.PentahoSessionHolder; import org.pentaho.platform.engine.core.system.PentahoSystem; import org.pentaho.platform.plugin.action.mondrian.catalog.IMondrianCatalogService; @@ -155,6 +156,22 @@ public Response purgeReportingDataCache() { } } + @GET + @Path( "/authorizationDecisionCache" ) + @Produces( { MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON } ) + @Facet ( name = "Unsupported" ) + public Response flushAuthorizationDecisionCache() { + if ( canAdminister() ) { + IAuthorizationDecisionCache decisionCache = PentahoSystem.get( IAuthorizationDecisionCache.class ); + if ( decisionCache != null ) { + decisionCache.invalidateAll(); + } + return Response.ok().type( MediaType.TEXT_PLAIN ).build(); + } else { + return Response.status( UNAUTHORIZED ).build(); + } + } + private boolean canAdminister() { return SystemUtils.canAdminister(); } diff --git a/repository/src/main/java/org/pentaho/platform/security/policy/rolebased/JcrRoleAuthorizationPolicyRoleBindingDao.java b/repository/src/main/java/org/pentaho/platform/security/policy/rolebased/JcrRoleAuthorizationPolicyRoleBindingDao.java index 3349d419400..596a082e322 100644 --- a/repository/src/main/java/org/pentaho/platform/security/policy/rolebased/JcrRoleAuthorizationPolicyRoleBindingDao.java +++ b/repository/src/main/java/org/pentaho/platform/security/policy/rolebased/JcrRoleAuthorizationPolicyRoleBindingDao.java @@ -14,10 +14,12 @@ package org.pentaho.platform.security.policy.rolebased; import org.pentaho.platform.api.engine.IAuthorizationAction; +import org.pentaho.platform.api.engine.security.authorization.caching.IAuthorizationDecisionCache; import org.pentaho.platform.api.engine.security.userroledao.NotFoundException; import org.pentaho.platform.api.mt.ITenant; import org.pentaho.platform.api.mt.ITenantedPrincipleNameResolver; import org.pentaho.platform.engine.core.system.TenantUtils; +import org.pentaho.platform.engine.security.authorization.core.AuthorizationRole; import org.pentaho.platform.repository2.unified.jcr.JcrTenantUtils; import org.springframework.extensions.jcr.JcrCallback; import org.springframework.extensions.jcr.JcrTemplate; @@ -26,14 +28,18 @@ import javax.jcr.RepositoryException; import javax.jcr.Session; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; /** * An {@link IRoleAuthorizationPolicyRoleBindingDao} implementation that uses JCR. Storage is done using nodes and * properties, not XML. Storage looks like this: - * + * *

- * {@code 
+ * {@code
  * - acme
  *   - .authz
  *     - roleBased
@@ -44,17 +50,17 @@
  *           - logicalRole2 (multi-valued property)
  * }
  * 
- * + * *

* Note: All multi-valued properties are ordered. *

- * + * *

* Note: This code runs as the repository superuser. Ideally this would run as the tenant admin but such a named * user doesn't exist for us to run as. Now that the repo uses IAuthorizationPolicy for access control, this code * MUST continue to run as the repository superuser. This is one reason not to implement this on top of PUR. *

- * + * * @author mlowery */ public class JcrRoleAuthorizationPolicyRoleBindingDao extends AbstractJcrBackedRoleBindingDao { @@ -65,18 +71,35 @@ public class JcrRoleAuthorizationPolicyRoleBindingDao extends AbstractJcrBackedR // ~ Instance fields // ================================================================================================= - private JcrTemplate jcrTemplate; + private final JcrTemplate jcrTemplate; + + private final IAuthorizationDecisionCache decisionCache; // ~ Constructors // ==================================================================================================== - public JcrRoleAuthorizationPolicyRoleBindingDao( final JcrTemplate jcrTemplate, final Map> immutableRoleBindings, - final Map> bootstrapRoleBindings, final String superAdminRoleName, - final ITenantedPrincipleNameResolver tenantedRoleNameUtils, final List authorizationActions ) { - super(immutableRoleBindings, bootstrapRoleBindings, superAdminRoleName, tenantedRoleNameUtils, - authorizationActions ); + public JcrRoleAuthorizationPolicyRoleBindingDao( final JcrTemplate jcrTemplate, + final Map> immutableRoleBindings, + final Map> bootstrapRoleBindings, + final String superAdminRoleName, + final ITenantedPrincipleNameResolver tenantedRoleNameUtils, + final List authorizationActions ) { + this( jcrTemplate, immutableRoleBindings, bootstrapRoleBindings, superAdminRoleName, tenantedRoleNameUtils, + authorizationActions, null ); + } + + public JcrRoleAuthorizationPolicyRoleBindingDao( final JcrTemplate jcrTemplate, + final Map> immutableRoleBindings, + final Map> bootstrapRoleBindings, + final String superAdminRoleName, + final ITenantedPrincipleNameResolver tenantedRoleNameUtils, + final List authorizationActions, + final IAuthorizationDecisionCache decisionCache ) { + super( immutableRoleBindings, bootstrapRoleBindings, superAdminRoleName, tenantedRoleNameUtils, + authorizationActions ); Assert.notNull( jcrTemplate, "The JCR template must not be null. Ensure a valid JCR template is provided." ); this.jcrTemplate = jcrTemplate; + this.decisionCache = decisionCache; } // ~ Methods @@ -98,7 +121,8 @@ public Object doInJcr( final Session session ) throws RepositoryException, IOExc @Override public RoleBindingStruct getRoleBindingStruct( final ITenant tenant, final String locale ) { if ( ( tenant != null ) && !TenantUtils.isAccessibleTenant( tenant ) ) { - return new RoleBindingStruct( new HashMap(), new HashMap>(), new HashSet() ); + return new RoleBindingStruct( new HashMap(), new HashMap>(), + new HashSet() ); } return (RoleBindingStruct) jcrTemplate.execute( new JcrCallback() { @Override @@ -126,7 +150,8 @@ public void setRoleBindings( final ITenant tenant, final String runtimeRoleName, if ( !TenantUtils.isAccessibleTenant( tempTenant ) ) { throw new NotFoundException( "Tenant " + tenant.getId() + " not found" ); } - Assert.notNull( logicalRoleNames, "The logical role names list must not be null. Ensure a valid list is provided." ); + Assert.notNull( logicalRoleNames, + "The logical role names list must not be null. Ensure a valid list is provided." ); jcrTemplate.execute( new JcrCallback() { @Override public Object doInJcr( final Session session ) throws RepositoryException, IOException { @@ -134,6 +159,22 @@ public Object doInJcr( final Session session ) throws RepositoryException, IOExc return null; } } ); + + invalidateDecisionCacheForRole( runtimeRoleName ); + } + + protected void invalidateDecisionCacheForRole( String runtimeRoleName ) { + if ( decisionCache != null ) { + // Invalidate all authorization requests which have a related role equal to the runtime-role. + // Regarding actions / logical-roles, because derived action rules exist, it would not be enough to invalidate + // requests which also reference the logical-roles being changed. So this condition is left out, at the cost of + // invalidating more requests than strictly necessary. + var role = new AuthorizationRole( runtimeRoleName ); + + decisionCache.invalidateAll( key -> + key.getRequest().getAllRoles().contains( role ) + ); + } } /** diff --git a/repository/src/main/java/org/pentaho/platform/security/userroledao/jackrabbit/AbstractJcrBackedUserRoleDao.java b/repository/src/main/java/org/pentaho/platform/security/userroledao/jackrabbit/AbstractJcrBackedUserRoleDao.java index 00c3d1b82f8..13ab6d4cefe 100644 --- a/repository/src/main/java/org/pentaho/platform/security/userroledao/jackrabbit/AbstractJcrBackedUserRoleDao.java +++ b/repository/src/main/java/org/pentaho/platform/security/userroledao/jackrabbit/AbstractJcrBackedUserRoleDao.java @@ -28,6 +28,7 @@ import org.apache.jackrabbit.spi.NameFactory; import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; import org.pentaho.platform.api.engine.ISystemConfig; +import org.pentaho.platform.api.engine.security.authorization.caching.IAuthorizationDecisionCache; import org.pentaho.platform.api.engine.security.userroledao.IPentahoRole; import org.pentaho.platform.api.engine.security.userroledao.IPentahoUser; import org.pentaho.platform.api.engine.security.userroledao.IUserRoleDao; @@ -119,6 +120,8 @@ public abstract class AbstractJcrBackedUserRoleDao implements IUserRoleDao { private UserCache userDetailsCache = new NullUserCache(); + private IAuthorizationDecisionCache decisionCache; + private boolean useJackrabbitUserCache = true; public AbstractJcrBackedUserRoleDao( ITenantedPrincipleNameResolver userNameUtils, @@ -129,8 +132,10 @@ public AbstractJcrBackedUserRoleDao( ITenantedPrincipleNameResolver userNameUtil final IPathConversionHelper pathConversionHelper, final ILockHelper lockHelper, final IRepositoryDefaultAclHandler defaultAclHandler, final List systemRoles, - final List extraRoles, UserCache userDetailsCache ) - throws NamespaceException { + final List extraRoles, + UserCache userDetailsCache, + IAuthorizationDecisionCache decisionCache ) + throws NamespaceException { this.tenantedUserNameUtils = userNameUtils; this.tenantedRoleNameUtils = roleNameUtils; this.authenticatedRoleName = authenticatedRoleName; @@ -144,6 +149,7 @@ public AbstractJcrBackedUserRoleDao( ITenantedPrincipleNameResolver userNameUtil this.systemRoles = systemRoles; this.extraRoles = extraRoles; this.userDetailsCache = userDetailsCache; + this.decisionCache = decisionCache; } public void setRoleMembers( Session session, final ITenant theTenant, final String roleName, @@ -155,24 +161,24 @@ public void setRoleMembers( Session session, final ITenant theTenant, final Stri // or a user designated as a system user. If it is then we // will display a message to the user. if ( ( oneOfUserIsMySelf( usersToBeRemoved ) || oneOfUserIsDefaultAdminUser( usersToBeRemoved ) ) - && tenantAdminRoleName.equals( roleName ) ) { + && tenantAdminRoleName.equals( roleName ) ) { throw new RepositoryException( Messages.getInstance().getString( - "AbstractJcrBackedUserRoleDao.ERROR_0009_USER_REMOVE_FAILED_YOURSELF_OR_DEFAULT_ADMIN_USER" ) ); + "AbstractJcrBackedUserRoleDao.ERROR_0009_USER_REMOVE_FAILED_YOURSELF_OR_DEFAULT_ADMIN_USER" ) ); } // If this is the last user from the Administrator role, we will not let the user remove. if ( tenantAdminRoleName.equals( roleName ) && ( currentRoleMembers != null && currentRoleMembers.size() > 0 ) - && memberUserNames.length == 0 ) { + && memberUserNames.length == 0 ) { throw new RepositoryException( Messages.getInstance().getString( - "AbstractJcrBackedUserRoleDao.ERROR_0001_LAST_ADMIN_ROLE", tenantAdminRoleName ) ); + "AbstractJcrBackedUserRoleDao.ERROR_0001_LAST_ADMIN_ROLE", tenantAdminRoleName ) ); } Group jackrabbitGroup = getJackrabbitGroup( theTenant, roleName, session ); if ( ( jackrabbitGroup == null ) - || !TenantUtils.isAccessibleTenant( theTenant == null ? tenantedRoleNameUtils.getTenant( jackrabbitGroup - .getID() ) : theTenant ) ) { + || !TenantUtils.isAccessibleTenant( theTenant == null ? tenantedRoleNameUtils.getTenant( jackrabbitGroup + .getID() ) : theTenant ) ) { throw new NotFoundException( Messages.getInstance().getString( - "AbstractJcrBackedUserRoleDao.ERROR_0002_ROLE_NOT_FOUND" ) ); + "AbstractJcrBackedUserRoleDao.ERROR_0002_ROLE_NOT_FOUND" ) ); } HashMap currentlyAssignedUsers = new HashMap(); Iterator currentMembers = jackrabbitGroup.getMembers(); @@ -190,7 +196,7 @@ public void setRoleMembers( Session session, final ITenant theTenant, final Stri User jackrabbitUser = getJackrabbitUser( tenant, user, session ); if ( jackrabbitUser != null ) { finalCollectionOfAssignedUsers.put( - getTenantedUserNameUtils().getPrincipleId( tenant, user ), jackrabbitUser ); + getTenantedUserNameUtils().getPrincipleId( tenant, user ), jackrabbitUser ); } } } @@ -203,14 +209,14 @@ public void setRoleMembers( Session session, final ITenant theTenant, final Stri for ( String userId : usersToRemove ) { jackrabbitGroup.removeMember( currentlyAssignedUsers.get( userId ) ); - purgeUserFromCache( userId ); + purgeUserFromCacheById( userId ); } for ( String userId : usersToAdd ) { jackrabbitGroup.addMember( finalCollectionOfAssignedUsers.get( userId ) ); // Purge the UserDetails cache - purgeUserFromCache( userId ); + purgeUserFromCacheById( userId ); } } @@ -225,10 +231,10 @@ private void setUserRolesForNewUser( Session session, final ITenant theTenant, f User jackrabbitUser = getJackrabbitUser( theTenant, userName, session ); if ( ( jackrabbitUser == null ) - || !TenantUtils.isAccessibleTenant( theTenant == null ? tenantedUserNameUtils - .getTenant( jackrabbitUser.getID() ) : theTenant ) ) { + || !TenantUtils.isAccessibleTenant( theTenant == null ? tenantedUserNameUtils + .getTenant( jackrabbitUser.getID() ) : theTenant ) ) { throw new NotFoundException( Messages.getInstance().getString( - "AbstractJcrBackedUserRoleDao.ERROR_0003_USER_NOT_FOUND" ) ); + "AbstractJcrBackedUserRoleDao.ERROR_0003_USER_NOT_FOUND" ) ); } HashMap finalCollectionOfAssignedGroups = new HashMap(); @@ -245,12 +251,30 @@ private void setUserRolesForNewUser( Session session, final ITenant theTenant, f for ( String groupId : groupsToAdd ) { finalCollectionOfAssignedGroups.get( groupId ).addMember( jackrabbitUser ); // Purge the UserDetails cache - purgeUserFromCache( userName ); + purgeUserFromCacheById( userName ); } } - private void purgeUserFromCache( String userName ) { - userDetailsCache.removeUserFromCache( getTenantedUserNameUtils().getPrincipleName( userName ) ); + private void purgeUserFromCacheById( String userId ) { + String userName = getTenantedUserNameUtils().getPrincipleName( userId ); + purgeUserFromCacheByName( userName ); + } + + private void purgeUserFromCacheByName( String userName ) { + userDetailsCache.removeUserFromCache( userName ); + invalidateDecisionCacheForUser( userName ); + } + + private void invalidateDecisionCacheForUser( String userName ) { + if ( decisionCache != null ) { + decisionCache.invalidateAll( key -> + key + .getRequest() + .getPrincipalAsUser() + .map( user -> user.getName().equals( userName ) ) + .orElse( false ) + ); + } } private boolean oneOfUserIsMySelf( String[] users ) { @@ -278,7 +302,7 @@ protected boolean isMyself( String userName ) { private boolean isDefaultAdminUser( String userName ) { String defaultAdminUser = - PentahoSystem.get( String.class, "singleTenantAdminUserName", PentahoSessionHolder.getSession() ); + PentahoSystem.get( String.class, "singleTenantAdminUserName", PentahoSessionHolder.getSession() ); if ( defaultAdminUser != null ) { return defaultAdminUser.equals( userName ); } @@ -290,11 +314,11 @@ private boolean adminRoleExist( String[] newRoles ) { } public void setUserRoles( Session session, final ITenant theTenant, final String userName, final String[] roles ) - throws RepositoryException, NotFoundException { + throws RepositoryException, NotFoundException { if ( ( isMyself( userName ) || isDefaultAdminUser( userName ) ) && !adminRoleExist( roles ) ) { throw new RepositoryException( Messages.getInstance().getString( - "AbstractJcrBackedUserRoleDao.ERROR_0005_YOURSELF_OR_DEFAULT_ADMIN_USER" ) ); + "AbstractJcrBackedUserRoleDao.ERROR_0005_YOURSELF_OR_DEFAULT_ADMIN_USER" ) ); } Set roleSet = new HashSet(); @@ -306,10 +330,10 @@ public void setUserRoles( Session session, final ITenant theTenant, final String User jackrabbitUser = getJackrabbitUser( theTenant, userName, session ); if ( ( jackrabbitUser == null ) - || !TenantUtils.isAccessibleTenant( theTenant == null ? tenantedUserNameUtils - .getTenant( jackrabbitUser.getID() ) : theTenant ) ) { + || !TenantUtils.isAccessibleTenant( theTenant == null ? tenantedUserNameUtils + .getTenant( jackrabbitUser.getID() ) : theTenant ) ) { throw new NotFoundException( Messages.getInstance().getString( - "AbstractJcrBackedUserRoleDao.ERROR_0003_USER_NOT_FOUND" ) ); + "AbstractJcrBackedUserRoleDao.ERROR_0003_USER_NOT_FOUND" ) ); } HashMap currentlyAssignedGroups = new HashMap(); Iterator currentGroups = jackrabbitUser.memberOf(); @@ -342,13 +366,13 @@ public void setUserRoles( Session session, final ITenant theTenant, final String } // Purge the UserDetails cache - purgeUserFromCache( userName ); + purgeUserFromCacheById( userName ); } public IPentahoRole createRole( Session session, final ITenant theTenant, final String roleName, final String description, final String[] memberUserNames ) - throws AuthorizableExistsException, - RepositoryException { + throws AuthorizableExistsException, + RepositoryException { ITenant tenant = theTenant; String role = roleName; if ( tenant == null ) { @@ -360,7 +384,7 @@ public IPentahoRole createRole( Session session, final ITenant theTenant, final } if ( !TenantUtils.isAccessibleTenant( tenant ) ) { throw new NotFoundException( Messages.getInstance().getString( - "AbstractJcrBackedUserRoleDao.ERROR_0006_TENANT_NOT_FOUND", theTenant.getId() ) ); + "AbstractJcrBackedUserRoleDao.ERROR_0006_TENANT_NOT_FOUND", theTenant.getId() ) ); } String roleId = tenantedRoleNameUtils.getPrincipleId( tenant, role ); @@ -379,8 +403,8 @@ public IPentahoRole createRole( Session session, final ITenant theTenant, final public IPentahoUser createUser( Session session, final ITenant theTenant, final String userName, final String password, final String description, final String[] roles ) - throws AuthorizableExistsException, - RepositoryException { + throws AuthorizableExistsException, + RepositoryException { ITenant tenant = theTenant; String user = userName; if ( tenant == null ) { @@ -392,7 +416,7 @@ public IPentahoUser createUser( Session session, final ITenant theTenant, final } if ( !TenantUtils.isAccessibleTenant( tenant ) ) { throw new NotFoundException( Messages.getInstance().getString( - "AbstractJcrBackedUserRoleDao.ERROR_0006_TENANT_NOT_FOUND", theTenant.getId() ) ); + "AbstractJcrBackedUserRoleDao.ERROR_0006_TENANT_NOT_FOUND", theTenant.getId() ) ); } String userId = tenantedUserNameUtils.getPrincipleId( tenant, user ); UserManager tenantUserMgr = getUserManager( tenant, session ); @@ -407,7 +431,7 @@ public IPentahoUser createUser( Session session, final ITenant theTenant, final session.save(); createUserHomeFolder( tenant, user, session ); session.save(); - this.userDetailsCache.removeUserFromCache( userName ); + purgeUserFromCacheByName( userName ); return getUser( session, tenant, userName ); } @@ -416,17 +440,17 @@ public void deleteRole( Session session, final IPentahoRole role ) throws NotFou final List roleMembers = this.getRoleMembers( session, role.getTenant(), role.getName() ); Group jackrabbitGroup = getJackrabbitGroup( role.getTenant(), role.getName(), session ); if ( jackrabbitGroup != null - && TenantUtils.isAccessibleTenant( tenantedRoleNameUtils.getTenant( jackrabbitGroup.getID() ) ) ) { + && TenantUtils.isAccessibleTenant( tenantedRoleNameUtils.getTenant( jackrabbitGroup.getID() ) ) ) { jackrabbitGroup.remove(); } else { throw new NotFoundException( "" ); //$NON-NLS-1$ } for ( IPentahoUser roleMember : roleMembers ) { - purgeUserFromCache( roleMember.getUsername() ); + purgeUserFromCacheById( roleMember.getUsername() ); } } else { throw new RepositoryException( Messages.getInstance().getString( - "AbstractJcrBackedUserRoleDao.ERROR_0007_ATTEMPTED_SYSTEM_ROLE_DELETE" ) ); + "AbstractJcrBackedUserRoleDao.ERROR_0007_ATTEMPTED_SYSTEM_ROLE_DELETE" ) ); } } @@ -434,7 +458,7 @@ public void deleteUser( Session session, final IPentahoUser user ) throws NotFou if ( canDeleteUser( session, user ) ) { User jackrabbitUser = getJackrabbitUser( user.getTenant(), user.getUsername(), session ); if ( jackrabbitUser != null - && TenantUtils.isAccessibleTenant( tenantedUserNameUtils.getTenant( jackrabbitUser.getID() ) ) ) { + && TenantUtils.isAccessibleTenant( tenantedUserNameUtils.getTenant( jackrabbitUser.getID() ) ) ) { // [BISERVER-9215] Adding new user with same user name as a previously deleted user, defaults to all // previous @@ -444,7 +468,7 @@ public void deleteUser( Session session, final IPentahoUser user ) throws NotFou currentGroups.next().removeMember( jackrabbitUser ); } getUserCache().remove( jackrabbitUser.getID() ); - purgeUserFromCache( user.getUsername() ); + purgeUserFromCacheById( user.getUsername() ); // [BISERVER-9215] jackrabbitUser.remove(); session.save(); @@ -453,7 +477,7 @@ public void deleteUser( Session session, final IPentahoUser user ) throws NotFou } } else { throw new RepositoryException( Messages.getInstance().getString( - "AbstractJcrBackedUserRoleDao.ERROR_0004_LAST_USER_NEEDED_IN_ROLE", tenantAdminRoleName ) ); + "AbstractJcrBackedUserRoleDao.ERROR_0004_LAST_USER_NEEDED_IN_ROLE", tenantAdminRoleName ) ); } } @@ -484,8 +508,8 @@ IPentahoUser convertToPentahoUser( User jackrabbitUser ) throws RepositoryExcept } pentahoUser = - new PentahoUser( getTenantedUserNameUtils().getTenant( jackrabbitUser.getID() ), getTenantedUserNameUtils() - .getPrincipleName( jackrabbitUser.getID() ), password, description, !jackrabbitUser.isDisabled() ); + new PentahoUser( getTenantedUserNameUtils().getTenant( jackrabbitUser.getID() ), getTenantedUserNameUtils() + .getPrincipleName( jackrabbitUser.getID() ), password, description, !jackrabbitUser.isDisabled() ); if ( isUseJackrabbitUserCache() ) { getUserCache().put( jackrabbitUser.getID(), pentahoUser ); @@ -507,8 +531,8 @@ private IPentahoRole convertToPentahoRole( Group jackrabbitGroup ) throws Reposi } role = - new PentahoRole( tenantedRoleNameUtils.getTenant( jackrabbitGroup.getID() ), tenantedRoleNameUtils - .getPrincipleName( jackrabbitGroup.getID() ), description ); + new PentahoRole( tenantedRoleNameUtils.getTenant( jackrabbitGroup.getID() ), tenantedRoleNameUtils + .getPrincipleName( jackrabbitGroup.getID() ), description ); return role; } @@ -520,17 +544,17 @@ public void setRoleDescription( Session session, final ITenant theTenant, final final String description ) throws NotFoundException, RepositoryException { Group jackrabbitGroup = getJackrabbitGroup( theTenant, roleName, session ); if ( jackrabbitGroup != null - && TenantUtils.isAccessibleTenant( theTenant == null ? tenantedRoleNameUtils - .getTenant( jackrabbitGroup.getID() ) : theTenant ) ) { + && TenantUtils.isAccessibleTenant( theTenant == null ? tenantedRoleNameUtils + .getTenant( jackrabbitGroup.getID() ) : theTenant ) ) { if ( description == null ) { jackrabbitGroup.removeProperty( "description" ); //$NON-NLS-1$ } else { jackrabbitGroup - .setProperty( "description", session.getValueFactory().createValue( description ) ); //$NON-NLS-1$ + .setProperty( "description", session.getValueFactory().createValue( description ) ); //$NON-NLS-1$ } } else { throw new NotFoundException( Messages.getInstance().getString( - "AbstractJcrBackedUserRoleDao.ERROR_0002_ROLE_NOT_FOUND" ) ); + "AbstractJcrBackedUserRoleDao.ERROR_0002_ROLE_NOT_FOUND" ) ); } } @@ -538,10 +562,10 @@ public void setUserDescription( Session session, final ITenant theTenant, final final String description ) throws NotFoundException, RepositoryException { User jackrabbitUser = getJackrabbitUser( theTenant, userName, session ); if ( ( jackrabbitUser == null ) - || !TenantUtils.isAccessibleTenant( theTenant == null ? tenantedUserNameUtils - .getTenant( jackrabbitUser.getID() ) : theTenant ) ) { + || !TenantUtils.isAccessibleTenant( theTenant == null ? tenantedUserNameUtils + .getTenant( jackrabbitUser.getID() ) : theTenant ) ) { throw new NotFoundException( Messages.getInstance().getString( - "AbstractJcrBackedUserRoleDao.ERROR_0003_USER_NOT_FOUND" ) ); + "AbstractJcrBackedUserRoleDao.ERROR_0003_USER_NOT_FOUND" ) ); } if ( description == null ) { jackrabbitUser.removeProperty( "description" ); //$NON-NLS-1$ @@ -551,20 +575,20 @@ public void setUserDescription( Session session, final ITenant theTenant, final } public void setPassword( Session session, final ITenant theTenant, final String userName, final String password ) - throws NotFoundException, RepositoryException { + throws NotFoundException, RepositoryException { User jackrabbitUser = getJackrabbitUser( theTenant, userName, session ); if ( ( jackrabbitUser == null ) - || !TenantUtils.isAccessibleTenant( theTenant == null ? tenantedUserNameUtils - .getTenant( jackrabbitUser.getID() ) : theTenant ) ) { + || !TenantUtils.isAccessibleTenant( theTenant == null ? tenantedUserNameUtils + .getTenant( jackrabbitUser.getID() ) : theTenant ) ) { throw new NotFoundException( Messages.getInstance().getString( - "AbstractJcrBackedUserRoleDao.ERROR_0003_USER_NOT_FOUND" ) ); + "AbstractJcrBackedUserRoleDao.ERROR_0003_USER_NOT_FOUND" ) ); } jackrabbitUser.changePassword( password ); /** * BISERVER-9906 Clear cache after changing password */ - purgeUserFromCache( userName ); + purgeUserFromCacheById( userName ); userCache.remove( jackrabbitUser.getID() ); } @@ -577,6 +601,7 @@ protected void setUserDetailsCache( UserCache userDetailsCache ) { protected void setTenantedUserNameUtils( ITenantedPrincipleNameResolver userNameUtils ) { tenantedUserNameUtils = userNameUtils; } + public ITenantedPrincipleNameResolver getTenantedUserNameUtils() { return tenantedUserNameUtils; } @@ -595,7 +620,7 @@ public List getRoles( Session session, ITenant tenant ) throws Rep } public List getRoles( Session session, ITenant theTenant, boolean includeSubtenants ) - throws RepositoryException { + throws RepositoryException { ArrayList roles = new ArrayList<>(); if ( theTenant == null || theTenant.getId() == null ) { theTenant = JcrTenantUtils.getTenant(); @@ -638,7 +663,7 @@ protected static SessionImpl getSessionImpl( Session session ) { InvocationHandler invocationHandler = Proxy.getInvocationHandler( session ); if ( invocationHandler instanceof CredentialsStrategySessionFactory.LogoutSuppressingInvocationHandler ) { return ( (CredentialsStrategySessionFactory.LogoutSuppressingInvocationHandler) invocationHandler ) - .getSession(); + .getSession(); } } throw new IllegalStateException( "Session is not a SessionImpl or a proxy of one." ); @@ -654,7 +679,7 @@ public List getUsers( Session session, ITenant tenant ) throws Rep } public List getUsers( Session session, ITenant theTenant, boolean includeSubtenants ) - throws RepositoryException { + throws RepositoryException { ArrayList users = new ArrayList(); if ( theTenant == null || theTenant.getId() == null ) { theTenant = JcrTenantUtils.getTenant(); @@ -681,24 +706,24 @@ public List getUsers( Session session, ITenant theTenant, boolean public IPentahoRole getRole( Session session, final ITenant tenant, final String name ) throws RepositoryException { Group jackrabbitGroup = getJackrabbitGroup( tenant, name, session ); return jackrabbitGroup != null - && TenantUtils.isAccessibleTenant( tenant == null ? tenantedRoleNameUtils.getTenant( jackrabbitGroup.getID() ) - : tenant ) ? convertToPentahoRole( jackrabbitGroup ) : null; + && TenantUtils.isAccessibleTenant( tenant == null ? tenantedRoleNameUtils.getTenant( jackrabbitGroup.getID() ) + : tenant ) ? convertToPentahoRole( jackrabbitGroup ) : null; } private PentahoUserManagerImpl getUserManager( ITenant theTenant, Session session ) throws RepositoryException { Properties tenantProperties = new Properties(); tenantProperties.put( PentahoUserManagerImpl.PARAM_USERS_PATH, PentahoUserManagerImpl.USERS_PATH - + theTenant.getRootFolderAbsolutePath() ); + + theTenant.getRootFolderAbsolutePath() ); tenantProperties.put( PentahoUserManagerImpl.PARAM_GROUPS_PATH, PentahoUserManagerImpl.GROUPS_PATH - + theTenant.getRootFolderAbsolutePath() ); + + theTenant.getRootFolderAbsolutePath() ); return new PentahoUserManagerImpl( getSessionImpl( session ), session.getUserID(), tenantProperties ); } public IPentahoUser getUser( Session session, final ITenant tenant, final String name ) throws RepositoryException { User jackrabbitUser = getJackrabbitUser( tenant, name, session ); return jackrabbitUser != null - && TenantUtils.isAccessibleTenant( tenant == null ? tenantedUserNameUtils.getTenant( jackrabbitUser.getID() ) - : tenant ) ? convertToPentahoUser( jackrabbitUser ) : null; + && TenantUtils.isAccessibleTenant( tenant == null ? tenantedUserNameUtils.getTenant( jackrabbitUser.getID() ) + : tenant ) ? convertToPentahoUser( jackrabbitUser ) : null; } private Group getJackrabbitGroup( ITenant theTenant, String name, Session session ) throws RepositoryException { @@ -760,12 +785,12 @@ protected boolean tenantExists( String tenantName ) { } public List getRoleMembers( Session session, final ITenant theTenant, final String roleName ) - throws RepositoryException { + throws RepositoryException { List users = new ArrayList(); Group jackrabbitGroup = getJackrabbitGroup( theTenant, roleName, session ); if ( ( jackrabbitGroup != null ) - && TenantUtils.isAccessibleTenant( theTenant == null ? tenantedRoleNameUtils - .getTenant( jackrabbitGroup.getID() ) : theTenant ) ) { + && TenantUtils.isAccessibleTenant( theTenant == null ? tenantedRoleNameUtils + .getTenant( jackrabbitGroup.getID() ) : theTenant ) ) { Iterator authorizables = jackrabbitGroup.getMembers(); while ( authorizables.hasNext() ) { Authorizable authorizable = authorizables.next(); @@ -778,12 +803,12 @@ public List getRoleMembers( Session session, final ITenant theTena } public List getUserRoles( Session session, final ITenant theTenant, final String userName ) - throws RepositoryException { + throws RepositoryException { ArrayList roles = new ArrayList(); User jackrabbitUser = getJackrabbitUser( theTenant, userName, session ); if ( ( jackrabbitUser != null ) - && TenantUtils.isAccessibleTenant( theTenant == null ? tenantedUserNameUtils.getTenant( jackrabbitUser.getID() ) - : theTenant ) ) { + && TenantUtils.isAccessibleTenant( theTenant == null ? tenantedUserNameUtils.getTenant( jackrabbitUser.getID() ) + : theTenant ) ) { Iterator groups = jackrabbitUser.memberOf(); while ( groups.hasNext() ) { IPentahoRole role = convertToPentahoRole( groups.next() ); @@ -798,7 +823,7 @@ public List getUserRoles( Session session, final ITenant theTenant @VisibleForTesting protected RepositoryFile createUserHomeFolder( ITenant theTenant, String username, Session session ) - throws RepositoryException { + throws RepositoryException { Builder aclsForUserHomeFolder = null; Builder aclsForTenantHomeFolder = null; @@ -820,13 +845,13 @@ protected RepositoryFile createUserHomeFolder( ITenant theTenant, String usernam RepositoryFileSid ownerSid = null; // Get the Tenant Root folder. If the Tenant Root folder does not exist then exit. tenantRootFolder = - JcrRepositoryFileUtils.getFileByAbsolutePath( session, ServerRepositoryPaths - .getTenantRootFolderPath( theTenant ), pathConversionHelper, lockHelper, false, null ); + JcrRepositoryFileUtils.getFileByAbsolutePath( session, ServerRepositoryPaths + .getTenantRootFolderPath( theTenant ), pathConversionHelper, lockHelper, false, null ); if ( tenantRootFolder != null ) { // Try to see if Tenant Home folder exist tenantHomeFolder = - JcrRepositoryFileUtils.getFileByAbsolutePath( session, ServerRepositoryPaths - .getTenantHomeFolderPath( theTenant ), pathConversionHelper, lockHelper, false, null ); + JcrRepositoryFileUtils.getFileByAbsolutePath( session, ServerRepositoryPaths + .getTenantHomeFolderPath( theTenant ), pathConversionHelper, lockHelper, false, null ); if ( tenantHomeFolder == null ) { String ownerId = tenantedUserNameUtils.getPrincipleId( theTenant, username ); @@ -836,27 +861,27 @@ protected RepositoryFile createUserHomeFolder( ITenant theTenant, String usernam RepositoryFileSid tenantAuthenticatedRoleSid = new RepositoryFileSid( tenantAuthenticatedRoleId, Type.ROLE ); aclsForTenantHomeFolder = - new RepositoryFileAcl.Builder( userSid ).ace( tenantAuthenticatedRoleSid, EnumSet - .of( RepositoryFilePermission.READ ) ); + new RepositoryFileAcl.Builder( userSid ).ace( tenantAuthenticatedRoleSid, EnumSet + .of( RepositoryFilePermission.READ ) ); aclsForUserHomeFolder = - new RepositoryFileAcl.Builder( userSid ).ace( ownerSid, EnumSet.of( RepositoryFilePermission.ALL ) ); + new RepositoryFileAcl.Builder( userSid ).ace( ownerSid, EnumSet.of( RepositoryFilePermission.ALL ) ); tenantHomeFolder = - internalCreateFolder( session, tenantRootFolder.getId(), new RepositoryFile.Builder( ServerRepositoryPaths - .getTenantHomeFolderName() ).folder( true ).title( - Messages.getInstance().getString( "AbstractJcrBackedUserRoleDao.usersFolderDisplayName" ) ).build(), - aclsForTenantHomeFolder.build(), "tenant home folder" ); //$NON-NLS-1$ + internalCreateFolder( session, tenantRootFolder.getId(), new RepositoryFile.Builder( ServerRepositoryPaths + .getTenantHomeFolderName() ).folder( true ).title( + Messages.getInstance().getString( "AbstractJcrBackedUserRoleDao.usersFolderDisplayName" ) ).build(), + aclsForTenantHomeFolder.build(), "tenant home folder" ); //$NON-NLS-1$ } else { String ownerId = tenantedUserNameUtils.getPrincipleId( theTenant, username ); ownerSid = new RepositoryFileSid( ownerId, Type.USER ); aclsForUserHomeFolder = - new RepositoryFileAcl.Builder( userSid ).ace( ownerSid, EnumSet.of( RepositoryFilePermission.ALL ) ); + new RepositoryFileAcl.Builder( userSid ).ace( ownerSid, EnumSet.of( RepositoryFilePermission.ALL ) ); } // now check if user's home folder exist userHomeFolder = - JcrRepositoryFileUtils.getFileByAbsolutePath( session, ServerRepositoryPaths.getUserHomeFolderPath( - theTenant, username ), pathConversionHelper, lockHelper, false, null ); + JcrRepositoryFileUtils.getFileByAbsolutePath( session, ServerRepositoryPaths.getUserHomeFolderPath( + theTenant, username ), pathConversionHelper, lockHelper, false, null ); if ( userHomeFolder == null ) { RepositoryFile.Builder newFolder = new RepositoryFile.Builder( username ).folder( true ); String hidePropertyValue = PentahoSystem.get( ISystemConfig.class ) @@ -864,34 +889,37 @@ protected RepositoryFile createUserHomeFolder( ITenant theTenant, String usernam Boolean hideUserHomeFolder = hidePropertyValue != null && "true".equals( hidePropertyValue.toLowerCase() ); newFolder = newFolder.hidden( hideUserHomeFolder ); userHomeFolder = - internalCreateFolder( session, tenantHomeFolder.getId(), newFolder.build(), - aclsForUserHomeFolder.build(), "user home folder" ); //$NON-NLS-1$ - } + internalCreateFolder( session, tenantHomeFolder.getId(), newFolder.build(), + aclsForUserHomeFolder.build(), "user home folder" ); //$NON-NLS-1$ + // Some actions, such as download / upload depend on a user's home folder. + invalidateDecisionCacheForUser( username ); + } } + return userHomeFolder; } private RepositoryFile internalCreateFolder( final Session session, final Serializable parentFolderId, final RepositoryFile folder, final RepositoryFileAcl acl, final String versionMessage ) - throws RepositoryException { + throws RepositoryException { PentahoJcrConstants pentahoJcrConstants = new PentahoJcrConstants( session ); JcrRepositoryFileUtils.checkoutNearestVersionableFileIfNecessary( session, pentahoJcrConstants, parentFolderId ); Node folderNode = JcrRepositoryFileUtils.createFolderNode( session, pentahoJcrConstants, parentFolderId, folder ); // we must create the acl during checkout JcrRepositoryFileAclUtils.createAcl( session, pentahoJcrConstants, folderNode.getIdentifier(), acl == null - ? defaultAclHandler.createDefaultAcl( folder ) : acl ); + ? defaultAclHandler.createDefaultAcl( folder ) : acl ); session.save(); if ( folder.isVersioned() ) { JcrRepositoryFileUtils.checkinNearestVersionableNodeIfNecessary( session, pentahoJcrConstants, folderNode, - versionMessage ); + versionMessage ); } JcrRepositoryFileUtils.checkinNearestVersionableFileIfNecessary( session, pentahoJcrConstants, parentFolderId, - Messages.getInstance().getString( "JcrRepositoryFileDao.USER_0001_VER_COMMENT_ADD_FOLDER", folder.getName(), - ( parentFolderId == null ? "root" : parentFolderId.toString() ) ) ); //$NON-NLS-1$ //$NON-NLS-2$ + Messages.getInstance().getString( "JcrRepositoryFileDao.USER_0001_VER_COMMENT_ADD_FOLDER", folder.getName(), + ( parentFolderId == null ? "root" : parentFolderId.toString() ) ) ); //$NON-NLS-1$ //$NON-NLS-2$ return JcrRepositoryFileUtils.nodeToFile( session, pentahoJcrConstants, pathConversionHelper, lockHelper, - folderNode ); + folderNode ); } /** @@ -914,14 +942,14 @@ protected boolean canDeleteUser( Session session, final IPentahoUser user ) thro if ( ( isMyself( user.getUsername() ) || isDefaultAdminUser( user.getUsername() ) ) && userHasAdminRole ) { throw new RepositoryException( Messages.getInstance().getString( - "AbstractJcrBackedUserRoleDao.ERROR_0008_UNABLE_TO_DELETE_USER_IS_YOURSELF_OR_DEFAULT_ADMIN_USER" ) ); + "AbstractJcrBackedUserRoleDao.ERROR_0008_UNABLE_TO_DELETE_USER_IS_YOURSELF_OR_DEFAULT_ADMIN_USER" ) ); } if ( userHasAdminRole ) { List usersWithAdminRole = getRoleMembers( session, null, tenantAdminRoleName ); if ( usersWithAdminRole == null ) { throw new RepositoryException( Messages.getInstance().getString( - "AbstractJcrBackedUserRoleDao.ERROR_0004_LAST_USER_NEEDED_IN_ROLE", tenantAdminRoleName ) ); + "AbstractJcrBackedUserRoleDao.ERROR_0004_LAST_USER_NEEDED_IN_ROLE", tenantAdminRoleName ) ); } if ( usersWithAdminRole.size() > 1 ) { return true; @@ -929,7 +957,7 @@ protected boolean canDeleteUser( Session session, final IPentahoUser user ) thro return false; } else { throw new RepositoryException( Messages.getInstance().getString( - "AbstractJcrBackedUserRoleDao.ERROR_0004_LAST_USER_NEEDED_IN_ROLE", tenantAdminRoleName ) ); + "AbstractJcrBackedUserRoleDao.ERROR_0004_LAST_USER_NEEDED_IN_ROLE", tenantAdminRoleName ) ); } } return true; diff --git a/repository/src/main/java/org/pentaho/platform/security/userroledao/jackrabbit/JcrUserRoleDao.java b/repository/src/main/java/org/pentaho/platform/security/userroledao/jackrabbit/JcrUserRoleDao.java index c50031ad03b..701be03f57b 100644 --- a/repository/src/main/java/org/pentaho/platform/security/userroledao/jackrabbit/JcrUserRoleDao.java +++ b/repository/src/main/java/org/pentaho/platform/security/userroledao/jackrabbit/JcrUserRoleDao.java @@ -17,6 +17,7 @@ import org.apache.jackrabbit.spi.Name; import org.apache.jackrabbit.spi.NameFactory; import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.pentaho.platform.api.engine.security.authorization.caching.IAuthorizationDecisionCache; import org.pentaho.platform.api.engine.security.userroledao.AlreadyExistsException; import org.pentaho.platform.api.engine.security.userroledao.IPentahoRole; import org.pentaho.platform.api.engine.security.userroledao.IPentahoUser; @@ -51,15 +52,26 @@ public class JcrUserRoleDao extends AbstractJcrBackedUserRoleDao { JcrTemplate adminJcrTemplate; + public JcrUserRoleDao( JcrTemplate adminJcrTemplate, ITenantedPrincipleNameResolver userNameUtils, + ITenantedPrincipleNameResolver roleNameUtils, String authenticatedRoleName, String tenantAdminRoleName, + String repositoryAdminUsername, IRepositoryFileAclDao repositoryFileAclDao, IRepositoryFileDao repositoryFileDao, + IPathConversionHelper pathConversionHelper, ILockHelper lockHelper, + IRepositoryDefaultAclHandler defaultAclHandler, final List systemRoles, final List extraRoles, + UserCache userCache ) throws NamespaceException { + this( adminJcrTemplate, userNameUtils, roleNameUtils, authenticatedRoleName, tenantAdminRoleName, repositoryAdminUsername, + repositoryFileAclDao, repositoryFileDao, pathConversionHelper, lockHelper, defaultAclHandler, systemRoles, + extraRoles, userCache, null ); + } + public JcrUserRoleDao( JcrTemplate adminJcrTemplate, ITenantedPrincipleNameResolver userNameUtils, ITenantedPrincipleNameResolver roleNameUtils, String authenticatedRoleName, String tenantAdminRoleName, String repositoryAdminUsername, IRepositoryFileAclDao repositoryFileAclDao, IRepositoryFileDao repositoryFileDao, IPathConversionHelper pathConversionHelper, ILockHelper lockHelper, IRepositoryDefaultAclHandler defaultAclHandler, final List systemRoles, final List extraRoles, - UserCache userCache ) throws NamespaceException { + UserCache userCache, IAuthorizationDecisionCache decisionCache ) throws NamespaceException { super( userNameUtils, roleNameUtils, authenticatedRoleName, tenantAdminRoleName, repositoryAdminUsername, repositoryFileAclDao, repositoryFileDao, pathConversionHelper, lockHelper, defaultAclHandler, systemRoles, - extraRoles, userCache ); + extraRoles, userCache, decisionCache ); this.adminJcrTemplate = adminJcrTemplate; } diff --git a/user-console/src/main/java/org/pentaho/mantle/client/commands/PurgeAuthorizationDecisionCacheCommand.java b/user-console/src/main/java/org/pentaho/mantle/client/commands/PurgeAuthorizationDecisionCacheCommand.java new file mode 100644 index 00000000000..de33fdb511d --- /dev/null +++ b/user-console/src/main/java/org/pentaho/mantle/client/commands/PurgeAuthorizationDecisionCacheCommand.java @@ -0,0 +1,64 @@ +/*! ****************************************************************************** + * + * 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.mantle.client.commands; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.http.client.Request; +import com.google.gwt.http.client.RequestBuilder; +import com.google.gwt.http.client.RequestCallback; +import com.google.gwt.http.client.RequestException; +import com.google.gwt.http.client.Response; +import com.google.gwt.user.client.Window; +import org.pentaho.gwt.widgets.client.dialogs.MessageDialogBox; +import org.pentaho.mantle.client.csrf.CsrfRequestBuilder; +import org.pentaho.mantle.client.messages.Messages; + +public class PurgeAuthorizationDecisionCacheCommand extends AbstractCommand { + + public PurgeAuthorizationDecisionCacheCommand() { + } + + protected void performOperation() { + String url = GWT.getHostPageBaseURL() + "api/system/refresh/authorizationDecisionCache"; + RequestBuilder requestBuilder = new CsrfRequestBuilder( RequestBuilder.GET, url ); + requestBuilder.setHeader( "If-Modified-Since", "01 Jan 1970 00:00:00 GMT" ); + requestBuilder.setHeader( "accept", "text/plain" ); + try { + requestBuilder.sendRequest( null, new RequestCallback() { + + public void onError( Request request, Throwable exception ) { + // showError(exception); + } + + public void onResponseReceived( Request request, Response response ) { + MessageDialogBox dialogBox = + new MessageDialogBox( + Messages.getString( "info" ), + Messages.getString( "authorizationDecisionCacheFlushedSuccessfully" ), + false, + false, + true ); + dialogBox.center(); + } + } ); + } catch ( RequestException e ) { + Window.alert( e.getMessage() ); + // showError(e); + } + } + + protected void performOperation( final boolean feedback ) { + // do nothing + } +} diff --git a/user-console/src/main/resources/org/pentaho/mantle/public/xul/mantle.xul b/user-console/src/main/resources/org/pentaho/mantle/public/xul/mantle.xul index 51d8307a76a..6fa3ab191ad 100644 --- a/user-console/src/main/resources/org/pentaho/mantle/public/xul/mantle.xul +++ b/user-console/src/main/resources/org/pentaho/mantle/public/xul/mantle.xul @@ -48,6 +48,8 @@ command="mantleXulHandler.executeMantleCommand('PurgeMondrianSchemaCacheCommand')"/> +