/**
* Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright ownership. Apereo
* licenses this file to you 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 the
* following location:
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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 org.apereo.portal.security.provider;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import net.sf.ehcache.Cache;
import net.sf.ehcache.Element;
import org.apereo.portal.AuthorizationException;
import org.apereo.portal.groups.GroupsException;
import org.apereo.portal.groups.IEntityGroup;
import org.apereo.portal.groups.IGroupMember;
import org.apereo.portal.permission.IPermissionActivity;
import org.apereo.portal.permission.IPermissionOwner;
import org.apereo.portal.permission.dao.IPermissionOwnerDao;
import org.apereo.portal.permission.target.IPermissionTarget;
import org.apereo.portal.permission.target.IPermissionTargetProviderRegistry;
import org.apereo.portal.security.IAuthorizationPrincipal;
import org.apereo.portal.security.IAuthorizationService;
import org.apereo.portal.security.IPermission;
import org.apereo.portal.security.IPermissionPolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
/**
* If there exists a GRANT explicitly for the Principal for the Activity under consideration, this
* permission policy will GRANT permission.
*
* <p>If there exists a GRANT for a group containing the Principal for the Activity under
* consideration, and there is a path up the groups tree from the Principal to that GRANTed group
* that is not interrupted by a Deny for an intervening group in the tree, then this permission
* policy will GRANT permission.
*
* <p>Otherwise, this permission policy will DENY permission.
*
* <p>Examples: Principal (GRANT) -- Small group -- Bigger group -- Huge group Results in GRANT
* because the Principal has an explicit permission.
*
* <p>Principal -- Small group -- Bigger group (GRANT) -- Huge group Results in GRANT because there
* is an unblocked path to a containing group with GRANT.
*
* <p>Principal -- Small group (DENY) -- Bigger group (GRANT) -- Huge group Results in DENY because
* there is no unblocked path to a grant -- the "Bigger group"'s GRANT does not apply because of the
* intervening DENY.
*
* <p>Principal -- Small group (DENY) -- Bigger group -- Huge group Principal -- Some other group --
* Bigger other group (GRANT) -- Huge group Results in GRANT because there is an unblocked path to a
* GRANT.
*/
@Service("anyUnblockedGrantPermissionPolicy")
public class AnyUnblockedGrantPermissionPolicy implements IPermissionPolicy {
protected final Logger log = LoggerFactory.getLogger(getClass());
@Autowired private IPermissionOwnerDao permissionOwnerDao;
@Autowired private IPermissionTargetProviderRegistry targetProviderRegistry;
@Autowired
@Qualifier(
value =
"org.apereo.portal.security.provider.AnyUnblockedGrantPermissionPolicy.HAS_UNBLOCKED_GRANT"
)
private Cache hasUnblockedGrantCache;
public boolean doesPrincipalHavePermission(
IAuthorizationService service,
IAuthorizationPrincipal principal,
IPermissionOwner owner,
IPermissionActivity activity,
IPermissionTarget target)
throws AuthorizationException {
/*
* The API states that the service, owner, and activity arguments must
* not be null. If for some reason they are null, log and fail closed.
* In our case, the principal and target must also be non-null.
*/
if (service == null
|| principal == null
|| owner == null
|| activity == null
|| target == null) {
log.error(
"Null argument to AnyUnblockedGrantPermissionPolicy doesPrincipalHavePermission() method "
+ "should not be possible. This is indicative of a potentially serious bug in the permissions "
+ "and authorization infrastructure; service='{}', principal='{}', owner='{}', activity='{}', "
+ "target='{}'",
service,
principal,
owner,
activity,
target);
// fail closed
return false;
}
// Is this user a super-user? (Should this logic be moved to AuthorizationImpl?)
final IPermissionActivity allPermissionsActivity =
permissionOwnerDao.getPermissionActivity(
IPermission.PORTAL_SYSTEM, IPermission.ALL_PERMISSIONS_ACTIVITY);
if (!activity.equals(
allPermissionsActivity)) { // NOTE: Must check to avoid infinite recursion
final IPermissionOwner allPermissionsOwner =
permissionOwnerDao.getPermissionOwner(IPermission.PORTAL_SYSTEM);
final IPermissionTarget allPermissionsTarget =
targetProviderRegistry
.getTargetProvider(allPermissionsActivity.getTargetProviderKey())
.getTarget(IPermission.ALL_TARGET);
if (doesPrincipalHavePermission(
service,
principal,
allPermissionsOwner,
allPermissionsActivity,
allPermissionsTarget)) {
// Stop checking; just return true
return true;
}
}
/*
* uPortal uses a few "special" targets that signal permission to
* perform the specified activity over an entire class of targets;
* see if one of those applies in this case.
*/
IPermissionTarget collectiveTarget =
null; // The "collective noun" representing a class of thing
switch (target.getTargetType()) {
case PORTLET:
collectiveTarget =
targetProviderRegistry
.getTargetProvider(activity.getTargetProviderKey())
.getTarget(IPermission.ALL_PORTLETS_TARGET);
break;
case CATEGORY:
collectiveTarget =
targetProviderRegistry
.getTargetProvider(activity.getTargetProviderKey())
.getTarget(IPermission.ALL_CATEGORIES_TARGET);
break;
case GROUP:
collectiveTarget =
targetProviderRegistry
.getTargetProvider(activity.getTargetProviderKey())
.getTarget(IPermission.ALL_GROUPS_TARGET);
break;
default:
// This sort of handling does not apply; just pass through
}
/*
* NOTE: Cannot generalize to a collective target if we are already on
* the collective target, else StackOverflowError.
*/
if (collectiveTarget != null && !collectiveTarget.equals(target)) {
if (doesPrincipalHavePermission(
service, principal, owner, activity, collectiveTarget)) {
/*
* There is a collective for this class of target,
* and the user DOES have this special permission
*/
return true;
}
}
// Search ourselves and all ancestors for an unblocked GRANT.
boolean rslt;
try {
// Track groups we've already explored to avoid infinite loop
final Set<IGroupMember> seenGroups = new HashSet<>();
rslt =
hasUnblockedPathToGrantWithCache(
service, principal, owner, activity, target, seenGroups);
} catch (Exception e) {
log.error(
"Error searching for unblocked path to grant for principal [" + principal + "]",
e);
// fail closed
return false;
}
if (log.isTraceEnabled()) {
if (rslt) {
log.trace(
"Principal '{}' is granted permission to perform activity "
+ "'{}' on target '{}' under permission owning system '{}' "
+ "because this principal has an unblocked path to a GRANT.",
principal,
activity.getFname(),
target.getKey(),
owner.getFname());
} else {
log.trace(
"Principal '{}' is denied permission to perform activity '{}' "
+ "on target '{}' under permission owning system '{}' because this "
+ "principal does not have an unblocked path to a GRANT.",
principal,
activity.getFname(),
target.getKey(),
owner.getFname());
}
}
return rslt;
}
/**
* Allows an outside actor to force this policy to evaluate and cache an authorization decision.
* Permissions checking can be expensive; a well-primed cache can make the task perform better.
* This method will create the cache entry whether it exists already or not, forcibly resetting
* the TTL.
*
* @since 4.3
*/
public void loadInCache(
IAuthorizationService service,
IAuthorizationPrincipal principal,
IPermissionOwner owner,
IPermissionActivity activity,
IPermissionTarget target) {
final Set<IGroupMember> seenGroups = new HashSet<>();
final CacheTuple cacheTuple =
new CacheTuple(
principal.getPrincipalString(),
owner.getFname(),
activity.getFname(),
target.getKey());
final boolean answer =
hasUnblockedPathToGrant(service, principal, owner, activity, target, seenGroups);
Element element = new Element(cacheTuple, answer);
hasUnblockedGrantCache.put(element);
}
private boolean hasUnblockedPathToGrantWithCache(
IAuthorizationService service,
IAuthorizationPrincipal principal,
IPermissionOwner owner,
IPermissionActivity activity,
IPermissionTarget target,
Set<IGroupMember> seenGroups)
throws GroupsException {
final CacheTuple cacheTuple =
new CacheTuple(
principal.getPrincipalString(),
owner.getFname(),
activity.getFname(),
target.getKey());
Element element = hasUnblockedGrantCache.get(cacheTuple);
if (element == null) {
final boolean answer =
hasUnblockedPathToGrant(
service, principal, owner, activity, target, seenGroups);
element = new Element(cacheTuple, answer);
hasUnblockedGrantCache.put(element);
}
return (Boolean) element.getObjectValue();
}
/**
* This method performs the actual, low-level checking of a single activity and target. Is IS
* responsible for performing the same check for affiliated groups in the Groups hierarchy, but
* it is NOT responsible for understanding the nuances of relationships some activities and/or
* targets have with one another (e.g. MANAGE_APPROVED, ALL_PORTLETS, etc.). It performs the
* following steps, in order:
*
* <ol>
* <li>Find out if the specified principal is <em>specifically</em> granted or denied; if an
* answer is found in this step, return it
* <li>Find out what groups this principal belongs to; convert each one to a principal and
* seek an answer by invoking ourselves recursively; if an answer is found in this step,
* return it
* <li>Return false (no explicit GRANT means no permission)
* </ol>
*/
private boolean hasUnblockedPathToGrant(
IAuthorizationService service,
IAuthorizationPrincipal principal,
IPermissionOwner owner,
IPermissionActivity activity,
IPermissionTarget target,
Set<IGroupMember> seenGroups)
throws GroupsException {
if (log.isTraceEnabled()) {
log.trace(
"Searching for unblocked path to GRANT for principal '{}' to "
+ "'{}' on target '{}' having already checked: {}",
principal.getKey(),
activity.getFname(),
target.getKey(),
seenGroups);
}
/*
* Step #1: Specific GRANT/DENY attached to this principal
*/
final IPermission[] permissions =
service.getPermissionsForPrincipal(
principal, owner.getFname(), activity.getFname(), target.getKey());
final Set<IPermission> activePermissions = removeInactivePermissions(permissions);
final boolean denyExists =
containsType(activePermissions, IPermission.PERMISSION_TYPE_DENY);
if (denyExists) {
// We need go no further; DENY trumps both GRANT & inherited permissions
return false;
}
final boolean grantExists =
containsType(activePermissions, IPermission.PERMISSION_TYPE_GRANT);
if (grantExists) {
// We need go no further; explicit GRANT at this level of the hierarchy
if (log.isTraceEnabled()) {
log.trace(
"Found unblocked path to this permission set including a GRANT: {}",
activePermissions);
}
return true;
}
/*
* Step #2: Seek an answer from affiliated groups
*/
IGroupMember principalAsGroupMember = service.getGroupMember(principal);
if (seenGroups.contains(principalAsGroupMember)) {
if (log.isTraceEnabled()) {
log.trace(
"Declining to re-examine principal '{}' for permission to '{}' "
+ "on '{}' because this group is among already checked groups: {}",
principal.getKey(),
activity.getFname(),
target.getKey(),
seenGroups);
}
return false;
}
seenGroups.add(principalAsGroupMember);
Set<IEntityGroup> immediatelyContainingGroups = principalAsGroupMember.getParentGroups();
for (IGroupMember parentGroup : immediatelyContainingGroups) {
try {
if (parentGroup != null) {
IAuthorizationPrincipal parentPrincipal = service.newPrincipal(parentGroup);
boolean parentHasUnblockedPathToGrant =
hasUnblockedPathToGrantWithCache(
service, parentPrincipal, owner, activity, target, seenGroups);
if (parentHasUnblockedPathToGrant) {
return true;
}
// Parent didn't have a path to grant, fall through and try another parent (if any)
}
} catch (Exception e) {
// problem evaluating this path, but let's not let it stop
// us from exploring other paths. Though a portion of the
// group structure is broken, permission may be granted by
// an unbroken portion
log.error("Error evaluating permissions of parent group [" + parentGroup + "]", e);
}
}
/*
* Step #3: No explicit GRANT means no permission
*/
return false;
}
/**
* Returns a Set containing those IPermission instances where the present date is neither after
* the permission expiration if present nor before the permission start date if present. Only
* permissions objects that have been filtered by this method should be checked.
*
* @return Potentially empty non-null Set of active permissions.
*/
private Set<IPermission> removeInactivePermissions(final IPermission[] perms) {
Date now = new Date();
Set<IPermission> rslt = new HashSet<IPermission>(1);
for (int i = 0; i < perms.length; i++) {
IPermission p = perms[i];
if ((p.getEffective() == null || !p.getEffective().after(now))
&& (p.getExpires() == null || p.getExpires().after(now))) {
rslt.add(p);
}
}
return rslt;
}
/**
* Returns true if a set of IPermission instances contains a permission of the specified type,
* otherwise false. Permissions passed to this method <em>must</em> already be filtered of
* inactive (expired) instances.
*
* @return True if the set contains a permission of the sought type, false otherwise
* @throws IllegalArgumentException if input set or type is null.
*/
private boolean containsType(final Set<IPermission> permissions, final String soughtType) {
// Assertions
if (permissions == null) {
throw new IllegalArgumentException("Cannot check null set for contents.");
}
if (soughtType == null) {
throw new IllegalArgumentException("Cannot search for type null.");
}
boolean rslt = false; // default
for (IPermission p : permissions) {
if (soughtType.equals(p.getType())) {
rslt = true;
}
}
return rslt;
}
/*
* Nested Types
*/
private static final class CacheTuple {
private final String principalName;
private final String owner;
private final String activity;
private final String target;
public CacheTuple(String principalName, String owner, String activity, String target) {
this.principalName = principalName;
this.owner = owner;
this.activity = activity;
this.target = target;
}
@SuppressWarnings("unused")
public String getPrincipalName() {
return principalName;
}
@SuppressWarnings("unused")
public String getOwner() {
return owner;
}
@SuppressWarnings("unused")
public String getActivity() {
return activity;
}
@SuppressWarnings("unused")
public String getTarget() {
return target;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((activity == null) ? 0 : activity.hashCode());
result = prime * result + ((owner == null) ? 0 : owner.hashCode());
result = prime * result + ((principalName == null) ? 0 : principalName.hashCode());
result = prime * result + ((target == null) ? 0 : target.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
CacheTuple other = (CacheTuple) obj;
if (activity == null) {
if (other.activity != null) return false;
} else if (!activity.equals(other.activity)) return false;
if (owner == null) {
if (other.owner != null) return false;
} else if (!owner.equals(other.owner)) return false;
if (principalName == null) {
if (other.principalName != null) return false;
} else if (!principalName.equals(other.principalName)) return false;
if (target == null) {
if (other.target != null) return false;
} else if (!target.equals(other.target)) return false;
return true;
}
@Override
public String toString() {
return "CacheTuple [principalName="
+ principalName
+ ", owner="
+ owner
+ ", activity="
+ activity
+ ", target="
+ target
+ "]";
}
}
}