001/*
002 * (C) Copyright 2006-2018 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 *     Bogdan Stefanescu
018 *     Florent Guillaume
019 */
020package org.nuxeo.ecm.core.api.security;
021
022import java.io.Serializable;
023import java.util.Calendar;
024import java.util.GregorianCalendar;
025import java.util.HashMap;
026import java.util.Map;
027import java.util.Objects;
028import java.util.regex.Matcher;
029import java.util.regex.Pattern;
030
031import org.apache.commons.lang3.StringUtils;
032
033/**
034 * Access control entry, assigning a permission to a user.
035 * <p>
036 * Optionally, the assignment can be denied instead of being granted.
037 */
038public final class ACE implements Serializable, Cloneable {
039
040    private static final long serialVersionUID = 1L;
041
042    public enum Status {
043        PENDING, EFFECTIVE, ARCHIVED
044    }
045
046    /**
047     * An ACE that blocks all permissions for everyone.
048     *
049     * @since 6.0
050     */
051    public static final ACE BLOCK = new ACE(SecurityConstants.EVERYONE, SecurityConstants.EVERYTHING, false);
052
053    private final String username;
054
055    private final String permission;
056
057    private final boolean isGranted;
058
059    private Calendar begin;
060
061    private Calendar end;
062
063    private String creator;
064
065    private Map<String, Serializable> contextData = new HashMap<>();
066
067    protected static final Pattern ID_PATTERN = Pattern.compile("^(.+):([^:]+):([^:]+):([^:]*):([^:]*):([^:]*)$");
068
069    /**
070     * Create an ACE from an id.
071     *
072     * @since 7.4
073     */
074    public static ACE fromId(String aceId) {
075        if (aceId == null) {
076            return null;
077        }
078
079        // An ACE is composed of tokens separated with ":" caracter
080        // First 3 tokens are mandatory; following 3 tokens are optional
081        // The ":" separator is still present even if the tokens are empty
082        // Example: jsmith:ReadWrite:true:::
083        // The first token (username) is allowed to contain embedded ":".
084        Matcher m = ID_PATTERN.matcher(aceId);
085        if (!m.matches()) {
086            throw new IllegalArgumentException(String.format("Invalid ACE id: %s", aceId));
087        }
088
089        String[] parts = new String[m.groupCount()];
090        for (int i = 1; i <= m.groupCount(); i++) {
091            parts[i - 1] = m.group(i);
092        }
093
094        String username = parts[0];
095        String permission = parts[1];
096        boolean isGranted = Boolean.parseBoolean(parts[2]);
097
098        ACEBuilder builder = ACE.builder(username, permission).isGranted(isGranted);
099
100        if (parts.length >= 4 && StringUtils.isNotBlank(parts[3])) {
101            builder.creator(parts[3]);
102        }
103
104        if (parts.length >= 5 && StringUtils.isNotBlank(parts[4])) {
105            Calendar begin = new GregorianCalendar();
106            begin.setTimeInMillis(Long.parseLong(parts[4]));
107            builder.begin(begin);
108        }
109
110        if (parts.length >= 6 && StringUtils.isNotBlank(parts[5])) {
111            Calendar end = new GregorianCalendar();
112            end.setTimeInMillis(Long.parseLong(parts[5]));
113            builder.end(end);
114        }
115
116        return builder.build();
117    }
118
119    public ACE() {
120        this(null, null, false);
121    }
122
123    /**
124     * Constructs an ACE for a given username and permission, and specifies whether to grant or deny it.
125     */
126    public ACE(String username, String permission, boolean isGranted) {
127        this(username, permission, isGranted, null, null, null, null);
128    }
129
130    /**
131     * Constructs an ACE for a given username and permission.
132     * <p>
133     * The ACE is granted.
134     *
135     * @since 6.0
136     */
137    public ACE(String username, String permission) {
138        this(username, permission, true);
139    }
140
141    /**
142     * Constructs an ACE for a given username, permission, specifying whether to grant or deny it, creator user, begin
143     * and end date.
144     *
145     * @since 7.4
146     */
147    ACE(String username, String permission, boolean isGranted, String creator, Calendar begin, Calendar end,
148            Map<String, Serializable> contextData) {
149        this.username = username;
150        this.permission = permission;
151        this.isGranted = isGranted;
152        this.creator = creator;
153        setBegin(begin);
154        setEnd(end);
155        if (contextData != null) {
156            this.contextData = new HashMap<>(contextData);
157        }
158
159        if (begin != null && end != null) {
160            if (begin.after(end)) {
161                throw new IllegalArgumentException("'begin' date cannot be after 'end' date");
162            }
163        }
164    }
165
166    /**
167     * Returns this ACE id.
168     * <p>
169     * This id is unique inside a given ACL.
170     *
171     * @since 7.4
172     */
173    public String getId() {
174        StringBuilder sb = new StringBuilder();
175        sb.append(username);
176        sb.append(':');
177        sb.append(permission);
178        sb.append(':');
179        sb.append(isGranted);
180
181        sb.append(':');
182        if (creator != null) {
183            sb.append(creator);
184        }
185
186        sb.append(':');
187        if (begin != null) {
188            sb.append(begin.getTimeInMillis());
189        }
190
191        sb.append(':');
192        if (end != null) {
193            sb.append(end.getTimeInMillis());
194        }
195
196        return sb.toString();
197    }
198
199    public String getUsername() {
200        return username;
201    }
202
203    public String getPermission() {
204        return permission;
205    }
206
207    /**
208     * Checks if this privilege is granted.
209     *
210     * @return true if the privilege is granted
211     */
212    public boolean isGranted() {
213        return isGranted;
214    }
215
216    /**
217     * Checks if this privilege is denied.
218     *
219     * @return true if privilege is denied
220     */
221    public boolean isDenied() {
222        return !isGranted;
223    }
224
225    public Calendar getBegin() {
226        return begin;
227    }
228
229    /**
230     * Sets the begin date of this ACE.
231     * <p>
232     * Sets the {@code Calendar.MILLISECOND} part of the Calendar to 0.
233     */
234    public void setBegin(Calendar begin) {
235        this.begin = begin;
236        if (this.begin != null) {
237            this.begin.set(Calendar.MILLISECOND, 0);
238        }
239    }
240
241    public Calendar getEnd() {
242        return end;
243    }
244
245    /**
246     * Sets the end date of this ACE.
247     * <p>
248     * Sets the {@code Calendar.MILLISECOND} part of the Calendar to 0.
249     */
250    public void setEnd(Calendar end) {
251        this.end = end;
252        if (this.end != null) {
253            this.end.set(Calendar.MILLISECOND, 0);
254        }
255    }
256
257    public String getCreator() {
258        return creator;
259    }
260
261    public void setCreator(String creator) {
262        this.creator = creator;
263    }
264
265    /**
266     * Returns the status of this ACE.
267     *
268     * @since 7.4
269     */
270    public Status getStatus() {
271        Status status = Status.EFFECTIVE;
272        Calendar now = new GregorianCalendar();
273        if (begin != null && now.before(begin)) {
274            status = Status.PENDING;
275        }
276        if (end != null && now.after(end)) {
277            status = Status.ARCHIVED;
278        }
279        return status;
280    }
281
282    /**
283     * Returns a Long value of this ACE status.
284     * <p>
285     * It returns {@code null} if there is no begin and end date, which means the ACE is effective. Otherwise, it
286     * returns 0 for PENDING, 1 for EFFECTIVE and 2 for ARCHIVED.
287     *
288     * @since 7.4
289     */
290    public Long getLongStatus() {
291        if (begin == null && end == null) {
292            return null;
293        }
294        return Long.valueOf(getStatus().ordinal());
295    }
296
297    public boolean isEffective() {
298        return getStatus() == Status.EFFECTIVE;
299    }
300
301    public boolean isPending() {
302        return getStatus() == Status.PENDING;
303    }
304
305    public boolean isArchived() {
306        return getStatus() == Status.ARCHIVED;
307    }
308
309    public Serializable getContextData(String key) {
310        return contextData.get(key);
311    }
312
313    public void putContextData(String key, Serializable value) {
314        contextData.put(key, value);
315    }
316
317    @Override
318    public boolean equals(Object obj) {
319        if (this == obj) {
320            return true;
321        }
322        if (obj instanceof ACE) {
323            ACE ace = (ACE) obj;
324            // check Calendars without handling timezone
325            boolean beginEqual = ace.begin == null && begin == null
326                    || !(ace.begin == null || begin == null) && ace.begin.getTimeInMillis() == begin.getTimeInMillis();
327            boolean endEqual = ace.end == null && end == null
328                    || !(ace.end == null || end == null) && ace.end.getTimeInMillis() == end.getTimeInMillis();
329            boolean creatorEqual = Objects.equals(ace.creator, creator);
330            boolean usernameEqual = Objects.equals(ace.username, username);
331            boolean permissionEqual = Objects.equals(ace.permission, permission);
332            return ace.isGranted == isGranted && usernameEqual && permissionEqual && creatorEqual && beginEqual
333                    && endEqual;
334        }
335        return super.equals(obj);
336    }
337
338    @Override
339    public int hashCode() {
340        int hash = 17;
341        hash = hash * 37 + (isGranted ? 1 : 0);
342        hash = username != null ? hash * 37 + username.hashCode() : hash;
343        hash = creator != null ? hash * 37 + creator.hashCode() : hash;
344        hash = begin != null ? hash * 37 + begin.hashCode() : hash;
345        hash = end != null ? hash * 37 + end.hashCode() : hash;
346        hash = permission != null ? hash * 37 + permission.hashCode() : hash;
347        return hash;
348    }
349
350    @Override
351    public String toString() {
352        StringBuilder sb = new StringBuilder();
353        sb.append(getClass().getSimpleName());
354        sb.append('(');
355        sb.append("username=").append(username);
356        sb.append(", ");
357        sb.append("permission=").append(permission);
358        sb.append(", ");
359        sb.append("isGranted=").append(isGranted);
360        sb.append(", ");
361        sb.append("creator=").append(creator);
362        sb.append(", ");
363        sb.append("begin=").append(begin != null ? begin.getTimeInMillis() : null);
364        sb.append(", ");
365        sb.append("end=").append(end != null ? end.getTimeInMillis() : null);
366        sb.append(')');
367        return sb.toString();
368    }
369
370    @Override
371    public Object clone() {
372        return new ACE(username, permission, isGranted, creator, begin, end, contextData);
373    }
374
375    /** @since 11.3 */
376    public static ACEBuilder builder(ACE ace) {
377        return new ACEBuilder(ace);
378    }
379
380    public static ACEBuilder builder(String username, String permission) {
381        return new ACEBuilder(username, permission);
382    }
383
384    public static class ACEBuilder {
385
386        private String username;
387
388        private String permission;
389
390        private boolean isGranted = true;
391
392        private Calendar begin;
393
394        private Calendar end;
395
396        private String creator;
397
398        private Map<String, Serializable> contextData;
399
400        /** @since 11.3 */
401        public ACEBuilder(ACE ace) {
402            this.username = ace.username;
403            this.permission = ace.permission;
404            this.isGranted = ace.isGranted;
405            this.begin = ace.begin;
406            this.end = ace.end;
407            this.creator = ace.creator;
408            this.contextData = ace.contextData;
409        }
410
411        public ACEBuilder(String username, String permission) {
412            this.username = username;
413            this.permission = permission;
414        }
415
416        /** @since 11.3 */
417        public ACEBuilder username(String username) {
418            this.username = username;
419            return this;
420        }
421
422        /** @since 11.3 */
423        public ACEBuilder permission(String permission) {
424            this.permission = permission;
425            return this;
426        }
427
428        public ACEBuilder isGranted(boolean isGranted) {
429            this.isGranted = isGranted;
430            return this;
431        }
432
433        public ACEBuilder begin(Calendar begin) {
434            this.begin = begin;
435            return this;
436        }
437
438        public ACEBuilder end(Calendar end) {
439            this.end = end;
440            return this;
441        }
442
443        public ACEBuilder creator(String creator) {
444            this.creator = creator;
445            return this;
446        }
447
448        public ACEBuilder contextData(Map<String, Serializable> contextData) {
449            this.contextData = contextData;
450            return this;
451        }
452
453        public ACE build() {
454            return new ACE(username, permission, isGranted, creator, begin, end, contextData);
455        }
456    }
457
458}