/** * Copyright (C) 2013 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.util.auth; import java.util.Collection; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.authz.Permission; import org.apache.shiro.authz.UnauthenticatedException; import org.apache.shiro.authz.UnauthorizedException; import org.apache.shiro.authz.permission.InvalidPermissionStringException; import org.apache.shiro.authz.permission.PermissionResolver; import com.google.common.base.Throwables; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.UncheckedExecutionException; import com.opengamma.util.ArgumentChecker; /** * An Apache Shiro {@code PermissionResolver} that resolves to OpenGamma permissions. * <p> * This resolver supports extended permission systems by registering a prefix for permissions. * If the requested permission matches a prefix then the associated registered resolver is used. * Otherwise, the standard permission is used. * <p> * For example, this could be used to check permission to access ticking data on an equity. * The user would be given the permission 'Data.BigDataProvider'. * The data would be given the permission 'Data.BigDataProvider.AnEquityIdentifier'. * A special permission resolver would then be registered for the 'Data.MyBigDataProvider' prefix. * When the prefix is seen, the resolver would return a different {@link Permission} implementation * that is capable of dynamically checking access to the specific equity identifier, which usually * requires contacting the big data provider. */ public final class ShiroPermissionResolver implements PermissionResolver { /** * The cached permissions. * ConcurrentHashMap cannot restrict size, so use LoadingCache. */ private final LoadingCache<String, Permission> _cache = CacheBuilder.newBuilder() .maximumSize(1000) .build(new CacheLoader<String, Permission>() { @Override public Permission load(String permissionStr) { return doResolvePermission(permissionStr); } }); /** * A pluggable set of resolvers by prefix. * Registration should occur only during startup, but still need concurrent map. */ private final ConcurrentMap<String, PrefixedPermissionResolver> _prefixed = new ConcurrentHashMap<>(16, 0.75f, 1); /** * Creates an instance. */ public ShiroPermissionResolver() { } //------------------------------------------------------------------------- /** * Registers a factory that handles permissions with a specific prefix. * <p> * This allows different implementations of the {@code Permission} interface * to be created based on a prefix. * * @param resolver the permission resolver, not null * @throws IllegalArgumentException if the prefix is already registered */ public void register(PrefixedPermissionResolver resolver) { ArgumentChecker.notNull(resolver, "resolver"); PrefixedPermissionResolver existing = _prefixed.putIfAbsent(resolver.getPrefix(), resolver); if (existing != null && existing.equals(resolver) == false) { throw new IllegalArgumentException("Prefix is already registered"); } } //------------------------------------------------------------------------- /** * Resolves the permission from string to object form. * <p> * This uses a cache to speed up comparisons. * * @param permissionString the permission string, not null * @return the permission object, not null * @throws InvalidPermissionStringException if the permission string is invalid */ @Override public Permission resolvePermission(String permissionString) { ArgumentChecker.notNull(permissionString, "permissionString"); try { return _cache.getUnchecked(permissionString); } catch (UncheckedExecutionException ex) { // cache annoyingly wraps underlying runtime exceptions, so unwrap and rethrow Throwables.propagateIfPossible(ex.getCause()); throw ex; } } /** * Resolves a set of permissions from string to object form. * <p> * The returned set of permissions may be smaller than the input set. * * @param permissionStrings the set of permission strings, not null * @return the set of permission objects, not null * @throws InvalidPermissionStringException if the permission string is invalid */ public ImmutableList<Permission> resolvePermissions(String... permissionStrings) { ArgumentChecker.notNull(permissionStrings, "permissionStrings"); ImmutableList.Builder<Permission> builder = ImmutableList.builder(); for (String permissionString : permissionStrings) { builder.add(resolvePermission(permissionString)); } return builder.build(); } /** * Resolves a set of permissions from string to object form. * <p> * The returned set of permissions may be smaller than the input set. * * @param permissionStrings the set of permission strings, not null * @return the set of permission objects, not null * @throws InvalidPermissionStringException if the permission string is invalid */ public ImmutableSet<Permission> resolvePermissions(Collection<String> permissionStrings) { ArgumentChecker.notNull(permissionStrings, "permissionStrings"); ImmutableSet.Builder<Permission> builder = ImmutableSet.builder(); for (String permissionString : permissionStrings) { builder.add(resolvePermission(permissionString)); } return builder.build(); } //------------------------------------------------------------------------- /** * Checks if the subject permissions grant all the required permissions. * <p> * The first collection contains the set of permissions held by the subject. * The second collection contains the permissions that are required. * This returns true if the set of subject permissions grants all the required permissions. * * @param subjectPermissions the set of permissions held by the subject, not null * @param requiredPermissions the permissions that are required, not null * @return true if all the required permissions are granted */ public boolean isPermittedAll(Collection<Permission> subjectPermissions, Collection<Permission> requiredPermissions) { // try bulk check for (Permission subjectPermission : subjectPermissions) { if (subjectPermission instanceof ExtendedPermission) { ExtendedPermission subjectPerm = (ExtendedPermission) subjectPermission; Boolean implied = subjectPerm.checkImpliesAll(requiredPermissions, false); if (implied != null) { return implied.booleanValue(); } } } // normal non-bulk check for (Permission requiredPermission : requiredPermissions) { if (implies(subjectPermissions, requiredPermission) == false) { return false; } } return true; } // does one of the subject permissions imply the required permission private boolean implies(Collection<? extends Permission> subjectPermissions, Permission requiredPermission) { for (Permission subjectPermission : subjectPermissions) { if (subjectPermission.implies(requiredPermission)) { return true; } } return false; } /** * Checks if the subject permissions grant all the required permissions. * <p> * The first collection contains the set of permissions held by the subject. * The second collection contains the permissions that are required. * This returns true if the set of subject permissions grants all the required permissions. * * @param subjectPermissions the set of permissions held by the subject, not null * @param requiredPermissions the permissions that are required, not null * @throws UnauthenticatedException if permission was denied due to invalid user authentication * @throws UnauthorizedException if the user does not have the requested permission * @throws AuthorizationException if permission was denied due to some other issue */ public void checkPermissions(Collection<Permission> subjectPermissions, Collection<Permission> requiredPermissions) { // try bulk check for (Permission subjectPermission : subjectPermissions) { if (subjectPermission instanceof ExtendedPermission) { ExtendedPermission subjectPerm = (ExtendedPermission) subjectPermission; Boolean implied = subjectPerm.checkImpliesAll(requiredPermissions, true); if (implied != null) { if (implied) { return; } throw new UnauthorizedException("Permission denied: " + requiredPermissions); } } } // normal non-bulk check for (Permission requiredPermission : requiredPermissions) { checkImplies(subjectPermissions, requiredPermission); } } // does one of the subject permissions imply the required permission, exception if not private void checkImplies(Collection<? extends Permission> subjectPermissions, Permission requiredPermission) { for (Permission subjectPermission : subjectPermissions) { if (subjectPermission instanceof ExtendedPermission) { if (((ExtendedPermission) subjectPermission).checkImplies(requiredPermission)) { return; } } else { if (subjectPermission.implies(requiredPermission)) { return; } } } throw new UnauthorizedException("Permission denied: " + requiredPermission); } //------------------------------------------------------------------------- /** * Resolves the permission. * <p> * If the requested permission matches a prefix then the associated resolver is used. * Otherwise, the standard permission is used. * <p> * This is called directly from the cache. * * @param permissionString the permission string, not null * @return the new permission object, not null * @throws InvalidPermissionStringException if the permission string is invalid */ private Permission doResolvePermission(String permissionString) { for (PrefixedPermissionResolver prefixedResolver : _prefixed.values()) { if (permissionString.startsWith(prefixedResolver.getPrefix())) { return prefixedResolver.resolvePermission(permissionString); } } return ShiroWildcardPermission.of(permissionString); } }