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