VYPR
Medium severity6.5NVD Advisory· Published May 15, 2024· Updated Apr 15, 2026

CVE-2024-28087

CVE-2024-28087

Description

In Bonitasoft runtime Community edition, the lack of dynamic permissions causes IDOR vulnerability. Dynamic permissions existed only in Subscription edition and have now been restored in Community edition, where they are not custmizable.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.bonitasoft.engine:bonita-serverMaven
< 10.1.0.W1110.1.0.W11

Patches

1
1b3ac00f0178

fix(REST API authorizations): add default dynamic permissions to community (#2869)

https://github.com/bonitasoft/bonita-engineabirembautFeb 27, 2024via ghsa
21 files changed · +800 617
  • bonita-integration-tests/bonita-integration-tests-client/src/test/java/org/bonitasoft/engine/application/ApplicationIT.java+17 0 modified
    @@ -258,6 +258,23 @@ public void should_access_identity_api_using_default_application_permissions() t
     
             loginOnDefaultTenantWith("baptiste", "bpm");
     
    +        long userId = user.getId();
    +        assertThat(
    +                getPermissionAPI().isAuthorized(new APICallContext("GET", "identity", "user", String.valueOf(userId))))
    +                        .isTrue();
    +        assertThat(getPermissionAPI()
    +                .isAuthorized(new APICallContext("GET", "identity", "user", String.valueOf(userId + 1)))).isFalse();
    +        assertThat(getPermissionAPI().isAuthorized(new APICallContext("GET", "identity", "user", null))).isFalse();
    +
    +        getProfileAPI().createProfileMember(
    +                getProfileAPI().searchProfiles(new SearchOptionsBuilder(0, 1).searchTerm("Admin").done()).getResult()
    +                        .get(0).getId(),
    +                user.getId(),
    +                -1L,
    +                -1L);
    +
    +        loginOnDefaultTenantWith("baptiste", "bpm");
    +
             assertThat(getPermissionAPI().isAuthorized(new APICallContext("GET", "identity", "user", null))).isTrue();
         }
     
    
  • bonita-integration-tests/bonita-integration-tests-local/src/main/resources/MyRule+0 49 removed
    @@ -1,49 +0,0 @@
    -/**
    - * Copyright (C) 2014 BonitaSoft S.A.
    - * BonitaSoft, 32 rue Gustave Eiffel - 38000 Grenoble
    - * This program is free software: you can redistribute it and/or modify
    - * it under the terms of the GNU General Public License as published by
    - * the Free Software Foundation, either version 2.0 of the License, or
    - * (at your option) any later version.
    - * This program is distributed in the hope that it will be useful,
    - * but WITHOUT ANY WARRANTY; without even the implied warranty of
    - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    - * GNU General Public License for more details.
    - * You should have received a copy of the GNU General Public License
    - * along with this program. If not, see <http://www.gnu.org/licenses/>.
    - **/
    -package org.test
    -
    -
    -import org.bonitasoft.engine.api.APIAccessor
    -import org.bonitasoft.engine.api.Logger
    -import org.bonitasoft.engine.api.permission.APICallContext
    -import org.bonitasoft.engine.api.permission.PermissionRule
    -import org.bonitasoft.engine.session.APISession
    -
    -/**
    - *
    - * test rule
    - *
    - * return true only for john on api call GET|API/identity/user/<johnId>
    - *
    - *
    - * @author Baptiste Mesta
    - */
    -class MyRule implements PermissionRule {
    -
    -    @Override
    -    boolean isAllowed(APISession apiSession, APICallContext apiCallContext, APIAccessor apiAccessor, Logger logger) {
    -        def userId = apiSession.getUserId()
    -        logger.warning("Executing a rule")
    -        if (apiCallContext.getMethod().equals("GET")
    -                && apiCallContext.getApiName().equals("identity")
    -                && apiCallContext.getResourceName().equals("user")
    -                && apiCallContext.getResourceId().equals(String.valueOf(userId))) {
    -
    -            def user = apiAccessor.getIdentityAPI().getUser(userId)
    -            return user.getUserName().equals("john");
    -        }
    -        return false
    -    }
    -}
    
  • bonita-integration-tests/bonita-integration-tests-local/src/main/resources/RuleWithException+0 39 removed
    @@ -1,39 +0,0 @@
    -/**
    - * Copyright (C) 2014 BonitaSoft S.A.
    - * BonitaSoft, 32 rue Gustave Eiffel - 38000 Grenoble
    - * This program is free software: you can redistribute it and/or modify
    - * it under the terms of the GNU General Public License as published by
    - * the Free Software Foundation, either version 2.0 of the License, or
    - * (at your option) any later version.
    - * This program is distributed in the hope that it will be useful,
    - * but WITHOUT ANY WARRANTY; without even the implied warranty of
    - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    - * GNU General Public License for more details.
    - * You should have received a copy of the GNU General Public License
    - * along with this program. If not, see <http://www.gnu.org/licenses/>.
    - **/
    -
    -
    -
    -import org.bonitasoft.engine.api.APIAccessor
    -import org.bonitasoft.engine.api.Logger
    -import org.bonitasoft.engine.api.permission.APICallContext
    -import org.bonitasoft.engine.api.permission.PermissionRule
    -import org.bonitasoft.engine.session.APISession
    -
    -/**
    - *
    - * test rule
    - *
    - * return true only for john on api call GET|API/identity/user/<johnId>
    - *
    - *
    - * @author Baptiste Mesta
    - */
    -class RuleWithException implements PermissionRule {
    -
    -    @Override
    -    boolean isAllowed(APISession apiSession, APICallContext apiCallContext, APIAccessor apiAccessor, Logger logger) {
    -        throw new RuntimeException();
    -    }
    -}
    
  • bonita-integration-tests/bonita-integration-tests-local/src/test/java/org/bonitasoft/engine/test/PermissionAPIIT.java+24 96 modified
    @@ -15,129 +15,57 @@
     
     import static org.assertj.core.api.Assertions.assertThat;
     
    -import java.io.IOException;
     import java.util.Collections;
     import java.util.Map;
     
    -import javax.naming.NamingException;
    -
    +import org.bonitasoft.engine.TestWithUser;
     import org.bonitasoft.engine.api.permission.APICallContext;
    -import org.bonitasoft.engine.authorization.PermissionService;
    -import org.bonitasoft.engine.commons.exceptions.SBonitaException;
    -import org.bonitasoft.engine.exception.BonitaHomeNotSetException;
    -import org.bonitasoft.engine.exception.ExecutionException;
    -import org.bonitasoft.engine.exception.NotFoundException;
    -import org.bonitasoft.engine.identity.User;
    -import org.bonitasoft.engine.service.ServiceAccessorSingleton;
    -import org.bonitasoft.platform.configuration.ConfigurationService;
    -import org.bonitasoft.platform.configuration.model.BonitaConfiguration;
    -import org.bonitasoft.platform.setup.PlatformSetupAccessor;
    -import org.junit.Before;
    -import org.junit.Rule;
     import org.junit.Test;
    -import org.junit.rules.ExpectedException;
     
     /**
      * @author Baptiste Mesta
      */
    -public class PermissionAPIIT extends CommonAPILocalIT {
    -
    -    @Rule
    -    public ExpectedException exception = ExpectedException.none();
    -    private APICallContext apiCallContext;
    -
    -    @Before
    -    public void before() throws Exception {
    -        loginOnDefaultTenantWithDefaultTechnicalUser();
    -        apiCallContext = new APICallContext("GET", "identity", "user", "1", "query", "body");
    -    }
    +public class PermissionAPIIT extends TestWithUser {
     
         @Test
    -    public void execute_security_script_that_throw_exception() throws Exception {
    -        //given
    -        writeScriptToBonitaHome(getContentOfResource("/RuleWithException"), "RuleWithException");
    -
    -        exception.expect(ExecutionException.class);
    -        //when
    -        getPermissionAPI().checkAPICallWithScript("RuleWithException", apiCallContext, false);
    +    public void should_allow_with_provided_dynamic_rule() throws Exception {
     
    -        //then: ExecutionException
    -    }
    -
    -    @Test
    -    public void execute_provided_security_script_works() throws Exception {
    -        //given
    -        apiCallContext = new APICallContext("GET", "identity", "user", null, "query", "body") {
    -
    -            @Override
    -            public Map<String, String> getFilters() {
    -                return Collections.singletonMap("user_id", String.valueOf(getSession().getUserId()));
    -            }
    -        };
    +        APICallContext apiCallContext = new APICallContext("GET", "bpm", "process", null, "", "body");
             //when
    -        boolean processPermissionRule = getPermissionAPI()
    -                .checkAPICallWithScript("org.bonitasoft.permissions.ProcessPermissionRule", apiCallContext, false);
    +        boolean isAllowedWithoutFilter = getPermissionAPI()
    +                .isAuthorized(apiCallContext);
     
             //then
    -        assertThat(processPermissionRule).isTrue();
    -    }
    -
    -    @Test
    -    public void execute_security_script_with_dependencies() throws Exception {
    +        assertThat(isAllowedWithoutFilter).isFalse();
     
             //given
    -        writeScriptToBonitaHome(getContentOfResource("/MyRule"), "MyRule", "org", "test");
    -        final User john = createUser("john", "bpm");
    -        final User jack = createUser("jack", "bpm");
    +        apiCallContext = getApiCallContextWithUserFilter(getSession().getUserId());
     
             //when
    -        loginOnDefaultTenantWith("jack", "bpm");
    -        final boolean jackResult = getPermissionAPI().checkAPICallWithScript("org.test.MyRule",
    -                new APICallContext("GET", "identity", "user", String.valueOf(jack.getId()), "query", "body"), false);
    -        logoutOnTenant();
    -        loginOnDefaultTenantWith("john", "bpm");
    -        final boolean johnResult = getPermissionAPI().checkAPICallWithScript("org.test.MyRule",
    -                new APICallContext("GET", "identity", "user", String.valueOf(john.getId()), "query", "body"), false);
    -        final boolean johnResultOnOtherAPI = getPermissionAPI().checkAPICallWithScript("org.test.MyRule",
    -                new APICallContext("GET", "identity", "user", String.valueOf(jack.getId()), "query", "body"), false);
    +        boolean isAllowedWithCurrentUserFilter = getPermissionAPI()
    +                .isAuthorized(apiCallContext);
     
    -        //then: ExecutionException
    -        assertThat(jackResult).isFalse();
    -        assertThat(johnResult).isTrue();
    -        assertThat(johnResultOnOtherAPI).isFalse();
    -
    -        deleteUser(john);
    -        deleteUser(jack);
    -    }
    +        //then
    +        assertThat(isAllowedWithCurrentUserFilter).isTrue();
     
    -    @Test
    -    public void execute_security_script_with_not_found_script() throws Exception {
             //given
    +        apiCallContext = getApiCallContextWithUserFilter(99999L);
     
    -        exception.expect(NotFoundException.class);
             //when
    -        getPermissionAPI().checkAPICallWithScript("unknown", apiCallContext, false);
    +        boolean isAllowedWithOtherUserFilter = getPermissionAPI()
    +                .isAuthorized(apiCallContext);
     
    -        //then: ExecutionException
    +        //then
    +        assertThat(isAllowedWithOtherUserFilter).isFalse();
         }
     
    -    private void writeScriptToBonitaHome(final String scriptFileContent, final String fileName, final String... folders)
    -            throws IOException, SBonitaException, BonitaHomeNotSetException, NamingException {
    -        ConfigurationService configurationService = PlatformSetupAccessor.getConfigurationService();
    -        String path = "";
    -        for (String folder : folders) {
    -            path += folder + "/";
    -        }
    -        path += fileName + ".groovy";
    -
    -        configurationService.storeTenantSecurityScripts(
    -                Collections.singletonList(new BonitaConfiguration(path, scriptFileContent.getBytes())),
    -                getServiceAccessor().getTenantId());
    -
    -        final PermissionService permissionService = ServiceAccessorSingleton.getInstance().getPermissionService();
    -        //restart the service to reload scripts
    -        permissionService.stop();
    -        permissionService.start();
    +    private static APICallContext getApiCallContextWithUserFilter(long userId) {
    +        return new APICallContext("GET", "bpm", "process", null, "", "body") {
     
    +            @Override
    +            public Map<String, String> getFilters() {
    +                return Collections.singletonMap("user_id", String.valueOf(userId));
    +            }
    +        };
         }
     }
    
  • bpm/bonita-common/src/main/java/org/bonitasoft/engine/api/PermissionAPI.java+0 41 modified
    @@ -17,7 +17,6 @@
     
     import org.bonitasoft.engine.api.permission.APICallContext;
     import org.bonitasoft.engine.exception.ExecutionException;
    -import org.bonitasoft.engine.exception.NotFoundException;
     
     /**
      * Handle permissions of users
    @@ -26,46 +25,6 @@
      */
     public interface PermissionAPI {
     
    -    /**
    -     * Execute a groovy class, identified by it's class name, stored either in the classpath for default provided
    -     * scripts, or in database for custom scripts.
    -     * You can also add a jar containing a class implementing
    -     * {@link org.bonitasoft.engine.api.permission.PermissionRule} and execute it using its
    -     * fully qualified class name.
    -     * <p>
    -     * The class MUST implements {@link org.bonitasoft.engine.api.permission.PermissionRule}.
    -     * The class must implement method isAllowed() that returns TRUE to authorize access, or FALSE to forbid access.
    -     * If the script throws exception, it is up to the calling application to decide if the access should be granted or
    -     * not.
    -     * </p>
    -     * <p>
    -     * To store your custom class in database, you must use the Setup Tool.
    -     * Your custom groovy script must be placed in folder platform_conf/current/tenants/&lt;tenant
    -     * id&gt;/tenant_security_scripts/.
    -     * For more information on using the setup tool, refer to <a
    -     * href="https://documentation.bonitasoft.com/?page=BonitaBPM_platform_setup">the Platform setup
    -     * tool documentation page</a>
    -     * </p>
    -     *
    -     * @param className
    -     *        the name of the class of the rule
    -     * @param apiCallContext
    -     *        the context of the api call
    -     * @param reload
    -     *        reload class when calling this method, warning if some class were called with reload set to false, they
    -     *        will never be reloadable
    -     * @return true if the user is permitted to make the api call, false otherwise.
    -     * @throws ExecutionException
    -     *         If there is an exception while executing the script
    -     * @throws NotFoundException if the script cannot be found under name <quote>className</quote> neither in the
    -     *         classpath, nor in the custom script folder.
    -     * @since 6.4.0
    -     * @deprecated From version 7.14.0, use {@link #isAuthorized(APICallContext)} instead
    -     */
    -    @Deprecated(forRemoval = true, since = "7.14.0")
    -    boolean checkAPICallWithScript(String className, APICallContext apiCallContext, boolean reload)
    -            throws ExecutionException, NotFoundException;
    -
         /**
          * Checks if the REST API request defined in the {@link APICallContext} is authorized for the logged in user
          *
    
  • bpm/bonita-common/src/main/java/org/bonitasoft/engine/api/permission/PermissionRule.java+1 1 modified
    @@ -26,7 +26,7 @@ public interface PermissionRule {
     
         /**
          * Called by the engine when using
    -     * {@link org.bonitasoft.engine.api.PermissionAPI#checkAPICallWithScript(String, APICallContext, boolean)}
    +     * {@link org.bonitasoft.engine.api.PermissionAPI#isAuthorized(APICallContext)}
          *
          * @param apiSession
          *        the api session from the user doing the api call
    
  • bpm/bonita-core/bonita-process-engine/src/main/groovy/org/bonitasoft/permissions/UserPermissionRule.groovy+3 1 modified
    @@ -51,7 +51,9 @@ class UserPermissionRule implements PermissionRule {
                 }
                 return false
             } else {
    -            if (apiCallContext.getQueryString().contains("d=professional_data") || apiCallContext.getQueryString().contains("d=personnal_data")) {
    +            if (apiCallContext.getQueryString() != null
    +                    && (apiCallContext.getQueryString().contains("d=professional_data")
    +                    || apiCallContext.getQueryString().contains("d=personnal_data"))) {
                     return false
                 }
                 def filters = apiCallContext.getFilters()
    
  • bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/api/impl/PermissionAPIImpl.java+0 19 modified
    @@ -18,10 +18,8 @@
     import lombok.extern.slf4j.Slf4j;
     import org.bonitasoft.engine.api.PermissionAPI;
     import org.bonitasoft.engine.api.permission.APICallContext;
    -import org.bonitasoft.engine.authorization.PermissionService;
     import org.bonitasoft.engine.commons.exceptions.SExecutionException;
     import org.bonitasoft.engine.exception.ExecutionException;
    -import org.bonitasoft.engine.exception.NotFoundException;
     import org.bonitasoft.engine.service.ServiceAccessor;
     import org.bonitasoft.engine.service.ServiceAccessorSingleton;
     
    @@ -32,23 +30,6 @@
     @AvailableInMaintenanceMode
     public class PermissionAPIImpl implements PermissionAPI {
     
    -    @Override
    -    @Deprecated(forRemoval = true, since = "7.14.0")
    -    public boolean checkAPICallWithScript(String className, APICallContext context, boolean reload)
    -            throws ExecutionException, NotFoundException {
    -        ServiceAccessor serviceAccessor = getServiceAccessor();
    -        PermissionService permissionService = serviceAccessor.getPermissionService();
    -        try {
    -            return permissionService.checkAPICallWithScript(className, context, reload);
    -        } catch (SExecutionException e) {
    -            throw new ExecutionException(
    -                    "Unable to execute the security rule " + className + " for the api call " + context, e);
    -        } catch (ClassNotFoundException e) {
    -            throw new NotFoundException("Unable to execute the security rule " + className + " for the api call "
    -                    + context + " because the class " + className + " is not found", e);
    -        }
    -    }
    -
         @Override
         public boolean isAuthorized(APICallContext apiCallContext) throws ExecutionException {
             ServiceAccessor serviceAccessor = getServiceAccessor();
    
  • bpm/bonita-core/bonita-process-engine/src/main/java/org/bonitasoft/engine/authorization/PermissionServiceImpl.java+125 30 modified
    @@ -13,29 +13,25 @@
      **/
     package org.bonitasoft.engine.authorization;
     
    +import static java.lang.String.format;
     import static org.bonitasoft.engine.classloader.ClassLoaderIdentifier.identifier;
     
    -import java.io.File;
    -import java.io.IOException;
     import java.util.*;
    +import java.util.stream.Collectors;
     
     import groovy.lang.GroovyClassLoader;
     import lombok.extern.slf4j.Slf4j;
     import org.bonitasoft.engine.api.impl.APIAccessorImpl;
     import org.bonitasoft.engine.api.permission.APICallContext;
     import org.bonitasoft.engine.api.permission.PermissionRule;
    -import org.bonitasoft.engine.authorization.properties.CompoundPermissionsMapping;
    -import org.bonitasoft.engine.authorization.properties.CustomPermissionsMapping;
    -import org.bonitasoft.engine.authorization.properties.PropertiesWithSet;
    -import org.bonitasoft.engine.authorization.properties.ResourcesPermissionsMapping;
    +import org.bonitasoft.engine.authorization.properties.*;
     import org.bonitasoft.engine.classloader.ClassLoaderService;
     import org.bonitasoft.engine.commons.exceptions.SBonitaException;
     import org.bonitasoft.engine.commons.exceptions.SExecutionException;
     import org.bonitasoft.engine.dependency.model.ScopeType;
    -import org.bonitasoft.engine.exception.BonitaHomeNotSetException;
    -import org.bonitasoft.engine.home.BonitaHomeServer;
     import org.bonitasoft.engine.page.ContentType;
     import org.bonitasoft.engine.page.PageService;
    +import org.bonitasoft.engine.properties.BooleanProperty;
     import org.bonitasoft.engine.service.ModelConvertor;
     import org.bonitasoft.engine.service.impl.ServerLoggerWrapper;
     import org.bonitasoft.engine.session.APISession;
    @@ -58,6 +54,7 @@
     @ConditionalOnSingleCandidate(PermissionService.class)
     public class PermissionServiceImpl implements PermissionService {
     
    +    public static final String PROPERTY_TO_ENABLE_DYNAMIC_PERMISSIONS = "bonita.runtime.authorization.dynamic-check.enabled";
         public static final String RESOURCES_PROPERTY = "resources";
         public static final String PROPERTY_CONTENT_TYPE = "contentType";
         public static final String PROPERTY_API_EXTENSIONS = "apiExtensions";
    @@ -71,10 +68,12 @@ public class PermissionServiceImpl implements PermissionService {
         private final ClassLoaderService classLoaderService;
         private final SessionAccessor sessionAccessor;
         private final SessionService sessionService;
    -    private GroovyClassLoader groovyClassLoader;
    +    protected GroovyClassLoader groovyClassLoader;
         private final CompoundPermissionsMapping compoundPermissionsMapping;
         private final ResourcesPermissionsMapping resourcesPermissionsMapping;
         private final CustomPermissionsMapping customPermissionsMapping;
    +    protected final DynamicPermissionsChecks dynamicPermissionsChecks;
    +    protected final BooleanProperty dynamicPermissionCheck;
     
         protected final long tenantId;
     
    @@ -83,28 +82,30 @@ public PermissionServiceImpl(final ClassLoaderService classLoaderService, final
                 @Value("${tenantId}") final long tenantId,
                 CompoundPermissionsMapping compoundPermissionsMapping,
                 ResourcesPermissionsMapping resourcesPermissionsMapping,
    -            CustomPermissionsMapping customPermissionsMapping) {
    +            CustomPermissionsMapping customPermissionsMapping,
    +            DynamicPermissionsChecks dynamicPermissionsChecks,
    +            @Value("${bonita.runtime.authorization.dynamic-check.enabled:true}") boolean dynamicPermissionCheck) {
             this.classLoaderService = classLoaderService;
             this.sessionAccessor = sessionAccessor;
             this.sessionService = sessionService;
             this.tenantId = tenantId;
             this.compoundPermissionsMapping = compoundPermissionsMapping;
             this.resourcesPermissionsMapping = resourcesPermissionsMapping;
             this.customPermissionsMapping = customPermissionsMapping;
    +        this.dynamicPermissionsChecks = dynamicPermissionsChecks;
    +        this.dynamicPermissionCheck = initDynamicPermissionsEnabledProperty(dynamicPermissionCheck);
         }
     
    -    @Override
    -    public boolean checkAPICallWithScript(final String className, final APICallContext context, final boolean reload)
    +    BooleanProperty initDynamicPermissionsEnabledProperty(boolean dynamicPermissionsEnabled) {
    +        return new BooleanProperty("Dynamic permissions", PROPERTY_TO_ENABLE_DYNAMIC_PERMISSIONS,
    +                dynamicPermissionsEnabled);
    +    }
    +
    +    protected boolean checkAPICallWithScript(final String className, final APICallContext context)
                 throws SExecutionException, ClassNotFoundException {
             checkStarted();
             //groovy class loader load class from files and cache then when loaded, no need to do some lazy loading or load all class on start
    -        Class<?> aClass;
    -        if (reload) {
    -            reload();
    -            aClass = groovyClassLoader.loadClass(className, true, true, true);
    -        } else {
    -            aClass = Class.forName(className, true, groovyClassLoader);
    -        }
    +        Class<?> aClass = getRuleClass(className);
             if (!PermissionRule.class.isAssignableFrom(aClass)) {
                 throw new SExecutionException("The class " + aClass.getName()
                         + " does not implements org.bonitasoft.engine.api.permission.PermissionRule");
    @@ -120,6 +121,10 @@ public boolean checkAPICallWithScript(final String className, final APICallConte
             }
         }
     
    +    protected Class<?> getRuleClass(String className) throws SExecutionException, ClassNotFoundException {
    +        return Class.forName(className, true, groovyClassLoader);
    +    }
    +
         public SSession getSession() throws SExecutionException {
             try {
                 return sessionService.getSession(sessionAccessor.getSessionId());
    @@ -128,7 +133,7 @@ public SSession getSession() throws SExecutionException {
             }
         }
     
    -    private void reload() throws SExecutionException {
    +    public void reload() throws SExecutionException {
             stop();
             try {
                 start();
    @@ -152,16 +157,6 @@ public void start() throws SBonitaException {
             groovyClassLoader = new GroovyClassLoader(
                     classLoaderService.getClassLoader(identifier(ScopeType.TENANT, tenantId)));
             groovyClassLoader.setShouldRecompile(true);
    -        try {
    -            final File folder = getBonitaHomeServer().getSecurityScriptsFolder(tenantId);
    -            groovyClassLoader.addClasspath(folder.getAbsolutePath());
    -        } catch (BonitaHomeNotSetException | IOException e) {
    -            throw new SExecutionException(e);
    -        }
    -    }
    -
    -    BonitaHomeServer getBonitaHomeServer() {
    -        return BonitaHomeServer.getInstance();
         }
     
         @Override
    @@ -174,6 +169,27 @@ public void stop() {
     
         @Override
         public boolean isAuthorized(APICallContext apiCallContext) throws SExecutionException {
    +        if (dynamicPermissionCheck.isEnabled()) {
    +            // Check if there is an active dynamic permission for this resource:
    +            final Set<String> resourceDynamicPermissions = getDeclaredPermissions(apiCallContext.getApiName(),
    +                    apiCallContext.getResourceName(), apiCallContext.getMethod(), apiCallContext.getResourceId(),
    +                    dynamicPermissionsChecks);
    +            if (!resourceDynamicPermissions.isEmpty()) {
    +                // if there is a dynamic rule, use it to check the permissions
    +                if (log.isTraceEnabled()) {
    +                    log.trace("Dynamic REST API permissions check");
    +                }
    +                return isAuthorizedByDynamicPermissions(apiCallContext,
    +                        getSession().getUserPermissions(),
    +                        resourceDynamicPermissions);
    +            }
    +        }
    +        // if there is no dynamic rule, use the static permissions
    +        return isAuthorizedByStaticPermissions(apiCallContext);
    +    }
    +
    +    protected boolean isAuthorizedByStaticPermissions(APICallContext apiCallContext)
    +            throws SExecutionException {
             if (log.isTraceEnabled()) {
                 log.trace("Static REST API permissions check");
             }
    @@ -196,6 +212,85 @@ public boolean isAuthorized(APICallContext apiCallContext) throws SExecutionExce
             return false;
         }
     
    +    protected boolean isAuthorizedByDynamicPermissions(APICallContext apiCallContext, Set<String> userPermissions,
    +            Set<String> resourceDynamicPermissions) throws SExecutionException {
    +        checkResourceAuthorizationsSyntax(resourceDynamicPermissions);
    +        if (checkDynamicPermissionsWithProfilesOrUsername(resourceDynamicPermissions, userPermissions)) {
    +            return true;
    +        }
    +        final String resourceClassName = getResourceClassName(resourceDynamicPermissions);
    +        if (resourceClassName != null) {
    +            try {
    +                return checkDynamicPermissionsWithScript(apiCallContext, resourceClassName);
    +            } catch (ClassNotFoundException e) {
    +                throw new SExecutionException(
    +                        "Unable to execute the security rule " + resourceClassName + " for the api call "
    +                                + apiCallContext + " because the class " + resourceClassName + " is not found",
    +                        e);
    +            }
    +        }
    +        return false;
    +    }
    +
    +    protected void checkResourceAuthorizationsSyntax(final Set<String> resourceAuthorizations) {
    +        for (final String resourceAuthorization : resourceAuthorizations) {
    +            if (!resourceAuthorization.matches("(" + USER_TYPE_AUTHORIZATION_PREFIX + "|"
    +                    + PROFILE_TYPE_AUTHORIZATION_PREFIX + "|" + SCRIPT_TYPE_AUTHORIZATION_PREFIX + ")\\|.+")) {
    +                if (log.isWarnEnabled()) {
    +                    log.warn("Error while getting dynamic authorizations. Unknown syntax: " + resourceAuthorization
    +                            + " defined in dynamic-permissions-checks.properties");
    +                }
    +                throw new IllegalArgumentException(
    +                        format("Dynamic permission rule %s is not valid", resourceAuthorization));
    +            }
    +        }
    +    }
    +
    +    protected Set<String> getResourceAuthorizationsForProfileOrUser(final Set<String> resourcePermissions) {
    +        return resourcePermissions.stream()
    +                .filter(item -> item.startsWith(PROFILE_TYPE_AUTHORIZATION_PREFIX + "|")
    +                        || item.startsWith(USER_TYPE_AUTHORIZATION_PREFIX + "|"))
    +                .collect(Collectors.toSet());
    +    }
    +
    +    protected boolean checkDynamicPermissionsWithProfilesOrUsername(final Set<String> resourceAuthorizations,
    +            final Set<String> userPermissions) {
    +        return getResourceAuthorizationsForProfileOrUser(resourceAuthorizations).stream()
    +                .anyMatch(userPermissions::contains);
    +    }
    +
    +    protected boolean checkDynamicPermissionsWithScript(final APICallContext apiCallContext,
    +            final String resourceClassName) throws SExecutionException, ClassNotFoundException {
    +        final boolean authorized = checkAPICallWithScript(resourceClassName, apiCallContext);
    +        if (!authorized) {
    +            if (log.isDebugEnabled()) {
    +                StringBuilder msg = new StringBuilder().append("Unauthorized access to ")
    +                        .append(apiCallContext.getMethod()).append(" ").append(apiCallContext.getApiName())
    +                        .append("/").append(apiCallContext.getResourceName())
    +                        .append(apiCallContext.getResourceId() != null ? "/" + apiCallContext.getResourceId()
    +                                : "")
    +                        .append(", Permission script: ").append(resourceClassName);
    +                msg.append(", attempted by ").append(getSession().getUserName());
    +                log.debug(msg.toString());
    +            }
    +        }
    +        return authorized;
    +    }
    +
    +    protected String getResourceClassName(final Set<String> resourcePermissions) {
    +        String className = null;
    +        for (final String resourcePermission : resourcePermissions) {
    +            if (resourcePermission.startsWith(SCRIPT_TYPE_AUTHORIZATION_PREFIX + "|")) {
    +                className = resourcePermission.substring((SCRIPT_TYPE_AUTHORIZATION_PREFIX + "|").length());
    +            }
    +        }
    +        return className;
    +    }
    +
    +    public Set<String> getResourceDynamicPermissions(final String resourceKey) {
    +        return dynamicPermissionsChecks.getPropertyAsSet(resourceKey);
    +    }
    +
         protected Set<String> getDeclaredPermissions(final String apiName, final String resourceName, final String method,
                 final String resourceQualifiers, final ResourcesPermissionsMapping resourcesPermissionsMapping) {
             List<String> resourceQualifiersIds = null;
    
  • bpm/bonita-core/bonita-process-engine/src/main/resources/bonita-tenant-community.properties+3 0 modified
    @@ -172,3 +172,6 @@ bonita.tenant.authorization.rule.mapping=defaultAuthorizationRuleMapping
     # Too much time elapsed since registration
     #bonita.tenant.work.audit.abnormal.execution.threshold.elapsed_duration_since_registration_amount=30
     #bonita.tenant.work.audit.abnormal.execution.threshold.elapsed_duration_since_registration_unit=MINUTES
    +
    +# Set this value to false to disable any dynamic permissions totally
    +bonita.runtime.authorization.dynamic-check.enabled=true
    
  • bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/api/impl/PermissionAPIImplTest.java+0 34 modified
    @@ -13,15 +13,13 @@
      **/
     package org.bonitasoft.engine.api.impl;
     
    -import static org.assertj.core.api.Assertions.assertThat;
     import static org.junit.Assert.assertThrows;
     import static org.mockito.Mockito.*;
     
     import org.bonitasoft.engine.api.permission.APICallContext;
     import org.bonitasoft.engine.authorization.PermissionService;
     import org.bonitasoft.engine.commons.exceptions.SExecutionException;
     import org.bonitasoft.engine.exception.ExecutionException;
    -import org.bonitasoft.engine.exception.NotFoundException;
     import org.bonitasoft.engine.service.ServiceAccessor;
     import org.junit.Before;
     import org.junit.Test;
    @@ -47,38 +45,6 @@ public void before() {
             apiCallContext = new APICallContext("GET", "identity", "user", "1", "query", "{\"body\":\"value\"}");
         }
     
    -    @Test
    -    public void should_executeSecurityScript_call_the_service() throws Exception {
    -        //given all ok
    -        doReturn(true).when(permissionService).checkAPICallWithScript("myScript", apiCallContext, false);
    -
    -        //when
    -        final boolean isAllowed = permissionAPI.checkAPICallWithScript("myScript", apiCallContext, false);
    -
    -        //then
    -        assertThat(isAllowed).isTrue();
    -    }
    -
    -    @Test
    -    public void should_executeSecurityScript_throw_not_found_exception_when_file_does_not_exists() throws Exception {
    -        doThrow(ClassNotFoundException.class).when(permissionService).checkAPICallWithScript("myScript", apiCallContext,
    -                false);
    -
    -        final String message = assertThrows(NotFoundException.class,
    -                () -> permissionAPI.checkAPICallWithScript("myScript", apiCallContext, false)).getMessage();
    -        assertThat(message).contains("the class myScript is not found");
    -    }
    -
    -    @Test
    -    public void should_executeSecurityScript_throw_execution_exception() throws Exception {
    -        //given
    -        doThrow(SExecutionException.class).when(permissionService).checkAPICallWithScript("myScript", apiCallContext,
    -                false);
    -
    -        assertThrows(ExecutionException.class,
    -                () -> permissionAPI.checkAPICallWithScript("myScript", apiCallContext, false));
    -    }
    -
         @Test
         public void should_isAuthorized_throw_execution_exception() throws Exception {
             //given
    
  • bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/authorization/PermissionServiceConfigurationTest.java+97 0 added
    @@ -0,0 +1,97 @@
    +/**
    + * Copyright (C) 2021 Bonitasoft S.A.
    + * Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble
    + * This library is free software; you can redistribute it and/or modify it under the terms
    + * of the GNU Lesser General Public License as published by the Free Software Foundation
    + * version 2.1 of the License.
    + * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
    + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
    + * See the GNU Lesser General Public License for more details.
    + * You should have received a copy of the GNU Lesser General Public License along with this
    + * program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth
    + * Floor, Boston, MA 02110-1301, USA.
    + **/
    +package org.bonitasoft.engine.authorization;
    +
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static org.mockito.Mockito.mock;
    +
    +import org.bonitasoft.engine.authorization.properties.CompoundPermissionsMapping;
    +import org.bonitasoft.engine.authorization.properties.CustomPermissionsMapping;
    +import org.bonitasoft.engine.authorization.properties.DynamicPermissionsChecks;
    +import org.bonitasoft.engine.authorization.properties.ResourcesPermissionsMapping;
    +import org.bonitasoft.engine.classloader.ClassLoaderService;
    +import org.bonitasoft.engine.session.SessionService;
    +import org.bonitasoft.engine.sessionaccessor.SessionAccessor;
    +import org.junit.Test;
    +import org.junit.runner.RunWith;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.ComponentScan;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.test.context.ContextConfiguration;
    +import org.springframework.test.context.TestPropertySource;
    +import org.springframework.test.context.junit4.SpringRunner;
    +
    +@RunWith(SpringRunner.class)
    +@ContextConfiguration(classes = { PermissionServiceConfigurationTest.TestConfiguration.class })
    +@TestPropertySource(properties = {
    +        "tenantId=1",
    +        "bonita.runtime.authorization.dynamic-check.enabled=false" })
    +public class PermissionServiceConfigurationTest {
    +
    +    @Autowired
    +    protected PermissionServiceImpl permissionService;
    +
    +    @Test
    +    public void should_dynamic_check_property_use_specific_property_value_if_overwritten() {
    +        assertThat(permissionService.dynamicPermissionCheck.isEnabled()).isFalse();
    +    }
    +
    +    @ComponentScan({
    +            "org.bonitasoft.engine.authorization" // for Spring to find PermissionService
    +    })
    +    @Configuration
    +    public static class TestConfiguration {
    +
    +        @Bean
    +        ClassLoaderService classLoaderService() {
    +            return mock(ClassLoaderService.class);
    +        }
    +
    +        @Bean
    +        SessionAccessor sessionAccessor() {
    +            return mock(SessionAccessor.class);
    +        }
    +
    +        @Bean
    +        SessionService sessionService() {
    +            return mock(SessionService.class);
    +        }
    +
    +        @Bean
    +        PermissionsBuilder permissionsBuilder() {
    +            return mock(PermissionsBuilder.class);
    +        }
    +
    +        @Bean
    +        CompoundPermissionsMapping compoundPermissionsMapping() {
    +            return mock(CompoundPermissionsMapping.class);
    +        }
    +
    +        @Bean
    +        ResourcesPermissionsMapping resourcesPermissionsMapping() {
    +            return mock(ResourcesPermissionsMapping.class);
    +        }
    +
    +        @Bean
    +        CustomPermissionsMapping customPermissionsMapping() {
    +            return mock(CustomPermissionsMapping.class);
    +        }
    +
    +        @Bean
    +        DynamicPermissionsChecks dynamicPermissionsChecks() {
    +            return mock(DynamicPermissionsChecks.class);
    +        }
    +    }
    +}
    
  • bpm/bonita-core/bonita-process-engine/src/test/java/org/bonitasoft/engine/authorization/PermissionServiceImplTest.java+258 182 modified
    @@ -14,41 +14,36 @@
     package org.bonitasoft.engine.authorization;
     
     import static org.assertj.core.api.Assertions.assertThat;
    -import static org.hamcrest.CoreMatchers.containsString;
    -import static org.mockito.ArgumentMatchers.any;
    -import static org.mockito.ArgumentMatchers.anyLong;
    -import static org.mockito.ArgumentMatchers.eq;
    +import static org.junit.Assert.assertThrows;
    +import static org.mockito.ArgumentMatchers.*;
     import static org.mockito.Mockito.*;
    +import static org.mockito.Mockito.anySet;
    +import static org.mockito.Mockito.anyString;
     
    -import java.io.File;
     import java.io.IOException;
    -import java.nio.charset.Charset;
     import java.util.*;
     
    -import org.apache.commons.io.FileUtils;
    -import org.bonitasoft.engine.api.impl.APIAccessorImpl;
     import org.bonitasoft.engine.api.permission.APICallContext;
     import org.bonitasoft.engine.authorization.properties.CompoundPermissionsMapping;
     import org.bonitasoft.engine.authorization.properties.CustomPermissionsMapping;
    +import org.bonitasoft.engine.authorization.properties.DynamicPermissionsChecks;
     import org.bonitasoft.engine.authorization.properties.ResourcesPermissionsMapping;
     import org.bonitasoft.engine.classloader.ClassLoaderService;
     import org.bonitasoft.engine.classloader.SClassLoaderException;
     import org.bonitasoft.engine.commons.exceptions.SBonitaException;
     import org.bonitasoft.engine.commons.exceptions.SExecutionException;
     import org.bonitasoft.engine.exception.BonitaHomeNotSetException;
    -import org.bonitasoft.engine.home.BonitaHomeServer;
     import org.bonitasoft.engine.page.ContentType;
     import org.bonitasoft.engine.session.SSessionNotFoundException;
     import org.bonitasoft.engine.session.SessionService;
     import org.bonitasoft.engine.session.model.SSession;
     import org.bonitasoft.engine.sessionaccessor.SessionAccessor;
    -import org.hamcrest.CoreMatchers;
     import org.junit.Before;
     import org.junit.Rule;
     import org.junit.Test;
    +import org.junit.contrib.java.lang.system.EnvironmentVariables;
    +import org.junit.contrib.java.lang.system.RestoreSystemProperties;
     import org.junit.contrib.java.lang.system.SystemOutRule;
    -import org.junit.rules.ExpectedException;
    -import org.junit.rules.TemporaryFolder;
     import org.junit.runner.RunWith;
     import org.mockito.Mock;
     import org.mockito.junit.MockitoJUnitRunner;
    @@ -60,25 +55,18 @@ public class PermissionServiceImplTest {
     
         @Rule
         public final SystemOutRule systemOutRule = new SystemOutRule().enableLog().muteForSuccessfulTests();
    -    @Rule
    -    public TemporaryFolder temporaryFolder = new TemporaryFolder();
    -    @Rule
    -    public ExpectedException expectedException = ExpectedException.none();
    +
         @Mock
         private ClassLoaderService classLoaderService;
    +
         @Mock
         private SessionAccessor sessionAccessor;
    -    @Mock
    -    private SessionService sessionService;
     
         @Mock
    -    private APIAccessorImpl apiIAccessorImpl;
    +    private SessionService sessionService;
     
         private PermissionServiceImpl permissionService;
     
    -    @Mock
    -    private BonitaHomeServer bonitaHomeServer;
    -
         @Mock
         private SSession session;
     
    @@ -91,23 +79,20 @@ public class PermissionServiceImplTest {
         @Mock
         private CustomPermissionsMapping customPermissionsMapping;
     
    -    private File securityFolder;
    +    @Mock
    +    private DynamicPermissionsChecks dynamicPermissionsChecks;
     
         @Before
         public void before()
                 throws IOException, SClassLoaderException, SSessionNotFoundException, BonitaHomeNotSetException {
    -        securityFolder = temporaryFolder.newFolder("security");
     
             doReturn(Thread.currentThread().getContextClassLoader()).when(classLoaderService)
                     .getClassLoader(any());
             permissionService = spy(
                     new PermissionServiceImpl(classLoaderService, sessionAccessor, sessionService, TENANT_ID,
    -                        compoundPermissionsMapping, resourcesPermissionsMapping, customPermissionsMapping));
    -        doReturn(bonitaHomeServer).when(permissionService).getBonitaHomeServer();
    -        doReturn(apiIAccessorImpl).when(permissionService).createAPIAccessorImpl();
    +                        compoundPermissionsMapping, resourcesPermissionsMapping, customPermissionsMapping,
    +                        dynamicPermissionsChecks, true));
             doReturn(session).when(sessionService).getSession(anyLong());
    -
    -        doReturn(securityFolder).when(bonitaHomeServer).getSecurityScriptsFolder(anyLong());
         }
     
         @Test
    @@ -117,9 +102,9 @@ public void should_start_then_stop_forbid_to_check_api() throws SBonitaException
             permissionService.stop();
     
             //when
    -        expectedException.expect(SExecutionException.class);
    -        expectedException.expectMessage(containsString("not started"));
    -        permissionService.checkAPICallWithScript("plop", new APICallContext(), false);
    +        Throwable throwable = assertThrows(SExecutionException.class,
    +                () -> permissionService.checkAPICallWithScript("plop", new APICallContext()));
    +        assertThat(throwable.getMessage()).contains("not started");
         }
     
         @Test
    @@ -133,142 +118,33 @@ public void should_pause_call_stop_tow_times() {
         public void should_checkAPICallWithScript_throw_exception_if_not_started()
                 throws SExecutionException, ClassNotFoundException {
             //given service not started
    -        expectedException.expect(SExecutionException.class);
    -        expectedException.expectMessage(containsString("not started"));
     
             //when
    -        permissionService.checkAPICallWithScript("plop", new APICallContext(), false);
    +        Throwable throwable = assertThrows(SExecutionException.class,
    +                () -> permissionService.checkAPICallWithScript("plop", new APICallContext()));
    +        assertThat(throwable.getMessage()).contains("not started");
         }
     
         @Test
         public void should_checkAPICallWithScript_with_wrong_class() throws SBonitaException, ClassNotFoundException {
             //given
             permissionService.start();
    -        expectedException.expect(SExecutionException.class);
    -        expectedException.expectMessage(
    -                containsString("does not implements org.bonitasoft.engine.api.permission.PermissionRule"));
    -
    -        //when
    -        permissionService.checkAPICallWithScript(String.class.getName(), new APICallContext(), false);
    -    }
    -
    -    @Test
    -    public void should_checkAPICallWithScript_run_the_class_in_script_folder()
    -            throws SBonitaException, ClassNotFoundException, IOException {
    -        //given
    -        final String methodBody = "        return true\n";
    -        FileUtils.writeStringToFile(new File(securityFolder, "MyCustomRule.groovy"), getRuleContent(methodBody),
    -                Charset.defaultCharset());
    -
    -        systemOutRule.clearLog();
    -        permissionService.start();
    -
    -        //when
    -        final boolean myCustomRule = permissionService.checkAPICallWithScript("MyCustomRule", new APICallContext(),
    -                false);
    -
    -        assertThat(myCustomRule).isTrue();
    -        assertThat(systemOutRule.getLog()).containsPattern("WARN.*.Executing my custom rule");
    -    }
    -
    -    @Test
    -    public void should_checkAPICallWithScript_run_the_class_with_package_in_script_root_folder()
    -            throws SBonitaException, ClassNotFoundException, IOException {
    -        //given
    -        File test = new File(securityFolder, "test");
    -        test.mkdir();
    -        FileUtils.writeStringToFile(new File(test, "MyCustomRule.groovy"),
    -                getRuleContent("test", "        return true\n"), Charset.defaultCharset());
    -
    -        systemOutRule.clearLog();
    -        permissionService.start();
     
             //when
    -        final boolean myCustomRule = permissionService.checkAPICallWithScript("test.MyCustomRule", new APICallContext(),
    -                false);
    -
    -        assertThat(myCustomRule).isTrue();
    -        assertThat(systemOutRule.getLog()).containsPattern("WARN.*.Executing my custom rule");
    -    }
    -
    -    /*
    -     * @Test
    -     * public void perf() throws SBonitaException, ClassNotFoundException, IOException {
    -     * //given
    -     * FileUtils.writeStringToFile(new File(scriptFolder, "MyCustomRule.groovy"), "" +
    -     * "import org.bonitasoft.engine.api.APIAccessor\n" +
    -     * "import org.bonitasoft.engine.api.Logger\n" +
    -     * "import org.bonitasoft.engine.api.permission.APICallContext\n" +
    -     * "import org.bonitasoft.engine.api.permission.PermissionRule\n" +
    -     * "import org.bonitasoft.engine.session.APISession\n" +
    -     * "\n" +
    -     * "class MyCustomRule implements PermissionRule {\n" +
    -     * "    @Override\n" +
    -     * "    boolean isAllowed(APISession apiSession, APICallContext apiCallContext, APIAccessor apiAccessor, Logger logger) {\n"
    -     * +
    -     * "        logger.warning(\"Executing my custom rule\")\n" +
    -     * "        return true\n" +
    -     * "    }\n" +
    -     * "}" +
    -     * "");
    -     * permissionService.start();
    -     * long before = System.nanoTime();
    -     * //when
    -     * for (int i = 0; i < 25000; i++) {
    -     * boolean myCustomRule = permissionService.checkAPICallWithScript("MyCustomRule", new APICallContext(), false);
    -     * assertThat(myCustomRule).isTrue();
    -     * }
    -     * fail("time= "+(System.nanoTime()-before)/250000);
    -     * }
    -     */
    -
    -    @Test
    -    public void should_checkAPICallWithScript_reload_classes() throws Exception {
    -        //given
    -        permissionService.start();
    -        FileUtils.writeStringToFile(new File(securityFolder, "MyCustomRule.groovy"),
    -                getRuleContent("        return true\n"), Charset.defaultCharset());
    -
    -        //when
    -        boolean myCustomRule = permissionService.checkAPICallWithScript("MyCustomRule", new APICallContext(), true);
    -
    -        assertThat(myCustomRule).isTrue();
    -        FileUtils.writeStringToFile(new File(securityFolder, "MyCustomRule.groovy"),
    -                getRuleContent("        return false\n"), Charset.defaultCharset());
    -
    -        myCustomRule = permissionService.checkAPICallWithScript("MyCustomRule", new APICallContext(), true);
    -
    -        assertThat(myCustomRule).isFalse();
    -        verify(bonitaHomeServer, times(3)).getSecurityScriptsFolder(TENANT_ID);
    -
    -    }
    -
    -    @Test
    -    public void should_checkAPICallWithScript_that_throw_exception() throws Exception {
    -        //given
    -        FileUtils.writeStringToFile(new File(securityFolder, "MyCustomRule.groovy"),
    -                getRuleContent("        throw new RuntimeException()\n"),
    -                Charset.defaultCharset());
    -
    -        permissionService.start();
    -
    -        expectedException.expect(SExecutionException.class);
    -        expectedException.expectCause(CoreMatchers.<Throwable> instanceOf(RuntimeException.class));
    -        //when
    -        permissionService.checkAPICallWithScript("MyCustomRule", new APICallContext(), false);
    -
    -        //then
    -        verify(bonitaHomeServer).getSecurityScriptsFolder(TENANT_ID);
    +        Throwable throwable = assertThrows(SExecutionException.class,
    +                () -> permissionService.checkAPICallWithScript(String.class.getName(), new APICallContext()));
    +        assertThat(throwable.getMessage())
    +                .contains("does not implements org.bonitasoft.engine.api.permission.PermissionRule");
         }
     
         @Test
         public void should_checkAPICallWithScript_with_unknown_class() throws SBonitaException, ClassNotFoundException {
             //given
             permissionService.start();
    -        expectedException.expect(ClassNotFoundException.class);
     
             //when
    -        permissionService.checkAPICallWithScript("plop", new APICallContext(), false);
    +        assertThrows(ClassNotFoundException.class,
    +                () -> permissionService.checkAPICallWithScript("plop", new APICallContext()));
         }
     
         @Test
    @@ -362,43 +238,177 @@ public void should_isAllowed_work_on_resource_with_wildcard() throws Exception {
             assertThat(isAuthorized).isTrue();
         }
     
    -    private void returnPermissionsFor(final String method, final String apiName, final String resourceName,
    -            final List<String> resourceQualifiers,
    -            final List<String> toBeReturned) {
    -        if (resourceQualifiers != null) {
    -            doReturn(new HashSet<>(toBeReturned)).when(resourcesPermissionsMapping).getResourcePermissions(method,
    -                    apiName, resourceName,
    -                    resourceQualifiers);
    -        } else {
    -            doReturn(new HashSet<>(toBeReturned)).when(resourcesPermissionsMapping).getResourcePermissions(method,
    -                    apiName, resourceName);
    -        }
    +    @Test
    +    public void isAuthorized_should_call_script_when_declared() throws Exception {
    +        returnDynamicPermissionsFor("GET", "bpm", "case", null, List.of("check|className"));
    +        final APICallContext apiCallContext = new APICallContext("GET", "bpm", "case", null, "", "");
    +        doReturn(true).when(permissionService).checkAPICallWithScript("className", apiCallContext);
    +
    +        final boolean isAuthorized = permissionService.isAuthorized(apiCallContext);
    +
    +        assertThat(isAuthorized).isTrue();
    +        verify(permissionService).checkAPICallWithScript("className", apiCallContext);
         }
     
    -    private String getRuleContent(String methodBody) {
    -        return getRuleContent(null, methodBody);
    +    @Test
    +    public void isAuthorized_should_return_false_when_script_returns_false() throws Exception {
    +        returnDynamicPermissionsFor("GET", "bpm", "case", null, List.of("check|className"));
    +        final APICallContext apiCallContext = new APICallContext("GET", "bpm", "case", null, "", "");
    +        doReturn(false).when(permissionService).checkAPICallWithScript("className", apiCallContext);
    +
    +        final boolean isAuthorized = permissionService.isAuthorized(apiCallContext);
    +
    +        assertThat(isAuthorized).isFalse();
    +        verify(permissionService).checkAPICallWithScript("className", apiCallContext);
         }
     
    -    private String getRuleContent(String packageName, String methodBody) {
    -        StringBuilder content = new StringBuilder();
    -        if (packageName != null) {
    -            content.append("package ").append(packageName).append(";\n");
    +    @Test
    +    public void isAuthorized_should_return_false_when_script_execution_fails() throws Exception {
    +        returnDynamicPermissionsFor("GET", "bpm", "case", null, List.of("check|className"));
    +        final APICallContext apiCallContext = new APICallContext("GET", "bpm", "case", null, "", "");
    +        doThrow(ClassNotFoundException.class).when(permissionService).checkAPICallWithScript("className",
    +                apiCallContext);
    +
    +        assertThrows(SExecutionException.class,
    +                () -> permissionService.isAuthorized(apiCallContext));
    +    }
     
    -        }
    -        content.append("import org.bonitasoft.engine.api.APIAccessor\n")
    -                .append("import org.bonitasoft.engine.api.Logger\n")
    -                .append("import org.bonitasoft.engine.api.permission.APICallContext\n")
    -                .append("import org.bonitasoft.engine.api.permission.PermissionRule\n")
    -                .append("import org.bonitasoft.engine.session.APISession\n")
    -                .append("\n")
    -                .append("class MyCustomRule implements PermissionRule {\n")
    -                .append("    @Override\n")
    -                .append("    boolean isAllowed(APISession apiSession, APICallContext apiCallContext, APIAccessor apiAccessor, Logger logger) {\n")
    -                .append("        logger.warning(\"Executing my custom rule\")\n")
    -                .append(methodBody)
    -                .append("    }\n")
    -                .append("}");
    -        return content.toString();
    +    @Test
    +    public void isAuthorized_should_check_static_permissions_if_no_script_declared() throws Exception {
    +        //given
    +        doReturn(false).when(permissionService).isAuthorizedByStaticPermissions(any(APICallContext.class));
    +        final APICallContext apiCallContext = new APICallContext("GET", "bpm", "case", null, "", "");
    +
    +        // when:
    +        final boolean authorized = permissionService.isAuthorized(apiCallContext);
    +
    +        // then:
    +        assertThat(authorized).isFalse();
    +        verify(permissionService).isAuthorizedByStaticPermissions(eq(apiCallContext));
    +    }
    +
    +    @Test
    +    public void isAuthorized_should_return_true_with_profile_check() throws Exception {
    +        final String sessionUserPermissions = "profile|admin";
    +        returnPermissionsFor("GET", "bpm", "case", null, List.of(sessionUserPermissions, "check|className"));
    +        final APICallContext apiCallContext = new APICallContext("GET", "bpm", "case", null, "", "");
    +        returnUserPermissionsFromSession(sessionUserPermissions);
    +
    +        final boolean isAuthorized = permissionService.isAuthorized(apiCallContext);
    +
    +        assertThat(isAuthorized).isTrue();
    +        verify(permissionService, never()).checkDynamicPermissionsWithScript(any(), any());
    +    }
    +
    +    @Test
    +    public void isAuthorized_should_return_true_with_user_check() throws Exception {
    +        // given
    +        final String userPermissions = "user|Juan-Carlos";
    +        returnPermissionsFor("GET", "bpm", "case", null, List.of(userPermissions, "check|className"));
    +        returnUserPermissionsFromSession(userPermissions);
    +
    +        // when
    +        final boolean isAuthorized = permissionService.isAuthorized(
    +                new APICallContext("GET", "bpm", "case", null, "", ""));
    +
    +        // then
    +        assertThat(isAuthorized).isTrue();
    +        verify(permissionService, never()).checkDynamicPermissionsWithScript(any(), anyString());
    +    }
    +
    +    @Test
    +    public void isAuthorized_should_work_with_default_rule() throws Exception {
    +        // given
    +        long userId = 10L;
    +        doReturn(userId).when(session).getUserId();
    +        final List<String> dynamicAuthorizations = List.of("check|org.bonitasoft.permissions.UserPermissionRule");
    +        returnDynamicPermissionsFor("GET", "identity", "user", null, dynamicAuthorizations);
    +
    +        // when
    +        permissionService.start();
    +        final boolean isAuthorizedOnAllUsers = permissionService.isAuthorized(
    +                new APICallContext("GET", "identity", "user", null, "", ""));
    +
    +        // then
    +        assertThat(isAuthorizedOnAllUsers).isFalse();
    +
    +        // when
    +        final boolean isAuthorizedOnCurrentUser = permissionService.isAuthorized(
    +                new APICallContext("GET", "identity", "user", String.valueOf(userId), "", ""));
    +
    +        // then
    +        assertThat(isAuthorizedOnCurrentUser).isTrue();
    +    }
    +
    +    @Test
    +    public void isAuthorized_should_throw_exception_if_the_script_is_not_found() throws Exception {
    +        returnDynamicPermissionsFor("GET", "bpm", "case", null, List.of("check|className"));
    +        final APICallContext apiCallContext = new APICallContext("GET", "bpm", "case", null, "", "");
    +        doThrow(ClassNotFoundException.class).when(permissionService).checkAPICallWithScript("className",
    +                apiCallContext);
    +
    +        assertThrows(SExecutionException.class,
    +                () -> permissionService.isAuthorized(apiCallContext));
    +    }
    +
    +    @Test
    +    public void isAuthorized_should_throw_exception_if_syntax_is_invalid() {
    +        returnDynamicPermissionsFor("GET", "bpm", "case", null, List.of("anyText"));
    +        final APICallContext apiCallContext = new APICallContext("GET", "bpm", "case", null, "", "");
    +
    +        assertThrows(IllegalArgumentException.class,
    +                () -> permissionService.isAuthorized(apiCallContext));
    +    }
    +
    +    @Test
    +    public void checkResourceAuthorizationsSyntax_should_throw_exception_if_syntax_is_invalid() {
    +        final Set<String> resourceAuthorizations = new HashSet<>();
    +        resourceAuthorizations.add("any string");
    +
    +        assertThrows(IllegalArgumentException.class,
    +                () -> permissionService.checkResourceAuthorizationsSyntax(resourceAuthorizations));
    +    }
    +
    +    @Test
    +    public void checkResourceAuthorizationsSyntax_should_not_throw_exception_if_syntax_is_valid() {
    +        final Set<String> resourceAuthorizations = Set.of("user|any.username", "profile|any.profile",
    +                "check|className");
    +
    +        permissionService.checkResourceAuthorizationsSyntax(resourceAuthorizations);
    +
    +        // no exception
    +    }
    +
    +    @Test
    +    public void should_checkPermissions_return_true_if_dynamic_authorized() throws Exception {
    +        //given
    +        final List<String> dynamicAuthorizations = List.of("check|className");
    +        returnDynamicPermissionsFor("GET", "bpm", "case", null, dynamicAuthorizations);
    +        final APICallContext apiCallContext = new APICallContext("GET", "bpm", "case", null, "", "");
    +        doReturn(true).when(permissionService).isAuthorizedByDynamicPermissions(eq(apiCallContext),
    +                anySet(), anySet());
    +
    +        //when
    +        final boolean isAuthorized = permissionService.isAuthorized(apiCallContext);
    +
    +        //then
    +        assertThat(isAuthorized).isTrue();
    +    }
    +
    +    @Test
    +    public void should_checkPermissions_return_false_if_dynamic_not_authorized() throws Exception {
    +        //given
    +        final List<String> dynamicAuthorizations = List.of("check|className");
    +        returnDynamicPermissionsFor("GET", "bpm", "case", null, dynamicAuthorizations);
    +        final APICallContext apiCallContext = new APICallContext("GET", "bpm", "case", null, "", "");
    +        doReturn(false).when(permissionService).isAuthorizedByDynamicPermissions(eq(apiCallContext),
    +                anySet(), anySet());
    +
    +        //when
    +        final boolean isAuthorized = permissionService.isAuthorized(apiCallContext);
    +
    +        //then
    +        assertThat(isAuthorized).isFalse();
         }
     
         @Test
    @@ -563,4 +573,70 @@ public void should_return_permissions_associated_with_the_resource() {
             assertThat(permissionService.getResourcePermissions("GET|bpm/case")).containsExactlyInAnyOrder("CasePermission",
                     "MyPermission");
         }
    +
    +    @Rule
    +    public RestoreSystemProperties restoreSystemProperties = new RestoreSystemProperties();
    +
    +    @Rule
    +    public EnvironmentVariables envVar = new EnvironmentVariables();
    +
    +    @Test
    +    public void initDynamicPermissionsEnabledProperty_should_take_System_property_if_set() {
    +        // given:
    +        System.setProperty("bonita.runtime.authorization.dynamic-check.enabled", "false");
    +        envVar.set("bonita.runtime.authorization.dynamic-check.enabled", "true");
    +
    +        // when:
    +        final boolean property = permissionService.initDynamicPermissionsEnabledProperty(true).isEnabled();
    +
    +        // then:
    +        assertThat(property).isFalse();
    +    }
    +
    +    @Test
    +    public void initDynamicPermissionsEnabledProperty_should_take_envVar_if_no_System_property_if_set() {
    +        // given:
    +        envVar.set("BONITA_RUNTIME_AUTHORIZATION_DYNAMICCHECK_ENABLED", "false");
    +
    +        // when:
    +        final boolean property = permissionService.initDynamicPermissionsEnabledProperty(true).isEnabled();
    +
    +        // then:
    +        assertThat(property).isFalse();
    +    }
    +
    +    @Test
    +    public void initDynamicPermissionsEnabledProperty_should_take_file_property_if_no_System__nor_env_property_if_set() {
    +        // when:
    +        final boolean property = permissionService.initDynamicPermissionsEnabledProperty(false).isEnabled();
    +
    +        // then:
    +        assertThat(property).isFalse();
    +    }
    +
    +    private void returnPermissionsFor(final String method, final String apiName, final String resourceName,
    +            final List<String> resourceQualifiers,
    +            final List<String> toBeReturned) {
    +        returnPermissionsFor(method, apiName, resourceName, resourceQualifiers, toBeReturned,
    +                resourcesPermissionsMapping);
    +    }
    +
    +    private void returnDynamicPermissionsFor(final String method, final String apiName, final String resourceName,
    +            final List<String> resourceQualifiers,
    +            final List<String> toBeReturned) {
    +        returnPermissionsFor(method, apiName, resourceName, resourceQualifiers, toBeReturned, dynamicPermissionsChecks);
    +    }
    +
    +    private void returnPermissionsFor(final String method, final String apiName, final String resourceName,
    +            final List<String> resourceQualifiers,
    +            final List<String> toBeReturned, final ResourcesPermissionsMapping resourcesPermissionsMapping) {
    +        if (resourceQualifiers != null) {
    +            doReturn(new HashSet<>(toBeReturned)).when(resourcesPermissionsMapping).getResourcePermissions(method,
    +                    apiName, resourceName,
    +                    resourceQualifiers);
    +        } else {
    +            doReturn(new HashSet<>(toBeReturned)).when(resourcesPermissionsMapping).getResourcePermissions(method,
    +                    apiName, resourceName);
    +        }
    +    }
     }
    
  • platform/platform-resources/src/main/resources/tenant_template_security_scripts/SamplePermissionRule.groovy.sample+0 55 removed
    @@ -1,55 +0,0 @@
    -/**
    - * Copyright (C) 2017 Acme Corp
    - * ...
    - **/
    -
    -// Ensure this .groovy class uses the default package declaration.
    -// Ensure this .groovy file is placed in the default directory, under 'initial/tenant_template_security_scripts/' or
    -//                                                   under 'current/tenants/TENANT_ID/tenant_security_scripts/'
    -// Then map your custom rule in 'dynamic-permissions-checks-custom.properties' file by writing something like:
    -// GET|bpm/caseVariable=[profile|User, check|SamplePermissionRule]
    -
    -import org.bonitasoft.engine.api.APIAccessor
    -import org.bonitasoft.engine.api.Logger
    -import org.bonitasoft.engine.api.permission.APICallContext
    -import org.bonitasoft.engine.api.permission.PermissionRule
    -import org.bonitasoft.engine.session.APISession
    -import org.bonitasoft.permissions.CaseVariablePermissionRule
    -
    -/**
    - * This is a code sample of a Groovy Permission rule.
    - *
    - * Let a user get and update a variable of a case only if he is the process supervisor (i.e. manager),
    - * or for other Business conditions... (to be implemented by the final Customer).
    - * This script reuses logic from superclass ActorPermissionRule, and enriches the
    - * behavior by also allowing other users under certain conditions...
    - * Find more examples of default scripts provided by Bonitasoft at
    - * https://github.com/bonitasoft/bonita-engine/tree/master/bpm/bonita-core/bonita-process-engine/src/main/groovy/org/bonitasoft/permissions
    - *
    - * A permission rule must inherit from an existing rule, or implement the PermissionRule interface, by overriding the isAllowed() method.
    - *
    - * @author John Doe
    - */
    -class SamplePermissionRule extends CaseVariablePermissionRule {
    -
    -    @Override
    -    public boolean isAllowed(APISession apiSession, APICallContext apiCallContext, APIAccessor apiAccessor, Logger logger) {
    -        boolean actorAllowed = super.isAllowed(apiSession, apiCallContext, apiAccessor, logger)
    -
    -        // If superclass CaseVariablePermissionRule returned false, check additional business rules from my company:
    -        return actorAllowed || someExtraBusinessChecks(...)
    -
    -    }
    -
    -    private boolean someExtraBusinessChecks(APIAccessor apiAccessor, String actorId, long currentUserId) {
    -        // return custom business checks...
    -        try {
    -            ...
    -        } catch()
    -            ...
    -            // in case of exception, decide whether to return true (grant access), false (deny access), or throw exception.
    -            // if you decide to throw exception, the web layer will log the exception and deny access automatically.
    -        }
    -    }
    -
    -}
    
  • platform/platform-setup/src/test/java/org/bonitasoft/platform/setup/PlatformSetupIT.java+1 37 modified
    @@ -178,41 +178,6 @@ public void init_method_should_store_platform_portal_resources_from_classpath()
             assertThat(rows.get(2).get("RESOURCE_NAME")).isEqualTo("security-config.properties");
         }
     
    -    @Test
    -    public void init_method_should_store_sample_security_script_from_classpath() throws Exception {
    -        //when
    -        platformSetup.init();
    -        //then
    -        List<Map<String, Object>> rows = jdbcTemplate
    -                .queryForList(
    -                        "SELECT * FROM CONFIGURATION WHERE content_type= '"
    -                                + ConfigurationType.TENANT_TEMPLATE_SECURITY_SCRIPTS + "' ORDER BY resource_name");
    -        assertThat(rows).hasSize(1);
    -        assertThat(rows.get(0).get("RESOURCE_NAME")).isEqualTo("SamplePermissionRule.groovy.sample");
    -    }
    -
    -    @Test
    -    public void init_method_should_store_security_scripts_from_folder_if_exists() throws Exception {
    -        //when
    -        final Path confFolder = temporaryFolder.newFolder().toPath();
    -        configurationFolderUtil.buildPlatformEngineFolder(confFolder);
    -        configurationFolderUtil.buildSqlFolder(confFolder, dbVendor);
    -
    -        System.setProperty(BONITA_SETUP_FOLDER, confFolder.toString());
    -
    -        FileUtils.write(confFolder.resolve(PLATFORM_CONF_FOLDER_NAME).resolve("initial")
    -                .resolve("tenant_template_security_scripts")
    -                .resolve("SomeCustomScript.groovy").toFile(), "custom content", Charset.defaultCharset());
    -        platformSetup.init();
    -        //then
    -        List<Map<String, Object>> rows = jdbcTemplate
    -                .queryForList(
    -                        "SELECT * FROM CONFIGURATION WHERE content_type= '"
    -                                + ConfigurationType.TENANT_TEMPLATE_SECURITY_SCRIPTS + "' ORDER BY resource_name");
    -        assertThat(rows).hasSize(1);
    -        assertThat(rows.get(0).get("RESOURCE_NAME")).isEqualTo("SomeCustomScript.groovy");
    -    }
    -
         @Test
         public void should_extract_configuration() throws Exception {
             final File destinationFolder = temporaryFolder.newFolder("setup");
    @@ -250,8 +215,7 @@ public void should_extract_configuration() throws Exception {
                     "custom-permissions-mapping.properties",
                     "resources-permissions-mapping.properties",
                     "resources-permissions-mapping-custom.properties",
    -                "resources-permissions-mapping-internal.properties",
    -                "SamplePermissionRule.groovy.sample");
    +                "resources-permissions-mapping-internal.properties");
         }
     
         @Test
    
  • services/bonita-authorization/src/main/java/org/bonitasoft/engine/authorization/PermissionService.java+0 16 modified
    @@ -29,22 +29,6 @@ public interface PermissionService extends TenantLifecycleService {
         public String PROFILE_TYPE_AUTHORIZATION_PREFIX = "profile";
         public String SCRIPT_TYPE_AUTHORIZATION_PREFIX = "check";
     
    -    /**
    -     * execute the {@link org.bonitasoft.engine.api.permission.PermissionRule} having the class name in parameter using
    -     * the given context
    -     *
    -     * @param className
    -     *        the class name of the rule to execute
    -     * @param context
    -     *        the context of the api call to check
    -     * @param reload
    -     *        reload class when calling this method, warning if some class were called with reload set to false, they
    -     *        will never be reloadable
    -     * @return true if the security script allows the user to make the api call
    -     */
    -    boolean checkAPICallWithScript(String className, APICallContext context, boolean reload)
    -            throws SExecutionException, ClassNotFoundException;
    -
         boolean isAuthorized(APICallContext apiCallContext) throws SExecutionException;
     
         void addPermissions(String pageName, Properties pageProperties);
    
  • services/bonita-authorization/src/main/java/org/bonitasoft/engine/authorization/properties/ConfigurationFile.java+28 12 modified
    @@ -33,11 +33,11 @@ public abstract class ConfigurationFile {
     
         protected static final String CONFIGURATION_FILES_CACHE = "CONFIGURATION_FILES_CACHE";
     
    -    private final long tenantId;
    +    protected final long tenantId;
     
         private final String propertiesFilename;
     
    -    private final String cacheKey;
    +    protected final String cacheKey;
     
         protected boolean setKeysToLowerCase;
     
    @@ -60,13 +60,29 @@ public ConfigurationFile(long tenantId, CacheService cacheService,
             this.setKeysToLowerCase = setKeysToLowerCase;
         }
     
    -    private Properties readPropertiesFromDatabaseAndStoreThemInCache() {
    +    protected Properties readPropertiesAndStoreThemInCache() {
    +        return readPropertiesFromDatabaseAndStoreThemInCache();
    +    }
    +
    +    protected Properties readPropertiesFromDatabaseAndStoreThemInCache() {
             Properties tenantProperties = configurationFilesManager.getTenantProperties(propertiesFilename, tenantId,
                     setKeysToLowerCase);
             storePropertiesInCache(tenantProperties);
             return tenantProperties;
         }
     
    +    protected Properties readPropertiesFromClasspathAndStoreThemInCache() {
    +        //Read properties from classpath
    +        Properties classpathProperties = new Properties();
    +        try {
    +            classpathProperties.load(ConfigurationFile.class.getResourceAsStream(getPropertiesFileName()));
    +            storePropertiesInCache(classpathProperties);
    +        } catch (IOException e) {
    +            log.error("Cannot retrieve dynamic authorizations", e);
    +        }
    +        return classpathProperties;
    +    }
    +
         protected abstract String getPropertiesFileName();
     
         abstract protected boolean hasCustomVersion();
    @@ -84,7 +100,7 @@ void storePropertiesInCache(Properties tenantProperties) {
             }
         }
     
    -    Properties getTenantProperties() {
    +    Properties getProperties() {
             Properties properties;
             try {
                 properties = (Properties) cacheService.get(CONFIGURATION_FILES_CACHE, cacheKey);
    @@ -94,13 +110,13 @@ Properties getTenantProperties() {
                 return new Properties(); // Should we return null?
             }
             if (properties == null) {
    -            properties = readPropertiesFromDatabaseAndStoreThemInCache();
    +            properties = readPropertiesAndStoreThemInCache();
             }
             return properties;
         }
     
         public String getProperty(final String propertyName) {
    -        final String propertyValue = getTenantProperties().getProperty(propertyName);
    +        final String propertyValue = getProperties().getProperty(propertyName);
             return propertyValue != null ? propertyValue.trim() : null;
         }
     
    @@ -114,7 +130,7 @@ public void removeProperty(final String propertyName) {
                     throw new IllegalArgumentException(
                             format("File %s cannot be modified directly, as a writable version exists", propertyName));
                 }
    -            final Properties tenantProperties = getTenantProperties();
    +            final Properties tenantProperties = getProperties();
                 if (tenantProperties.remove(propertyName) != null) { // if the property was present
                     storePropertiesInCache(tenantProperties);
                     configurationFilesManager.removeProperty(propertiesFilename, tenantId, propertyName);
    @@ -129,7 +145,7 @@ public void removeCustomProperty(final String propertyName) {
                 if (!hasCustomVersion()) {
                     throw new IllegalArgumentException(format("File %s does not have a -custom version", propertyName));
                 }
    -            final Properties tenantProperties = getTenantProperties();
    +            final Properties tenantProperties = getProperties();
                 // FIXME: is there a risk to remove a property that is not custom, here?
                 if (tenantProperties.remove(propertyName) != null) { // if the property was present
                     storePropertiesInCache(tenantProperties);
    @@ -146,7 +162,7 @@ public void removeInternalProperty(final String propertyName) {
                 if (!hasInternalVersion()) {
                     throw new IllegalArgumentException(format("File %s does not have a -internal version", propertyName));
                 }
    -            final Properties tenantProperties = getTenantProperties();
    +            final Properties tenantProperties = getProperties();
                 // FIXME: is there a risk to remove a property that is not internal, here?
                 if (tenantProperties.remove(propertyName) != null) { // if the property was present
                     storePropertiesInCache(tenantProperties);
    @@ -164,7 +180,7 @@ public void setProperty(final String propertyName, final String propertyValue) {
                     throw new IllegalArgumentException(
                             format("File %s cannot be modified directly, as a writable version exists", propertyName));
                 }
    -            final Properties tenantProperties = getTenantProperties();
    +            final Properties tenantProperties = getProperties();
                 tenantProperties.setProperty(propertyName, propertyValue);
                 storePropertiesInCache(tenantProperties);
                 configurationFilesManager.setProperty(propertiesFilename, tenantId, propertyName,
    @@ -179,7 +195,7 @@ public void setCustomProperty(final String propertyName, final String propertyVa
                 if (!hasCustomVersion()) {
                     throw new IllegalArgumentException(format("File %s does not have a -custom version", propertyName));
                 }
    -            final Properties tenantProperties = getTenantProperties();
    +            final Properties tenantProperties = getProperties();
                 tenantProperties.setProperty(propertyName, propertyValue);
                 storePropertiesInCache(tenantProperties);
                 configurationFilesManager.setProperty(getCustomPropertiesFilename(propertiesFilename),
    @@ -194,7 +210,7 @@ public void setInternalProperty(final String propertyName, final String property
                 if (!hasInternalVersion()) {
                     throw new IllegalArgumentException(format("File %s does not have a -internal version", propertyName));
                 }
    -            final Properties tenantProperties = getTenantProperties();
    +            final Properties tenantProperties = getProperties();
                 tenantProperties.setProperty(propertyName, propertyValue);
                 storePropertiesInCache(tenantProperties);
                 configurationFilesManager.setProperty(getInternalPropertiesFilename(propertiesFilename),
    
  • services/bonita-authorization/src/main/java/org/bonitasoft/engine/authorization/properties/DynamicPermissionsChecks.java+59 0 added
    @@ -0,0 +1,59 @@
    +/**
    + * Copyright (C) 2021 Bonitasoft S.A.
    + * Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble
    + * This library is free software; you can redistribute it and/or modify it under the terms
    + * of the GNU Lesser General Public License as published by the Free Software Foundation
    + * version 2.1 of the License.
    + * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
    + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
    + * See the GNU Lesser General Public License for more details.
    + * You should have received a copy of the GNU Lesser General Public License along with this
    + * program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth
    + * Floor, Boston, MA 02110-1301, USA.
    + **/
    +package org.bonitasoft.engine.authorization.properties;
    +
    +import java.util.Properties;
    +
    +import lombok.extern.slf4j.Slf4j;
    +import org.bonitasoft.engine.cache.CacheService;
    +import org.springframework.beans.factory.annotation.Value;
    +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
    +import org.springframework.core.annotation.Order;
    +import org.springframework.stereotype.Component;
    +
    +/**
    + * @author Anthony Birembaut
    + */
    +@Component
    +@Order(3)
    +@Slf4j
    +@ConditionalOnSingleCandidate(DynamicPermissionsChecks.class)
    +public class DynamicPermissionsChecks extends ResourcesPermissionsMapping {
    +
    +    /**
    +     * Default name of the preferences file
    +     */
    +    public static final String PROPERTIES_FILENAME = "dynamic-permissions-checks.properties";
    +
    +    public DynamicPermissionsChecks(@Value("${tenantId}") long tenantId, CacheService cacheService,
    +            ConfigurationFilesManager configurationFilesManager) {
    +        super(tenantId, cacheService, configurationFilesManager);
    +    }
    +
    +    @Override
    +    protected String getPropertiesFileName() {
    +        return PROPERTIES_FILENAME;
    +    }
    +
    +    @Override
    +    protected boolean hasInternalVersion() {
    +        return false;
    +    }
    +
    +    @Override
    +    protected Properties readPropertiesAndStoreThemInCache() {
    +        return readPropertiesFromClasspathAndStoreThemInCache();
    +    }
    +
    +}
    
  • services/bonita-authorization/src/main/resources/org/bonitasoft/engine/authorization/properties/dynamic-permissions-checks.properties+179 0 added
    @@ -0,0 +1,179 @@
    +# Dynamic checks on resources
    +#
    +# If a dynamic check is defined on a resource it overrides the static check behavior.
    +# dynamic checks are defined like this
    +# <method>|<resource>=[<exclusions>, check <class name of the rule>]
    +# exclusions is a list of elements like this: <type>|<identifier> where type is user or profile and identifier is the username or the profile name.
    +# Special characters like white space must be replaced with their unicode value (For example \u0020 for the white space)
    +#
    +# example: to protect a case to only users that can start the process and to william.jobs, walter.bates and all users having the Administrator or Process manager profile
    +# POST|bpm/case=[user|william.jobs, user|walter.bates, profile|Administrator, profile|Process\u0020manager, check|org.bonitasoft.permissions.CasePermissionRule]
    +#
    +##
    +# rules bellow are included by default in Bonita
    +#
    +# CasePermissionRule
    +# Let a user access only cases that he is involved in and start cases that he can start
    +GET|bpm/case=[profile|Administrator, check|org.bonitasoft.permissions.CasePermissionRule]
    +POST|bpm/case=[profile|Administrator, check|org.bonitasoft.permissions.CasePermissionRule]
    +DELETE|bpm/case=[profile|Administrator, check|org.bonitasoft.permissions.CasePermissionRule]
    +GET|bpm/archivedCase=[profile|Administrator, check|org.bonitasoft.permissions.CasePermissionRule]
    +GET|bpm/caseInfo=[profile|Administrator, check|org.bonitasoft.permissions.CasePermissionRule]
    +GET|bpm/case/*/context=[profile|Administrator, check|org.bonitasoft.permissions.CaseContextPermissionRule]
    +GET|bpm/archivedCase/*/context=[profile|Administrator, check|org.bonitasoft.permissions.CaseContextPermissionRule]
    +
    +
    +# CaseVariablePermissionRule
    +# Let a user get and update a variable of a case only if he is the process owner
    +GET|bpm/caseVariable=[profile|Administrator, check|org.bonitasoft.permissions.CaseVariablePermissionRule]
    +PUT|bpm/caseVariable=[profile|Administrator, check|org.bonitasoft.permissions.CaseVariablePermissionRule]
    +
    +
    +# CommentPermissionRule
    +# Let a user access only comments on cases that he is involved in
    +GET|bpm/comment=[profile|Administrator, check|org.bonitasoft.permissions.CommentPermissionRule]
    +POST|bpm/comment=[profile|Administrator, check|org.bonitasoft.permissions.CommentPermissionRule]
    +GET|bpm/archivedComment=[profile|Administrator, check|org.bonitasoft.permissions.CommentPermissionRule]
    +
    +
    +# DocumentPermissionRule
    +# Let a user access only document on cases that he is involved in
    +GET|bpm/document=[profile|Administrator, check|org.bonitasoft.permissions.DocumentPermissionRule]
    +POST|bpm/document=[profile|Administrator, check|org.bonitasoft.permissions.DocumentPermissionRule]
    +PUT|bpm/document=[profile|Administrator, check|org.bonitasoft.permissions.DocumentPermissionRule]
    +DELETE|bpm/document=[profile|Administrator, check|org.bonitasoft.permissions.DocumentPermissionRule]
    +GET|bpm/archiveddocument=[profile|Administrator, check|org.bonitasoft.permissions.DocumentPermissionRule]
    +GET|bpm/archivedCaseDocument=[profile|Administrator, check|org.bonitasoft.permissions.DocumentPermissionRule]
    +GET|bpm/caseDocument=[profile|Administrator, check|org.bonitasoft.permissions.DocumentPermissionRule]
    +POST|bpm/caseDocument=[profile|Administrator, check|org.bonitasoft.permissions.DocumentPermissionRule]
    +PUT|bpm/caseDocument=[profile|Administrator, check|org.bonitasoft.permissions.DocumentPermissionRule]
    +DELETE|bpm/caseDocument=[profile|Administrator, check|org.bonitasoft.permissions.DocumentPermissionRule]
    +
    +
    +# ProcessPermissionRule
    +# Let the user do get only on processes he deployed or that he supervised
    +GET|bpm/process=[profile|Administrator, check|org.bonitasoft.permissions.ProcessPermissionRule]
    +POST|bpm/process=[profile|Administrator, check|org.bonitasoft.permissions.ProcessPermissionRule]
    +PUT|bpm/process=[profile|Administrator, check|org.bonitasoft.permissions.ProcessPermissionRule]
    +DELETE|bpm/process=[profile|Administrator, check|org.bonitasoft.permissions.ProcessPermissionRule]
    +GET|bpm/process/*/contract=[profile|Administrator, check|org.bonitasoft.permissions.ProcessPermissionRule]
    +GET|bpm/processInfo=[profile|Administrator, check|org.bonitasoft.permissions.ProcessPermissionRule]
    +GET|bpm/diagram=[profile|Administrator, check|org.bonitasoft.permissions.ProcessPermissionRule]
    +POST|bpm/process/*/instantiation=[profile|Administrator, check|org.bonitasoft.permissions.ProcessInstantiationPermissionRule]
    +
    +
    +# ProcessResolutionProblemPermissionRule
    +# Let a user see process resolution problem only if he is process owner
    +GET|bpm/processResolutionProblem=[profile|Administrator, check|org.bonitasoft.permissions.ProcessResolutionProblemPermissionRule]
    +
    +
    +# ProcessConfigurationPermissionRule
    +# Let a user manage process connectors and parameters only if he is process owner
    +GET|bpm/processParameter=[profile|Administrator, check|org.bonitasoft.permissions.ProcessConfigurationPermissionRule]
    +GET|bpm/processConnector=[profile|Administrator, check|org.bonitasoft.permissions.ProcessConfigurationPermissionRule]
    +PUT|bpm/processConnector=[profile|Administrator, check|org.bonitasoft.permissions.ProcessConfigurationPermissionRule]
    +
    +
    +# ProcessConnectorDependencyPermissionRule
    +# Let a user see process connector dependency problem only if he is process owner
    +GET|bpm/processConnectorDependency=[profile|Administrator, check|org.bonitasoft.permissions.ProcessConnectorDependencyPermissionRule]
    +
    +
    +# ActorPermissionRule
    +# Let a user manage actors only if he is process owner
    +GET|bpm/actor=[profile|Administrator, check|org.bonitasoft.permissions.ActorPermissionRule]
    +PUT|bpm/actor=[profile|Administrator, check|org.bonitasoft.permissions.ActorPermissionRule]
    +
    +
    +# ActorMemberPermissionRule
    +# Let a user add an actorMember only if he is process owner
    +GET|bpm/actorMember=[profile|Administrator, check|org.bonitasoft.permissions.ActorMemberPermissionRule]
    +POST|bpm/actorMember=[profile|Administrator, check|org.bonitasoft.permissions.ActorMemberPermissionRule]
    +DELETE|bpm/actorMember=[profile|Administrator, profile|Process\u0020manager, check|org.bonitasoft.permissions.ActorMemberPermissionRule]
    +
    +
    +# ProcessSupervisorPermissionRule
    +# Let a user view and add process only if he is process owner
    +GET|bpm/processSupervisor=[profile|Administrator, check|org.bonitasoft.permissions.ProcessSupervisorPermissionRule]
    +POST|bpm/processSupervisor=[profile|Administrator, check|org.bonitasoft.permissions.ProcessSupervisorPermissionRule]
    +DELETE|bpm/processSupervisor=[profile|Administrator, check|org.bonitasoft.permissions.ProcessSupervisorPermissionRule]
    +
    +
    +# TaskPermissionRule
    +# Let a user access only tasks that are assigned or pending to him
    +GET|bpm/flowNode=[profile|Administrator, check|org.bonitasoft.permissions.TaskPermissionRule]
    +PUT|bpm/flowNode=[profile|Administrator, check|org.bonitasoft.permissions.TaskPermissionRule]
    +GET|bpm/activity=[profile|Administrator, check|org.bonitasoft.permissions.TaskPermissionRule]
    +PUT|bpm/activity=[profile|Administrator, check|org.bonitasoft.permissions.TaskPermissionRule]
    +PUT|bpm/activityReplay=[profile|Administrator, check|org.bonitasoft.permissions.TaskPermissionRule]
    +GET|bpm/task=[profile|Administrator, check|org.bonitasoft.permissions.TaskPermissionRule]
    +PUT|bpm/task=[profile|Administrator, check|org.bonitasoft.permissions.TaskPermissionRule]
    +GET|bpm/humanTask=[profile|Administrator, check|org.bonitasoft.permissions.TaskPermissionRule]
    +PUT|bpm/humanTask=[profile|Administrator, check|org.bonitasoft.permissions.TaskPermissionRule]
    +GET|bpm/userTask=[profile|Administrator, check|org.bonitasoft.permissions.TaskPermissionRule]
    +PUT|bpm/userTask=[profile|Administrator, check|org.bonitasoft.permissions.TaskPermissionRule]
    +GET|bpm/hiddenUserTask=[profile|Administrator, check|org.bonitasoft.permissions.TaskPermissionRule]
    +POST|bpm/hiddenUserTask=[profile|Administrator, check|org.bonitasoft.permissions.TaskPermissionRule]
    +DELETE|bpm/hiddenUserTask=[profile|Administrator, check|org.bonitasoft.permissions.TaskPermissionRule]
    +GET|bpm/manualTask=[profile|Administrator, check|org.bonitasoft.permissions.TaskPermissionRule]
    +POST|bpm/manualTask=[profile|Administrator, check|org.bonitasoft.permissions.TaskPermissionRule]
    +PUT|bpm/manualTask=[profile|Administrator, check|org.bonitasoft.permissions.TaskPermissionRule]
    +GET|bpm/archivedFlowNode=[profile|Administrator, check|org.bonitasoft.permissions.TaskPermissionRule]
    +GET|bpm/archivedActivity=[profile|Administrator, check|org.bonitasoft.permissions.TaskPermissionRule]
    +GET|bpm/archivedTask=[profile|Administrator, check|org.bonitasoft.permissions.TaskPermissionRule]
    +GET|bpm/archivedHumanTask=[profile|Administrator, check|org.bonitasoft.permissions.TaskPermissionRule]
    +GET|bpm/archivedUserTask=[profile|Administrator, check|org.bonitasoft.permissions.TaskPermissionRule]
    +GET|bpm/archivedManualTask=[profile|Administrator, check|org.bonitasoft.permissions.TaskPermissionRule]
    +GET|bpm/archivedUserTask/*/context=[profile|Administrator, check|org.bonitasoft.permissions.TaskExecutionPermissionRule]
    +GET|bpm/userTask/*/context=[profile|Administrator, check|org.bonitasoft.permissions.TaskExecutionPermissionRule]
    +GET|bpm/userTask/*/contract=[profile|Administrator, check|org.bonitasoft.permissions.TaskExecutionPermissionRule]
    +POST|bpm/userTask/*/execution=[profile|Administrator, check|org.bonitasoft.permissions.TaskExecutionPermissionRule]
    +
    +# ConnectorInstancePermissionRule
    +# Let a user see process configuration only if he is process owner
    +GET|bpm/connectorInstance=[profile|Administrator, check|org.bonitasoft.permissions.ConnectorInstancePermissionRule]
    +PUT|bpm/connectorInstance=[profile|Administrator, profile|Process\u0020manager, check|org.bonitasoft.permissions.ConnectorInstancePermissionRule]
    +GET|bpm/archivedConnectorInstance=[profile|Administrator, check|org.bonitasoft.permissions.ConnectorInstancePermissionRule]
    +GET|bpm/connectorFailure=[profile|Administrator, profile|Process\u0020manager]
    +
    +
    +# UserPermissionRule
    +# Let the user access and modify only himself
    +GET|identity/user=[profile|Administrator, profile|Process\u0020manager, check|org.bonitasoft.permissions.UserPermissionRule]
    +POST|identity/user=[profile|Administrator, check|org.bonitasoft.permissions.UserPermissionRule]
    +PUT|identity/user=[profile|Administrator, check|org.bonitasoft.permissions.UserPermissionRule]
    +GET|identity/personalcontactdata=[profile|Administrator, check|org.bonitasoft.permissions.UserPermissionRule]
    +POST|identity/personalcontactdata=[profile|Administrator, check|org.bonitasoft.permissions.UserPermissionRule]
    +PUT|identity/personalcontactdata=[profile|Administrator, check|org.bonitasoft.permissions.UserPermissionRule]
    +GET|identity/professionalcontactdata=[profile|Administrator, check|org.bonitasoft.permissions.UserPermissionRule]
    +POST|identity/professionalcontactdata=[profile|Administrator, check|org.bonitasoft.permissions.UserPermissionRule]
    +PUT|identity/professionalcontactdata=[profile|Administrator, check|org.bonitasoft.permissions.UserPermissionRule]
    +
    +
    +#ProfilePermissionRule
    +# Secure profile related resources
    +GET|portal/profile=[profile|Administrator, check|org.bonitasoft.permissions.ProfilePermissionRule]
    +POST|portal/profile=[profile|Administrator, check|org.bonitasoft.permissions.ProfilePermissionRule]
    +PUT|portal/profile=[profile|Administrator, check|org.bonitasoft.permissions.ProfilePermissionRule]
    +DELETE|portal/profile=[profile|Administrator, check|org.bonitasoft.permissions.ProfilePermissionRule]
    +
    +
    +#ApplicationPermissionRule
    +# Secure application resource
    +GET|living/application=[profile|Administrator, check|org.bonitasoft.permissions.ApplicationPermissionRule]
    +
    +
    +#ApplicationMenuPermissionRule
    +# Secure application menu resource
    +GET|living/application-menu=[profile|Administrator, check|org.bonitasoft.permissions.ApplicationMenuPermissionRule]
    +
    +
    +#Servlets
    +GET|portal/documentDownload=[profile|Administrator, check|org.bonitasoft.permissions.DownloadDocumentPermissionRule]
    +GET|API/documentDownload=[profile|Administrator, check|org.bonitasoft.permissions.DownloadDocumentPermissionRule]
    +GET|portal/custom-page/API/documentDownload=[profile|Administrator, check|org.bonitasoft.permissions.DownloadDocumentPermissionRule]
    +GET|portal/formsDocumentDownload=[profile|Administrator, check|org.bonitasoft.permissions.DownloadDocumentPermissionRule]
    +GET|portal/downloadDocument=[profile|Administrator, check|org.bonitasoft.permissions.DownloadDocumentPermissionRule]
    +GET|portal/formsDocumentImage=[profile|Administrator, check|org.bonitasoft.permissions.DownloadDocumentPermissionRule]
    +GET|API/formsDocumentImage=[profile|Administrator, check|org.bonitasoft.permissions.DownloadDocumentPermissionRule]
    +GET|portal/custom-page/API/formsDocumentImage=[profile|Administrator, check|org.bonitasoft.permissions.DownloadDocumentPermissionRule]
    
  • services/bonita-authorization/src/test/java/org/bonitasoft/engine/authorization/properties/ConfigurationFileTest.java+4 4 modified
    @@ -63,15 +63,15 @@ public static void init() throws Exception {
         public void setupMocksAndSpies() {
             resourcesPermissionsMapping = spy(
                     new ResourcesPermissionsMapping(TENANT_ID, cacheService, configurationFilesManager));
    -        doReturn(resourcesProperties).when(resourcesPermissionsMapping).getTenantProperties();
    +        doReturn(resourcesProperties).when(resourcesPermissionsMapping).getProperties();
     
             customPermissionsMapping = spy(
                     new CustomPermissionsMapping(TENANT_ID, cacheService, configurationFilesManager));
    -        doReturn(customProperties).when(customPermissionsMapping).getTenantProperties();
    +        doReturn(customProperties).when(customPermissionsMapping).getProperties();
     
             compoundPermissionsMapping = spy(
                     new CompoundPermissionsMapping(TENANT_ID, cacheService, configurationFilesManager));
    -        doReturn(compoundProperties).when(compoundPermissionsMapping).getTenantProperties();
    +        doReturn(compoundProperties).when(compoundPermissionsMapping).getProperties();
         }
     
         @Test
    @@ -135,7 +135,7 @@ public void getTenantProperties_should_get_from_cache_and_store_to_cache_if_not_
                     TENANT_ID, false);
     
             // when:
    -        configFile.getTenantProperties();
    +        configFile.getProperties();
     
             // then:
             verify(cacheService).get(CONFIGURATION_FILES_CACHE, "resources-permissions-mapping.properties");
    
  • services/bonita-authorization/src/test/java/org/bonitasoft/engine/authorization/properties/ResourcesPermissionsMappingTest.java+1 1 modified
    @@ -94,7 +94,7 @@ public void testGetResourcePermissionWithWildCard() {
         public ResourcesPermissionsMapping getResourcesPermissionsMapping(final String fileContent) {
             final ResourcesPermissionsMapping resourcesPermissionsMapping = spy(
                     new ResourcesPermissionsMapping(423L, cacheService, configurationFilesManager));
    -        doReturn(getProperties(fileContent.getBytes())).when(resourcesPermissionsMapping).getTenantProperties();
    +        doReturn(getProperties(fileContent.getBytes())).when(resourcesPermissionsMapping).getProperties();
             return resourcesPermissionsMapping;
         }
     }
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.