You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by sa...@apache.org on 2019/07/05 18:09:43 UTC

[cassandra] branch cassandra-3.11 updated (c08ce93 -> 8f33dc0)

This is an automated email from the ASF dual-hosted git repository.

samt pushed a change to branch cassandra-3.11
in repository https://gitbox.apache.org/repos/asf/cassandra.git.


    from c08ce93  Merge branch 'cassandra-3.0' into cassandra-3.11
     new b2f6953  Handle exceptions during authentication/authorization
     new 63097a3  Merge branch 'cassandra-2.2' into cassandra-3.0
     new 8f33dc0  Merge branch 'cassandra-3.0' into cassandra-3.11

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 CHANGES.txt                                        |   1 +
 src/java/org/apache/cassandra/auth/AuthCache.java  |  22 ++-
 .../apache/cassandra/auth/CassandraAuthorizer.java |  23 ++-
 .../cassandra/auth/CassandraRoleManager.java       |  45 +++---
 .../cassandra/auth/PasswordAuthenticator.java      |  45 ++----
 .../apache/cassandra/auth/PermissionsCache.java    |  10 +-
 src/java/org/apache/cassandra/auth/Roles.java      |  25 +++-
 src/java/org/apache/cassandra/auth/RolesCache.java |  10 +-
 .../cassandra/auth/jmx/AuthorizationProxy.java     |  15 --
 .../exceptions/AuthenticationException.java        |   5 +
 .../exceptions/UnauthorizedException.java          |   5 +
 .../org/apache/cassandra/service/ClientState.java  |  13 +-
 .../org/apache/cassandra/auth/AuthCacheTest.java   | 164 +++++++++++++++++++++
 13 files changed, 274 insertions(+), 109 deletions(-)
 create mode 100644 test/unit/org/apache/cassandra/auth/AuthCacheTest.java


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org


[cassandra] 01/01: Merge branch 'cassandra-3.0' into cassandra-3.11

Posted by sa...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

samt pushed a commit to branch cassandra-3.11
in repository https://gitbox.apache.org/repos/asf/cassandra.git

commit 8f33dc0f138731f2bbe9febaf209c584f783a497
Merge: c08ce93 63097a3
Author: Sam Tunnicliffe <sa...@beobal.com>
AuthorDate: Fri Jul 5 19:02:28 2019 +0100

    Merge branch 'cassandra-3.0' into cassandra-3.11

 CHANGES.txt                                        |   1 +
 src/java/org/apache/cassandra/auth/AuthCache.java  |  22 ++-
 .../apache/cassandra/auth/CassandraAuthorizer.java |  23 ++-
 .../cassandra/auth/CassandraRoleManager.java       |  45 +++---
 .../cassandra/auth/PasswordAuthenticator.java      |  45 ++----
 .../apache/cassandra/auth/PermissionsCache.java    |  10 +-
 src/java/org/apache/cassandra/auth/Roles.java      |  25 +++-
 src/java/org/apache/cassandra/auth/RolesCache.java |  10 +-
 .../cassandra/auth/jmx/AuthorizationProxy.java     |  15 --
 .../exceptions/AuthenticationException.java        |   5 +
 .../exceptions/UnauthorizedException.java          |   5 +
 .../org/apache/cassandra/service/ClientState.java  |  13 +-
 .../org/apache/cassandra/auth/AuthCacheTest.java   | 164 +++++++++++++++++++++
 13 files changed, 274 insertions(+), 109 deletions(-)

diff --cc CHANGES.txt
index 0d0b759,364720d..ab5cb66
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@@ -17,9 -14,10 +17,10 @@@ Merged from 3.0
   * Avoid double closing the iterator to avoid overcounting the number of requests (CASSANDRA-15058)
   * Improve `nodetool status -r` speed (CASSANDRA-14847)
   * Improve merkle tree size and time on heap (CASSANDRA-14096)
 - * Add missing commands to nodetool-completion (CASSANDRA-14916)
 + * Add missing commands to nodetool_completion (CASSANDRA-14916)
   * Anti-compaction temporarily corrupts sstable state for readers (CASSANDRA-15004)
 - Merged from 2.2:
 +Merged from 2.2:
+  * Handle exceptions during authentication/authorization (CASSANDRA-15041)
   * Support cross version messaging in in-jvm upgrade dtests (CASSANDRA-15078)
   * Fix index summary redistribution cancellation (CASSANDRA-15045)
   * Refactor Circle CI configuration (CASSANDRA-14806)
diff --cc src/java/org/apache/cassandra/auth/AuthCache.java
index 02b3c0c,0000000..80664d1
mode 100644,000000..100644
--- a/src/java/org/apache/cassandra/auth/AuthCache.java
+++ b/src/java/org/apache/cassandra/auth/AuthCache.java
@@@ -1,196 -1,0 +1,212 @@@
 +/*
 + * 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;
 +
 +import java.util.concurrent.ExecutionException;
 +import java.util.concurrent.ThreadPoolExecutor;
 +import java.util.concurrent.TimeUnit;
 +import java.util.function.Consumer;
 +import java.util.function.Function;
 +import java.util.function.Supplier;
 +
++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.util.concurrent.ListenableFuture;
 +import com.google.common.util.concurrent.ListenableFutureTask;
++import com.google.common.util.concurrent.UncheckedExecutionException;
 +import org.slf4j.Logger;
 +import org.slf4j.LoggerFactory;
++
 +import org.apache.cassandra.utils.MBeanWrapper;
 +
 +import org.apache.cassandra.concurrent.DebuggableThreadPoolExecutor;
 +
 +public class AuthCache<K, V> implements AuthCacheMBean
 +{
 +    private static final Logger logger = LoggerFactory.getLogger(AuthCache.class);
 +
 +    private static final String MBEAN_NAME_BASE = "org.apache.cassandra.auth:type=";
 +
 +    private volatile LoadingCache<K, V> cache;
 +    private ThreadPoolExecutor cacheRefreshExecutor;
 +
 +    private final String name;
 +    private final Consumer<Integer> setValidityDelegate;
 +    private final Supplier<Integer> getValidityDelegate;
 +    private final Consumer<Integer> setUpdateIntervalDelegate;
 +    private final Supplier<Integer> getUpdateIntervalDelegate;
 +    private final Consumer<Integer> setMaxEntriesDelegate;
 +    private final Supplier<Integer> getMaxEntriesDelegate;
 +    private final Function<K, V> loadFunction;
 +    private final Supplier<Boolean> enableCache;
 +
 +    protected AuthCache(String name,
 +                        Consumer<Integer> setValidityDelegate,
 +                        Supplier<Integer> getValidityDelegate,
 +                        Consumer<Integer> setUpdateIntervalDelegate,
 +                        Supplier<Integer> getUpdateIntervalDelegate,
 +                        Consumer<Integer> setMaxEntriesDelegate,
 +                        Supplier<Integer> getMaxEntriesDelegate,
 +                        Function<K, V> loadFunction,
 +                        Supplier<Boolean> enableCache)
 +    {
 +        this.name = name;
 +        this.setValidityDelegate = setValidityDelegate;
 +        this.getValidityDelegate = getValidityDelegate;
 +        this.setUpdateIntervalDelegate = setUpdateIntervalDelegate;
 +        this.getUpdateIntervalDelegate = getUpdateIntervalDelegate;
 +        this.setMaxEntriesDelegate = setMaxEntriesDelegate;
 +        this.getMaxEntriesDelegate = getMaxEntriesDelegate;
 +        this.loadFunction = loadFunction;
 +        this.enableCache = enableCache;
 +        init();
 +    }
 +
 +    protected void init()
 +    {
-         this.cacheRefreshExecutor = new DebuggableThreadPoolExecutor(name + "Refresh", Thread.NORM_PRIORITY);
++        this.cacheRefreshExecutor = new DebuggableThreadPoolExecutor(name + "Refresh", Thread.NORM_PRIORITY)
++        {
++            protected void afterExecute(Runnable r, Throwable t)
++            {
++                // empty to avoid logging on background updates
++            }
++        };
 +        this.cache = initCache(null);
 +        MBeanWrapper.instance.registerMBean(this, getObjectName());
 +    }
 +
 +    protected String getObjectName()
 +    {
 +        return MBEAN_NAME_BASE + name;
 +    }
 +
-     public V get(K k) throws ExecutionException
++    public V get(K k)
 +    {
 +        if (cache == null)
 +            return loadFunction.apply(k);
 +
-         return cache.get(k);
++        try {
++            return cache.get(k);
++        }
++        catch (ExecutionException | UncheckedExecutionException e)
++        {
++            Throwables.propagateIfInstanceOf(e.getCause(), RuntimeException.class);
++            throw Throwables.propagate(e);
++        }
 +    }
 +
 +    public void invalidate()
 +    {
 +        cache = initCache(null);
 +    }
 +
 +    public void invalidate(K k)
 +    {
 +        if (cache != null)
 +            cache.invalidate(k);
 +    }
 +
 +    public void setValidity(int validityPeriod)
 +    {
 +        if (Boolean.getBoolean("cassandra.disable_auth_caches_remote_configuration"))
 +            throw new UnsupportedOperationException("Remote configuration of auth caches is disabled");
 +
 +        setValidityDelegate.accept(validityPeriod);
 +        cache = initCache(cache);
 +    }
 +
 +    public int getValidity()
 +    {
 +        return getValidityDelegate.get();
 +    }
 +
 +    public void setUpdateInterval(int updateInterval)
 +    {
 +        if (Boolean.getBoolean("cassandra.disable_auth_caches_remote_configuration"))
 +            throw new UnsupportedOperationException("Remote configuration of auth caches is disabled");
 +
 +        setUpdateIntervalDelegate.accept(updateInterval);
 +        cache = initCache(cache);
 +    }
 +
 +    public int getUpdateInterval()
 +    {
 +        return getUpdateIntervalDelegate.get();
 +    }
 +
 +    public void setMaxEntries(int maxEntries)
 +    {
 +        if (Boolean.getBoolean("cassandra.disable_auth_caches_remote_configuration"))
 +            throw new UnsupportedOperationException("Remote configuration of auth caches is disabled");
 +
 +        setMaxEntriesDelegate.accept(maxEntries);
 +        cache = initCache(cache);
 +    }
 +
 +    public int getMaxEntries()
 +    {
 +        return getMaxEntriesDelegate.get();
 +    }
 +
 +    private LoadingCache<K, V> initCache(LoadingCache<K, V> existing)
 +    {
 +        if (!enableCache.get())
 +            return null;
 +
 +        if (getValidity() <= 0)
 +            return null;
 +
 +        logger.info("(Re)initializing {} (validity period/update interval/max entries) ({}/{}/{})",
 +                    name, getValidity(), getUpdateInterval(), getMaxEntries());
 +
 +        LoadingCache<K, V> newcache = CacheBuilder.newBuilder()
 +                           .refreshAfterWrite(getUpdateInterval(), TimeUnit.MILLISECONDS)
 +                           .expireAfterWrite(getValidity(), TimeUnit.MILLISECONDS)
 +                           .maximumSize(getMaxEntries())
 +                           .build(new CacheLoader<K, V>()
 +                           {
 +                               public V load(K k)
 +                               {
 +                                   return loadFunction.apply(k);
 +                               }
 +
 +                               public ListenableFuture<V> reload(final K k, final V oldV)
 +                               {
 +                                   ListenableFutureTask<V> task = ListenableFutureTask.create(() -> {
 +                                       try
 +                                       {
 +                                           return loadFunction.apply(k);
 +                                       }
 +                                       catch (Exception e)
 +                                       {
 +                                           logger.trace("Error performing async refresh of auth data in {}", name, e);
 +                                           throw e;
 +                                       }
 +                                   });
 +                                   cacheRefreshExecutor.execute(task);
 +                                   return task;
 +                               }
 +                           });
 +        if (existing != null)
 +            newcache.putAll(existing.asMap());
 +        return newcache;
 +    }
 +}
diff --cc src/java/org/apache/cassandra/auth/CassandraRoleManager.java
index 932f1c6,aa98b7e..38862d9
--- a/src/java/org/apache/cassandra/auth/CassandraRoleManager.java
+++ b/src/java/org/apache/cassandra/auth/CassandraRoleManager.java
@@@ -499,23 -514,16 +515,16 @@@ public class CassandraRoleManager imple
       */
      private Role getRole(String name)
      {
-         try
-         {
-             // If it exists, try the legacy users table in case the cluster
-             // is in the process of being upgraded and so is running with mixed
-             // versions of the authn schema.
-             if (Schema.instance.getCFMetaData(SchemaConstants.AUTH_KEYSPACE_NAME, "users") == null)
-                 return getRoleFromTable(name, loadRoleStatement, ROW_TO_ROLE);
-             else
-             {
-                 if (legacySelectUserStatement == null)
-                     legacySelectUserStatement = prepareLegacySelectUserStatement();
-                 return getRoleFromTable(name, legacySelectUserStatement, LEGACY_ROW_TO_ROLE);
-             }
-         }
-         catch (RequestExecutionException | RequestValidationException e)
+         // If it exists, try the legacy users table in case the cluster
+         // is in the process of being upgraded and so is running with mixed
+         // versions of the authn schema.
 -        if (Schema.instance.getCFMetaData(AuthKeyspace.NAME, "users") == null)
++        if (Schema.instance.getCFMetaData(SchemaConstants.AUTH_KEYSPACE_NAME, "users") == null)
+             return getRoleFromTable(name, loadRoleStatement, ROW_TO_ROLE);
+         else
          {
-             throw new RuntimeException(e);
+             if (legacySelectUserStatement == null)
+                 legacySelectUserStatement = prepareLegacySelectUserStatement();
+             return getRoleFromTable(name, legacySelectUserStatement, LEGACY_ROW_TO_ROLE);
          }
      }
  
diff --cc src/java/org/apache/cassandra/auth/PasswordAuthenticator.java
index 54f7985,2b65783..b1a7227
--- a/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java
+++ b/src/java/org/apache/cassandra/auth/PasswordAuthenticator.java
@@@ -98,59 -90,15 +96,40 @@@ public class PasswordAuthenticator impl
  
      private AuthenticatedUser authenticate(String username, String password) throws AuthenticationException
      {
-         try
-         {
-             String hash = cache.get(username);
-             if (!checkpw(password, hash))
-                 throw new AuthenticationException(String.format("Provided username %s and/or password are incorrect", username));
- 
-             return new AuthenticatedUser(username);
-         }
-         catch (ExecutionException | UncheckedExecutionException e)
-         {
-             // the credentials were somehow invalid - either a non-existent role, or one without a defined password
-             if (e.getCause() instanceof NoSuchCredentialsException)
-                 throw new AuthenticationException(String.format("Provided username %s and/or password are incorrect", username));
- 
-             // an unanticipated exception occured whilst querying the credentials table
-             if (e.getCause() instanceof RequestExecutionException)
-             {
-                 logger.trace("Error performing internal authentication", e);
-                 throw new AuthenticationException(String.format("Error during authentication of user %s : %s", username, e.getMessage()));
-             }
++        String hash = cache.get(username);
++        if (!checkpw(password, hash))
++            throw new AuthenticationException(String.format("Provided username %s and/or password are incorrect", username));
 +
-             throw new RuntimeException(e);
-         }
++        return new AuthenticatedUser(username);
 +    }
 +
-     private String queryHashedPassword(String username) throws NoSuchCredentialsException
++    private String queryHashedPassword(String username) throws AuthenticationException
 +    {
          try
          {
              SelectStatement authenticationStatement = authenticationStatement();
 -            return doAuthenticate(username, password, authenticationStatement);
 +
 +            ResultMessage.Rows rows =
 +                authenticationStatement.execute(QueryState.forInternalCalls(),
 +                                                QueryOptions.forInternalCalls(consistencyForRole(username),
 +                                                                              Lists.newArrayList(ByteBufferUtil.bytes(username))),
 +                                                System.nanoTime());
 +
 +            // If either a non-existent role name was supplied, or no credentials
 +            // were found for that role we don't want to cache the result so we throw
-             // a specific, but unchecked, exception to keep LoadingCache happy.
++            // an exception.
 +            if (rows.result.isEmpty())
-                 throw new NoSuchCredentialsException();
++                throw new AuthenticationException(String.format("Provided username %s and/or password are incorrect", username));
 +
 +            UntypedResultSet result = UntypedResultSet.create(rows.result);
 +            if (!result.one().has(SALTED_HASH))
-                 throw new NoSuchCredentialsException();
++                throw new AuthenticationException(String.format("Provided username %s and/or password are incorrect", username));
 +
 +            return result.one().getString(SALTED_HASH);
          }
          catch (RequestExecutionException e)
          {
--            logger.trace("Error performing internal authentication", e);
-             throw e;
+             throw new AuthenticationException("Unable to perform authentication: " + e.getMessage(), e);
          }
      }
  
@@@ -293,36 -252,4 +272,30 @@@
              password = new String(pass, StandardCharsets.UTF_8);
          }
      }
 +
 +    private static class CredentialsCache extends AuthCache<String, String> implements CredentialsCacheMBean
 +    {
 +        private CredentialsCache(PasswordAuthenticator authenticator)
 +        {
 +            super("CredentialsCache",
 +                  DatabaseDescriptor::setCredentialsValidity,
 +                  DatabaseDescriptor::getCredentialsValidity,
 +                  DatabaseDescriptor::setCredentialsUpdateInterval,
 +                  DatabaseDescriptor::getCredentialsUpdateInterval,
 +                  DatabaseDescriptor::setCredentialsCacheMaxEntries,
 +                  DatabaseDescriptor::getCredentialsCacheMaxEntries,
 +                  authenticator::queryHashedPassword,
 +                  () -> true);
 +        }
 +
 +        public void invalidateCredentials(String roleName)
 +        {
 +            invalidate(roleName);
 +        }
 +    }
 +
 +    public static interface CredentialsCacheMBean extends AuthCacheMBean
 +    {
 +        public void invalidateCredentials(String roleName);
 +    }
- 
-     // Just a marker so we can identify that invalid credentials were the
-     // cause of a loading exception from the cache
-     private static final class NoSuchCredentialsException extends RuntimeException
-     {
-     }
  }
diff --cc src/java/org/apache/cassandra/auth/PermissionsCache.java
index 875c473,ddd6348..981ede8
--- a/src/java/org/apache/cassandra/auth/PermissionsCache.java
+++ b/src/java/org/apache/cassandra/auth/PermissionsCache.java
@@@ -18,35 -18,135 +18,27 @@@
  package org.apache.cassandra.auth;
  
  import java.util.Set;
- import java.util.concurrent.ExecutionException;
 -import java.util.concurrent.*;
  
  import org.apache.cassandra.config.DatabaseDescriptor;
 -
 -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.util.concurrent.ListenableFuture;
 -import com.google.common.util.concurrent.ListenableFutureTask;
 -import com.google.common.util.concurrent.UncheckedExecutionException;
 -import org.slf4j.Logger;
 -import org.slf4j.LoggerFactory;
 -
 -import org.apache.cassandra.concurrent.DebuggableThreadPoolExecutor;
 -import org.apache.cassandra.utils.MBeanWrapper;
  import org.apache.cassandra.utils.Pair;
  
 -public class PermissionsCache implements PermissionsCacheMBean
 +public class PermissionsCache extends AuthCache<Pair<AuthenticatedUser, IResource>, Set<Permission>> implements PermissionsCacheMBean
  {
 -    private static final Logger logger = LoggerFactory.getLogger(PermissionsCache.class);
 -
 -    private final String MBEAN_NAME = "org.apache.cassandra.auth:type=PermissionsCache";
 -
 -    private final ThreadPoolExecutor cacheRefreshExecutor = new DebuggableThreadPoolExecutor("PermissionsCacheRefresh",
 -                                                                                             Thread.NORM_PRIORITY)
 -    {
 -        protected void afterExecute(Runnable r, Throwable t)
 -        {
 -            // empty to avoid logging on background updates
 -        }
 -    };
 -    private final IAuthorizer authorizer;
 -    private volatile LoadingCache<Pair<AuthenticatedUser, IResource>, Set<Permission>> cache;
 -
      public PermissionsCache(IAuthorizer authorizer)
      {
 -        this.authorizer = authorizer;
 -        this.cache = initCache(null);
 -        MBeanWrapper.instance.registerMBean(this, MBEAN_NAME);
 +        super("PermissionsCache",
 +              DatabaseDescriptor::setPermissionsValidity,
 +              DatabaseDescriptor::getPermissionsValidity,
 +              DatabaseDescriptor::setPermissionsUpdateInterval,
 +              DatabaseDescriptor::getPermissionsUpdateInterval,
 +              DatabaseDescriptor::setPermissionsCacheMaxEntries,
 +              DatabaseDescriptor::getPermissionsCacheMaxEntries,
 +              (p) -> authorizer.authorize(p.left, p.right),
 +              () -> DatabaseDescriptor.getAuthorizer().requireAuthorization());
      }
  
      public Set<Permission> getPermissions(AuthenticatedUser user, IResource resource)
      {
 -        if (cache == null)
 -            return authorizer.authorize(user, resource);
 -
--        try
--        {
-             return get(Pair.create(user, resource));
 -            return cache.get(Pair.create(user, resource));
--        }
-         catch (ExecutionException e)
 -        catch (ExecutionException | UncheckedExecutionException e)
--        {
-             throw new RuntimeException(e);
 -            Throwables.propagateIfInstanceOf(e.getCause(), RuntimeException.class);
 -            throw Throwables.propagate(e);
--        }
 -    }
 -
 -    public void invalidate()
 -    {
 -        cache = initCache(null);
 -    }
 -
 -    public void setValidity(int validityPeriod)
 -    {
 -        DatabaseDescriptor.setPermissionsValidity(validityPeriod);
 -        cache = initCache(cache);
 -    }
 -
 -    public int getValidity()
 -    {
 -        return DatabaseDescriptor.getPermissionsValidity();
 -    }
 -
 -    public void setUpdateInterval(int updateInterval)
 -    {
 -        DatabaseDescriptor.setPermissionsUpdateInterval(updateInterval);
 -        cache = initCache(cache);
 -    }
 -
 -    public int getUpdateInterval()
 -    {
 -        return DatabaseDescriptor.getPermissionsUpdateInterval();
 -    }
 -
 -    private LoadingCache<Pair<AuthenticatedUser, IResource>, Set<Permission>> initCache(
 -                                                             LoadingCache<Pair<AuthenticatedUser, IResource>, Set<Permission>> existing)
 -    {
 -        if (authorizer instanceof AllowAllAuthorizer)
 -            return null;
 -
 -        if (DatabaseDescriptor.getPermissionsValidity() <= 0)
 -            return null;
 -
 -        LoadingCache<Pair<AuthenticatedUser, IResource>, Set<Permission>> newcache = CacheBuilder.newBuilder()
 -                           .refreshAfterWrite(DatabaseDescriptor.getPermissionsUpdateInterval(), TimeUnit.MILLISECONDS)
 -                           .expireAfterWrite(DatabaseDescriptor.getPermissionsValidity(), TimeUnit.MILLISECONDS)
 -                           .maximumSize(DatabaseDescriptor.getPermissionsCacheMaxEntries())
 -                           .build(new CacheLoader<Pair<AuthenticatedUser, IResource>, Set<Permission>>()
 -                           {
 -                               public Set<Permission> load(Pair<AuthenticatedUser, IResource> userResource)
 -                               {
 -                                   return authorizer.authorize(userResource.left, userResource.right);
 -                               }
 -
 -                               public ListenableFuture<Set<Permission>> reload(final Pair<AuthenticatedUser, IResource> userResource,
 -                                                                               final Set<Permission> oldValue)
 -                               {
 -                                   ListenableFutureTask<Set<Permission>> task = ListenableFutureTask.create(new Callable<Set<Permission>>()
 -                                   {
 -                                       public Set<Permission>call() throws Exception
 -                                       {
 -                                           try
 -                                           {
 -                                               return authorizer.authorize(userResource.left, userResource.right);
 -                                           }
 -                                           catch (Exception e)
 -                                           {
 -                                               logger.trace("Error performing async refresh of user permissions", e);
 -                                               throw e;
 -                                           }
 -                                       }
 -                                   });
 -                                   cacheRefreshExecutor.execute(task);
 -                                   return task;
 -                               }
 -                           });
 -        if (existing != null)
 -            newcache.putAll(existing.asMap());
 -        return newcache;
++        return get(Pair.create(user, resource));
      }
  }
diff --cc src/java/org/apache/cassandra/auth/RolesCache.java
index 8b9c322,a8dae21..a02828a
--- a/src/java/org/apache/cassandra/auth/RolesCache.java
+++ b/src/java/org/apache/cassandra/auth/RolesCache.java
@@@ -18,34 -18,132 +18,26 @@@
  package org.apache.cassandra.auth;
  
  import java.util.Set;
- import java.util.concurrent.ExecutionException;
 -import java.util.concurrent.*;
  
 -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.util.concurrent.ListenableFuture;
 -import com.google.common.util.concurrent.ListenableFutureTask;
 -import com.google.common.util.concurrent.UncheckedExecutionException;
 -import org.slf4j.Logger;
 -import org.slf4j.LoggerFactory;
 -
 -import org.apache.cassandra.concurrent.DebuggableThreadPoolExecutor;
  import org.apache.cassandra.config.DatabaseDescriptor;
 -import org.apache.cassandra.utils.MBeanWrapper;
  
 -public class RolesCache implements RolesCacheMBean
 +public class RolesCache extends AuthCache<RoleResource, Set<RoleResource>> implements RolesCacheMBean
  {
 -    private static final Logger logger = LoggerFactory.getLogger(RolesCache.class);
 -
 -    private final String MBEAN_NAME = "org.apache.cassandra.auth:type=RolesCache";
 -    private final ThreadPoolExecutor cacheRefreshExecutor = new DebuggableThreadPoolExecutor("RolesCacheRefresh",
 -                                                                                             Thread.NORM_PRIORITY)
 -    {
 -        protected void afterExecute(Runnable r, Throwable t)
 -        {
 -            // empty to avoid logging on background updates
 -        }
 -    };
 -    private final IRoleManager roleManager;
 -    private volatile LoadingCache<RoleResource, Set<RoleResource>> cache;
 -
      public RolesCache(IRoleManager roleManager)
      {
 -        this.roleManager = roleManager;
 -        this.cache = initCache(null);
 -        MBeanWrapper.instance.registerMBean(this, MBEAN_NAME);
 +        super("RolesCache",
 +              DatabaseDescriptor::setRolesValidity,
 +              DatabaseDescriptor::getRolesValidity,
 +              DatabaseDescriptor::setRolesUpdateInterval,
 +              DatabaseDescriptor::getRolesUpdateInterval,
 +              DatabaseDescriptor::setRolesCacheMaxEntries,
 +              DatabaseDescriptor::getRolesCacheMaxEntries,
 +              (r) -> roleManager.getRoles(r, true),
 +              () -> DatabaseDescriptor.getAuthenticator().requireAuthentication());
      }
  
      public Set<RoleResource> getRoles(RoleResource role)
      {
 -        if (cache == null)
 -            return roleManager.getRoles(role, true);
 -
--        try
--        {
-             return get(role);
 -            return cache.get(role);
--        }
-         catch (ExecutionException e)
 -        catch (ExecutionException | UncheckedExecutionException e)
--        {
-             throw new RuntimeException(e);
 -            Throwables.propagateIfInstanceOf(e.getCause(), RuntimeException.class);
 -            throw Throwables.propagate(e);
--        }
 -    }
 -
 -    public void invalidate()
 -    {
 -        cache = initCache(null);
 -    }
 -
 -    public void setValidity(int validityPeriod)
 -    {
 -        DatabaseDescriptor.setRolesValidity(validityPeriod);
 -        cache = initCache(cache);
 -    }
 -
 -    public int getValidity()
 -    {
 -        return DatabaseDescriptor.getRolesValidity();
 -    }
 -
 -    public void setUpdateInterval(int updateInterval)
 -    {
 -        DatabaseDescriptor.setRolesUpdateInterval(updateInterval);
 -        cache = initCache(cache);
 -    }
 -
 -    public int getUpdateInterval()
 -    {
 -        return DatabaseDescriptor.getRolesUpdateInterval();
 -    }
 -
 -
 -    private LoadingCache<RoleResource, Set<RoleResource>> initCache(LoadingCache<RoleResource, Set<RoleResource>> existing)
 -    {
 -        if (!DatabaseDescriptor.getAuthenticator().requireAuthentication())
 -            return null;
 -
 -        if (DatabaseDescriptor.getRolesValidity() <= 0)
 -            return null;
 -
 -        LoadingCache<RoleResource, Set<RoleResource>> newcache = CacheBuilder.newBuilder()
 -                .refreshAfterWrite(DatabaseDescriptor.getRolesUpdateInterval(), TimeUnit.MILLISECONDS)
 -                .expireAfterWrite(DatabaseDescriptor.getRolesValidity(), TimeUnit.MILLISECONDS)
 -                .maximumSize(DatabaseDescriptor.getRolesCacheMaxEntries())
 -                .build(new CacheLoader<RoleResource, Set<RoleResource>>()
 -                {
 -                    public Set<RoleResource> load(RoleResource primaryRole)
 -                    {
 -                        return roleManager.getRoles(primaryRole, true);
 -                    }
 -
 -                    public ListenableFuture<Set<RoleResource>> reload(final RoleResource primaryRole,
 -                                                                      final Set<RoleResource> oldValue)
 -                    {
 -                        ListenableFutureTask<Set<RoleResource>> task;
 -                        task = ListenableFutureTask.create(new Callable<Set<RoleResource>>()
 -                        {
 -                            public Set<RoleResource> call() throws Exception
 -                            {
 -                                try
 -                                {
 -                                    return roleManager.getRoles(primaryRole, true);
 -                                } catch (Exception e)
 -                                {
 -                                    logger.trace("Error performing async refresh of user roles", e);
 -                                    throw e;
 -                                }
 -                            }
 -                        });
 -                        cacheRefreshExecutor.execute(task);
 -                        return task;
 -                    }
 -                });
 -        if (existing != null)
 -            newcache.putAll(existing.asMap());
 -        return newcache;
++        return get(role);
      }
  }
diff --cc src/java/org/apache/cassandra/auth/jmx/AuthorizationProxy.java
index f19b19f,0000000..7bfbf52
mode 100644,000000..100644
--- a/src/java/org/apache/cassandra/auth/jmx/AuthorizationProxy.java
+++ b/src/java/org/apache/cassandra/auth/jmx/AuthorizationProxy.java
@@@ -1,509 -1,0 +1,494 @@@
 +/*
 + * 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<String> 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<String> 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<RoleResource, Boolean> 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<RoleResource, Set<PermissionDetails>> 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<Boolean> 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<ObjectName, Set<ObjectName>> 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<Boolean> 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<Principal> 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<JMXResource> 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<JMXResource> 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<JMXResource> 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<ObjectName> 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<ObjectName> 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<JMXResource> 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<PermissionDetails> 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<RoleResource, Set<PermissionDetails>>
 +    {
 +        protected JMXPermissionsCache()
 +        {
 +            super("JMXPermissionsCache",
 +                  DatabaseDescriptor::setPermissionsValidity,
 +                  DatabaseDescriptor::getPermissionsValidity,
 +                  DatabaseDescriptor::setPermissionsUpdateInterval,
 +                  DatabaseDescriptor::getPermissionsUpdateInterval,
 +                  DatabaseDescriptor::setPermissionsCacheMaxEntries,
 +                  DatabaseDescriptor::getPermissionsCacheMaxEntries,
 +                  AuthorizationProxy::loadPermissions,
 +                  () -> true);
 +        }
- 
-         public Set<PermissionDetails> get(RoleResource roleResource)
-         {
-             try
-             {
-                 return super.get(roleResource);
-             }
-             catch (Exception e)
-             {
-                 // because the outer class uses this method as Function<RoleResource, Set<PermissionDetails>>,
-                 // which can be overridden for testing, it cannot throw checked exceptions. So here we simply
-                 // use guava's propagation helper.
-                 throw Throwables.propagate(e);
-             }
-         }
 +    }
 +}
diff --cc test/unit/org/apache/cassandra/auth/AuthCacheTest.java
index 0000000,0000000..0030603
new file mode 100644
--- /dev/null
+++ b/test/unit/org/apache/cassandra/auth/AuthCacheTest.java
@@@ -1,0 -1,0 +1,164 @@@
++/*
++ * 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;
++
++import java.util.function.Consumer;
++import java.util.function.Function;
++import java.util.function.Supplier;
++
++import org.junit.Test;
++
++import org.apache.cassandra.db.ConsistencyLevel;
++import org.apache.cassandra.exceptions.UnavailableException;
++
++import static org.junit.Assert.assertEquals;
++
++public class AuthCacheTest
++{
++    private int loadCounter = 0;
++    private int validity = 2000;
++    private boolean isCacheEnabled = true;
++
++    @Test
++    public void testCacheLoaderIsCalledOnFirst()
++    {
++        TestCache<String, Integer> authCache = new TestCache<>(this::countingLoader, this::setValidity, () -> validity, () -> isCacheEnabled);
++
++        int result = authCache.get("10");
++
++        assertEquals(10, result);
++        assertEquals(1, loadCounter);
++    }
++
++    @Test
++    public void testCacheLoaderIsNotCalledOnSecond()
++    {
++        TestCache<String, Integer> authCache = new TestCache<>(this::countingLoader, this::setValidity, () -> validity, () -> isCacheEnabled);
++        authCache.get("10");
++        assertEquals(1, loadCounter);
++
++        int result = authCache.get("10");
++
++        assertEquals(10, result);
++        assertEquals(1, loadCounter);
++    }
++
++    @Test
++    public void testCacheLoaderIsAlwaysCalledWhenDisabled()
++    {
++        isCacheEnabled = false;
++        TestCache<String, Integer> authCache = new TestCache<>(this::countingLoader, this::setValidity, () -> validity, () -> isCacheEnabled);
++
++        authCache.get("10");
++        int result = authCache.get("10");
++
++        assertEquals(10, result);
++        assertEquals(2, loadCounter);
++    }
++
++    @Test
++    public void testCacheLoaderIsAlwaysCalledWhenValidityIsZero()
++    {
++        setValidity(0);
++        TestCache<String, Integer> authCache = new TestCache<>(this::countingLoader, this::setValidity, () -> validity, () -> isCacheEnabled);
++
++        authCache.get("10");
++        int result = authCache.get("10");
++
++        assertEquals(10, result);
++        assertEquals(2, loadCounter);
++    }
++
++    @Test
++    public void testCacheLoaderIsCalledAfterFullInvalidate()
++    {
++        TestCache<String, Integer> authCache = new TestCache<>(this::countingLoader, this::setValidity, () -> validity, () -> isCacheEnabled);
++        authCache.get("10");
++
++        authCache.invalidate();
++        int result = authCache.get("10");
++
++        assertEquals(10, result);
++        assertEquals(2, loadCounter);
++    }
++
++    @Test
++    public void testCacheLoaderIsCalledAfterInvalidateKey()
++    {
++        TestCache<String, Integer> authCache = new TestCache<>(this::countingLoader, this::setValidity, () -> validity, () -> isCacheEnabled);
++        authCache.get("10");
++
++        authCache.invalidate("10");
++        int result = authCache.get("10");
++
++        assertEquals(10, result);
++        assertEquals(2, loadCounter);
++    }
++
++    @Test(expected = UnavailableException.class)
++    public void testCassandraExceptionPassThroughWhenCacheEnabled()
++    {
++        TestCache<String, Integer> cache = new TestCache<>(s -> {
++            throw new UnavailableException(ConsistencyLevel.QUORUM, 3, 1);
++        }, this::setValidity, () -> validity, () -> isCacheEnabled);
++
++        cache.get("expect-exception");
++    }
++
++    @Test(expected = UnavailableException.class)
++    public void testCassandraExceptionPassThroughWhenCacheDisable()
++    {
++        isCacheEnabled = false;
++        TestCache<String, Integer> cache = new TestCache<>(s -> {
++            throw new UnavailableException(ConsistencyLevel.QUORUM, 3, 1);
++        }, this::setValidity, () -> validity, () -> isCacheEnabled);
++
++        cache.get("expect-exception");
++    }
++
++    private void setValidity(int validity)
++    {
++        this.validity = validity;
++    }
++
++    private Integer countingLoader(String s)
++    {
++        loadCounter++;
++        return Integer.parseInt(s);
++    }
++
++    private static class TestCache<K, V> extends AuthCache<K, V>
++    {
++        private static int nameCounter = 0; // Allow us to create many instances of cache with same name prefix
++
++        TestCache(Function<K, V> loadFunction, Consumer<Integer> setValidityDelegate, Supplier<Integer> getValidityDelegate, Supplier<Boolean> cacheEnabledDelegate)
++        {
++            super("TestCache" + nameCounter++,
++                  setValidityDelegate,
++                  getValidityDelegate,
++                  (updateInterval) -> {
++                  },
++                  () -> 1000,
++                  (maxEntries) -> {
++                  },
++                  () -> 10,
++                  loadFunction,
++                  cacheEnabledDelegate);
++        }
++    }
++}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org