Skip to content

Commit

Permalink
fix(REST API authorizations): add default dynamic permissions to comm…
Browse files Browse the repository at this point in the history
…unity (#2869)

* remove deprecated checkAPICallWithScript method from API
* read dynamic authorizations from classpath in community
* fix NPE in user Rule (can happen in tests only)

Covers CVE-59
  • Loading branch information
abirembaut committed Feb 27, 2024
1 parent bfd93f9 commit 1b3ac00
Show file tree
Hide file tree
Showing 21 changed files with 800 additions and 617 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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();
Expand Down
Loading

0 comments on commit 1b3ac00

Please sign in to comment.