blob: 73e0d0e29866df36c77734a190d4ba2e6e1031fa [file] [log] [blame]
// 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.*;
import java.util.logging.*;
/**
* 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()) {
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();
}
}