AccessControlUtil.java
/*
* Licensed to The Apereo Foundation under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
*
* The Apereo Foundation licenses this file to you under the Educational
* Community 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://opensource.org/licenses/ecl2.txt
*
* 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.
*
*/
package org.opencastproject.security.api;
import static org.opencastproject.security.api.SecurityConstants.GLOBAL_ADMIN_ROLE;
import static org.opencastproject.security.util.SecurityUtil.getEpisodeRoleId;
import static org.opencastproject.util.EqualsUtil.bothNotNull;
import static org.opencastproject.util.EqualsUtil.eqListUnsorted;
import static org.opencastproject.util.data.Either.left;
import static org.opencastproject.util.data.Either.right;
import org.opencastproject.util.Checksum;
import org.opencastproject.util.data.Either;
import org.opencastproject.util.data.Tuple;
import org.apache.commons.lang3.StringUtils;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* Provides common functions helpful in dealing with {@link AccessControlList}s.
*/
public final class AccessControlUtil {
/** Disallow construction of this utility class */
private AccessControlUtil() {
}
/**
* Determines whether the {@link AccessControlList} permits a user to perform an action.
*
* There are three ways a user can be allowed to perform an action:
* <ol>
* <li>They have the superuser role</li>
* <li>They have their local organization's admin role</li>
* <li>They have a role listed in the series ACL, with write permission</li>
* </ol>
*
* @param acl
* the {@link AccessControlList}
* @param user
* the user
* @param org
* the organization
* @param action
* The action to perform. <code>action</code> may be an arbitrary object. The authorization check is done on
* the string representation of the object (<code>#toString()</code>). This allows to group actions as enums
* and use them without converting them to a string manually. See
* {@link org.opencastproject.security.api.Permissions.Action}.
* @return whether this action should be allowed
* @throws IllegalArgumentException
* if any of the arguments are null
*/
public static boolean isAuthorized(AccessControlList acl, User user, Organization org, Object action) {
return isAuthorized(acl, user, org, action, null);
}
/**
* Determines whether the {@link AccessControlList} permits a user to perform an action.
*
* There are three ways a user can be allowed to perform an action:
* <ol>
* <li>They have the superuser role</li>
* <li>They have their local organization's admin role</li>
* <li>They have a role listed in the series ACL, with write permission</li>
* </ol>
*
* @param acl
* the {@link AccessControlList}
* @param user
* the user
* @param org
* the organization
* @param action
* The action to perform. <code>action</code> may be an arbitrary object. The authorization check is done on
* the string representation of the object (<code>#toString()</code>). This allows to group actions as enums
* and use them without converting them to a string manually. See
* {@link org.opencastproject.security.api.Permissions.Action}.
* @param mediaPackageId
* Only required if episodeRoleId is true.
* @return whether this action should be allowed
* @throws IllegalArgumentException
* if any of the arguments are null
*/
public static boolean isAuthorized(AccessControlList acl, User user, Organization org, Object action,
String mediaPackageId) {
if (action == null || user == null || acl == null || org == null) {
throw new IllegalArgumentException();
}
// Check for the global and local admin role
if (user.hasRole(GLOBAL_ADMIN_ROLE) || user.hasRole(org.getAdminRole())) {
return true;
}
// Check for episode role ids, if activated
if (mediaPackageId != null && user.hasRole(getEpisodeRoleId(mediaPackageId, action.toString()))) {
return true;
}
Set<Role> userRoles = user.getRoles();
for (AccessControlEntry entry : acl.getEntries()) {
if (action.toString().equals(entry.getAction())) {
for (Role role : userRoles) {
if (role.getName().equals(entry.getRole())) {
return entry.isAllow();
}
}
}
}
return false;
}
/**
* {@link AccessControlUtil#isAuthorized(org.opencastproject.security.api.AccessControlList, org.opencastproject.security.api.User, org.opencastproject.security.api.Organization, Object)}
* as a predicate function.
*/
private static Predicate<Object> isAuthorizedFn(final AccessControlList acl, final User user, final Organization org) {
return action -> isAuthorized(acl, user, org, action);
}
/**
* Returns true only if <em>all</em> actions are authorized.
*
* @see #isAuthorized(AccessControlList, User, Organization, Object)
*/
public static boolean isAuthorizedAll(AccessControlList acl, User user, Organization org, Object... actions) {
Predicate<Object> isAuthorized = isAuthorizedFn(acl, user, org);
return Arrays.stream(actions).allMatch(isAuthorized);
}
/**
* Returns true if at least <em>one</em> action is authorized.
*
* @see #isAuthorized(AccessControlList, User, Organization, Object)
*/
public static boolean isAuthorizedOne(AccessControlList acl, User user, Organization org, Object... actions) {
Predicate<Object> isAuthorized = isAuthorizedFn(acl, user, org);
return Arrays.stream(actions).anyMatch(isAuthorized);
}
/**
* Returns true if <em>all</em> actions are prohibited.
*
* @see #isAuthorized(AccessControlList, User, Organization, Object)
*/
public static boolean isProhibitedAll(AccessControlList acl, User user, Organization org, Object... actions) {
Predicate<Object> isAuthorized = isAuthorizedFn(acl, user, org);
return Arrays.stream(actions).noneMatch(isAuthorized);
}
/**
* Returns true if at least <em>one</em> action is prohibited.
*
* @see #isAuthorized(AccessControlList, User, Organization, Object)
*/
public static boolean isProhibitedOne(AccessControlList acl, User user, Organization org, Object... actions) {
Predicate<Object> isAuthorized = isAuthorizedFn(acl, user, org);
return Arrays.stream(actions).anyMatch(isAuthorized.negate());
}
/**
* Extends an access control list with an access control entry
*
* @param acl
* the access control list to extend
* @param role
* the access control entry role
* @param action
* the access control entry action
* @param allow
* whether this access control entry role is allowed to take this action
* @return the extended access control list or the same if already contained
*/
public static AccessControlList extendAcl(AccessControlList acl, String role, String action, boolean allow) {
AccessControlList newAcl = new AccessControlList();
boolean foundAce = false;
for (AccessControlEntry ace : acl.getEntries()) {
if (ace.getAction().equalsIgnoreCase(action) && ace.getRole().equalsIgnoreCase(role)) {
if (ace.isAllow() == allow) {
// Entry is already the same so just return the acl
return acl;
} else {
// We need to change the allow on the one entry.
foundAce = true;
newAcl.getEntries().add(new AccessControlEntry(role, action, allow));
}
} else {
newAcl.getEntries().add(ace);
}
}
if (!foundAce)
newAcl.getEntries().add(new AccessControlEntry(role, action, allow));
return newAcl;
}
/**
* Reduces an access control list by an access control entry
*
* @param acl
* the access control list to reduce
* @param role
* the role of the access control entry to remove
* @param action
* the action of the access control entry to remove
* @return the reduced access control list or the same if already contained
*/
public static AccessControlList reduceAcl(AccessControlList acl, String role, String action) {
AccessControlList newAcl = new AccessControlList();
for (AccessControlEntry ace : acl.getEntries()) {
if (!ace.getAction().equalsIgnoreCase(action) || !ace.getRole().equalsIgnoreCase(role)) {
newAcl.getEntries().add(ace);
}
}
return newAcl;
}
/**
* Constructor function for ACLs.
*
* @see #entry(String, String, boolean)
* @see #entries(String, org.opencastproject.util.data.Tuple[])
*/
public static AccessControlList acl(Either<AccessControlEntry, List<AccessControlEntry>>... entries) {
// sequence entries
List<AccessControlEntry> seq = new ArrayList<>();
for (Either<AccessControlEntry, List<AccessControlEntry>> current : entries) {
if (current.isLeft()) {
seq.add(current.left().value());
} else {
seq.addAll(current.right().value());
}
}
return new AccessControlList(seq);
}
/** Create a single access control entry. */
public static Either<AccessControlEntry, List<AccessControlEntry>> entry(String role, String action, boolean allow) {
return left(new AccessControlEntry(role, action, allow));
}
/** Create a list of access control entries for a given role. */
public static Either<AccessControlEntry, List<AccessControlEntry>> entries(final String role,
Tuple<String, Boolean>... actions) {
List<AccessControlEntry> entries = Arrays.stream(actions)
.map(action -> new AccessControlEntry(role, action.getA(), action.getB()))
.toList();
return right(entries);
}
/**
* Define equality on AccessControlLists. Two AccessControlLists are considered equal if they contain the exact same
* entries no matter in which order.
* <p>
* This has not been implemented in terms of #equals and #hashCode because the list of entries is not immutable and
* therefore not suitable to be put in a set.
*/
public static boolean equals(AccessControlList a, AccessControlList b) {
return bothNotNull(a, b) && eqListUnsorted(a.getEntries(), b.getEntries());
}
/** Calculate an MD5 checksum for an {@link AccessControlList}. */
public static Checksum calculateChecksum(AccessControlList acl) {
// Use 0 as a word separator. This is safe since none of the UTF-8 code points
// except \u0000 contains a null byte when converting to a byte array.
final byte[] sep = new byte[] { 0 };
// Sort ACL entries
List<AccessControlEntry> sortedEntries = acl.getEntries().stream()
.sorted(sortAcl)
.collect(Collectors.toList());
MessageDigest digest = mkMd5MessageDigest();
for (AccessControlEntry entry : sortedEntries) {
String[] fields = {
entry.getRole(),
entry.getAction(),
Boolean.toString(entry.isAllow())
};
for (String field : fields) {
digest.update(field.getBytes(StandardCharsets.UTF_8));
// add separator byte (see definition above)
digest.update(sep);
}
}
try {
return Checksum.create("md5", Checksum.convertToHex(digest.digest()));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
private static MessageDigest mkMd5MessageDigest() {
try {
return MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
private static Comparator<AccessControlEntry> sortAcl = new Comparator<AccessControlEntry>() {
@Override
public int compare(AccessControlEntry o1, AccessControlEntry o2) {
// compare role
int compareTo = StringUtils.trimToEmpty(o1.getRole()).compareTo(StringUtils.trimToEmpty(o2.getRole()));
if (compareTo != 0)
return compareTo;
// compare action
compareTo = StringUtils.trimToEmpty(o1.getAction()).compareTo(StringUtils.trimToEmpty(o2.getAction()));
if (compareTo != 0)
return compareTo;
// compare allow
return Boolean.valueOf(o1.isAllow()).compareTo(o2.isAllow());
}
};
}