Server

Security Policy Service

Updated: March 18, 2024

The Security Policy Service provides an extension point to plug custom security policies that do not rely on the standard ACLs for security. For instance, it can be used to define permissions according to the document metadata, or information about the logged in user.

Watch the related course on Hyland University:
Security Policies on Hyland University.
university_security_policies.png
university_security_policies.png

Security Policy Architecture

A security policy is a class implementing the org.nuxeo.ecm.core.security.SecurityPolicy interface; it is strongly advised to extend org.nuxeo.ecm.core.security.AbstractSecurityPolicy for future compatibility.

The class must be registered through the policies extension point of the org.nuxeo.ecm.core.security.SecurityService component.

A security policy has two important aspects, materialized by different methods of the interface:

  • how security is checked on a given document (method checkPermission),
  • how security is applied to NXQL, CMISQL and Elasticsearch Passthrough searches (methods getQueryTransformer).

Document Security Check

To check security on a given document, Nuxeo Core calls checkPermission with a number of parameters, among which the document, the user and the permission to check, on all the security policies registered. The policies should return Access.DENY or Access.UNKNOWN based on the information provided. If Access.DENY is returned, then access to the document (for the given permission) will be denied. If Access.UNKNOWN is returned, then other policies will be checked. Finally if all policies return Access.UNKNOWN then standard Nuxeo EP ACLs will be checked.

There is a third possible value, Access.GRANT, which can immediately grant access for the given permission, but this bypasses ACL checks. This is all right for most permissions but not for the Browse permission, because Browse is used for NXQL and CMISQL searches in which case it's recommended to implement getQueryTransformer instead (see below).

Note that checkPermission receives a document which is a org.nuxeo.ecm.core.model.Document instance, different from and at a lower level than the usual org.nuxeo.ecm.core.api.DocumentModel manipulated by user code.

NXQL Security Check

All NXQL queries have ACL-based security automatically applied with the Browser permission (except for superusers).

A dedicated security policy can modify this behavior by adding new restrictions on top of the ACLs. This is done by overriding isExpressibleInQuery (it has to return true) and implementing getQueryTransformer.

The getQueryTransformer(repositoryName) method returns a SQLQuery.Transformer instance. The custom SQLQuery.Transformer has to override the transform method, taking as parameter an NXQL query in the form of a org.nuxeo.ecm.core.query.sql.model.SQLQuery Abstract Syntax Tree (AST) and a NuxeoPrincipal. The custom transformer has to manipulate the AST to add new restrictions to implement the security policy. Note that ACL checks will always be applied after this transformation.

Memo
To formalize the security policy, we advice to express it using the following pattern: Given that the current user has these characteristics, We are searching for documents from the original query that in addition are filtered by X criteria.

Unrestricted sessions
If the query has been called in the context of an unrestricted session, the principal will be system. It is a good practice to check for that username since if the query is run unrestrictedly, it functionally means that you should not restrict anything with the query transformer

CMISQL Security Check

Since Nuxeo 5.6.0-HF21 and Nuxeo 5.7.2, all CMISQL queries also require implementation of the relevant getQueryTransformer API in order to secure CMIS-based searches.

The getQueryTransformer(repositoryName, "CMISQL") method returns a SecurityPolicy.QueryTransformer instance, which is a class with one transform method taking a query in the form of String. It should transform this query in order to add whatever restrictions are needed (this will require parsing the CMISQL and adding whatever clauses are needed). Note that ACL checks will always be applied after this transformation.

Elasticsearch Passthrough Check

Nuxeo Elasticsearch Passthrough adds filters to take in account ACL security and security policy that are expressible in NXQL (isExpressibleInQuery returns true).

If you define a custom security policy that is not expressible in NXQL you should not enable the Nuxeo Elasticsearch passthrough.

Example Security Policy Contribution

To register a security policy, you need to write a contribution specifying the class name of your implementation.

<?xml version="1.0"?>
<component name="com.example.myproject.securitypolicy">
  <extension target="org.nuxeo.ecm.core.security.SecurityService"
    point="policies">
    <policy name="myPolicy"
      class="com.example.myproject.NoFileSecurityPolicy" order="0" />
  </extension>
</component>

Here is a sample contributed class:

import static org.nuxeo.ecm.core.query.sql.model.Predicates.noteq;
import static org.nuxeo.ecm.core.query.sql.model.Predicates.and;

import org.nuxeo.ecm.core.api.NuxeoPrincipal;
import org.nuxeo.ecm.core.api.security.ACP;
import org.nuxeo.ecm.core.api.security.Access;
import org.nuxeo.ecm.core.model.Document;
import org.nuxeo.ecm.core.query.sql.NXQL;
import org.nuxeo.ecm.core.query.sql.model.Predicate;
import org.nuxeo.ecm.core.query.sql.model.SQLQuery;
import org.nuxeo.ecm.core.query.sql.model.WhereClause;
import org.nuxeo.ecm.core.security.AbstractSecurityPolicy;
import org.nuxeo.ecm.core.security.SecurityPolicy;

/**
 * Sample policy that denies access to File objects.
 */
public class NoFileSecurityPolicy extends AbstractSecurityPolicy implements SecurityPolicy {

    @Override
    public Access checkPermission(Document doc, ACP mergedAcp,
                                  NuxeoPrincipal principal, String permission,
                                  String[] resolvedPermissions, String[] additionalPrincipals) {
        // Note that doc is NOT a DocumentModel
        if ("File".equals(doc.getType().getName())) {
            return Access.DENY;
        }
        return Access.UNKNOWN;
    }

    @Override
    public boolean isRestrictingPermission(String permission) {
        // could only restrict Browse permission, or others
        return true;
    }

    @Override
    public boolean isExpressibleInQuery(String repositoryName) {
        return true;
    }

    @Override
    public SQLQuery.Transformer getQueryTransformer(String repositoryName) {
        return NO_FILE_TRANSFORMER;
    }

    public static final SQLQuery.Transformer NO_FILE_TRANSFORMER = new NoFileTransformer();

    /**
     * Sample Transformer that adds {@code AND ecm:primaryType <> 'File'} to the query.
     */
    public static class NoFileTransformer implements SQLQuery.Transformer {

        /* {@code ecm:primaryType <> 'File'} */
        public static final Predicate NO_FILE = noteq(NXQL.ECM_PRIMARYTYPE, "File");

        @Override
        public SQLQuery transform(NuxeoPrincipal principal, SQLQuery query) {
            if (!principal.isAdministrator()) {
                WhereClause where = query.where;
                Predicate predicate;
                if (where == null || where.predicate == null) {
                    predicate = NO_FILE;
                } else {
                    // adds an AND ecm:primaryType <> 'File' to the WHERE clause
                    predicate = and(NO_FILE, where.predicate);
                }
                // return query with updated WHERE clause
                return new SQLQuery(query.select, query.from, new WhereClause(predicate),
                        query.groupBy, query.having, query.orderBy, query.limit, query.offset);
            }
            return query;
        }
    }

}

CMISQL Security Checks

To find examples of security policies using CMISQL query transformers, please check the TitleFilteringSecurityPolicy2 in the unit tests.