/** * 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 com.entwinemedia.fn.Prelude.chuck; import static com.entwinemedia.fn.Stream.$; import static org.opencastproject.security.api.SecurityConstants.GLOBAL_ADMIN_ROLE; 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 static org.opencastproject.util.data.Monadics.mlist; import static org.opencastproject.util.data.Option.none; import static org.opencastproject.util.data.Option.some; import org.opencastproject.util.Checksum; import org.opencastproject.util.data.Either; import org.opencastproject.util.data.Function; import org.opencastproject.util.data.Function2; import org.opencastproject.util.data.Option; import org.opencastproject.util.data.Tuple; import com.entwinemedia.fn.Fn; import com.entwinemedia.fn.Fn2; import com.entwinemedia.fn.Pred; import com.entwinemedia.fn.Stream; import com.entwinemedia.fn.fns.Booleans; 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.Comparator; import java.util.List; import java.util.Set; /** * 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) { 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; Set<Role> userRoles = user.getRoles(); for (AccessControlEntry entry : acl.getEntries()) { if (!action.toString().equals(entry.getAction())) continue; String aceRole = entry.getRole(); for (Role role : userRoles) { if (!role.getName().equals(aceRole)) continue; 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 Pred<Object> isAuthorizedFn(final AccessControlList acl, final User user, final Organization org) { return new Pred<Object>() { @Override public Boolean apply(Object action) { return 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) { return !$(actions).exists(Booleans.not(isAuthorizedFn(acl, user, org))); } /** * 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) { return $(actions).exists(isAuthorizedFn(acl, user, org)); } /** * 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) { return !$(actions).exists(isAuthorizedFn(acl, user, org)); } /** * 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) { return $(actions).exists(Booleans.not(isAuthorizedFn(acl, user, org))); } /** * 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; } public static final Function<String, Option<AclScope>> toAclScope = new Function<String, Option<AclScope>>() { @Override public Option<AclScope> apply(String s) { try { return some(AclScope.valueOf(s)); } catch (IllegalArgumentException e) { return none(); } } }; /** * 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 final List<AccessControlEntry> seq = mlist(entries) .foldl(new ArrayList<AccessControlEntry>(), new Function2<List<AccessControlEntry>, Either<AccessControlEntry, List<AccessControlEntry>>, List<AccessControlEntry>>() { @Override public List<AccessControlEntry> apply(List<AccessControlEntry> sum, Either<AccessControlEntry, List<AccessControlEntry>> current) { if (current.isLeft()) sum.add(current.left().value()); else sum.addAll(current.right().value()); return sum; } }); 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) { final List<AccessControlEntry> entries = mlist(actions).map( new Function<Tuple<String, Boolean>, AccessControlEntry>() { @Override public AccessControlEntry apply(Tuple<String, Boolean> action) { return new AccessControlEntry(role, action.getA(), action.getB()); } }).value(); 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 }; final MessageDigest md = $(acl.getEntries()).sort(sortAcl).bind(new Fn<AccessControlEntry, Stream<String>>() { @Override public Stream<String> apply(AccessControlEntry entry) { return $(entry.getRole(), entry.getAction(), Boolean.toString(entry.isAllow())); } }).foldl(mkMd5MessageDigest(), new Fn2<MessageDigest, String, MessageDigest>() { @Override public MessageDigest apply(MessageDigest digest, String s) { digest.update(s.getBytes(StandardCharsets.UTF_8)); // add separator byte (see definition above) digest.update(sep); return digest; } }); try { return Checksum.create("md5", Checksum.convertToHex(md.digest())); } catch (NoSuchAlgorithmException e) { return chuck(e); } } private static MessageDigest mkMd5MessageDigest() { try { return MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { return chuck(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()); } }; }