All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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);
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy