View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.security.api;
23  
24  import static org.opencastproject.security.api.SecurityConstants.GLOBAL_ADMIN_ROLE;
25  import static org.opencastproject.security.util.SecurityUtil.getEpisodeRoleId;
26  import static org.opencastproject.util.EqualsUtil.bothNotNull;
27  import static org.opencastproject.util.EqualsUtil.eqListUnsorted;
28  import static org.opencastproject.util.data.Either.left;
29  import static org.opencastproject.util.data.Either.right;
30  
31  import org.opencastproject.util.Checksum;
32  import org.opencastproject.util.data.Either;
33  import org.opencastproject.util.data.Tuple;
34  
35  import org.apache.commons.lang3.StringUtils;
36  
37  import java.nio.charset.StandardCharsets;
38  import java.security.MessageDigest;
39  import java.security.NoSuchAlgorithmException;
40  import java.util.ArrayList;
41  import java.util.Arrays;
42  import java.util.Comparator;
43  import java.util.List;
44  import java.util.Set;
45  import java.util.function.Predicate;
46  import java.util.stream.Collectors;
47  
48  /**
49   * Provides common functions helpful in dealing with {@link AccessControlList}s.
50   */
51  public final class AccessControlUtil {
52  
53    /** Disallow construction of this utility class */
54    private AccessControlUtil() {
55    }
56  
57    /**
58     * Determines whether the {@link AccessControlList} permits a user to perform an action.
59     *
60     * There are three ways a user can be allowed to perform an action:
61     * <ol>
62     * <li>They have the superuser role</li>
63     * <li>They have their local organization's admin role</li>
64     * <li>They have a role listed in the series ACL, with write permission</li>
65     * </ol>
66     *
67     * @param acl
68     *          the {@link AccessControlList}
69     * @param user
70     *          the user
71     * @param org
72     *          the organization
73     * @param action
74     *          The action to perform. <code>action</code> may be an arbitrary object. The authorization check is done on
75     *          the string representation of the object (<code>#toString()</code>). This allows to group actions as enums
76     *          and use them without converting them to a string manually. See
77     *          {@link org.opencastproject.security.api.Permissions.Action}.
78     * @return whether this action should be allowed
79     * @throws IllegalArgumentException
80     *           if any of the arguments are null
81     */
82    public static boolean isAuthorized(AccessControlList acl, User user, Organization org, Object action) {
83      return isAuthorized(acl, user, org, action, null);
84    }
85  
86    /**
87     * Determines whether the {@link AccessControlList} permits a user to perform an action.
88     *
89     * There are three ways a user can be allowed to perform an action:
90     * <ol>
91     * <li>They have the superuser role</li>
92     * <li>They have their local organization's admin role</li>
93     * <li>They have a role listed in the series ACL, with write permission</li>
94     * </ol>
95     *
96     * @param acl
97     *          the {@link AccessControlList}
98     * @param user
99     *          the user
100    * @param org
101    *          the organization
102    * @param action
103    *          The action to perform. <code>action</code> may be an arbitrary object. The authorization check is done on
104    *          the string representation of the object (<code>#toString()</code>). This allows to group actions as enums
105    *          and use them without converting them to a string manually. See
106    *          {@link org.opencastproject.security.api.Permissions.Action}.
107    * @param mediaPackageId
108    *          Only required if episodeRoleId is true.
109    * @return whether this action should be allowed
110    * @throws IllegalArgumentException
111    *           if any of the arguments are null
112    */
113   public static boolean isAuthorized(AccessControlList acl, User user, Organization org, Object action,
114       String mediaPackageId) {
115     if (action == null || user == null || acl == null || org == null) {
116       throw new IllegalArgumentException();
117     }
118 
119     // Check for the global and local admin role
120     if (user.hasRole(GLOBAL_ADMIN_ROLE) || user.hasRole(org.getAdminRole())) {
121       return true;
122     }
123 
124     // Check for episode role ids, if activated
125     if (mediaPackageId != null && user.hasRole(getEpisodeRoleId(mediaPackageId, action.toString()))) {
126         return true;
127     }
128 
129     Set<Role> userRoles = user.getRoles();
130     for (AccessControlEntry entry : acl.getEntries()) {
131       if (action.toString().equals(entry.getAction())) {
132         for (Role role : userRoles) {
133           if (role.getName().equals(entry.getRole())) {
134             return entry.isAllow();
135           }
136         }
137       }
138     }
139 
140     return false;
141   }
142 
143   /**
144    * {@link AccessControlUtil#isAuthorized(org.opencastproject.security.api.AccessControlList, org.opencastproject.security.api.User, org.opencastproject.security.api.Organization, Object)}
145    * as a predicate function.
146    */
147   private static Predicate<Object> isAuthorizedFn(final AccessControlList acl, final User user, final Organization org) {
148     return action -> isAuthorized(acl, user, org, action);
149   }
150 
151   /**
152    * Returns true only if <em>all</em> actions are authorized.
153    *
154    * @see #isAuthorized(AccessControlList, User, Organization, Object)
155    */
156   public static boolean isAuthorizedAll(AccessControlList acl, User user, Organization org, Object... actions) {
157     Predicate<Object> isAuthorized = isAuthorizedFn(acl, user, org);
158     return Arrays.stream(actions).allMatch(isAuthorized);
159   }
160 
161   /**
162    * Returns true if at least <em>one</em> action is authorized.
163    *
164    * @see #isAuthorized(AccessControlList, User, Organization, Object)
165    */
166   public static boolean isAuthorizedOne(AccessControlList acl, User user, Organization org, Object... actions) {
167     Predicate<Object> isAuthorized = isAuthorizedFn(acl, user, org);
168     return Arrays.stream(actions).anyMatch(isAuthorized);
169   }
170 
171   /**
172    * Returns true if <em>all</em> actions are prohibited.
173    *
174    * @see #isAuthorized(AccessControlList, User, Organization, Object)
175    */
176   public static boolean isProhibitedAll(AccessControlList acl, User user, Organization org, Object... actions) {
177     Predicate<Object> isAuthorized = isAuthorizedFn(acl, user, org);
178     return Arrays.stream(actions).noneMatch(isAuthorized);
179   }
180 
181   /**
182    * Returns true if at least <em>one</em> action is prohibited.
183    *
184    * @see #isAuthorized(AccessControlList, User, Organization, Object)
185    */
186   public static boolean isProhibitedOne(AccessControlList acl, User user, Organization org, Object... actions) {
187     Predicate<Object> isAuthorized = isAuthorizedFn(acl, user, org);
188     return Arrays.stream(actions).anyMatch(isAuthorized.negate());
189   }
190 
191   /**
192    * Extends an access control list with an access control entry
193    *
194    * @param acl
195    *          the access control list to extend
196    * @param role
197    *          the access control entry role
198    * @param action
199    *          the access control entry action
200    * @param allow
201    *          whether this access control entry role is allowed to take this action
202    * @return the extended access control list or the same if already contained
203    */
204   public static AccessControlList extendAcl(AccessControlList acl, String role, String action, boolean allow) {
205     AccessControlList newAcl = new AccessControlList();
206     boolean foundAce = false;
207     for (AccessControlEntry ace : acl.getEntries()) {
208       if (ace.getAction().equalsIgnoreCase(action) && ace.getRole().equalsIgnoreCase(role)) {
209         if (ace.isAllow() == allow) {
210           // Entry is already the same so just return the acl
211           return acl;
212         } else {
213           // We need to change the allow on the one entry.
214           foundAce = true;
215           newAcl.getEntries().add(new AccessControlEntry(role, action, allow));
216         }
217       } else {
218         newAcl.getEntries().add(ace);
219       }
220     }
221     if (!foundAce)
222       newAcl.getEntries().add(new AccessControlEntry(role, action, allow));
223 
224     return newAcl;
225   }
226 
227   /**
228    * Reduces an access control list by an access control entry
229    *
230    * @param acl
231    *          the access control list to reduce
232    * @param role
233    *          the role of the access control entry to remove
234    * @param action
235    *          the action of the access control entry to remove
236    * @return the reduced access control list or the same if already contained
237    */
238   public static AccessControlList reduceAcl(AccessControlList acl, String role, String action) {
239     AccessControlList newAcl = new AccessControlList();
240     for (AccessControlEntry ace : acl.getEntries()) {
241       if (!ace.getAction().equalsIgnoreCase(action) || !ace.getRole().equalsIgnoreCase(role)) {
242         newAcl.getEntries().add(ace);
243       }
244     }
245     return newAcl;
246   }
247 
248   /**
249    * Constructor function for ACLs.
250    *
251    * @see #entry(String, String, boolean)
252    * @see #entries(String, org.opencastproject.util.data.Tuple[])
253    */
254   public static AccessControlList acl(Either<AccessControlEntry, List<AccessControlEntry>>... entries) {
255     // sequence entries
256     List<AccessControlEntry> seq = new ArrayList<>();
257     for (Either<AccessControlEntry, List<AccessControlEntry>> current : entries) {
258       if (current.isLeft()) {
259         seq.add(current.left().value());
260       } else {
261         seq.addAll(current.right().value());
262       }
263     }
264     return new AccessControlList(seq);
265   }
266 
267 
268   /** Create a single access control entry. */
269   public static Either<AccessControlEntry, List<AccessControlEntry>> entry(String role, String action, boolean allow) {
270     return left(new AccessControlEntry(role, action, allow));
271   }
272 
273   /** Create a list of access control entries for a given role. */
274   public static Either<AccessControlEntry, List<AccessControlEntry>> entries(final String role,
275       Tuple<String, Boolean>... actions) {
276     List<AccessControlEntry> entries = Arrays.stream(actions)
277         .map(action -> new AccessControlEntry(role, action.getA(), action.getB()))
278         .toList();
279     return right(entries);
280   }
281 
282   /**
283    * Define equality on AccessControlLists. Two AccessControlLists are considered equal if they contain the exact same
284    * entries no matter in which order.
285    * <p>
286    * This has not been implemented in terms of #equals and #hashCode because the list of entries is not immutable and
287    * therefore not suitable to be put in a set.
288    */
289   public static boolean equals(AccessControlList a, AccessControlList b) {
290     return bothNotNull(a, b) && eqListUnsorted(a.getEntries(), b.getEntries());
291   }
292 
293   /** Calculate an MD5 checksum for an {@link AccessControlList}. */
294   public static Checksum calculateChecksum(AccessControlList acl) {
295     // Use 0 as a word separator. This is safe since none of the UTF-8 code points
296     // except \u0000 contains a null byte when converting to a byte array.
297     final byte[] sep = new byte[] { 0 };
298 
299     // Sort ACL entries
300     List<AccessControlEntry> sortedEntries = acl.getEntries().stream()
301         .sorted(sortAcl)
302         .collect(Collectors.toList());
303 
304     MessageDigest digest = mkMd5MessageDigest();
305 
306     for (AccessControlEntry entry : sortedEntries) {
307       String[] fields = {
308           entry.getRole(),
309           entry.getAction(),
310           Boolean.toString(entry.isAllow())
311       };
312       for (String field : fields) {
313         digest.update(field.getBytes(StandardCharsets.UTF_8));
314         // add separator byte (see definition above)
315         digest.update(sep);
316       }
317     }
318 
319     try {
320       return Checksum.create("md5", Checksum.convertToHex(digest.digest()));
321     } catch (NoSuchAlgorithmException e) {
322       throw new RuntimeException(e);
323     }
324   }
325 
326   private static MessageDigest mkMd5MessageDigest() {
327     try {
328       return MessageDigest.getInstance("MD5");
329     } catch (NoSuchAlgorithmException e) {
330       throw new RuntimeException(e);
331     }
332   }
333 
334   private static Comparator<AccessControlEntry> sortAcl = new Comparator<AccessControlEntry>() {
335     @Override
336     public int compare(AccessControlEntry o1, AccessControlEntry o2) {
337       // compare role
338       int compareTo = StringUtils.trimToEmpty(o1.getRole()).compareTo(StringUtils.trimToEmpty(o2.getRole()));
339       if (compareTo != 0)
340         return compareTo;
341 
342       // compare action
343       compareTo = StringUtils.trimToEmpty(o1.getAction()).compareTo(StringUtils.trimToEmpty(o2.getAction()));
344       if (compareTo != 0)
345         return compareTo;
346 
347       // compare allow
348       return Boolean.valueOf(o1.isAllow()).compareTo(o2.isAllow());
349     }
350   };
351 
352 }