org.apache.cassandra.auth.jmx.AuthorizationProxy Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of cassandra-unit-shaded Show documentation
Show all versions of cassandra-unit-shaded Show documentation
Shaded version of cassandra-unit
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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
*
* 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 org.apache.cassandra.auth.jmx;
import java.lang.reflect.*;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.Principal;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.security.auth.Subject;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.auth.*;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.service.StorageService;
/**
* Provides a proxy interface to the platform's MBeanServer instance to perform
* role-based authorization on method invocation.
*
* When used in conjunction with a suitable JMXAuthenticator, which attaches a CassandraPrincipal
* to authenticated Subjects, this class uses the configured IAuthorizer to verify that the
* subject has the required permissions to execute methods on the MBeanServer and the MBeans it
* manages.
*
* Because an ObjectName may contain wildcards, meaning it represents a set of individual MBeans,
* JMX resources don't fit well with the hierarchical approach modelled by other IResource
* implementations and utilised by ClientState::ensureHasPermission etc. To enable grants to use
* pattern-type ObjectNames, this class performs its own custom matching and filtering of resources
* rather than pushing that down to the configured IAuthorizer. To that end, during authorization
* it pulls back all permissions for the active subject, filtering them to retain only grants on
* JMXResources. It then uses ObjectName::apply to assert whether the target MBeans are wholly
* represented by the resources with permissions. This means that it cannot use the PermissionsCache
* as IAuthorizer can, so it manages its own cache locally.
*
* Methods are split into 2 categories; those which are to be invoked on the MBeanServer itself
* and those which apply to MBean instances. Actually, this is somewhat of a construct as in fact
* *all* invocations are performed on the MBeanServer instance, the distinction is made here on
* those methods which take an ObjectName as their first argument and those which do not.
* Invoking a method of the former type, e.g. MBeanServer::getAttribute(ObjectName name, String attribute),
* implies that the caller is concerned with a specific MBean. Conversely, invoking a method such as
* MBeanServer::getDomains is primarily a function of the MBeanServer itself. This class makes
* such a distinction in order to identify which JMXResource the subject requires permissions on.
*
* Certain operations are never allowed for users and these are recorded in a blacklist so that we
* can short circuit authorization process if one is attempted by a remote subject.
*
*/
public class AuthorizationProxy implements InvocationHandler
{
private static final Logger logger = LoggerFactory.getLogger(AuthorizationProxy.class);
/*
A whitelist of permitted methods on the MBeanServer interface which *do not* take an ObjectName
as their first argument. These methods can be thought of as relating to the MBeanServer itself,
rather than to the MBeans it manages. All of the whitelisted methods are essentially descriptive,
hence they require the Subject to have the DESCRIBE permission on the root JMX resource.
*/
private static final Set MBEAN_SERVER_METHOD_WHITELIST = ImmutableSet.of("getDefaultDomain",
"getDomains",
"getMBeanCount",
"hashCode",
"queryMBeans",
"queryNames",
"toString");
/*
A blacklist of method names which are never permitted to be executed by a remote user,
regardless of privileges they may be granted.
*/
private static final Set METHOD_BLACKLIST = ImmutableSet.of("createMBean",
"deserialize",
"getClassLoader",
"getClassLoaderFor",
"instantiate",
"registerMBean",
"unregisterMBean");
private static final JMXPermissionsCache permissionsCache = new JMXPermissionsCache();
private MBeanServer mbs;
/*
Used to check whether the Role associated with the authenticated Subject has superuser
status. By default, just delegates to Roles::hasSuperuserStatus, but can be overridden for testing.
*/
protected Function isSuperuser = Roles::hasSuperuserStatus;
/*
Used to retrieve the set of all permissions granted to a given role. By default, this fetches
the permissions from the local cache, which in turn loads them from the configured IAuthorizer
but can be overridden for testing.
*/
protected Function> getPermissions = permissionsCache::get;
/*
Used to decide whether authorization is enabled or not, usually this depends on the configured
IAuthorizer, but can be overridden for testing.
*/
protected Supplier isAuthzRequired = () -> DatabaseDescriptor.getAuthorizer().requireAuthorization();
/*
Used to find matching MBeans when the invocation target is a pattern type ObjectName.
Defaults to querying the MBeanServer but can be overridden for testing. See checkPattern for usage.
*/
protected Function> queryNames = (name) -> mbs.queryNames(name, null);
/*
Used to determine whether auth setup has completed so we know whether the expect the IAuthorizer
to be ready. Can be overridden for testing.
*/
protected Supplier isAuthSetupComplete = () -> StorageService.instance.isAuthSetupComplete();
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
{
String methodName = method.getName();
if ("getMBeanServer".equals(methodName))
throw new SecurityException("Access denied");
// Retrieve Subject from current AccessControlContext
AccessControlContext acc = AccessController.getContext();
Subject subject = Subject.getSubject(acc);
// Allow setMBeanServer iff performed on behalf of the connector server itself
if (("setMBeanServer").equals(methodName))
{
if (subject != null)
throw new SecurityException("Access denied");
if (args[0] == null)
throw new IllegalArgumentException("Null MBeanServer");
if (mbs != null)
throw new IllegalArgumentException("MBeanServer already initialized");
mbs = (MBeanServer) args[0];
return null;
}
if (authorize(subject, methodName, args))
return invoke(method, args);
throw new SecurityException("Access Denied");
}
/**
* Performs the actual authorization of an identified subject to execute a remote method invocation.
* @param subject The principal making the execution request. A null value represents a local invocation
* from the JMX connector itself
* @param methodName Name of the method being invoked
* @param args Array containing invocation argument. If the first element is an ObjectName instance, for
* authz purposes we consider this an invocation of an MBean method, otherwise it is treated
* as an invocation of a method on the MBeanServer.
*/
@VisibleForTesting
boolean authorize(Subject subject, String methodName, Object[] args)
{
logger.trace("Authorizing JMX method invocation {} for {}",
methodName,
subject == null ? "" :subject.toString().replaceAll("\\n", " "));
if (!isAuthSetupComplete.get())
{
logger.trace("Auth setup is not complete, refusing access");
return false;
}
// Permissive authorization is enabled
if (!isAuthzRequired.get())
return true;
// Allow operations performed locally on behalf of the connector server itself
if (subject == null)
return true;
// Restrict access to certain methods by any remote user
if (METHOD_BLACKLIST.contains(methodName))
{
logger.trace("Access denied to blacklisted method {}", methodName);
return false;
}
// Reject if the user has not authenticated
Set principals = subject.getPrincipals();
if (principals == null || principals.isEmpty())
return false;
// Currently, we assume that the first Principal returned from the Subject
// is the one to use for authorization. It would be good to make this more
// robust, but we have no control over which Principals a given LoginModule
// might choose to associate with the Subject following successful authentication
RoleResource userResource = RoleResource.role(principals.iterator().next().getName());
// A role with superuser status can do anything
if (isSuperuser.apply(userResource))
return true;
// The method being invoked may be a method on an MBean, or it could belong
// to the MBeanServer itself
if (args != null && args[0] instanceof ObjectName)
return authorizeMBeanMethod(userResource, methodName, args);
else
return authorizeMBeanServerMethod(userResource, methodName);
}
/**
* Authorize execution of a method on the MBeanServer which does not take an MBean ObjectName
* as its first argument. The whitelisted methods that match this criteria are generally
* descriptive methods concerned with the MBeanServer itself, rather than with any particular
* set of MBeans managed by the server and so we check the DESCRIBE permission on the root
* JMXResource (representing the MBeanServer)
*
* @param subject
* @param methodName
* @return the result of the method invocation, if authorized
* @throws Throwable
* @throws SecurityException if authorization fails
*/
private boolean authorizeMBeanServerMethod(RoleResource subject, String methodName)
{
logger.trace("JMX invocation of {} on MBeanServer requires permission {}", methodName, Permission.DESCRIBE);
return (MBEAN_SERVER_METHOD_WHITELIST.contains(methodName) &&
hasPermission(subject, Permission.DESCRIBE, JMXResource.root()));
}
/**
* Authorize execution of a method on an MBean (or set of MBeans) which may be
* managed by the MBeanServer. Note that this also includes the queryMBeans and queryNames
* methods of MBeanServer as those both take an ObjectName (possibly a pattern containing
* wildcards) as their first argument. They both of those methods also accept null arguments,
* in which case they will be handled by authorizedMBeanServerMethod
*
* @param role
* @param methodName
* @param args
* @return the result of the method invocation, if authorized
* @throws Throwable
* @throws SecurityException if authorization fails
*/
private boolean authorizeMBeanMethod(RoleResource role, String methodName, Object[] args)
{
ObjectName targetBean = (ObjectName)args[0];
// work out which permission we need to execute the method being called on the mbean
Permission requiredPermission = getRequiredPermission(methodName);
if (null == requiredPermission)
return false;
logger.trace("JMX invocation of {} on {} requires permission {}", methodName, targetBean, requiredPermission);
// find any JMXResources upon which the authenticated subject has been granted the
// reqired permission. We'll do ObjectName-specific filtering & matching of resources later
Set permittedResources = getPermittedResources(role, requiredPermission);
if (permittedResources.isEmpty())
return false;
// finally, check the JMXResource from the grants to see if we have either
// an exact match or a wildcard match for the target resource, whichever is
// applicable
return targetBean.isPattern()
? checkPattern(targetBean, permittedResources)
: checkExact(targetBean, permittedResources);
}
/**
* Get any grants of the required permission for the authenticated subject, regardless
* of the resource the permission applies to as we'll do the filtering & matching in
* the calling method
* @param subject
* @param required
* @return the set of JMXResources upon which the subject has been granted the required permission
*/
private Set getPermittedResources(RoleResource subject, Permission required)
{
return getPermissions.apply(subject)
.stream()
.filter(details -> details.permission == required)
.map(details -> (JMXResource)details.resource)
.collect(Collectors.toSet());
}
/**
* Check whether a required permission has been granted to the authenticated subject on a specific resource
* @param subject
* @param permission
* @param resource
* @return true if the Subject has been granted the required permission on the specified resource; false otherwise
*/
private boolean hasPermission(RoleResource subject, Permission permission, JMXResource resource)
{
return getPermissions.apply(subject)
.stream()
.anyMatch(details -> details.permission == permission && details.resource.equals(resource));
}
/**
* Given a set of JMXResources upon which the Subject has been granted a particular permission,
* check whether any match the pattern-type ObjectName representing the target of the method
* invocation. At this point, we are sure that whatever the required permission, the Subject
* has definitely been granted it against this set of JMXResources. The job of this method is
* only to verify that the target of the invocation is covered by the members of the set.
*
* @param target
* @param permittedResources
* @return true if all registered beans which match the target can also be matched by the
* JMXResources the subject has been granted permissions on; false otherwise
*/
private boolean checkPattern(ObjectName target, Set permittedResources)
{
// if the required permission was granted on the root JMX resource, then we're done
if (permittedResources.contains(JMXResource.root()))
return true;
// Get the full set of beans which match the target pattern
Set targetNames = queryNames.apply(target);
// Iterate over the resources the permission has been granted on. Some of these may
// be patterns, so query the server to retrieve the full list of matching names and
// remove those from the target set. Once the target set is empty (i.e. all required
// matches have been satisfied), the requirement is met.
// If there are still unsatisfied targets after all the JMXResources have been processed,
// there are insufficient grants to permit the operation.
for (JMXResource resource : permittedResources)
{
try
{
Set matchingNames = queryNames.apply(ObjectName.getInstance(resource.getObjectName()));
targetNames.removeAll(matchingNames);
if (targetNames.isEmpty())
return true;
}
catch (MalformedObjectNameException e)
{
logger.warn("Permissions for JMX resource contains invalid ObjectName {}", resource.getObjectName());
}
}
logger.trace("Subject does not have sufficient permissions on all MBeans matching the target pattern {}", target);
return false;
}
/**
* Given a set of JMXResources upon which the Subject has been granted a particular permission,
* check whether any match the ObjectName representing the target of the method invocation.
* At this point, we are sure that whatever the required permission, the Subject has definitely
* been granted it against this set of JMXResources. The job of this method is only to verify
* that the target of the invocation is matched by a member of the set.
*
* @param target
* @param permittedResources
* @return true if at least one of the permitted resources matches the target; false otherwise
*/
private boolean checkExact(ObjectName target, Set permittedResources)
{
// if the required permission was granted on the root JMX resource, then we're done
if (permittedResources.contains(JMXResource.root()))
return true;
for (JMXResource resource : permittedResources)
{
try
{
if (ObjectName.getInstance(resource.getObjectName()).apply(target))
return true;
}
catch (MalformedObjectNameException e)
{
logger.warn("Permissions for JMX resource contains invalid ObjectName {}", resource.getObjectName());
}
}
logger.trace("Subject does not have sufficient permissions on target MBean {}", target);
return false;
}
/**
* Mapping between method names and the permission required to invoke them. Note, these
* names refer to methods on MBean instances invoked via the MBeanServer.
* @param methodName
* @return
*/
private static Permission getRequiredPermission(String methodName)
{
switch (methodName)
{
case "getAttribute":
case "getAttributes":
return Permission.SELECT;
case "setAttribute":
case "setAttributes":
return Permission.MODIFY;
case "invoke":
return Permission.EXECUTE;
case "getInstanceOf":
case "getMBeanInfo":
case "hashCode":
case "isInstanceOf":
case "isRegistered":
case "queryMBeans":
case "queryNames":
return Permission.DESCRIBE;
default:
logger.debug("Access denied, method name {} does not map to any defined permission", methodName);
return null;
}
}
/**
* Invoke a method on the MBeanServer instance. This is called when authorization is not required (because
* AllowAllAuthorizer is configured, or because the invocation is being performed by the JMXConnector
* itself rather than by a connected client), and also when a call from an authenticated subject
* has been successfully authorized
*
* @param method
* @param args
* @return
* @throws Throwable
*/
private Object invoke(Method method, Object[] args) throws Throwable
{
try
{
return method.invoke(mbs, args);
}
catch (InvocationTargetException e) //Catch any exception that might have been thrown by the mbeans
{
Throwable t = e.getCause(); //Throw the exception that nodetool etc expects
throw t;
}
}
/**
* Query the configured IAuthorizer for the set of all permissions granted on JMXResources to a specific subject
* @param subject
* @return All permissions granted to the specfied subject (including those transitively inherited from
* any roles the subject has been granted), filtered to include only permissions granted on
* JMXResources
*/
private static Set loadPermissions(RoleResource subject)
{
// get all permissions for the specified subject. We'll cache them as it's likely
// we'll receive multiple lookups for the same subject (but for different resources
// and permissions) in quick succession
return DatabaseDescriptor.getAuthorizer().list(AuthenticatedUser.SYSTEM_USER, Permission.ALL, null, subject)
.stream()
.filter(details -> details.resource instanceof JMXResource)
.collect(Collectors.toSet());
}
private static final class JMXPermissionsCache extends AuthCache>
{
protected JMXPermissionsCache()
{
super("JMXPermissionsCache",
DatabaseDescriptor::setPermissionsValidity,
DatabaseDescriptor::getPermissionsValidity,
DatabaseDescriptor::setPermissionsUpdateInterval,
DatabaseDescriptor::getPermissionsUpdateInterval,
DatabaseDescriptor::setPermissionsCacheMaxEntries,
DatabaseDescriptor::getPermissionsCacheMaxEntries,
AuthorizationProxy::loadPermissions,
() -> true);
}
public Set get(RoleResource roleResource)
{
try
{
return super.get(roleResource);
}
catch (Exception e)
{
// because the outer class uses this method as Function>,
// which can be overridden for testing, it cannot throw checked exceptions. So here we simply
// use guava's propagation helper.
throw Throwables.propagate(e);
}
}
}
}