001/*
002 * (C) Copyright 2020 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume
018 */
019package org.nuxeo.ecm.core.model;
020
021import java.util.HashMap;
022import java.util.ListIterator;
023import java.util.Map;
024import java.util.Optional;
025
026import org.apache.logging.log4j.LogManager;
027import org.apache.logging.log4j.Logger;
028import org.nuxeo.ecm.core.api.security.ACE;
029import org.nuxeo.ecm.core.api.security.ACL;
030import org.nuxeo.ecm.core.api.security.ACP;
031import org.nuxeo.ecm.core.api.security.Access;
032import org.nuxeo.ecm.core.api.security.SecurityConstants;
033import org.nuxeo.ecm.core.api.security.impl.ACLImpl;
034import org.nuxeo.ecm.core.api.security.impl.ACPImpl;
035import org.nuxeo.ecm.core.blob.DocumentBlobManager;
036import org.nuxeo.ecm.core.query.QueryFilter;
037import org.nuxeo.runtime.api.Framework;
038import org.nuxeo.runtime.services.config.ConfigurationService;
039
040/**
041 * Common code for VCS and DBS {@link Session} implementations.
042 *
043 * @since 11.3
044 */
045public abstract class BaseSession implements Session<QueryFilter> {
046
047    private static final Logger log = LogManager.getLogger(BaseSession.class);
048
049    /**
050     * Configuration property controlling whether ACLs on versions are disabled.
051     *
052     * @since 11.3
053     */
054    public static final String VERSION_ACL_DISABLED_PROP = "org.nuxeo.version.acl.disabled";
055
056    /**
057     * Configuration property controlling whether ReadVersion permission is disabled.
058     *
059     * @since 11.3
060     */
061    public static final String READ_VERSION_PERM_DISABLED_PROP = "org.nuxeo.version.readversion.disabled";
062
063    /** INTERNAL. How we deal with ACLs on versions. */
064    public enum VersionAclMode {
065        /** Version ACL enabled. */
066        ENABLED,
067        /** Version ACL disabled. */
068        DISABLED,
069        /** Version ACL disabled for direct access but enabled for queries. */
070        LEGACY;
071
072        public static VersionAclMode getConfiguration() {
073            if (!Framework.isInitialized()) {
074                // unit tests
075                return ENABLED;
076            }
077            ConfigurationService configurationService = Framework.getService(ConfigurationService.class);
078            String val = configurationService.getString(VERSION_ACL_DISABLED_PROP).orElse("false");
079            switch (val) {
080            case "false":
081                return ENABLED;
082            case "true":
083                return DISABLED;
084            case "legacy":
085                return LEGACY;
086            default:
087                log.error("Invalid value for configuration property {}: '{}'", VERSION_ACL_DISABLED_PROP, val);
088                return ENABLED;
089            }
090        }
091    }
092
093    public static boolean isReadVersionPermissionDisabled() {
094        if (!Framework.isInitialized()) {
095            // unit tests
096            return false;
097        }
098        ConfigurationService configurationService = Framework.getService(ConfigurationService.class);
099        return configurationService.isBooleanTrue(READ_VERSION_PERM_DISABLED_PROP);
100    }
101
102    protected final Repository repository;
103
104    protected final VersionAclMode versionAclMode;
105
106    protected final boolean disableReadVersionPermission;
107
108    protected BaseSession(Repository repository) {
109        this.repository = repository;
110        versionAclMode = VersionAclMode.getConfiguration();
111        disableReadVersionPermission = isReadVersionPermissionDisabled();
112    }
113
114    protected DocumentBlobManager getDocumentBlobManager() {
115        return Framework.getService(DocumentBlobManager.class);
116    }
117
118    protected void notifyAfterCopy(Document doc) {
119        getDocumentBlobManager().notifyAfterCopy(doc);
120    }
121
122    /*
123     * ----- Common ACP code -----
124     */
125
126    protected void checkNegativeAcl(ACP acp) {
127        if (acp == null || isNegativeAclAllowed()) {
128            return;
129        }
130        for (ACL acl : acp.getACLs()) {
131            if (acl.getName().equals(ACL.INHERITED_ACL)) {
132                continue;
133            }
134            for (ACE ace : acl.getACEs()) {
135                if (ace.isGranted()) {
136                    continue;
137                }
138                String permission = ace.getPermission();
139                if (permission.equals(SecurityConstants.EVERYTHING)
140                        && ace.getUsername().equals(SecurityConstants.EVERYONE)) {
141                    continue;
142                }
143                // allow Write, as we're sure it doesn't include Read/Browse
144                if (permission.equals(SecurityConstants.WRITE)) {
145                    continue;
146                }
147                throw new IllegalArgumentException("Negative ACL not allowed: " + ace);
148            }
149        }
150    }
151
152    /**
153     * Gets the ACP for the document (without any inheritance).
154     *
155     * @param doc the document
156     * @return the ACP
157     */
158    public abstract ACP getACP(Document doc);
159
160    protected ACP getACP(Document doc, boolean replaceReadVersionPermission) {
161        ACP acp = getACP(doc);
162        if (acp != null && replaceReadVersionPermission) {
163            // if ReadVersion is present in an ACE, turn it into a Read
164            acp.replacePermission(SecurityConstants.READ_VERSION, SecurityConstants.READ);
165        }
166        return acp;
167    }
168
169    @Override
170    public ACP getMergedACP(Document doc) {
171        boolean replaceReadVersionPermission = false;
172        if (doc.isVersion()) {
173            replaceReadVersionPermission = !disableReadVersionPermission;
174            if (versionAclMode != VersionAclMode.ENABLED) {
175                doc = doc.getSourceDocument();
176                if (doc == null) {
177                    // version with no live doc
178                    return null;
179                }
180            }
181        }
182        ACP acp = getACP(doc, replaceReadVersionPermission);
183        ACP mergedAcp = acp;
184        ACL inherited = new ACLImpl(ACL.INHERITED_ACL, true); // collected inherited ACEs
185        for (;;) {
186            if (acp != null && acp.getAccess(SecurityConstants.EVERYONE, SecurityConstants.EVERYTHING) == Access.DENY) {
187                // blocking, no need to continue
188                break;
189            }
190            if (doc.isVersion()) {
191                replaceReadVersionPermission = !disableReadVersionPermission;
192                doc = doc.getSourceDocument();
193            } else {
194                doc = doc.getParent();
195            }
196            if (doc == null) {
197                // can't go up
198                break;
199            }
200            // collect inherited ACEs for this level
201            acp = getACP(doc, replaceReadVersionPermission);
202            if (acp != null) {
203                inherited.addAll(acp.getMergedACLs(ACL.INHERITED_ACL));
204            }
205        }
206        if (!inherited.isEmpty()) {
207            if (mergedAcp == null) {
208                mergedAcp = new ACPImpl();
209            }
210            mergedAcp.addACL(inherited);
211        }
212        return mergedAcp;
213    }
214
215    /**
216     * Returns the merge of two ACPs.
217     */
218    protected ACP updateACP(ACP curAcp, ACP addAcp) {
219        if (curAcp == null) {
220            return addAcp;
221        }
222        ACP newAcp = curAcp.clone(); // clone as we may modify ACLs and ACPs
223        Map<String, ACL> acls = new HashMap<>();
224        for (ACL acl : newAcp.getACLs()) {
225            String name = acl.getName();
226            if (ACL.INHERITED_ACL.equals(name)) {
227                throw new IllegalStateException(curAcp.toString());
228            }
229            acls.put(name, acl);
230        }
231        for (ACL acl : addAcp.getACLs()) {
232            String name = acl.getName();
233            if (ACL.INHERITED_ACL.equals(name)) {
234                continue;
235            }
236            ACL curAcl = acls.get(name);
237            if (curAcl != null) {
238                // TODO avoid duplicates
239                curAcl.addAll(acl);
240            } else {
241                newAcp.addACL(acl);
242            }
243        }
244        return newAcp;
245    }
246
247}