| // Copyright 2012 Google Inc. All Rights Reserved. |
| // |
| // Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // 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 com.google.enterprise.adaptor; |
| |
| import com.google.common.collect.Sets; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Locale; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TreeSet; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| |
| /** |
| * Immutable access control list. For description of the semantics of the |
| * various fields, see {@link #isAuthorizedLocal isAuthorizedLocal} and |
| * {@link #isAuthorized isAuthorized}. Users and groups must not be {@code |
| * null}, {@code ""}, or have surrounding whitespace. These values are |
| * disallowed to prevent confusion since {@code null} doesn't make sense, {@code |
| * ""} would be ignored by the GSA, and surrounding whitespace is automatically |
| * trimmed by the GSA. |
| */ |
| public class Acl { |
| /** |
| * Empty convenience instance with all defaults used. |
| * |
| * @see Builder#Acl.Builder() |
| */ |
| public static final Acl EMPTY = new Acl.Builder().build(); |
| /** |
| * An almost-empty ACL that can be used instead of {@link #EMPTY} when sending |
| * ACLs to the GSA. This allows the GSA to distinguish between an empty ACL |
| * and a non-existant ACL. |
| */ |
| static final Acl FAKE_EMPTY = new Acl.Builder() |
| .setDenyUsers(Arrays.asList( |
| new UserPrincipal("google:fakeUserToPreventMissingAcl"))) |
| .build(); |
| |
| private static final Logger log = Logger.getLogger(Acl.class.getName()); |
| |
| /** Locale used for case insensitivity related operations. */ |
| private static final Locale CASE_LOCALE = Locale.ENGLISH; |
| |
| private final Set<GroupPrincipal> permitGroups; |
| private final Set<GroupPrincipal> denyGroups; |
| private final Set<UserPrincipal> permitUsers; |
| private final Set<UserPrincipal> denyUsers; |
| private final DocId inheritFrom; |
| private final String inheritFromFragment; |
| private final InheritanceType inheritType; |
| private final boolean caseSensitive; |
| |
| private Acl(Set<GroupPrincipal> permitGroups, Set<GroupPrincipal> denyGroups, |
| Set<UserPrincipal> permitUsers, Set<UserPrincipal> denyUsers, |
| DocId inheritFrom, String inheritFromFragment, |
| InheritanceType inheritType, boolean caseSensitive) { |
| if (!caseSensitive) { |
| permitGroups = Collections.unmodifiableSet(cmpWrap(permitGroups)); |
| denyGroups = Collections.unmodifiableSet(cmpWrap(denyGroups)); |
| permitUsers = Collections.unmodifiableSet(cmpWrap(permitUsers)); |
| denyUsers = Collections.unmodifiableSet(cmpWrap(denyUsers)); |
| } |
| this.permitGroups = permitGroups; |
| this.denyGroups = denyGroups; |
| this.permitUsers = permitUsers; |
| this.denyUsers = denyUsers; |
| this.inheritFrom = inheritFrom; |
| this.inheritFromFragment = inheritFromFragment; |
| this.inheritType = inheritType; |
| this.caseSensitive = caseSensitive; |
| } |
| |
| private <P extends Principal> Set<P> cmpWrap(Set<P> unwrapped) { |
| Set<P> tmp = new TreeSet<P>(new CaseInsensitiveCmp<P>()); |
| tmp.addAll(unwrapped); |
| return tmp; |
| } |
| |
| private static class CaseInsensitiveCmp<P extends Principal> |
| implements Comparator<P> { |
| /** Does not differentiate between UserPrincipal and GroupPrincipal */ |
| public int compare(P p1, P p2) { |
| String ns1 = p1.getNamespace().toLowerCase(CASE_LOCALE); |
| String ns2 = p2.getNamespace().toLowerCase(CASE_LOCALE); |
| int nscmp = ns1.compareTo(ns2); |
| if (0 != nscmp) { |
| return nscmp; |
| } |
| // OK, same namespace |
| |
| String n1 = p1.getName().toLowerCase(CASE_LOCALE); |
| String n2 = p2.getName().toLowerCase(CASE_LOCALE); |
| return n1.compareTo(n2); |
| } |
| |
| public boolean equals(Object o) { |
| return o instanceof CaseInsensitiveCmp; |
| } |
| } |
| |
| /** |
| * Returns immutable set of permitted groups. |
| */ |
| public Set<GroupPrincipal> getPermitGroups() { |
| return permitGroups; |
| } |
| |
| /** |
| * Returns immutable set of denied groups. |
| */ |
| public Set<GroupPrincipal> getDenyGroups() { |
| return denyGroups; |
| } |
| |
| /** |
| * Returns immutable set of permitted users. |
| */ |
| public Set<UserPrincipal> getPermitUsers() { |
| return permitUsers; |
| } |
| |
| /** |
| * Returns immutable set of denied users. |
| */ |
| public Set<UserPrincipal> getDenyUsers() { |
| return denyUsers; |
| } |
| |
| /** |
| * Returns immutable set of permitted users and groups. |
| */ |
| public Set<Principal> getPermits() { |
| return Sets.union(permitUsers, permitGroups); |
| } |
| |
| /** |
| * Returns immutable set of denied users and groups; |
| */ |
| public Set<Principal> getDenies() { |
| return Sets.union(denyUsers, denyGroups); |
| } |
| |
| /** |
| * Returns {@code DocId} these ACLs are inherited from. This is also known as |
| * the "parent's" ACLs. Note that the parent's {@code InheritanceType} |
| * determines how to combine results with this ACL. |
| * |
| * @see #getInheritanceType |
| */ |
| public DocId getInheritFrom() { |
| return inheritFrom; |
| } |
| |
| /** |
| * Returns fragment, if there is one, that specifies which of the parent's |
| * ACLs is to to be inhertied from. |
| * |
| * @see #getInheritanceType |
| */ |
| public String getInheritFromFragment() { |
| return inheritFromFragment; |
| } |
| |
| /** |
| * Returns the inheritance type used to combine authz decisions of these ACLs |
| * with its <em>child</em>. The inheritance type applies to the interaction |
| * between this ACL and any <em>children</em> it has. |
| * |
| * @see #getInheritFrom |
| */ |
| public InheritanceType getInheritanceType() { |
| return inheritType; |
| } |
| |
| /** |
| * Says whether letter casing differentiates names during authorization. |
| */ |
| public boolean isEverythingCaseSensitive() { |
| return caseSensitive; |
| } |
| |
| /** |
| * Says whether letter casing doesn't matter during authorization. |
| */ |
| public boolean isEverythingCaseInsensitive() { |
| return !caseSensitive; |
| } |
| |
| /** |
| * Determine if the provided {@code userIdentifier} belonging to {@code |
| * groups} is authorized, ignoring inheritance. Deny trumps permit, |
| * independent of how specific the rule is. So if a user is in permitUsers and |
| * one of the user's groups is in denyGroups, that user will be denied. If a |
| * user and his groups are unspecified in the ACL, then the response is |
| * indeterminate. |
| */ |
| public AuthzStatus isAuthorizedLocal(AuthnIdentity userIdentity) { |
| UserPrincipal userIdentifier = userIdentity.getUser(); |
| Set<GroupPrincipal> commonGroups; |
| if (caseSensitive) { |
| commonGroups = new HashSet<GroupPrincipal>(denyGroups); |
| } else { |
| commonGroups = cmpWrap(denyGroups); |
| } |
| |
| Set<GroupPrincipal> userGroups = userIdentity.getGroups(); |
| if (!caseSensitive) { |
| userGroups = Collections.unmodifiableSet(cmpWrap(userGroups)); |
| } |
| |
| commonGroups.retainAll(userGroups); |
| if (denyUsers.contains(userIdentifier) || !commonGroups.isEmpty()) { |
| return AuthzStatus.DENY; |
| } |
| |
| commonGroups.clear(); |
| commonGroups.addAll(permitGroups); |
| commonGroups.retainAll(userGroups); |
| if (permitUsers.contains(userIdentifier) || !commonGroups.isEmpty()) { |
| return AuthzStatus.PERMIT; |
| } |
| |
| return AuthzStatus.INDETERMINATE; |
| } |
| |
| /** |
| * Determine if the provided {@code userIdentity} belonging to {@code |
| * groups} is authorized for the provided {@code aclChain}. The chain should |
| * be in order of root to leaf; that means that the particular file or folder |
| * you are checking for authz will be at the end of the chain. |
| * |
| * <p>If you have an ACL and wish to determine if a user is authorized, you |
| * should manually generate an aclChain by recursively retrieving the ACLs of |
| * the {@code inheritFrom} {@link DocId}. The ACL you started with should be |
| * at the end of the chain. Alternatively, you can use {@link |
| * #isAuthorizedBatch isAuthorizedBatch()}. |
| * |
| * <p>If the entire chain has empty permit/deny sets, then the result is |
| * {@link AuthzStatus#INDETERMINATE}. |
| * |
| * <p>The result of the entire chain is the non-local decision of the root. |
| * The non-local decision of any entry in the chain is the local decision of |
| * that entry (as calculated with {@link #isAuthorizedLocal |
| * isAuthorizedLocal()}) combined with the non-local decision of the next |
| * entry in the chain via the {@code InheritanceType} of the original entry. |
| * To repeat, the non-local decision of an entry is that entry's local |
| * decision combined using its {@code InheritanceType} with its child's |
| * non-local decision (which is recursive). Thus, if the root's inheritance |
| * type is {@link InheritanceType#PARENT_OVERRIDES} and its local decision is |
| * {@link AuthzStatus#DENY}, then independent of any decendant's local |
| * decision, the decision of the chain will be {@code DENY}. |
| * |
| * <p>It should also be noted that the leaf's inheritance type does not matter |
| * and is ignored. |
| * |
| * <p>It is very important to note that a completely empty ACL (one that has |
| * all defaults) is equivalent to having no ACLs. The GSA considers content |
| * from the Adaptor as public unless it provides an ACL. Thus, empty ACLs |
| * cause a document to become public and the GSA does not use ACLs when |
| * considering public documents (and all results are PERMIT). However, for |
| * non-Adaptor situations, you can get a document to be private and have no |
| * ACLs. In these situations the ACLs are checked, but the result is |
| * INDETERMINATE and different authz checks must be made. |
| * |
| * @param userIdentity identity containing the user's username and all the |
| * groups the user belongs to |
| * @param aclChain ordered list of ACLs from root to leaf |
| * @throws IllegalArgumentException if the chain is empty, the first element |
| * of the chain's {@code getInheritFrom() != null}, or if any element but |
| * the first has {@code getInheritFrom() == null}. |
| * @see #isAuthorizedLocal |
| * @see InheritanceType |
| */ |
| public static AuthzStatus isAuthorized(AuthnIdentity userIdentity, |
| List<Acl> aclChain) { |
| // Check for completely broken chains. Users of the API should be aware |
| // enough to easily prevent these from happening. These also don't directly |
| // relate to a case on the GSA because the GSA is working more on the |
| // isAuthorizedRecurse level. |
| if (aclChain.size() < 1) { |
| throw new IllegalArgumentException( |
| "aclChain must contain at least one ACL"); |
| } |
| if (aclChain.get(0).getInheritFrom() != null) { |
| throw new IllegalArgumentException( |
| "Chain must start at the root, which must not have an inheritFrom"); |
| } |
| for (int i = 1; i < aclChain.size(); i++) { |
| if (aclChain.get(i).getInheritFrom() == null) { |
| throw new IllegalArgumentException( |
| "Each ACL in the chain except the first should have an " |
| + "inheritFrom"); |
| } |
| } |
| |
| // Check for broken chain constructions. These don't throw an exception to |
| // 1) match the GSA's identical handling of these situations and 2) because |
| // we don't want to throw an exception if the caller can't easily prevent |
| // it from ever occuring. |
| if (aclChain.size() == 1) { |
| Acl acl = aclChain.get(0); |
| if (acl.equals(EMPTY)) { |
| log.log(Level.FINE, "Chain only has one ACL and it is empty. This " |
| + "implies 'no ACLs.'"); |
| return AuthzStatus.INDETERMINATE; |
| } |
| } |
| for (int i = 0; i < aclChain.size() - 1; i++) { |
| if (aclChain.get(i).getInheritanceType() == InheritanceType.LEAF_NODE) { |
| log.log(Level.WARNING, "Only the last ACL in a chain can have the " |
| + "inheritance type LEAF"); |
| return AuthzStatus.INDETERMINATE; |
| } |
| } |
| AuthzStatus result = isAuthorizedRecurse(userIdentity, aclChain); |
| return (result == AuthzStatus.INDETERMINATE) ? AuthzStatus.DENY : result; |
| } |
| |
| private static AuthzStatus isAuthorizedRecurse( |
| final AuthnIdentity userIdentity, final List<Acl> aclChain) { |
| if (aclChain.size() == 1) { |
| return aclChain.get(0).isAuthorizedLocal(userIdentity); |
| } |
| Decision parentDecision = new Decision() { |
| @Override |
| protected AuthzStatus computeDecision() { |
| return aclChain.get(0).isAuthorizedLocal(userIdentity); |
| } |
| }; |
| Decision childDecision = new Decision() { |
| @Override |
| protected AuthzStatus computeDecision() { |
| // Recurse. |
| return isAuthorizedRecurse(userIdentity, |
| aclChain.subList(1, aclChain.size())); |
| } |
| }; |
| return aclChain.get(0).getInheritanceType() |
| .isAuthorized(childDecision, parentDecision); |
| } |
| |
| /** |
| * Check authz for many DocIds at once. This will only fetch ACL information |
| * for a DocId once, even when considering inheritFrom. It will then create |
| * the appropriate chains and call {@link #isAuthorized isAuthorized()}. |
| * |
| * <p>If there is an inheritance cycle, an ACL for a DocId in {@code ids} was |
| * not returned by {@code retriever} when requested, or an inherited ACL was |
| * not returned by {@code retriever} when requested, its response will be |
| * {@link AuthzStatus#INDETERMINATE} for that DocId. |
| * |
| * @param userIdentity identity containing the user's username and all the |
| * groups the user belongs to |
| * @param ids collection of DocIds that need authz performed |
| * @param retriever object to use to obtain an ACL for a given DocId |
| * @throws IOException if the retriever throws an IOException |
| */ |
| public static Map<DocId, AuthzStatus> isAuthorizedBatch( |
| AuthnIdentity userIdentity, Collection<DocId> ids, |
| BatchRetriever retriever) throws IOException { |
| Map<DocId, Acl> acls = retrieveNecessaryAcls(ids, retriever); |
| Map<DocId, AuthzStatus> results |
| = new HashMap<DocId, AuthzStatus>(ids.size() * 2); |
| for (DocId docId : ids) { |
| List<Acl> chain = createChain(docId, acls); |
| AuthzStatus result; |
| if (chain == null) { |
| // There was a cycle or other problem generating the chain. |
| result = AuthzStatus.INDETERMINATE; |
| } else { |
| result = isAuthorized(userIdentity, chain); |
| } |
| results.put(docId, result); |
| } |
| return Collections.unmodifiableMap(results); |
| } |
| |
| private static Map<DocId, Acl> retrieveNecessaryAcls(Collection<DocId> ids, |
| BatchRetriever retriever) throws IOException { |
| Map<DocId, Acl> acls = new HashMap<DocId, Acl>(ids.size() * 2); |
| Set<DocId> missingAcls = new HashSet<DocId>(); |
| Set<DocId> pendingRetrieval = new HashSet<DocId>(ids); |
| Set<Acl> checkedAcl = new HashSet<Acl>(ids.size() * 2); |
| Set<Acl> toProcess = new HashSet<Acl>(ids.size() * 2); |
| while (!pendingRetrieval.isEmpty()) { |
| Map<DocId, Acl> returned = retriever.retrieveAcls(pendingRetrieval); |
| toProcess.clear(); |
| for (Map.Entry<DocId, Acl> me : returned.entrySet()) { |
| if (me.getValue() == null) { |
| throw new NullPointerException( |
| "BatchRetriever returned null for a DocId"); |
| } |
| DocId key = me.getKey(); |
| if (acls.containsKey(key) || missingAcls.contains(key)) { |
| // Don't replace previous results since we have already checked them. |
| continue; |
| } |
| acls.put(key, me.getValue()); |
| // If we requested this ACL, follow its inheritance. |
| if (pendingRetrieval.contains(key)) { |
| toProcess.add(me.getValue()); |
| } |
| } |
| // Compute ACLs that we requested, but did not receive. |
| pendingRetrieval.removeAll(returned.keySet()); |
| missingAcls.addAll(pendingRetrieval); |
| |
| pendingRetrieval.clear(); |
| |
| for (Acl acl : toProcess) { |
| // Follow the inheritance chain until it terminates. |
| while (true) { |
| if (checkedAcl.contains(acl)) { |
| // Already processed. |
| break; |
| } |
| checkedAcl.add(acl); |
| |
| DocId parent = acl.getInheritFrom(); |
| if (parent == null) { |
| // Inheritance chain terminated; everything looks good. |
| break; |
| } else if (missingAcls.contains(parent)) { |
| // Failed to retrieve parent, so give up. |
| break; |
| } else if (acls.containsKey(parent)) { |
| // Already have the parent ACLs, so check parent. |
| acl = acls.get(parent); |
| } else { |
| // Request parent ACLs. |
| pendingRetrieval.add(parent); |
| break; |
| } |
| } |
| } |
| } |
| return acls; |
| } |
| |
| private static List<Acl> createChain(DocId docId, Map<DocId, Acl> acls) { |
| List<Acl> chain = new LinkedList<Acl>(); |
| Set<Acl> used = new HashSet<Acl>(); |
| DocId cur = docId; |
| while (cur != null) { |
| Acl acl = acls.get(cur); |
| if (acl == null) { |
| if (chain.isEmpty()) { |
| // The GSA turns this into a chain containing only an empty ACL (which |
| // eventually becomes indeterminate), but we want this to be |
| // indeterminate immediately because we do not have public/private |
| // flags for documents and we don't want to accidentally cause a |
| // document to become public. |
| log.log(Level.FINE, "Document does not seem to use ACLs: {0}", cur); |
| } else { |
| log.log(Level.WARNING, "Missing ACLs for document ''{0}'' inherited " |
| + "from another document", cur); |
| } |
| return null; |
| } |
| if (used.contains(acl)) { |
| log.log(Level.WARNING, "Detected ACL cycle at ''{0}''", cur); |
| return null; |
| } |
| used.add(acl); |
| chain.add(0, acl); |
| cur = acl.getInheritFrom(); |
| } |
| return Collections.unmodifiableList(chain); |
| } |
| |
| /** |
| * Equality is determined if all the permit/deny sets are equal and the |
| * inheritance is equal. |
| */ |
| @Override |
| public boolean equals(Object o) { |
| if (!(o instanceof Acl)) { |
| return false; |
| } |
| if (this == o) { |
| return true; |
| } |
| Acl a = (Acl) o; |
| return inheritType == a.inheritType |
| // Handle null case. |
| && (inheritFrom == a.inheritFrom |
| || (inheritFrom != null && inheritFrom.equals(a.inheritFrom))) |
| && (inheritFromFragment == a.inheritFromFragment |
| || (inheritFromFragment != null |
| && inheritFromFragment.equals(a.inheritFromFragment))) |
| && permitGroups.equals(a.permitGroups) |
| && denyGroups.equals(a.denyGroups) |
| && permitUsers.equals(a.permitUsers) && denyUsers.equals(a.denyUsers) |
| && caseSensitive == a.caseSensitive; |
| } |
| |
| /** |
| * Returns a hash code for this object that agrees with {@code equals}. |
| */ |
| @Override |
| public int hashCode() { |
| return Arrays.hashCode(new Object[] { |
| permitGroups, denyGroups, permitUsers, denyUsers, |
| inheritFrom, inheritFromFragment, inheritType, caseSensitive |
| }); |
| } |
| |
| /** |
| * Generates a string useful for debugging that contains users and groups |
| * along with inheritance information. |
| */ |
| @Override |
| public String toString() { |
| return "Acl(caseSensitive=" + caseSensitive |
| + ", inheritFrom=" + inheritFrom |
| + (inheritFromFragment == null ? "" : "#" + inheritFromFragment) |
| + ", inheritType=" + inheritType |
| + ", permitGroups=" + permitGroups + ", denyGroups=" + denyGroups |
| + ", permitUsers=" + permitUsers + ", denyUsers=" + denyUsers + ")"; |
| } |
| |
| /** |
| * Batch retrieval of ACLs for efficent processing of many authz checks at |
| * once. |
| * |
| * @see Acl#isAuthorizedBatch |
| */ |
| public static interface BatchRetriever { |
| /** |
| * Retrieve the ACLs for the requested DocIds. This method is permitted to |
| * return ACLs for DocIds not requested, but it should never provide a |
| * {@code null} value for a DocId's ACLs. If a DocId does not exist, then it |
| * should be missing in the returned map. |
| * |
| * <p>This method should provide any ACLs for named resources (if any are in |
| * use, which is not the common case) in addition to any normal documents. |
| * For more information about named resources, see {@link |
| * DocIdPusher#pushNamedResources}. |
| * |
| * @throws IOException if there was an error contacting the data store |
| */ |
| public Map<DocId, Acl> retrieveAcls(Set<DocId> ids) |
| throws IOException; |
| } |
| |
| /** |
| * Mutable ACL for creating instances of {@link Acl}. |
| */ |
| public static class Builder { |
| private Set<GroupPrincipal> permitGroups = Collections.emptySet(); |
| private Set<GroupPrincipal> denyGroups = Collections.emptySet(); |
| private Set<UserPrincipal> permitUsers = Collections.emptySet(); |
| private Set<UserPrincipal> denyUsers = Collections.emptySet(); |
| private DocId inheritFrom; |
| private String inheritFromFragment; |
| private InheritanceType inheritType = InheritanceType.LEAF_NODE; |
| private boolean caseSensitive = true; |
| |
| /** |
| * Create new empty builder. All sets are empty, inheritFrom is {@code |
| * null}, and inheritType is {@link InheritanceType#LEAF_NODE}. |
| */ |
| public Builder() {} |
| |
| /** |
| * Create and initialize builder with ACL information provided in {@code |
| * acl}. |
| */ |
| public Builder(Acl acl) { |
| permitGroups = sanitizeSet(acl.getPermitGroups()); |
| denyGroups = sanitizeSet(acl.getDenyGroups()); |
| permitUsers = sanitizeSet(acl.getPermitUsers()); |
| denyUsers = sanitizeSet(acl.getDenyUsers()); |
| inheritFrom = acl.getInheritFrom(); |
| inheritFromFragment = acl.getInheritFromFragment(); |
| inheritType = acl.getInheritanceType(); |
| caseSensitive = acl.isEverythingCaseSensitive(); |
| } |
| |
| private <P extends Principal> Set<P> sanitizeSet(Collection<P> set) { |
| if (set.isEmpty()) { |
| return Collections.emptySet(); |
| } |
| // Check all the values to make sure they are valid. |
| for (P item : set) { |
| if (item == null) { |
| throw new NullPointerException("Entries in set may not be null"); |
| } |
| } |
| // Use TreeSets so that sets have predictable order when serializing. |
| return Collections.unmodifiableSet(new TreeSet<P>(set)); |
| } |
| |
| /** |
| * Create immutable {@link Acl} instance of the current state. |
| */ |
| public Acl build() { |
| return new Acl(permitGroups, denyGroups, permitUsers, denyUsers, |
| inheritFrom, inheritFromFragment, inheritType, caseSensitive); |
| } |
| |
| /** |
| * Replace existing permit groups. |
| * |
| * @return the same instance of the builder, for chaining calls |
| * @throws NullPointerException if the collection is {@code null} or |
| * contains {@code null} |
| * @throws IllegalArgumentException if the collection contains {@code ""} |
| * or a value that has leading or trailing whitespace |
| */ |
| public Builder setPermitGroups(Collection<GroupPrincipal> permitGroups) { |
| this.permitGroups = sanitizeSet(permitGroups); |
| return this; |
| } |
| |
| /** |
| * Replace existing deny groups. |
| * |
| * @return the same instance of the builder, for chaining calls |
| * @throws NullPointerException if the collection is {@code null} or |
| * contains {@code null} |
| * @throws IllegalArgumentException if the collection contains {@code ""} |
| * or a value that has leading or trailing whitespace |
| */ |
| public Builder setDenyGroups(Collection<GroupPrincipal> denyGroups) { |
| this.denyGroups = sanitizeSet(denyGroups); |
| return this; |
| } |
| |
| /** |
| * Replace existing permit users. |
| * |
| * @return the same instance of the builder, for chaining calls |
| * @throws NullPointerException if the collection is {@code null} or |
| * contains {@code null} |
| * @throws IllegalArgumentException if the collection contains {@code ""} |
| * or a value that has leading or trailing whitespace |
| */ |
| public Builder setPermitUsers(Collection<UserPrincipal> permitUsers) { |
| this.permitUsers = sanitizeSet(permitUsers); |
| return this; |
| } |
| |
| /** |
| * Replace existing deny users. |
| * |
| * @return the same instance of the builder, for chaining calls |
| * @throws NullPointerException if the collection is {@code null} or |
| * contains {@code null} |
| * @throws IllegalArgumentException if the collection contains {@code ""} |
| * or a value that has leading or trailing whitespace |
| */ |
| public Builder setDenyUsers(Collection<UserPrincipal> denyUsers) { |
| this.denyUsers = sanitizeSet(denyUsers); |
| return this; |
| } |
| |
| /** |
| * Replace existing permit users and groups. |
| * |
| * @return the same instance of the builder, for chaining calls |
| * @throws NullPointerException if the collection is {@code null} or |
| * contains {@code null} |
| * @throws IllegalArgumentException if the collection contains {@code ""} |
| * or a value that has leading or trailing whitespace |
| */ |
| public Builder setPermits(Collection<Principal> permits) { |
| Collection<GroupPrincipal> groups = new ArrayList<GroupPrincipal>(); |
| Collection<UserPrincipal> users = new ArrayList<UserPrincipal>(); |
| for (Principal principal : permits) { |
| if (principal.isGroup()) { |
| groups.add((GroupPrincipal) principal); |
| } else { |
| users.add((UserPrincipal) principal); |
| } |
| } |
| Set<GroupPrincipal> sanitizedGroups = sanitizeSet(groups); |
| Set<UserPrincipal> sanitizedUsers = sanitizeSet(users); |
| this.permitGroups = sanitizedGroups; |
| this.permitUsers = sanitizedUsers; |
| return this; |
| } |
| |
| /** |
| * Replace existing deny users and groups. |
| * |
| * @return the same instance of the builder, for chaining calls |
| * @throws NullPointerException if the collection is {@code null} or |
| * contains {@code null} |
| * @throws IllegalArgumentException if the collection contains {@code ""} |
| * or a value that has leading or trailing whitespace |
| */ |
| public Builder setDenies(Collection<Principal> denies) { |
| Collection<GroupPrincipal> groups = new ArrayList<GroupPrincipal>(); |
| Collection<UserPrincipal> users = new ArrayList<UserPrincipal>(); |
| for (Principal principal : denies) { |
| if (principal.isGroup()) { |
| groups.add((GroupPrincipal) principal); |
| } else { |
| users.add((UserPrincipal) principal); |
| } |
| } |
| Set<GroupPrincipal> sanitizedGroups = sanitizeSet(groups); |
| Set<UserPrincipal> sanitizedUsers = sanitizeSet(users); |
| this.denyGroups = sanitizedGroups; |
| this.denyUsers = sanitizedUsers; |
| return this; |
| } |
| |
| /** |
| * Set {@code DocId} to inherit ACLs from. This is also known as the |
| * "parent's" ACLs. Note that the parent's {@code InheritanceType} |
| * determines how to combine results with this ACL. |
| * |
| * @return the same instance of the builder, for chaining calls |
| * @see #setInheritanceType |
| */ |
| public Builder setInheritFrom(DocId inheritFrom) { |
| this.inheritFrom = inheritFrom; |
| this.inheritFromFragment = null; |
| return this; |
| } |
| |
| /** |
| * Set the parent to inherit ACLs from. |
| * Note that the parent's {@code InheritanceType} |
| * determines how to combine results with this ACL. |
| * <p> |
| * The fragment facilitates a single parent {@code DocId} |
| * having multiple ACLs to inherit from. For example |
| * a single parent DocId could have ACLs that are to be inherited |
| * by sub-folder {@code DocId} instances and different |
| * ACLs that are to be inherited by children files. |
| * The fragment allows specifying which of the parent's ACLs |
| * is to be inherited from. |
| * |
| * @return the same instance of the builder, for chaining calls |
| * @see #setInheritanceType |
| */ |
| public Builder setInheritFrom(DocId inheritFrom, String fragment) { |
| this.inheritFrom = inheritFrom; |
| this.inheritFromFragment = fragment; |
| return this; |
| } |
| |
| /** |
| * Set the type of inheritance of ACL information used to combine authz |
| * decisions of these ACLs with its <em>child</em>. The inheritance type |
| * applies to the interaction between this ACL and any <em>children</em> it |
| * has. |
| * |
| * @return the same instance of the builder, for chaining calls |
| * @throws NullPointerException if {@code inheritType} is {@code null} |
| * @see #setInheritFrom |
| */ |
| public Builder setInheritanceType(InheritanceType inheritType) { |
| if (inheritType == null) { |
| throw new NullPointerException(); |
| } |
| this.inheritType = inheritType; |
| return this; |
| } |
| |
| public Builder setEverythingCaseSensitive() { |
| caseSensitive = true; |
| return this; |
| } |
| |
| public Builder setEverythingCaseInsensitive() { |
| caseSensitive = false; |
| return this; |
| } |
| } |
| |
| /** |
| * The rule for combining a parent's authz response with its child's. This is |
| * stored as part of the parent's ACLs. |
| */ |
| public static enum InheritanceType { |
| /** |
| * The child's authz result is used, unless it is indeterminate, in which |
| * case this ACL's authz result is used. |
| */ |
| CHILD_OVERRIDES("child-overrides") { |
| @Override |
| AuthzStatus isAuthorized(Decision child, Decision parent) { |
| if (child.getStatus() == AuthzStatus.INDETERMINATE) { |
| return parent.getStatus(); |
| } |
| return child.getStatus(); |
| } |
| }, |
| /** |
| * This ACL's authz result is used, unless it is indeterminate, in which |
| * case the child's authz result is used. |
| */ |
| PARENT_OVERRIDES("parent-overrides") { |
| @Override |
| AuthzStatus isAuthorized(Decision child, Decision parent) { |
| if (parent.getStatus() == AuthzStatus.INDETERMINATE) { |
| return child.getStatus(); |
| } |
| return parent.getStatus(); |
| } |
| }, |
| /** |
| * The user is denied, unless both this ACL and the child's authz result is |
| * permit. |
| */ |
| AND_BOTH_PERMIT("and-both-permit") { |
| @Override |
| AuthzStatus isAuthorized(Decision child, Decision parent) { |
| if (parent.getStatus() == AuthzStatus.PERMIT |
| && child.getStatus() == AuthzStatus.PERMIT) { |
| return AuthzStatus.PERMIT; |
| } |
| return AuthzStatus.DENY; |
| } |
| }, |
| /** |
| * The ACL should never have a child and thus the inheritance type is |
| * unnecessary. If a child inherits from this ACL then the result is deny. |
| */ |
| LEAF_NODE("leaf-node") { |
| @Override |
| AuthzStatus isAuthorized(Decision child, Decision parent) { |
| log.log(Level.WARNING, "Illegal ACL information. A LEAF_NODE is the " |
| + "parent of another node."); |
| return AuthzStatus.DENY; |
| } |
| }, |
| ; |
| |
| private final String commonForm; |
| |
| private InheritanceType(String commonForm) { |
| this.commonForm = commonForm; |
| } |
| |
| /** |
| * The identifier used to represent enum value during communication with the |
| * GSA. |
| */ |
| String getCommonForm() { |
| return commonForm; |
| } |
| |
| /** |
| * Combine the result of a child and a parent. |
| */ |
| abstract AuthzStatus isAuthorized(Decision child, Decision parent); |
| } |
| |
| /** |
| * Lazy-computing of AuthzStatus. |
| */ |
| abstract static class Decision { |
| private AuthzStatus status; |
| |
| public AuthzStatus getStatus() { |
| if (status == null) { |
| status = computeDecision(); |
| if (status == null) { |
| throw new AssertionError(); |
| } |
| } |
| return status; |
| } |
| |
| /** |
| * Compute the actual decision. The response will be cached, so this will be |
| * called at most once. |
| * |
| * @return a non-{@code null} authz status |
| */ |
| protected abstract AuthzStatus computeDecision(); |
| } |
| } |