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.core.security;
021
022import static org.nuxeo.ecm.core.api.event.CoreEventConstants.CHANGED_ACL_NAME;
023import static org.nuxeo.ecm.core.api.event.CoreEventConstants.DOCUMENT_REFS;
024import static org.nuxeo.ecm.core.api.event.CoreEventConstants.REPOSITORY_NAME;
025import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.ACE_STATUS_UPDATED;
026
027import java.io.Serializable;
028import java.time.ZonedDateTime;
029import java.time.format.DateTimeFormatter;
030import java.util.ArrayList;
031import java.util.Calendar;
032import java.util.HashMap;
033import java.util.List;
034import java.util.Map;
035
036import org.nuxeo.ecm.core.api.DocumentRef;
037import org.nuxeo.ecm.core.api.IdRef;
038import org.nuxeo.ecm.core.api.IterableQueryResult;
039import org.nuxeo.ecm.core.api.security.ACE;
040import org.nuxeo.ecm.core.api.security.ACP;
041import org.nuxeo.ecm.core.event.EventContext;
042import org.nuxeo.ecm.core.event.EventService;
043import org.nuxeo.ecm.core.event.impl.EventContextImpl;
044import org.nuxeo.ecm.core.query.sql.NXQL;
045import org.nuxeo.ecm.core.work.AbstractWork;
046import org.nuxeo.runtime.api.Framework;
047import org.nuxeo.runtime.transaction.TransactionRuntimeException;
048
049/**
050 * Work updating ACE status.
051 *
052 * @since 7.4
053 */
054public class UpdateACEStatusWork extends AbstractWork {
055
056    private static final long serialVersionUID = 1L;
057
058    public static final int DEFAULT_BATCH_SIZE = 20;
059
060    public static final String CATEGORY = "updateACEStatus";
061
062    public static final String QUERY = "SELECT ecm:uuid, ecm:acl/*1/principal, ecm:acl/*1/permission,"
063            + " ecm:acl/*1/grant, ecm:acl/*1/creator, ecm:acl/*1/begin, ecm:acl/*1/end, ecm:acl/*1/name FROM Document"
064            + " WHERE (ecm:acl/*1/status = 0 AND ecm:acl/*1/begin <= TIMESTAMP '%s')"
065            + " OR (ecm:acl/*1/status = 1 AND ecm:acl/*1/end <= TIMESTAMP '%s')";
066
067    public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
068
069    protected int batchSize = DEFAULT_BATCH_SIZE;
070
071    @Override
072    public void work() {
073        setStatus("Updating ACE status");
074        openSystemSession();
075
076        String formattedDate = FORMATTER.format(ZonedDateTime.now());
077
078        IterableQueryResult result = session.queryAndFetch(String.format(QUERY, formattedDate, formattedDate),
079                NXQL.NXQL);
080        Map<String, List<ACE>> docIdsToACEs = new HashMap<>();
081        try {
082            for (Map<String, Serializable> map : result) {
083                String docId = (String) map.get("ecm:uuid");
084                List<ACE> aces = docIdsToACEs.get(docId);
085                if (aces == null) {
086                    aces = new ArrayList<>();
087                    docIdsToACEs.put(docId, aces);
088                }
089
090                String username = (String) map.get("ecm:acl/*1/principal");
091                String permission = (String) map.get("ecm:acl/*1/permission");
092                Boolean grant = (Boolean) map.get("ecm:acl/*1/grant");
093                String creator = (String) map.get("ecm:acl/*1/creator");
094                Calendar begin = (Calendar) map.get("ecm:acl/*1/begin");
095                Calendar end = (Calendar) map.get("ecm:acl/*1/end");
096                String aclName = (String) map.get("ecm:acl/*1/name");
097                Map<String, Serializable> contextData = new HashMap<>();
098                contextData.put(CHANGED_ACL_NAME, aclName);
099                ACE ace = ACE.builder(username, permission)
100                        .isGranted(grant)
101                        .creator(creator)
102                        .begin(begin)
103                        .end(end)
104                        .contextData(contextData)
105                        .build();
106                aces.add(ace);
107            }
108        } finally {
109            result.close();
110        }
111
112        int acpUpdatedCount = 0;
113        Map<DocumentRef, List<ACE>> processedRefToACEs = new HashMap<>();
114        for (Map.Entry<String, List<ACE>> entry : docIdsToACEs.entrySet()) {
115            try {
116                DocumentRef ref = new IdRef(entry.getKey());
117                ACP acp = session.getACP(ref);
118                // re-set the ACP to actually write the new status
119                session.setACP(ref, acp, true);
120                acpUpdatedCount++;
121                processedRefToACEs.put(ref, entry.getValue());
122                if (acpUpdatedCount % batchSize == 0) {
123                    fireACEStatusUpdatedEvent(processedRefToACEs);
124                    commitOrRollbackTransaction();
125                    startTransaction();
126                    processedRefToACEs.clear();
127                }
128            } catch (TransactionRuntimeException e) {
129                if (e.getMessage().contains("Transaction timeout")) {
130                    batchSize = 1;
131                }
132                throw e;
133            }
134        }
135        fireACEStatusUpdatedEvent(processedRefToACEs);
136
137        setStatus(null);
138    }
139
140    protected void fireACEStatusUpdatedEvent(Map<DocumentRef, List<ACE>> refToACEs) {
141        EventContext eventContext = new EventContextImpl(session, session.getPrincipal());
142        eventContext.setProperty(DOCUMENT_REFS, (Serializable) refToACEs);
143        eventContext.setProperty(REPOSITORY_NAME, session.getRepositoryName());
144        Framework.getService(EventService.class).fireEvent(ACE_STATUS_UPDATED, eventContext);
145    }
146
147    @Override
148    public String getCategory() {
149        return CATEGORY;
150    }
151
152    @Override
153    public String getTitle() {
154        return "Updating ACE status";
155    }
156
157    @Override
158    public int getRetryCount() {
159        return 10;
160    }
161}