diff --git a/CHANGES.txt b/CHANGES.txt index 2419c4e9b..2ae83f91e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,6 @@ 0.3.0 ----- + * NullPointerException When Authentication Is Enabled but sidecar_internal Schema Is Disabled (CASSSIDECAR-331) * Update logging dependencies (CASSSIDECAR-337) 0.2.0 diff --git a/server/src/main/java/org/apache/cassandra/sidecar/acl/AuthCache.java b/server/src/main/java/org/apache/cassandra/sidecar/acl/AuthCache.java index f60ab1033..d1e9a68ad 100644 --- a/server/src/main/java/org/apache/cassandra/sidecar/acl/AuthCache.java +++ b/server/src/main/java/org/apache/cassandra/sidecar/acl/AuthCache.java @@ -119,6 +119,18 @@ public Map getAll() return Collections.unmodifiableMap(cache.asMap()); } + /** + * Invalidate a key. + * @param k key to invalidate + */ + public void invalidate(K k) + { + if (cache != null) + { + cache.invalidate(k); + } + } + private LoadingCache initCache() { return Caffeine.newBuilder() diff --git a/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/AuthenticationHandlerFactory.java b/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/AuthenticationHandlerFactory.java index a12094ca0..aa7e4baae 100644 --- a/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/AuthenticationHandlerFactory.java +++ b/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/AuthenticationHandlerFactory.java @@ -23,6 +23,7 @@ import io.vertx.core.Vertx; import io.vertx.ext.web.handler.impl.AuthenticationHandlerInternal; import org.apache.cassandra.sidecar.config.AccessControlConfiguration; +import org.apache.cassandra.sidecar.config.SidecarConfiguration; import org.apache.cassandra.sidecar.exceptions.ConfigurationException; /** @@ -45,4 +46,16 @@ public interface AuthenticationHandlerFactory AuthenticationHandlerInternal create(Vertx vertx, AccessControlConfiguration accessControlConfiguration, Map parameters) throws ConfigurationException; + + /** + * Validates that this authentication handler factory can be used with the given configuration. + * This gives the implementation flexibility to establish pre-requisites in order to properly function + * correctly. + * + * @param sidecarConfiguration the sidecar configuration to validate against + * @throws ConfigurationException if the authentication handler factory cannot be used with the given configuration + */ + default void validatePrerequisites(SidecarConfiguration sidecarConfiguration) throws ConfigurationException + { + } } diff --git a/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/JwtAuthenticationHandlerFactory.java b/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/JwtAuthenticationHandlerFactory.java index d14ca715a..46c919de5 100644 --- a/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/JwtAuthenticationHandlerFactory.java +++ b/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/JwtAuthenticationHandlerFactory.java @@ -25,6 +25,7 @@ import io.vertx.core.Vertx; import io.vertx.ext.web.handler.impl.AuthenticationHandlerInternal; import org.apache.cassandra.sidecar.config.AccessControlConfiguration; +import org.apache.cassandra.sidecar.config.SidecarConfiguration; import org.apache.cassandra.sidecar.exceptions.ConfigurationException; import org.apache.cassandra.sidecar.tasks.PeriodicTaskExecutor; @@ -65,4 +66,16 @@ protected JwtParameters parameterParser(Map parameters) { return new JwtParameterExtractor(parameters); } + + @Override + public void validatePrerequisites(SidecarConfiguration sidecarConfiguration) throws ConfigurationException + { + boolean isSidecarSchemaEnabled = sidecarConfiguration.serviceConfiguration() + .schemaKeyspaceConfiguration() + .isEnabled(); + if (!isSidecarSchemaEnabled) + { + throw new ConfigurationException("JwtAuthenticationHandlerFactory requires Sidecar schema to be enabled for role processing"); + } + } } diff --git a/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/MutualTlsAuthenticationHandlerFactory.java b/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/MutualTlsAuthenticationHandlerFactory.java index 22075bf86..1011bd721 100644 --- a/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/MutualTlsAuthenticationHandlerFactory.java +++ b/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/MutualTlsAuthenticationHandlerFactory.java @@ -32,6 +32,7 @@ import org.apache.cassandra.sidecar.acl.AdminIdentityResolver; import org.apache.cassandra.sidecar.acl.IdentityToRoleCache; import org.apache.cassandra.sidecar.config.AccessControlConfiguration; +import org.apache.cassandra.sidecar.config.SidecarConfiguration; import org.apache.cassandra.sidecar.exceptions.ConfigurationException; /** @@ -106,4 +107,16 @@ private MutualTlsAuthenticationHandler createInternal(Vertx vertx, MutualTlsAuthentication mTLSAuthProvider = new MutualTlsAuthenticationImpl(vertx, certificateValidator, certificateIdentityExtractor); return new MutualTlsAuthenticationHandler(mTLSAuthProvider, identityToRoleCache); } + + @Override + public void validatePrerequisites(SidecarConfiguration sidecarConfiguration) throws ConfigurationException + { + boolean isSidecarSchemaEnabled = sidecarConfiguration.serviceConfiguration() + .schemaKeyspaceConfiguration() + .isEnabled(); + if (!isSidecarSchemaEnabled) + { + throw new ConfigurationException("MutualTlsAuthenticationHandlerFactory requires Sidecar schema to be enabled for role processing"); + } + } } diff --git a/server/src/main/java/org/apache/cassandra/sidecar/modules/AuthModule.java b/server/src/main/java/org/apache/cassandra/sidecar/modules/AuthModule.java index 04e72bb1a..472b31ed2 100644 --- a/server/src/main/java/org/apache/cassandra/sidecar/modules/AuthModule.java +++ b/server/src/main/java/org/apache/cassandra/sidecar/modules/AuthModule.java @@ -141,6 +141,7 @@ VertxRoute chainAuthHandler(Vertx vertx, throw new RuntimeException(String.format("Implementation for class %s has not been registered", config.className())); } + factory.validatePrerequisites(sidecarConfiguration); chainAuthHandler.add(factory.create(vertx, accessControlConfiguration, config.namedParameters())); } @@ -180,6 +181,10 @@ AuthorizationProvider authorizationProvider(SidecarConfiguration sidecarConfigur } if (config.className().equalsIgnoreCase(RoleBasedAuthorizationProvider.class.getName())) { + if (!sidecarConfiguration.serviceConfiguration().schemaKeyspaceConfiguration().isEnabled()) + { + throw new ConfigurationException(config.className() + " requires Sidecar schema to be enabled for role permissions used by Sidecar"); + } return new RoleBasedAuthorizationProvider(roleAuthorizationsCache); } throw new ConfigurationException("Unrecognized authorization provider " + config.className() + " set"); diff --git a/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/JWTAuthenticationHandlerFactoryTest.java b/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/JWTAuthenticationHandlerFactoryTest.java index de9f20c4b..bc6165c0f 100644 --- a/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/JWTAuthenticationHandlerFactoryTest.java +++ b/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/JWTAuthenticationHandlerFactoryTest.java @@ -24,10 +24,15 @@ import io.vertx.core.Vertx; import org.apache.cassandra.sidecar.config.AccessControlConfiguration; +import org.apache.cassandra.sidecar.config.SchemaKeyspaceConfiguration; +import org.apache.cassandra.sidecar.config.ServiceConfiguration; +import org.apache.cassandra.sidecar.config.SidecarConfiguration; +import org.apache.cassandra.sidecar.exceptions.ConfigurationException; import org.apache.cassandra.sidecar.tasks.PeriodicTaskExecutor; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Test for {@link JwtAuthenticationHandlerFactory} @@ -55,4 +60,24 @@ void testInvalidParameters() .isInstanceOf(IllegalArgumentException.class) .hasMessage("Missing client_id JWT parameter"); } + + @Test + void testValidatePrerequisitesWithSchemaDisabled() + { + PeriodicTaskExecutor mockTaskExecutor = mock(PeriodicTaskExecutor.class); + JwtAuthenticationHandlerFactory factory = new JwtAuthenticationHandlerFactory(mockRoleProcessor, mockTaskExecutor); + + SidecarConfiguration mockSidecarConfig = mock(SidecarConfiguration.class); + ServiceConfiguration mockServiceConfig = mock(ServiceConfiguration.class); + SchemaKeyspaceConfiguration mockSchemaConfig = mock(SchemaKeyspaceConfiguration.class); + + when(mockSidecarConfig.serviceConfiguration()).thenReturn(mockServiceConfig); + when(mockServiceConfig.schemaKeyspaceConfiguration()).thenReturn(mockSchemaConfig); + when(mockSchemaConfig.isEnabled()).thenReturn(false); + + // Should throw exception when schema is disabled + assertThatThrownBy(() -> factory.validatePrerequisites(mockSidecarConfig)) + .isInstanceOf(ConfigurationException.class) + .hasMessage("JwtAuthenticationHandlerFactory requires Sidecar schema to be enabled for role processing"); + } } diff --git a/server/src/test/java/org/apache/cassandra/sidecar/modules/AuthModuleTest.java b/server/src/test/java/org/apache/cassandra/sidecar/modules/AuthModuleTest.java new file mode 100644 index 000000000..7a9921ec6 --- /dev/null +++ b/server/src/test/java/org/apache/cassandra/sidecar/modules/AuthModuleTest.java @@ -0,0 +1,118 @@ +/* + * 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.sidecar.modules; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import io.vertx.core.Vertx; +import org.apache.cassandra.sidecar.acl.authentication.AuthenticationHandlerFactory; +import org.apache.cassandra.sidecar.acl.authentication.AuthenticationHandlerFactoryRegistry; +import org.apache.cassandra.sidecar.acl.authentication.MutualTlsAuthenticationHandlerFactory; +import org.apache.cassandra.sidecar.acl.authorization.RoleBasedAuthorizationProvider; +import org.apache.cassandra.sidecar.config.AccessControlConfiguration; +import org.apache.cassandra.sidecar.config.ParameterizedClassConfiguration; +import org.apache.cassandra.sidecar.config.SchemaKeyspaceConfiguration; +import org.apache.cassandra.sidecar.config.ServiceConfiguration; +import org.apache.cassandra.sidecar.config.SidecarConfiguration; +import org.apache.cassandra.sidecar.exceptions.ConfigurationException; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Test for {@link AuthModule} + */ +class AuthModuleTest +{ + @Test + void testAuthorizationProviderWithSchemaEnabled() + { + AuthModule authModule = new AuthModule(); + SidecarConfiguration mockSidecarConfig = createSidecarConfiguration(true, true, true); + + // Should not throw when schema is enabled + authModule.authorizationProvider(mockSidecarConfig, null); + } + + @Test + void testRoleBasedAuthorizationProviderWithSchemaDisabled() + { + AuthModule authModule = new AuthModule(); + SidecarConfiguration mockSidecarConfig = createSidecarConfiguration(true, true, false); + + // Should throw when RoleBasedAuthorizationProvider is used but schema is disabled + assertThatThrownBy(() -> authModule.authorizationProvider(mockSidecarConfig, null)) + .isInstanceOf(ConfigurationException.class) + .hasMessage(RoleBasedAuthorizationProvider.class.getName() + + " requires Sidecar schema to be enabled for role permissions used by Sidecar"); + } + + @Test + void testChainAuthHandlerValidatesPrerequisites() + { + AuthModule authModule = new AuthModule(); + Vertx mockVertx = mock(Vertx.class); + + SidecarConfiguration mockSidecarConfig = createSidecarConfiguration(true, false, false); + + AuthenticationHandlerFactoryRegistry mockRegistry = mock(AuthenticationHandlerFactoryRegistry.class); + AuthenticationHandlerFactory mtlsFactory = new MutualTlsAuthenticationHandlerFactory(null, null); + when(mockRegistry.getFactory(any())).thenReturn(mtlsFactory); + + // Should throw ConfigurationException when factory validates prerequisites + assertThatThrownBy(() -> authModule.chainAuthHandler(mockVertx, mockSidecarConfig, mockRegistry)) + .isInstanceOf(ConfigurationException.class) + .hasMessage("MutualTlsAuthenticationHandlerFactory requires Sidecar schema to be enabled for role processing"); + } + + private SidecarConfiguration createSidecarConfiguration(boolean authenticationEnabled, + boolean authorizationEnabled, + boolean schemaEnabled) + { + SidecarConfiguration mockSidecarConfig = mock(SidecarConfiguration.class); + ServiceConfiguration mockServiceConfig = mock(ServiceConfiguration.class); + SchemaKeyspaceConfiguration mockSchemaConfig = mock(SchemaKeyspaceConfiguration.class); + AccessControlConfiguration mockAccessControlConfig = mock(AccessControlConfiguration.class); + ParameterizedClassConfiguration mockAuthenticatorConfig = mock(ParameterizedClassConfiguration.class); + ParameterizedClassConfiguration mockAuthorizerConfig = mock(ParameterizedClassConfiguration.class); + + when(mockSidecarConfig.serviceConfiguration()).thenReturn(mockServiceConfig); + when(mockServiceConfig.schemaKeyspaceConfiguration()).thenReturn(mockSchemaConfig); + when(mockSchemaConfig.isEnabled()).thenReturn(schemaEnabled); + when(mockSidecarConfig.accessControlConfiguration()).thenReturn(mockAccessControlConfig); + when(mockAccessControlConfig.enabled()).thenReturn(true); + + if (authenticationEnabled) + { + when(mockAccessControlConfig.authenticatorsConfiguration()).thenReturn(List.of(mockAuthenticatorConfig)); + when(mockAuthenticatorConfig.className()).thenReturn(MutualTlsAuthenticationHandlerFactory.class.getName()); + } + + if (authorizationEnabled) + { + when(mockAccessControlConfig.authorizerConfiguration()).thenReturn(mockAuthorizerConfig); + when(mockAuthorizerConfig.className()).thenReturn(RoleBasedAuthorizationProvider.class.getName()); + } + return mockSidecarConfig; + } +}