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,
145    *        org.opencastproject.security.api.User, org.opencastproject.security.api.Organization, Object)}
146    * as a predicate function.
147    */
148   private static Predicate<Object> isAuthorizedFn(final AccessControlList acl, final User user,
149       final Organization org) {
150     return action -> isAuthorized(acl, user, org, action);
151   }
152 
153   /**
154    * Returns true only if <em>all</em> actions are authorized.
155    *
156    * @see #isAuthorized(AccessControlList, User, Organization, Object)
157    */
158   public static boolean isAuthorizedAll(AccessControlList acl, User user, Organization org, Object... actions) {
159     Predicate<Object> isAuthorized = isAuthorizedFn(acl, user, org);
160     return Arrays.stream(actions).allMatch(isAuthorized);
161   }
162 
163   /**
164    * Returns true if at least <em>one</em> action is authorized.
165    *
166    * @see #isAuthorized(AccessControlList, User, Organization, Object)
167    */
168   public static boolean isAuthorizedOne(AccessControlList acl, User user, Organization org, Object... actions) {
169     Predicate<Object> isAuthorized = isAuthorizedFn(acl, user, org);
170     return Arrays.stream(actions).anyMatch(isAuthorized);
171   }
172 
173   /**
174    * Returns true if <em>all</em> actions are prohibited.
175    *
176    * @see #isAuthorized(AccessControlList, User, Organization, Object)
177    */
178   public static boolean isProhibitedAll(AccessControlList acl, User user, Organization org, Object... actions) {
179     Predicate<Object> isAuthorized = isAuthorizedFn(acl, user, org);
180     return Arrays.stream(actions).noneMatch(isAuthorized);
181   }
182 
183   /**
184    * Returns true if at least <em>one</em> action is prohibited.
185    *
186    * @see #isAuthorized(AccessControlList, User, Organization, Object)
187    */
188   public static boolean isProhibitedOne(AccessControlList acl, User user, Organization org, Object... actions) {
189     Predicate<Object> isAuthorized = isAuthorizedFn(acl, user, org);
190     return Arrays.stream(actions).anyMatch(isAuthorized.negate());
191   }
192 
193   /**
194    * Extends an access control list with an access control entry
195    *
196    * @param acl
197    *          the access control list to extend
198    * @param role
199    *          the access control entry role
200    * @param action
201    *          the access control entry action
202    * @param allow
203    *          whether this access control entry role is allowed to take this action
204    * @return the extended access control list or the same if already contained
205    */
206   public static AccessControlList extendAcl(AccessControlList acl, String role, String action, boolean allow) {
207     AccessControlList newAcl = new AccessControlList();
208     boolean foundAce = false;
209     for (AccessControlEntry ace : acl.getEntries()) {
210       if (ace.getAction().equalsIgnoreCase(action) && ace.getRole().equalsIgnoreCase(role)) {
211         if (ace.isAllow() == allow) {
212           // Entry is already the same so just return the acl
213           return acl;
214         } else {
215           // We need to change the allow on the one entry.
216           foundAce = true;
217           newAcl.getEntries().add(new AccessControlEntry(role, action, allow));
218         }
219       } else {
220         newAcl.getEntries().add(ace);
221       }
222     }
223     if (!foundAce) {
224       newAcl.getEntries().add(new AccessControlEntry(role, action, allow));
225     }
226 
227     return newAcl;
228   }
229 
230   /**
231    * Reduces an access control list by an access control entry
232    *
233    * @param acl
234    *          the access control list to reduce
235    * @param role
236    *          the role of the access control entry to remove
237    * @param action
238    *          the action of the access control entry to remove
239    * @return the reduced access control list or the same if already contained
240    */
241   public static AccessControlList reduceAcl(AccessControlList acl, String role, String action) {
242     AccessControlList newAcl = new AccessControlList();
243     for (AccessControlEntry ace : acl.getEntries()) {
244       if (!ace.getAction().equalsIgnoreCase(action) || !ace.getRole().equalsIgnoreCase(role)) {
245         newAcl.getEntries().add(ace);
246       }
247     }
248     return newAcl;
249   }
250 
251   /**
252    * Constructor function for ACLs.
253    *
254    * @see #entry(String, String, boolean)
255    * @see #entries(String, org.opencastproject.util.data.Tuple[])
256    */
257   public static AccessControlList acl(Either<AccessControlEntry, List<AccessControlEntry>>... entries) {
258     // sequence entries
259     List<AccessControlEntry> seq = new ArrayList<>();
260     for (Either<AccessControlEntry, List<AccessControlEntry>> current : entries) {
261       if (current.isLeft()) {
262         seq.add(current.left().value());
263       } else {
264         seq.addAll(current.right().value());
265       }
266     }
267     return new AccessControlList(seq);
268   }
269 
270 
271   /** Create a single access control entry. */
272   public static Either<AccessControlEntry, List<AccessControlEntry>> entry(String role, String action, boolean allow) {
273     return left(new AccessControlEntry(role, action, allow));
274   }
275 
276   /** Create a list of access control entries for a given role. */
277   public static Either<AccessControlEntry, List<AccessControlEntry>> entries(final String role,
278       Tuple<String, Boolean>... actions) {
279     List<AccessControlEntry> entries = Arrays.stream(actions)
280         .map(action -> new AccessControlEntry(role, action.getA(), action.getB()))
281         .toList();
282     return right(entries);
283   }
284 
285   /**
286    * Define equality on AccessControlLists. Two AccessControlLists are considered equal if they contain the exact same
287    * entries no matter in which order.
288    * <p>
289    * This has not been implemented in terms of #equals and #hashCode because the list of entries is not immutable and
290    * therefore not suitable to be put in a set.
291    */
292   public static boolean equals(AccessControlList a, AccessControlList b) {
293     return bothNotNull(a, b) && eqListUnsorted(a.getEntries(), b.getEntries());
294   }
295 
296   /** Calculate an MD5 checksum for an {@link AccessControlList}. */
297   public static Checksum calculateChecksum(AccessControlList acl) {
298     // Use 0 as a word separator. This is safe since none of the UTF-8 code points
299     // except \u0000 contains a null byte when converting to a byte array.
300     final byte[] sep = new byte[] { 0 };
301 
302     // Sort ACL entries
303     List<AccessControlEntry> sortedEntries = acl.getEntries().stream()
304         .sorted(sortAcl)
305         .collect(Collectors.toList());
306 
307     MessageDigest digest = mkMd5MessageDigest();
308 
309     for (AccessControlEntry entry : sortedEntries) {
310       String[] fields = {
311           entry.getRole(),
312           entry.getAction(),
313           Boolean.toString(entry.isAllow())
314       };
315       for (String field : fields) {
316         digest.update(field.getBytes(StandardCharsets.UTF_8));
317         // add separator byte (see definition above)
318         digest.update(sep);
319       }
320     }
321 
322     try {
323       return Checksum.create("md5", Checksum.convertToHex(digest.digest()));
324     } catch (NoSuchAlgorithmException e) {
325       throw new RuntimeException(e);
326     }
327   }
328 
329   private static MessageDigest mkMd5MessageDigest() {
330     try {
331       return MessageDigest.getInstance("MD5");
332     } catch (NoSuchAlgorithmException e) {
333       throw new RuntimeException(e);
334     }
335   }
336 
337   private static Comparator<AccessControlEntry> sortAcl = new Comparator<AccessControlEntry>() {
338     @Override
339     public int compare(AccessControlEntry o1, AccessControlEntry o2) {
340       // compare role
341       int compareTo = StringUtils.trimToEmpty(o1.getRole()).compareTo(StringUtils.trimToEmpty(o2.getRole()));
342       if (compareTo != 0) {
343         return compareTo;
344       }
345 
346       // compare action
347       compareTo = StringUtils.trimToEmpty(o1.getAction()).compareTo(StringUtils.trimToEmpty(o2.getAction()));
348       if (compareTo != 0) {
349         return compareTo;
350       }
351 
352       // compare allow
353       return Boolean.valueOf(o1.isAllow()).compareTo(o2.isAllow());
354     }
355   };
356 
357 }