org.bonitasoft.engine.authorization.PermissionServiceImpl Maven / Gradle / Ivy
The newest version!
/**
* Copyright (C) 2019 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 java.lang.String.format;
import static org.bonitasoft.engine.classloader.ClassLoaderIdentifier.identifier;
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.*;
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.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;
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.bonitasoft.engine.sessionaccessor.SessionIdNotSetException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.stereotype.Component;
/**
* Permission service implementation
*
* @author Baptiste Mesta
*/
@Component
@Slf4j
@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";
public static final String PROPERTY_METHOD_MASK = "%s.method";
public static final String PROPERTY_PATH_TEMPLATE_MASK = "%s.pathTemplate";
public static final String PROPERTY_PERMISSIONS_MASK = "%s.permissions";
public static final String RESOURCE_PERMISSION_KEY_MASK = "%s|extension/%s";
public static final String RESOURCE_PERMISSION_VALUE = "[%s]";
public static final String EXTENSION_SEPARATOR = ",";
private final ClassLoaderService classLoaderService;
private final SessionAccessor sessionAccessor;
private final SessionService sessionService;
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;
public PermissionServiceImpl(final ClassLoaderService classLoaderService, final SessionAccessor sessionAccessor,
final SessionService sessionService,
@Value("${tenantId}") final long tenantId,
CompoundPermissionsMapping compoundPermissionsMapping,
ResourcesPermissionsMapping resourcesPermissionsMapping,
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);
}
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 = getRuleClass(className);
if (!PermissionRule.class.isAssignableFrom(aClass)) {
throw new SExecutionException("The class " + aClass.getName()
+ " does not implements org.bonitasoft.engine.api.permission.PermissionRule");
}
SSession session = getSession();
try {
final APISession apiSession = ModelConvertor.toAPISession(session, null);
final PermissionRule permissionRule = (PermissionRule) aClass.getDeclaredConstructor().newInstance();
return permissionRule.isAllowed(apiSession, context, createAPIAccessorImpl(),
new ServerLoggerWrapper(permissionRule.getClass(), log));
} catch (final Throwable e) {
throw new SExecutionException("The permission rule " + aClass.getName() + " threw an exception", e);
}
}
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());
} catch (SSessionNotFoundException | SessionIdNotSetException e) {
throw new SExecutionException("The session is not set.", e);
}
}
public void reload() throws SExecutionException {
stop();
try {
start();
} catch (SBonitaException e) {
throw new SExecutionException("The permission rule service could not be reloaded", e);
}
}
protected APIAccessorImpl createAPIAccessorImpl() {
return new APIAccessorImpl();
}
private void checkStarted() throws SExecutionException {
if (groovyClassLoader == null) {
throw new SExecutionException("The permission rule service is not started");
}
}
@Override
public void start() throws SBonitaException {
groovyClassLoader = new GroovyClassLoader(
classLoaderService.getClassLoader(identifier(ScopeType.TENANT, tenantId)));
groovyClassLoader.setShouldRecompile(true);
}
@Override
public void stop() {
if (groovyClassLoader != null) {
groovyClassLoader.clearCache();
groovyClassLoader = null;
}
}
@Override
public boolean isAuthorized(APICallContext apiCallContext) throws SExecutionException {
if (dynamicPermissionCheck.isEnabled()) {
// Check if there is an active dynamic permission for this resource:
final Set 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");
}
final Set resourcePermissions = getDeclaredPermissions(apiCallContext.getApiName(),
apiCallContext.getResourceName(), apiCallContext.getMethod(), apiCallContext.getResourceId(),
resourcesPermissionsMapping);
final Set userPermissions = getSession().getUserPermissions();
for (final String resourcePermission : resourcePermissions) {
if (userPermissions.contains(resourcePermission)) {
return true;
}
}
log.debug(
"Unauthorized access to " + apiCallContext.getMethod() + " " + apiCallContext.getApiName() + "/"
+ apiCallContext.getResourceName()
+ (apiCallContext.getResourceId() != null ? "/" + apiCallContext.getResourceId() : "")
+ " attempted by " + getSession().getUserName()
+ ", required permissions: " + resourcePermissions);
return false;
}
protected boolean isAuthorizedByDynamicPermissions(APICallContext apiCallContext, Set userPermissions,
Set 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 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 getResourceAuthorizationsForProfileOrUser(final Set 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 resourceAuthorizations,
final Set 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 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 getResourceDynamicPermissions(final String resourceKey) {
return dynamicPermissionsChecks.getPropertyAsSet(resourceKey);
}
protected Set getDeclaredPermissions(final String apiName, final String resourceName, final String method,
final String resourceQualifiers, final ResourcesPermissionsMapping resourcesPermissionsMapping) {
List resourceQualifiersIds = null;
if (resourceQualifiers != null) {
resourceQualifiersIds = Arrays
.asList(resourceQualifiers.split(ResourcesPermissionsMapping.RESOURCE_IDS_SEPARATOR));
}
Set resourcePermissions = resourcesPermissionsMapping.getResourcePermissions(method, apiName,
resourceName, resourceQualifiersIds);
if (resourcePermissions.isEmpty()) {
resourcePermissions = resourcesPermissionsMapping.getResourcePermissionsWithWildCard(method, apiName,
resourceName, resourceQualifiersIds);
}
if (resourcePermissions.isEmpty()) {
resourcePermissions = resourcesPermissionsMapping.getResourcePermissions(method, apiName, resourceName);
}
return resourcePermissions;
}
@Override
public void addPermissions(final String pageName, final Properties pageProperties) {
Set customPagePermissions = getCustomPagePermissions(
pageProperties.getProperty(RESOURCES_PROPERTY),
resourcesPermissionsMapping);
addRestApiExtensionPermissions(resourcesPermissionsMapping, pageProperties);
addPagePermissions(pageName, pageProperties, customPagePermissions);
}
private void addPagePermissions(String pageName, Properties pageProperties, Set customPagePermissions) {
if (ContentType.PAGE.equals(pageProperties.getProperty(PROPERTY_CONTENT_TYPE))
|| ContentType.LAYOUT.equals(pageProperties.getProperty(PROPERTY_CONTENT_TYPE))) {
compoundPermissionsMapping.setInternalPropertyAsSet(pageName, customPagePermissions);
}
}
@Override
public void removePermissions(Properties pageProperties) {
for (String key : getApiExtensionResourcesPermissionsMapping(pageProperties).keySet()) {
resourcesPermissionsMapping.removeInternalProperty(key);
}
compoundPermissionsMapping.removeInternalProperty(pageProperties.getProperty(PageService.PROPERTIES_NAME));
}
public Set getCustomPagePermissions(final String declaredPageResources,
final ResourcesPermissionsMapping resourcesPermissionsMapping) {
final Set pageRestResources = PropertiesWithSet.stringToSet(declaredPageResources);
final Set permissions = new HashSet<>();
for (final String pageRestResource : pageRestResources) {
final Set resourcePermissions = resourcesPermissionsMapping.getPropertyAsSet(pageRestResource);
if (resourcePermissions.isEmpty()) {
log.warn("Error while getting resources permissions. Unknown resource: {} defined in page.properties",
pageRestResource);
}
permissions.addAll(resourcePermissions);
}
return permissions;
}
void addRestApiExtensionPermissions(final ResourcesPermissionsMapping resourcesPermissionsMapping,
final Properties pageProperties) {
final Map permissionsMapping = getApiExtensionResourcesPermissionsMapping(
pageProperties);
permissionsMapping.keySet()
.forEach(key -> resourcesPermissionsMapping.setInternalProperty(key, permissionsMapping.get(key)));
}
private Map getApiExtensionResourcesPermissionsMapping(Properties pageProperties) {
final Properties propertiesWithSet = new PropertiesWithSet(pageProperties);
final Map permissionsMap = new HashMap<>();
if (ContentType.API_EXTENSION.equals(propertiesWithSet.getProperty(PROPERTY_CONTENT_TYPE))) {
final String apiExtensionList = propertiesWithSet.getProperty(PROPERTY_API_EXTENSIONS);
final String[] apiExtensions = apiExtensionList.split(EXTENSION_SEPARATOR);
for (final String apiExtension : apiExtensions) {
final String method = propertiesWithSet
.getProperty(String.format(PROPERTY_METHOD_MASK, apiExtension.trim()));
String pathTemplate = propertiesWithSet
.getProperty(String.format(PROPERTY_PATH_TEMPLATE_MASK, apiExtension.trim()));
// Remove '/' prefix if declared in page.properties
if (pathTemplate != null && pathTemplate.startsWith("/")) {
pathTemplate = pathTemplate.substring(1);
}
final String permissions = propertiesWithSet
.getProperty(String.format(PROPERTY_PERMISSIONS_MASK, apiExtension.trim()));
permissionsMap.put(String.format(RESOURCE_PERMISSION_KEY_MASK, method, pathTemplate),
String.format(RESOURCE_PERMISSION_VALUE, permissions));
}
}
return permissionsMap;
}
@Override
public Set getResourcePermissions(final String resourceKey) {
return resourcesPermissionsMapping.getPropertyAsSet(resourceKey);
}
public void addCustomEntityPermissions(final String entity, final Set resourcePermissions) {
customPermissionsMapping.setPropertyAsSet(entity, resourcePermissions);
}
public void removeCustomEntityPermissions(String entity) {
customPermissionsMapping.removeProperty(entity);
}
}