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