001/*
002 * (C) Copyright 2015 Nuxeo SA (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 *     Thomas Roger
018 */
019
020package org.nuxeo.ecm.permissions;
021
022import org.nuxeo.ecm.core.api.CoreInstance;
023import org.nuxeo.ecm.core.api.DocumentModel;
024import org.nuxeo.ecm.core.api.NuxeoPrincipal;
025import org.nuxeo.ecm.core.api.security.ACE;
026import org.nuxeo.ecm.core.api.security.ACL;
027import org.nuxeo.ecm.core.api.security.ACP;
028import org.nuxeo.ecm.core.query.sql.NXQL;
029import org.nuxeo.ecm.tokenauth.service.TokenAuthenticationService;
030import org.nuxeo.runtime.api.Framework;
031import org.nuxeo.runtime.services.config.ConfigurationService;
032
033/**
034 * @since 8.1
035 */
036public class TransientUserPermissionHelper {
037
038    /**
039     * @since 10.3
040     */
041    // status = 0 is PENDING, status = 1 or status = NULL is EFFECTIVE, excludes ARCHIVED ACLs
042    public static final String OTHER_DOCUMENT_WITH_PENDING_OR_EFFECTIVE_ACL_QUERY = "SELECT ecm:uuid FROM Document, Relation"
043            + " WHERE (ecm:acl/*1/status is NULL OR ecm:acl/*1/status = 0 OR ecm:acl/*1/status = 1)"
044            + " AND ecm:acl/*1/principal = %s AND ecm:uuid <> %s";
045
046    /**
047     * @since 10.3
048     */
049    public static final String TRANSIENT_APP_NAME = "transient/appName";
050
051    /**
052     * @since 10.3
053     */
054    public static final String TRANSIENT_DEVICE_ID = "transient/deviceId";
055
056    /**
057     * @since 10.3
058     */
059    public static final String TRANSIENT_PERMISSION = "transient/permission";
060
061    private TransientUserPermissionHelper() {
062        // helper class
063    }
064
065    /**
066     * @deprecated since 10.3. Use {@link #addToken(String)} instead.
067     */
068    @Deprecated
069    public static String acquireToken(String username, DocumentModel doc, String permission) {
070        addToken(username);
071        // return value was never used anyway
072        return null;
073    }
074
075    /**
076     * Adds a token for the given {@code username}.
077     * <p>
078     * Does nothing if {@code username} is not a transient username or if a token already exists.
079     *
080     * @since 10.3
081     */
082    public static void addToken(String username) {
083        if (NuxeoPrincipal.isTransientUsername(username)) {
084            TokenAuthenticationService tokenAuthenticationService = Framework.getService(
085                    TokenAuthenticationService.class);
086            tokenAuthenticationService.acquireToken(username, TRANSIENT_APP_NAME, TRANSIENT_DEVICE_ID, null,
087                    TRANSIENT_PERMISSION);
088        }
089    }
090
091    /**
092     * Returns the token linked to the given transient {@code username}, or {@code null} if no token can be found.
093     *
094     * @since 10.3
095     */
096    public static String getToken(String username) {
097        return Framework.getService(TokenAuthenticationService.class)
098                        .getToken(username, TRANSIENT_APP_NAME, TRANSIENT_DEVICE_ID);
099    }
100
101    public static void revokeToken(String username, DocumentModel doc) {
102        if (NuxeoPrincipal.isTransientUsername(username)) {
103            if (hasOtherPermission(username, doc)) {
104                // do not remove the token as username has a permission on another document
105                return;
106            }
107
108            // check if the transient user has other ACE on the document
109            ACP acp = doc.getACP();
110            for (ACL acl : acp.getACLs()) {
111                if (ACL.INHERITED_ACL.equals(acl.getName())) {
112                    continue;
113                }
114
115                for (ACE ace : acl) {
116                    if (username.equals(ace.getUsername()) && !ace.isArchived()) {
117                        // skip token removal
118                        return;
119                    }
120                }
121            }
122
123            TokenAuthenticationService tokenAuthenticationService = Framework.getService(
124                    TokenAuthenticationService.class);
125            String token = tokenAuthenticationService.getToken(username, TRANSIENT_APP_NAME, TRANSIENT_DEVICE_ID);
126            if (token != null) {
127                tokenAuthenticationService.revokeToken(token);
128            }
129
130            // for compatibility, remove also token that may be stored based on the document
131            token = tokenAuthenticationService.getToken(username, doc.getRepositoryName(), doc.getId());
132            if (token != null) {
133                tokenAuthenticationService.revokeToken(token);
134            }
135        }
136    }
137
138    /**
139     * Returns {@code true} if the given {@code username} has a non-archived ACE on another document than {@code doc},
140     * {@code false} otherwise.
141     * <p>
142     * Always returns {@code false} if the configuration property {@link NuxeoPrincipal#TRANSIENT_USERNAME_UNIQUE_PROP}
143     * is {@code true}.
144     *
145     * @since 10.3
146     */
147    protected static boolean hasOtherPermission(String username, DocumentModel doc) {
148        if (Framework.getService(ConfigurationService.class)
149                     .isBooleanTrue(NuxeoPrincipal.TRANSIENT_USERNAME_UNIQUE_PROP)) {
150            // as the transient username is unique, assume there is no other document with a permission
151            // for username.
152            return false;
153        }
154
155        String query = String.format(OTHER_DOCUMENT_WITH_PENDING_OR_EFFECTIVE_ACL_QUERY, NXQL.escapeString(username),
156                NXQL.escapeString(doc.getId()));
157        return CoreInstance.doPrivileged(doc.getRepositoryName(),
158                session -> !session.queryProjection(query, 1, 0).isEmpty());
159    }
160}