/* * (C) Copyright 2006-2014 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Bogdan Stefanescu * Florent Guillaume */ package org.nuxeo.ecm.core.api.security; import java.io.Serializable; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.regex.Pattern; import java.util.regex.Matcher; import org.apache.commons.lang.StringUtils; /** * Access control entry, assigning a permission to a user. * <p> * Optionally, the assignment can be denied instead of being granted. */ public final class ACE implements Serializable, Cloneable { public enum Status { PENDING, EFFECTIVE, ARCHIVED; } /** * An ACE that blocks all permissions for everyone. * * @since 6.0 */ public static final ACE BLOCK = new ACE(SecurityConstants.EVERYONE, SecurityConstants.EVERYTHING, false); private final String username; private final String permission; private final boolean isGranted; private Calendar begin; private Calendar end; private String creator; private Map<String, Serializable> contextData = new HashMap<>(); protected static final Pattern ID_PATTERN = Pattern.compile("^(.+):([^:]+):([^:]+):([^:]*):([^:]*):([^:]*)$"); /** * Create an ACE from an id. * * @since 7.4 */ public static ACE fromId(String aceId) { if (aceId == null) { return null; } // An ACE is composed of tokens separated with ":" caracter // First 3 tokens are mandatory; following 3 tokens are optional // The ":" separator is still present even if the tokens are empty // Example: jsmith:ReadWrite:true::: // The first token (username) is allowed to contain embedded ":". Matcher m = ID_PATTERN.matcher(aceId); if (!m.matches()) { throw new IllegalArgumentException(String.format("Invalid ACE id: %s", aceId)); } String[] parts = new String[m.groupCount()]; for (int i = 1; i <= m.groupCount(); i++) { parts[i - 1] = m.group(i); } String username = parts[0]; String permission = parts[1]; boolean isGranted = Boolean.valueOf(parts[2]); ACEBuilder builder = ACE.builder(username, permission).isGranted(isGranted); if (parts.length >= 4 && StringUtils.isNotBlank(parts[3])) { builder.creator(parts[3]); } if (parts.length >= 5 && StringUtils.isNotBlank(parts[4])) { Calendar begin = new GregorianCalendar(); begin.setTimeInMillis(Long.valueOf(parts[4])); builder.begin(begin); } if (parts.length >= 6 && StringUtils.isNotBlank(parts[5])) { Calendar end = new GregorianCalendar(); end.setTimeInMillis(Long.valueOf(parts[5])); builder.end(end); } return builder.build(); } public ACE() { this(null, null, false); } /** * Constructs an ACE for a given username and permission, and specifies whether to grant or deny it. */ public ACE(String username, String permission, boolean isGranted) { this(username, permission, isGranted, null, null, null, null); } /** * Constructs an ACE for a given username and permission. * <p> * The ACE is granted. * * @since 6.0 */ public ACE(String username, String permission) { this(username, permission, true); } /** * Constructs an ACE for a given username, permission, specifying whether to grant or deny it, creator user, begin * and end date. * * @since 7.4 */ ACE(String username, String permission, boolean isGranted, String creator, Calendar begin, Calendar end, Map<String, Serializable> contextData) { this.username = username; this.permission = permission; this.isGranted = isGranted; this.creator = creator; setBegin(begin); setEnd(end); if (contextData != null) { this.contextData = new HashMap<>(contextData); } if (begin != null && end != null) { if (begin.after(end)) { throw new IllegalArgumentException("'begin' date cannot be after 'end' date"); } } } /** * Returns this ACE id. * <p> * This id is unique inside a given ACL. * * @since 7.4 */ public String getId() { StringBuilder sb = new StringBuilder(); sb.append(username); sb.append(':'); sb.append(permission); sb.append(':'); sb.append(isGranted); sb.append(':'); if (creator != null) { sb.append(creator); } sb.append(':'); if (begin != null) { sb.append(begin.getTimeInMillis()); } sb.append(':'); if (end != null) { sb.append(end.getTimeInMillis()); } return sb.toString(); } public String getUsername() { return username; } public String getPermission() { return permission; } /** * Checks if this privilege is granted. * * @return true if the privilege is granted */ public boolean isGranted() { return isGranted; } /** * Checks if this privilege is denied. * * @return true if privilege is denied */ public boolean isDenied() { return !isGranted; } public Calendar getBegin() { return begin; } /** * Sets the begin date of this ACE. * <p> * Sets the {@code Calendar.MILLISECOND} part of the Calendar to 0. */ public void setBegin(Calendar begin) { this.begin = begin; if (this.begin != null) { this.begin.set(Calendar.MILLISECOND, 0); } } public Calendar getEnd() { return end; } /** * Sets the end date of this ACE. * <p> * Sets the {@code Calendar.MILLISECOND} part of the Calendar to 0. */ public void setEnd(Calendar end) { this.end = end; if (this.end!= null) { this.end.set(Calendar.MILLISECOND, 0); } } public String getCreator() { return creator; } public void setCreator(String creator) { this.creator = creator; } /** * Returns the status of this ACE. * * @since 7.4 */ public Status getStatus() { Status status = Status.EFFECTIVE; Calendar now = new GregorianCalendar(); if (begin != null && now.before(begin)) { status = Status.PENDING; } if (end != null && now.after(end)) { status = Status.ARCHIVED; } return status; } /** * Returns a Long value of this ACE status. * <p> * It returns {@code null} if there is no begin and end date, which means the ACE is effective. Otherwise, it * returns 0 for PENDING, 1 for EFFECTIVE and 2 for ARCHIVED. * * @since 7.4 */ public Long getLongStatus() { if (begin == null && end == null) { return null; } return Long.valueOf(getStatus().ordinal()); } public boolean isEffective() { return getStatus() == Status.EFFECTIVE; } public boolean isPending() { return getStatus() == Status.PENDING; } public boolean isArchived() { return getStatus() == Status.ARCHIVED; } public Serializable getContextData(String key) { return contextData.get(key); } public void putContextData(String key, Serializable value) { contextData.put(key, value); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof ACE) { ACE ace = (ACE) obj; // check Calendars without handling timezone boolean beginEqual = ace.begin == null && begin == null || !(ace.begin == null || begin == null) && ace.begin.getTimeInMillis() == begin.getTimeInMillis(); boolean endEqual = ace.end == null && end == null || !(ace.end == null || end == null) && ace.end.getTimeInMillis() == end.getTimeInMillis(); boolean creatorEqual = Objects.equals(ace.creator, creator); boolean usernameEqual = Objects.equals(ace.username, username); boolean permissionEqual = Objects.equals(ace.permission, permission); return ace.isGranted == isGranted && usernameEqual && permissionEqual && creatorEqual && beginEqual && endEqual; } return super.equals(obj); } @Override public int hashCode() { int hash = 17; hash = hash * 37 + (isGranted ? 1 : 0); hash = username != null ? hash * 37 + username.hashCode() : hash; hash = creator != null ? hash * 37 + creator.hashCode() : hash; hash = begin != null ? hash * 37 + begin.hashCode() : hash; hash = end != null ? hash * 37 + end.hashCode() : hash; hash = permission != null ? hash * 37 + permission.hashCode() : hash; return hash; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(getClass().getSimpleName()); sb.append('('); sb.append("username=").append(username); sb.append(", "); sb.append("permission=" + permission); sb.append(", "); sb.append("isGranted=" + isGranted); sb.append(", "); sb.append("creator=" + creator); sb.append(", "); sb.append("begin=" + (begin != null ? begin.getTimeInMillis() : null)); sb.append(", "); sb.append("end=" + (end != null ? end.getTimeInMillis() : null)); sb.append(')'); return sb.toString(); } @Override public Object clone() { return new ACE(username, permission, isGranted, creator, begin, end, contextData); } public static ACEBuilder builder(String username, String permission) { return new ACEBuilder(username, permission); } public static class ACEBuilder { private String username; private String permission; private boolean isGranted = true; private Calendar begin; private Calendar end; private String creator; private Map<String, Serializable> contextData; public ACEBuilder(String username, String permission) { this.username = username; this.permission = permission; } public ACEBuilder isGranted(boolean isGranted) { this.isGranted = isGranted; return this; } public ACEBuilder begin(Calendar begin) { this.begin = begin; return this; } public ACEBuilder end(Calendar end) { this.end = end; return this; } public ACEBuilder creator(String creator) { this.creator = creator; return this; } public ACEBuilder contextData(Map<String, Serializable> contextData) { this.contextData = contextData; return this; } public ACE build() { return new ACE(username, permission, isGranted, creator, begin, end, contextData); } } }