yifan-c commented on code in PR #145: URL: https://github.com/apache/cassandra-sidecar/pull/145#discussion_r1830197515
########## server/src/main/java/org/apache/cassandra/sidecar/accesscontrol/AuthCache.java: ########## @@ -0,0 +1,214 @@ +/* + * 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.accesscontrol; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.google.common.util.concurrent.Uninterruptibles; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; + +/** + * {@link AuthCache} caches information needed for authenticating sidecar users. + * + * @param <K> key stored in cache for retrieving value + * @param <V> value cached + * + * for {@link IdentityRoleCache} key will be String representing identity extracted from certificate and value will + * be String representing Cassandra role associated with the identity + */ +public abstract class AuthCache<K, V> +{ + protected static final Logger LOGGER = LoggerFactory.getLogger(AuthCache.class); + protected final String name; + protected final boolean enabled; + protected final Function<K, V> loadFunction; + protected final Supplier<Map<K, V>> bulkLoadFunction; + protected final int warmingRetries; + protected final long warmingRetryIntervalInMillis; + + protected long expireAfterMillis; + protected long maxEntries; + + protected volatile LoadingCache<K, V> cache; + + protected AuthCache(String name, + boolean enabled, + Function<K, V> loadFunction, + Supplier<Map<K, V>> bulkLoadFunction, + int warmingRetries, + long warmingRetryIntervalInMillis, + long expireAfterMillis, + long maxEntries) + { + this.name = name; + this.enabled = enabled; + this.loadFunction = loadFunction; + this.bulkLoadFunction = bulkLoadFunction; + this.warmingRetries = warmingRetries; + this.warmingRetryIntervalInMillis = warmingRetryIntervalInMillis; + this.expireAfterMillis = expireAfterMillis; + this.maxEntries = maxEntries; + this.cache = initCache(null); + } + + public void setExpireAfterMillis(long expireAfterMillis) Review Comment: `@VisibleForTesting`? ########## server/src/main/java/org/apache/cassandra/sidecar/accesscontrol/AuthCache.java: ########## @@ -0,0 +1,214 @@ +/* + * 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.accesscontrol; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.google.common.util.concurrent.Uninterruptibles; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; + +/** + * {@link AuthCache} caches information needed for authenticating sidecar users. + * + * @param <K> key stored in cache for retrieving value + * @param <V> value cached + * + * for {@link IdentityRoleCache} key will be String representing identity extracted from certificate and value will + * be String representing Cassandra role associated with the identity + */ Review Comment: ```suggestion /** * Caches information needed for authenticating sidecar users. * * @param <K> Key type * @param <V> Value type */ ``` I removed the paragraph about "for {@link IdentityRoleCache}" because it is specific to one implementation, and similar comment has added to the class in question. ########## server/src/main/java/org/apache/cassandra/sidecar/accesscontrol/AuthCache.java: ########## @@ -0,0 +1,214 @@ +/* + * 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.accesscontrol; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.google.common.util.concurrent.Uninterruptibles; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; + +/** + * {@link AuthCache} caches information needed for authenticating sidecar users. + * + * @param <K> key stored in cache for retrieving value + * @param <V> value cached + * + * for {@link IdentityRoleCache} key will be String representing identity extracted from certificate and value will + * be String representing Cassandra role associated with the identity + */ +public abstract class AuthCache<K, V> +{ + protected static final Logger LOGGER = LoggerFactory.getLogger(AuthCache.class); + protected final String name; + protected final boolean enabled; + protected final Function<K, V> loadFunction; + protected final Supplier<Map<K, V>> bulkLoadFunction; + protected final int warmingRetries; + protected final long warmingRetryIntervalInMillis; + + protected long expireAfterMillis; + protected long maxEntries; + + protected volatile LoadingCache<K, V> cache; + + protected AuthCache(String name, + boolean enabled, + Function<K, V> loadFunction, + Supplier<Map<K, V>> bulkLoadFunction, + int warmingRetries, + long warmingRetryIntervalInMillis, + long expireAfterMillis, + long maxEntries) + { + this.name = name; + this.enabled = enabled; + this.loadFunction = loadFunction; + this.bulkLoadFunction = bulkLoadFunction; + this.warmingRetries = warmingRetries; + this.warmingRetryIntervalInMillis = warmingRetryIntervalInMillis; + this.expireAfterMillis = expireAfterMillis; + this.maxEntries = maxEntries; + this.cache = initCache(null); + } + + public void setExpireAfterMillis(long expireAfterMillis) + { + if (enabled && this.expireAfterMillis != expireAfterMillis) + { + synchronized (this) + { + if (this.expireAfterMillis == expireAfterMillis) + { + return; + } + this.expireAfterMillis = expireAfterMillis; + cache = initCache(cache); + } + } + } + + public long getExpireAfterMillis() + { + return this.expireAfterMillis; + } + + public void setMaxEntries(long maxEntries) + { + if (enabled && this.maxEntries != maxEntries) + { + synchronized (this) + { + if (this.maxEntries == maxEntries) + { + return; + } + this.maxEntries = maxEntries; + cache = initCache(cache); + } + } + } + + public long getMaxEntries() + { + return this.maxEntries; + } + + /** + * Retrieve a value from the cache. Will call {@link LoadingCache#get(Object)} which will + * "load" the value if it's not present, thus populating the key. + * @param k key + * @return The current value of {@code K} if cached or loaded. + * + * See {@link LoadingCache#get(Object)} for possible exceptions. + */ + public V get(K k) + { + if (cache == null) + { + return loadFunction.apply(k); + } + return cache.get(k); + } + + /** + * Retrieve all cached entries. Will call {@link LoadingCache#asMap()} which does not trigger "load". + * @return a map of cached key-value pairs + */ + public Map<K, V> getAll() + { + if (cache == null) + { + return Collections.emptyMap(); + } + return Collections.unmodifiableMap(cache.asMap()); + } + + // We might need to add support for invalidating cache entries when we support removing auth entries from + // Cassandra auth tables through sidecar + public void invalidate(K k) + { + throw new UnsupportedOperationException("Invalidate functionality is not yet supported for AuthCache"); + } + + private LoadingCache<K, V> initCache(LoadingCache<K, V> existing) + { + if (!enabled) + return null; + + LoadingCache<K, V> updatedCache; + if (existing == null) + { + updatedCache = Caffeine.newBuilder() + // setting refreshAfterWrite and expireAfterWrite to same value makes sure no stale + // data is fetched after expire time + .refreshAfterWrite(expireAfterMillis, TimeUnit.MILLISECONDS) + .expireAfterWrite(expireAfterMillis, TimeUnit.MILLISECONDS) + .maximumSize(getMaxEntries()) + .build(loadFunction::apply); + } + else + { + updatedCache = cache; + // Always set as mandatory + cache.policy().expireAfterWrite().ifPresent(policy -> policy.setExpiresAfter(expireAfterMillis, TimeUnit.MILLISECONDS)); + cache.policy().eviction().ifPresent(policy -> policy.setMaximum(getMaxEntries())); + } + return updatedCache; Review Comment: Simplify it once the cache config cannot be updated. ########## server/src/main/java/org/apache/cassandra/sidecar/accesscontrol/AuthCache.java: ########## @@ -0,0 +1,214 @@ +/* + * 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.accesscontrol; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.google.common.util.concurrent.Uninterruptibles; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; + +/** + * {@link AuthCache} caches information needed for authenticating sidecar users. + * + * @param <K> key stored in cache for retrieving value + * @param <V> value cached + * + * for {@link IdentityRoleCache} key will be String representing identity extracted from certificate and value will + * be String representing Cassandra role associated with the identity + */ +public abstract class AuthCache<K, V> +{ + protected static final Logger LOGGER = LoggerFactory.getLogger(AuthCache.class); + protected final String name; + protected final boolean enabled; + protected final Function<K, V> loadFunction; + protected final Supplier<Map<K, V>> bulkLoadFunction; + protected final int warmingRetries; + protected final long warmingRetryIntervalInMillis; + + protected long expireAfterMillis; + protected long maxEntries; + + protected volatile LoadingCache<K, V> cache; + + protected AuthCache(String name, + boolean enabled, + Function<K, V> loadFunction, + Supplier<Map<K, V>> bulkLoadFunction, + int warmingRetries, + long warmingRetryIntervalInMillis, + long expireAfterMillis, + long maxEntries) + { + this.name = name; + this.enabled = enabled; + this.loadFunction = loadFunction; + this.bulkLoadFunction = bulkLoadFunction; + this.warmingRetries = warmingRetries; + this.warmingRetryIntervalInMillis = warmingRetryIntervalInMillis; + this.expireAfterMillis = expireAfterMillis; + this.maxEntries = maxEntries; + this.cache = initCache(null); + } + + public void setExpireAfterMillis(long expireAfterMillis) + { + if (enabled && this.expireAfterMillis != expireAfterMillis) + { + synchronized (this) + { + if (this.expireAfterMillis == expireAfterMillis) + { + return; + } + this.expireAfterMillis = expireAfterMillis; + cache = initCache(cache); + } + } + } + + public long getExpireAfterMillis() + { + return this.expireAfterMillis; + } + + public void setMaxEntries(long maxEntries) + { + if (enabled && this.maxEntries != maxEntries) + { + synchronized (this) + { + if (this.maxEntries == maxEntries) + { + return; + } + this.maxEntries = maxEntries; + cache = initCache(cache); + } + } + } + + public long getMaxEntries() + { + return this.maxEntries; + } + + /** + * Retrieve a value from the cache. Will call {@link LoadingCache#get(Object)} which will + * "load" the value if it's not present, thus populating the key. + * @param k key + * @return The current value of {@code K} if cached or loaded. + * + * See {@link LoadingCache#get(Object)} for possible exceptions. + */ + public V get(K k) + { + if (cache == null) + { + return loadFunction.apply(k); + } + return cache.get(k); + } + + /** + * Retrieve all cached entries. Will call {@link LoadingCache#asMap()} which does not trigger "load". + * @return a map of cached key-value pairs + */ + public Map<K, V> getAll() + { + if (cache == null) + { + return Collections.emptyMap(); + } + return Collections.unmodifiableMap(cache.asMap()); + } Review Comment: The implementation conflicting with `get(k)` When the cache is disabled, `get(k)` returns something, but `getAll()` returns empty map. Can you make it consistent? Either call `bulkLoadFunction` in this method or return null in `get(k)` ########## server/src/main/java/org/apache/cassandra/sidecar/accesscontrol/IdentityRoleCache.java: ########## @@ -0,0 +1,57 @@ +/* + * 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.accesscontrol; + +import org.apache.cassandra.sidecar.config.CacheConfiguration; +import org.apache.cassandra.sidecar.db.SystemAuthDatabaseAccessor; + +/** + * Caches entries from identity_to_role table. identity_to_role table maps valid certificate identity to Cassandra + * role the identity holds. identity_to_role table is created in Cassandra versions 5.x and above + */ +public class IdentityRoleCache extends AuthCache<String, String> +{ + protected static final String NAME = "identity_to_role_cache"; + protected final SystemAuthDatabaseAccessor systemAuthDatabaseAccessor; + + public IdentityRoleCache(CacheConfiguration config, + SystemAuthDatabaseAccessor systemAuthDatabaseAccessor) + { + super(NAME, + config.enabled(), + systemAuthDatabaseAccessor::findRoleFromIdentity, + systemAuthDatabaseAccessor::findAllIdentityRoles, + config.warmingRetries(), + 2000, + config.expireAfterAccessMillis(), + config.maximumSize()); + this.systemAuthDatabaseAccessor = systemAuthDatabaseAccessor; + } + + public boolean contains(String identity) Review Comment: Plz rename to `containsKey`. The method is to check the existence of a key. In the context of map-like data structure, the `contains` can also mean the value existence check. Rename to `containsKey` to be clear. ########## server/src/main/java/org/apache/cassandra/sidecar/accesscontrol/authentication/CassandraIdentityExtractor.java: ########## @@ -0,0 +1,65 @@ +/* + * 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.accesscontrol.authentication; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import io.vertx.ext.auth.authentication.CertificateCredentials; +import io.vertx.ext.auth.authentication.CredentialValidationException; +import io.vertx.ext.auth.mtls.impl.SpiffeIdentityExtractor; +import org.apache.cassandra.sidecar.accesscontrol.IdentityRoleCache; + +/** + * {@link CassandraIdentityExtractor} verifies SPIFFE identities extracted from certificate are mapped to a valid + * role in Cassandra. + */ +public class CassandraIdentityExtractor extends SpiffeIdentityExtractor +{ + protected final IdentityRoleCache identityRoleCache; + protected final Set<String> adminIdentities = new HashSet<>(); + + public CassandraIdentityExtractor(IdentityRoleCache identityRoleCache, + Set<String> adminIdentities) + { + this.identityRoleCache = identityRoleCache; + this.adminIdentities.addAll(adminIdentities); + } + + public List<String> validIdentities(CertificateCredentials certificateCredentials) throws CredentialValidationException Review Comment: Add `@Override` ########## server/src/main/java/org/apache/cassandra/sidecar/config/yaml/AuthenticatorsConfigurationImpl.java: ########## @@ -0,0 +1,55 @@ +/* + * 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.config.yaml; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.cassandra.sidecar.config.AuthenticatorsConfiguration; +import org.apache.cassandra.sidecar.config.MutualTlsAuthenticatorConfiguration; + +/** + * Encapsulates configuration needed to support multiple authenticators in Sidecar. Review Comment: ```suggestion * {@inheritDoc} ``` ########## vertx-auth-mtls/src/main/java/io/vertx/ext/auth/mtls/CertificateIdentityExtractor.java: ########## @@ -40,8 +42,8 @@ public interface CertificateIdentityExtractor * </ul> * * @param certificateCredentials certificate chain of user that is already verified - * @return {@code String} identity string extracted from certificate, uniquely represents client + * @return list of valid identities extracted from certificate. * @throws CredentialValidationException when a valid identity cannot be extracted from certificate chain. */ - String validIdentity(CertificateCredentials certificateCredentials) throws CredentialValidationException; + List<String> validIdentities(CertificateCredentials certificateCredentials) throws CredentialValidationException; Review Comment: What is the reason for the interface change? ########## server/src/main/java/org/apache/cassandra/sidecar/db/schema/SystemAuthSchema.java: ########## @@ -0,0 +1,107 @@ +/* + * 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.db.schema; + +import com.datastax.driver.core.KeyspaceMetadata; +import com.datastax.driver.core.Metadata; +import com.datastax.driver.core.PreparedStatement; +import com.datastax.driver.core.Session; +import org.jetbrains.annotations.NotNull; + +/** + * Schema for getting information stored in system_auth keyspace. + */ +public class SystemAuthSchema extends TableSchema +{ + private static final String IDENTITY_TO_ROLE_TABLE = "identity_to_role"; + PreparedStatement selectRoleFromIdentity; + PreparedStatement getAllRolesAndIdentities; + + protected String keyspaceName() + { + return "system_auth"; + } + + @Override + protected void prepareStatements(@NotNull Session session) + { + selectRoleFromIdentity = prepare(selectRoleFromIdentity, + session, + CqlLiterals.selectRoleFromIdentity()); + + getAllRolesAndIdentities = prepare(getAllRolesAndIdentities, + session, + CqlLiterals.getAllRolesAndIdentities()); + } + + protected String tableName() + { + throw new UnsupportedOperationException("SystemAuthSchema supports reading information from multiple " + + "tables in system_auth keyspace"); + } + + @Override + protected boolean exists(@NotNull Metadata metadata) Review Comment: Is there integration test for the scenario when connecting to cassandra version, say 4.0, and the table does not exist. ########## server/src/main/dist/conf/sidecar.yaml: ########## @@ -136,6 +136,22 @@ vertx: # path: "path/to/truststore.p12" # password: password +access_control: + enabled: false + authenticators: + mtls_authenticator: + enabled: false + certificate_validator: io.vertx.ext.auth.mtls.impl.AllowAllCertificateValidator + certificate_identity_extractor: org.apache.cassandra.sidecar.accesscontrol.authentication.CassandraIdentityExtractor + admin_identities: +# - spiffe://authorized/admin/identities + permission_cache: + enabled: true + expire_after_access_millis: 300000 + maximum_size: 10000 + warming_retries: 5 + warming_retry_interval_millis: 2000 + Review Comment: It would be nice to add comments to describe the configurations. For example, https://github.com/apache/cassandra/blob/7feb03e75b7249a8f86023713b28471a0fb6d309/conf/cassandra.yaml#L185-L219 The yaml file is operator-oriented. It would be nice for them to learn the feature without navigating through code. ########## server/src/main/java/org/apache/cassandra/sidecar/accesscontrol/AuthCache.java: ########## @@ -0,0 +1,214 @@ +/* + * 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.accesscontrol; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.google.common.util.concurrent.Uninterruptibles; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; + +/** + * {@link AuthCache} caches information needed for authenticating sidecar users. + * + * @param <K> key stored in cache for retrieving value + * @param <V> value cached + * + * for {@link IdentityRoleCache} key will be String representing identity extracted from certificate and value will + * be String representing Cassandra role associated with the identity + */ +public abstract class AuthCache<K, V> +{ + protected static final Logger LOGGER = LoggerFactory.getLogger(AuthCache.class); + protected final String name; + protected final boolean enabled; + protected final Function<K, V> loadFunction; + protected final Supplier<Map<K, V>> bulkLoadFunction; + protected final int warmingRetries; + protected final long warmingRetryIntervalInMillis; + + protected long expireAfterMillis; + protected long maxEntries; + + protected volatile LoadingCache<K, V> cache; + + protected AuthCache(String name, + boolean enabled, + Function<K, V> loadFunction, + Supplier<Map<K, V>> bulkLoadFunction, + int warmingRetries, + long warmingRetryIntervalInMillis, + long expireAfterMillis, + long maxEntries) + { + this.name = name; + this.enabled = enabled; + this.loadFunction = loadFunction; + this.bulkLoadFunction = bulkLoadFunction; + this.warmingRetries = warmingRetries; + this.warmingRetryIntervalInMillis = warmingRetryIntervalInMillis; + this.expireAfterMillis = expireAfterMillis; + this.maxEntries = maxEntries; + this.cache = initCache(null); + } + + public void setExpireAfterMillis(long expireAfterMillis) + { + if (enabled && this.expireAfterMillis != expireAfterMillis) + { + synchronized (this) + { + if (this.expireAfterMillis == expireAfterMillis) + { + return; + } + this.expireAfterMillis = expireAfterMillis; + cache = initCache(cache); + } + } + } + + public long getExpireAfterMillis() + { + return this.expireAfterMillis; + } Review Comment: The method is not used. Its value should be retrieved from `CacheConfiguration`. Maybe remove the method. ########## server/src/main/java/org/apache/cassandra/sidecar/accesscontrol/AuthCache.java: ########## @@ -0,0 +1,214 @@ +/* + * 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.accesscontrol; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.google.common.util.concurrent.Uninterruptibles; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; + +/** + * {@link AuthCache} caches information needed for authenticating sidecar users. + * + * @param <K> key stored in cache for retrieving value + * @param <V> value cached + * + * for {@link IdentityRoleCache} key will be String representing identity extracted from certificate and value will + * be String representing Cassandra role associated with the identity + */ +public abstract class AuthCache<K, V> +{ + protected static final Logger LOGGER = LoggerFactory.getLogger(AuthCache.class); + protected final String name; + protected final boolean enabled; + protected final Function<K, V> loadFunction; + protected final Supplier<Map<K, V>> bulkLoadFunction; + protected final int warmingRetries; + protected final long warmingRetryIntervalInMillis; + + protected long expireAfterMillis; + protected long maxEntries; + + protected volatile LoadingCache<K, V> cache; + + protected AuthCache(String name, + boolean enabled, + Function<K, V> loadFunction, + Supplier<Map<K, V>> bulkLoadFunction, + int warmingRetries, + long warmingRetryIntervalInMillis, + long expireAfterMillis, + long maxEntries) Review Comment: How about just passing cacheConfig as input? Making a more succinct version ```java protected AuthCache(String name, Function<K, V> loadFunction, Supplier<Map<K, V>> bulkLoadFunction, CacheConfiguration cacheConfiguration) ``` ########## server/src/main/java/org/apache/cassandra/sidecar/accesscontrol/AuthCache.java: ########## @@ -0,0 +1,214 @@ +/* + * 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.accesscontrol; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.google.common.util.concurrent.Uninterruptibles; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; + +/** + * {@link AuthCache} caches information needed for authenticating sidecar users. + * + * @param <K> key stored in cache for retrieving value + * @param <V> value cached + * + * for {@link IdentityRoleCache} key will be String representing identity extracted from certificate and value will + * be String representing Cassandra role associated with the identity + */ +public abstract class AuthCache<K, V> +{ + protected static final Logger LOGGER = LoggerFactory.getLogger(AuthCache.class); + protected final String name; + protected final boolean enabled; + protected final Function<K, V> loadFunction; + protected final Supplier<Map<K, V>> bulkLoadFunction; + protected final int warmingRetries; + protected final long warmingRetryIntervalInMillis; + + protected long expireAfterMillis; + protected long maxEntries; + + protected volatile LoadingCache<K, V> cache; + + protected AuthCache(String name, + boolean enabled, + Function<K, V> loadFunction, + Supplier<Map<K, V>> bulkLoadFunction, + int warmingRetries, + long warmingRetryIntervalInMillis, + long expireAfterMillis, + long maxEntries) + { + this.name = name; + this.enabled = enabled; + this.loadFunction = loadFunction; + this.bulkLoadFunction = bulkLoadFunction; + this.warmingRetries = warmingRetries; + this.warmingRetryIntervalInMillis = warmingRetryIntervalInMillis; + this.expireAfterMillis = expireAfterMillis; + this.maxEntries = maxEntries; + this.cache = initCache(null); + } + + public void setExpireAfterMillis(long expireAfterMillis) + { + if (enabled && this.expireAfterMillis != expireAfterMillis) + { + synchronized (this) + { + if (this.expireAfterMillis == expireAfterMillis) + { + return; + } + this.expireAfterMillis = expireAfterMillis; + cache = initCache(cache); + } + } + } + + public long getExpireAfterMillis() + { + return this.expireAfterMillis; + } + + public void setMaxEntries(long maxEntries) + { + if (enabled && this.maxEntries != maxEntries) + { + synchronized (this) + { + if (this.maxEntries == maxEntries) + { + return; + } + this.maxEntries = maxEntries; + cache = initCache(cache); + } + } + } + + public long getMaxEntries() + { + return this.maxEntries; + } + + /** + * Retrieve a value from the cache. Will call {@link LoadingCache#get(Object)} which will + * "load" the value if it's not present, thus populating the key. + * @param k key + * @return The current value of {@code K} if cached or loaded. + * + * See {@link LoadingCache#get(Object)} for possible exceptions. + */ + public V get(K k) + { + if (cache == null) Review Comment: It is probably easier to understand with the condition, `if (!enabled)` ########## server/src/main/java/org/apache/cassandra/sidecar/accesscontrol/AuthCache.java: ########## @@ -0,0 +1,214 @@ +/* + * 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.accesscontrol; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.google.common.util.concurrent.Uninterruptibles; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; + +/** + * {@link AuthCache} caches information needed for authenticating sidecar users. + * + * @param <K> key stored in cache for retrieving value + * @param <V> value cached + * + * for {@link IdentityRoleCache} key will be String representing identity extracted from certificate and value will + * be String representing Cassandra role associated with the identity + */ +public abstract class AuthCache<K, V> +{ + protected static final Logger LOGGER = LoggerFactory.getLogger(AuthCache.class); + protected final String name; + protected final boolean enabled; + protected final Function<K, V> loadFunction; + protected final Supplier<Map<K, V>> bulkLoadFunction; + protected final int warmingRetries; + protected final long warmingRetryIntervalInMillis; + + protected long expireAfterMillis; + protected long maxEntries; + + protected volatile LoadingCache<K, V> cache; + + protected AuthCache(String name, + boolean enabled, + Function<K, V> loadFunction, + Supplier<Map<K, V>> bulkLoadFunction, + int warmingRetries, + long warmingRetryIntervalInMillis, + long expireAfterMillis, + long maxEntries) + { + this.name = name; + this.enabled = enabled; + this.loadFunction = loadFunction; + this.bulkLoadFunction = bulkLoadFunction; + this.warmingRetries = warmingRetries; + this.warmingRetryIntervalInMillis = warmingRetryIntervalInMillis; + this.expireAfterMillis = expireAfterMillis; + this.maxEntries = maxEntries; + this.cache = initCache(null); + } + + public void setExpireAfterMillis(long expireAfterMillis) + { + if (enabled && this.expireAfterMillis != expireAfterMillis) + { + synchronized (this) + { + if (this.expireAfterMillis == expireAfterMillis) + { + return; + } + this.expireAfterMillis = expireAfterMillis; + cache = initCache(cache); + } + } + } + + public long getExpireAfterMillis() + { + return this.expireAfterMillis; + } + + public void setMaxEntries(long maxEntries) + { + if (enabled && this.maxEntries != maxEntries) + { + synchronized (this) + { + if (this.maxEntries == maxEntries) + { + return; + } + this.maxEntries = maxEntries; + cache = initCache(cache); + } + } + } + + public long getMaxEntries() + { + return this.maxEntries; + } + + /** + * Retrieve a value from the cache. Will call {@link LoadingCache#get(Object)} which will + * "load" the value if it's not present, thus populating the key. Review Comment: Write javadoc to explain the behavior of the method when cache is disabled. ########## server/src/main/java/org/apache/cassandra/sidecar/accesscontrol/AuthCache.java: ########## @@ -0,0 +1,214 @@ +/* + * 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.accesscontrol; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.google.common.util.concurrent.Uninterruptibles; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; + +/** + * {@link AuthCache} caches information needed for authenticating sidecar users. + * + * @param <K> key stored in cache for retrieving value + * @param <V> value cached + * + * for {@link IdentityRoleCache} key will be String representing identity extracted from certificate and value will + * be String representing Cassandra role associated with the identity + */ +public abstract class AuthCache<K, V> +{ + protected static final Logger LOGGER = LoggerFactory.getLogger(AuthCache.class); + protected final String name; + protected final boolean enabled; + protected final Function<K, V> loadFunction; + protected final Supplier<Map<K, V>> bulkLoadFunction; + protected final int warmingRetries; + protected final long warmingRetryIntervalInMillis; + + protected long expireAfterMillis; + protected long maxEntries; + + protected volatile LoadingCache<K, V> cache; Review Comment: add comment to explain when cache can be null. ########## server/src/main/java/org/apache/cassandra/sidecar/accesscontrol/AuthCache.java: ########## @@ -0,0 +1,214 @@ +/* + * 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.accesscontrol; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.google.common.util.concurrent.Uninterruptibles; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; + +/** + * {@link AuthCache} caches information needed for authenticating sidecar users. + * + * @param <K> key stored in cache for retrieving value + * @param <V> value cached + * + * for {@link IdentityRoleCache} key will be String representing identity extracted from certificate and value will + * be String representing Cassandra role associated with the identity + */ +public abstract class AuthCache<K, V> +{ + protected static final Logger LOGGER = LoggerFactory.getLogger(AuthCache.class); + protected final String name; + protected final boolean enabled; + protected final Function<K, V> loadFunction; + protected final Supplier<Map<K, V>> bulkLoadFunction; + protected final int warmingRetries; + protected final long warmingRetryIntervalInMillis; + + protected long expireAfterMillis; + protected long maxEntries; + + protected volatile LoadingCache<K, V> cache; + + protected AuthCache(String name, + boolean enabled, + Function<K, V> loadFunction, + Supplier<Map<K, V>> bulkLoadFunction, + int warmingRetries, + long warmingRetryIntervalInMillis, + long expireAfterMillis, + long maxEntries) + { + this.name = name; + this.enabled = enabled; + this.loadFunction = loadFunction; + this.bulkLoadFunction = bulkLoadFunction; + this.warmingRetries = warmingRetries; + this.warmingRetryIntervalInMillis = warmingRetryIntervalInMillis; + this.expireAfterMillis = expireAfterMillis; + this.maxEntries = maxEntries; + this.cache = initCache(null); + } + + public void setExpireAfterMillis(long expireAfterMillis) + { + if (enabled && this.expireAfterMillis != expireAfterMillis) + { + synchronized (this) + { + if (this.expireAfterMillis == expireAfterMillis) + { + return; + } + this.expireAfterMillis = expireAfterMillis; + cache = initCache(cache); + } + } + } + + public long getExpireAfterMillis() + { + return this.expireAfterMillis; + } + + public void setMaxEntries(long maxEntries) + { + if (enabled && this.maxEntries != maxEntries) + { + synchronized (this) + { + if (this.maxEntries == maxEntries) + { + return; + } + this.maxEntries = maxEntries; + cache = initCache(cache); + } + } + } + + public long getMaxEntries() + { + return this.maxEntries; + } Review Comment: Unused methods in prod code. ########## server/src/main/java/org/apache/cassandra/sidecar/accesscontrol/AuthCache.java: ########## @@ -0,0 +1,214 @@ +/* + * 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.accesscontrol; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.google.common.util.concurrent.Uninterruptibles; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; + +/** + * {@link AuthCache} caches information needed for authenticating sidecar users. + * + * @param <K> key stored in cache for retrieving value + * @param <V> value cached + * + * for {@link IdentityRoleCache} key will be String representing identity extracted from certificate and value will + * be String representing Cassandra role associated with the identity + */ +public abstract class AuthCache<K, V> +{ + protected static final Logger LOGGER = LoggerFactory.getLogger(AuthCache.class); + protected final String name; + protected final boolean enabled; + protected final Function<K, V> loadFunction; + protected final Supplier<Map<K, V>> bulkLoadFunction; + protected final int warmingRetries; + protected final long warmingRetryIntervalInMillis; + + protected long expireAfterMillis; + protected long maxEntries; + + protected volatile LoadingCache<K, V> cache; + + protected AuthCache(String name, + boolean enabled, + Function<K, V> loadFunction, + Supplier<Map<K, V>> bulkLoadFunction, + int warmingRetries, + long warmingRetryIntervalInMillis, + long expireAfterMillis, + long maxEntries) + { + this.name = name; + this.enabled = enabled; + this.loadFunction = loadFunction; + this.bulkLoadFunction = bulkLoadFunction; + this.warmingRetries = warmingRetries; + this.warmingRetryIntervalInMillis = warmingRetryIntervalInMillis; + this.expireAfterMillis = expireAfterMillis; + this.maxEntries = maxEntries; + this.cache = initCache(null); + } + + public void setExpireAfterMillis(long expireAfterMillis) + { + if (enabled && this.expireAfterMillis != expireAfterMillis) + { + synchronized (this) + { + if (this.expireAfterMillis == expireAfterMillis) + { + return; + } + this.expireAfterMillis = expireAfterMillis; + cache = initCache(cache); + } + } + } + + public long getExpireAfterMillis() + { + return this.expireAfterMillis; + } + + public void setMaxEntries(long maxEntries) + { + if (enabled && this.maxEntries != maxEntries) + { + synchronized (this) + { + if (this.maxEntries == maxEntries) + { + return; + } + this.maxEntries = maxEntries; + cache = initCache(cache); + } + } + } + + public long getMaxEntries() + { + return this.maxEntries; + } + + /** + * Retrieve a value from the cache. Will call {@link LoadingCache#get(Object)} which will + * "load" the value if it's not present, thus populating the key. + * @param k key + * @return The current value of {@code K} if cached or loaded. + * + * See {@link LoadingCache#get(Object)} for possible exceptions. + */ + public V get(K k) + { + if (cache == null) + { + return loadFunction.apply(k); + } + return cache.get(k); + } + + /** + * Retrieve all cached entries. Will call {@link LoadingCache#asMap()} which does not trigger "load". + * @return a map of cached key-value pairs + */ + public Map<K, V> getAll() + { + if (cache == null) + { + return Collections.emptyMap(); + } + return Collections.unmodifiableMap(cache.asMap()); + } + + // We might need to add support for invalidating cache entries when we support removing auth entries from + // Cassandra auth tables through sidecar + public void invalidate(K k) + { + throw new UnsupportedOperationException("Invalidate functionality is not yet supported for AuthCache"); + } Review Comment: If it is not supported, just remove the method. It can always be introduced later when adding the functionality described in the comment. ########## server/src/main/java/org/apache/cassandra/sidecar/accesscontrol/AuthCache.java: ########## @@ -0,0 +1,214 @@ +/* + * 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.accesscontrol; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.google.common.util.concurrent.Uninterruptibles; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; + +/** + * {@link AuthCache} caches information needed for authenticating sidecar users. + * + * @param <K> key stored in cache for retrieving value + * @param <V> value cached + * + * for {@link IdentityRoleCache} key will be String representing identity extracted from certificate and value will + * be String representing Cassandra role associated with the identity + */ +public abstract class AuthCache<K, V> +{ + protected static final Logger LOGGER = LoggerFactory.getLogger(AuthCache.class); + protected final String name; + protected final boolean enabled; + protected final Function<K, V> loadFunction; + protected final Supplier<Map<K, V>> bulkLoadFunction; + protected final int warmingRetries; + protected final long warmingRetryIntervalInMillis; + + protected long expireAfterMillis; + protected long maxEntries; + + protected volatile LoadingCache<K, V> cache; + + protected AuthCache(String name, + boolean enabled, + Function<K, V> loadFunction, + Supplier<Map<K, V>> bulkLoadFunction, + int warmingRetries, + long warmingRetryIntervalInMillis, + long expireAfterMillis, + long maxEntries) + { + this.name = name; + this.enabled = enabled; + this.loadFunction = loadFunction; + this.bulkLoadFunction = bulkLoadFunction; + this.warmingRetries = warmingRetries; + this.warmingRetryIntervalInMillis = warmingRetryIntervalInMillis; + this.expireAfterMillis = expireAfterMillis; + this.maxEntries = maxEntries; + this.cache = initCache(null); + } + + public void setExpireAfterMillis(long expireAfterMillis) + { + if (enabled && this.expireAfterMillis != expireAfterMillis) + { + synchronized (this) + { + if (this.expireAfterMillis == expireAfterMillis) + { + return; + } + this.expireAfterMillis = expireAfterMillis; + cache = initCache(cache); + } + } + } + + public long getExpireAfterMillis() + { + return this.expireAfterMillis; + } + + public void setMaxEntries(long maxEntries) + { + if (enabled && this.maxEntries != maxEntries) + { + synchronized (this) + { + if (this.maxEntries == maxEntries) + { + return; + } + this.maxEntries = maxEntries; + cache = initCache(cache); + } + } + } + + public long getMaxEntries() + { + return this.maxEntries; + } + + /** + * Retrieve a value from the cache. Will call {@link LoadingCache#get(Object)} which will + * "load" the value if it's not present, thus populating the key. + * @param k key + * @return The current value of {@code K} if cached or loaded. + * + * See {@link LoadingCache#get(Object)} for possible exceptions. + */ + public V get(K k) + { + if (cache == null) + { + return loadFunction.apply(k); + } + return cache.get(k); + } + + /** + * Retrieve all cached entries. Will call {@link LoadingCache#asMap()} which does not trigger "load". + * @return a map of cached key-value pairs + */ + public Map<K, V> getAll() + { + if (cache == null) + { + return Collections.emptyMap(); + } + return Collections.unmodifiableMap(cache.asMap()); + } + + // We might need to add support for invalidating cache entries when we support removing auth entries from + // Cassandra auth tables through sidecar + public void invalidate(K k) + { + throw new UnsupportedOperationException("Invalidate functionality is not yet supported for AuthCache"); + } + + private LoadingCache<K, V> initCache(LoadingCache<K, V> existing) + { + if (!enabled) + return null; + + LoadingCache<K, V> updatedCache; + if (existing == null) + { + updatedCache = Caffeine.newBuilder() + // setting refreshAfterWrite and expireAfterWrite to same value makes sure no stale + // data is fetched after expire time + .refreshAfterWrite(expireAfterMillis, TimeUnit.MILLISECONDS) + .expireAfterWrite(expireAfterMillis, TimeUnit.MILLISECONDS) + .maximumSize(getMaxEntries()) + .build(loadFunction::apply); + } + else + { + updatedCache = cache; + // Always set as mandatory + cache.policy().expireAfterWrite().ifPresent(policy -> policy.setExpiresAfter(expireAfterMillis, TimeUnit.MILLISECONDS)); + cache.policy().eviction().ifPresent(policy -> policy.setMaximum(getMaxEntries())); + } + return updatedCache; + } + + public void warm() + { + if (cache == null) + { + LOGGER.warn("Cache {} not enabled, skipping pre-warming", name); + return; + } + + warm(warmingRetries); + } + + private void warm(int retry) + { + if (retry < 1) + { + LOGGER.warn("Retries exhausted, unexpected error pre warming cache {}", name); + return; + } + try + { + Map<K, V> entries = bulkLoadFunction.get(); + cache.putAll(entries); + } + catch (Exception e) + { + LOGGER.warn("Unexpected error encountered during cache {} pre-warming, ", name, e); + Uninterruptibles.sleepUninterruptibly(warmingRetryIntervalInMillis, TimeUnit.MILLISECONDS); + warm(retry - 1); Review Comment: avoid tail recursion. it can be easily rewrite as a while loop, which is more performant. ########## server/src/main/java/org/apache/cassandra/sidecar/accesscontrol/IdentityRoleCache.java: ########## @@ -0,0 +1,57 @@ +/* + * 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.accesscontrol; + +import org.apache.cassandra.sidecar.config.CacheConfiguration; +import org.apache.cassandra.sidecar.db.SystemAuthDatabaseAccessor; + +/** + * Caches entries from identity_to_role table. identity_to_role table maps valid certificate identity to Cassandra + * role the identity holds. identity_to_role table is created in Cassandra versions 5.x and above + */ +public class IdentityRoleCache extends AuthCache<String, String> Review Comment: Let's rename the class to `IdentityToRoleCache`. It is import to keep "TO" to avoid ambiguity. ########## server/src/main/java/org/apache/cassandra/sidecar/accesscontrol/IdentityRoleCache.java: ########## @@ -0,0 +1,57 @@ +/* + * 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.accesscontrol; + +import org.apache.cassandra.sidecar.config.CacheConfiguration; +import org.apache.cassandra.sidecar.db.SystemAuthDatabaseAccessor; + +/** + * Caches entries from identity_to_role table. identity_to_role table maps valid certificate identity to Cassandra + * role the identity holds. identity_to_role table is created in Cassandra versions 5.x and above + */ +public class IdentityRoleCache extends AuthCache<String, String> +{ + protected static final String NAME = "identity_to_role_cache"; + protected final SystemAuthDatabaseAccessor systemAuthDatabaseAccessor; + + public IdentityRoleCache(CacheConfiguration config, + SystemAuthDatabaseAccessor systemAuthDatabaseAccessor) + { + super(NAME, + config.enabled(), + systemAuthDatabaseAccessor::findRoleFromIdentity, + systemAuthDatabaseAccessor::findAllIdentityRoles, + config.warmingRetries(), + 2000, + config.expireAfterAccessMillis(), + config.maximumSize()); + this.systemAuthDatabaseAccessor = systemAuthDatabaseAccessor; + } + + public boolean contains(String identity) + { + Review Comment: remove the empty line ########## server/src/main/java/org/apache/cassandra/sidecar/accesscontrol/AuthCache.java: ########## @@ -0,0 +1,214 @@ +/* + * 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.accesscontrol; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.google.common.util.concurrent.Uninterruptibles; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; + +/** + * {@link AuthCache} caches information needed for authenticating sidecar users. + * + * @param <K> key stored in cache for retrieving value + * @param <V> value cached + * + * for {@link IdentityRoleCache} key will be String representing identity extracted from certificate and value will + * be String representing Cassandra role associated with the identity + */ +public abstract class AuthCache<K, V> +{ + protected static final Logger LOGGER = LoggerFactory.getLogger(AuthCache.class); + protected final String name; + protected final boolean enabled; + protected final Function<K, V> loadFunction; + protected final Supplier<Map<K, V>> bulkLoadFunction; + protected final int warmingRetries; + protected final long warmingRetryIntervalInMillis; + + protected long expireAfterMillis; + protected long maxEntries; + + protected volatile LoadingCache<K, V> cache; + + protected AuthCache(String name, + boolean enabled, + Function<K, V> loadFunction, + Supplier<Map<K, V>> bulkLoadFunction, + int warmingRetries, + long warmingRetryIntervalInMillis, + long expireAfterMillis, + long maxEntries) + { + this.name = name; + this.enabled = enabled; + this.loadFunction = loadFunction; + this.bulkLoadFunction = bulkLoadFunction; + this.warmingRetries = warmingRetries; + this.warmingRetryIntervalInMillis = warmingRetryIntervalInMillis; + this.expireAfterMillis = expireAfterMillis; + this.maxEntries = maxEntries; + this.cache = initCache(null); + } + + public void setExpireAfterMillis(long expireAfterMillis) + { + if (enabled && this.expireAfterMillis != expireAfterMillis) + { + synchronized (this) + { + if (this.expireAfterMillis == expireAfterMillis) + { + return; + } + this.expireAfterMillis = expireAfterMillis; + cache = initCache(cache); + } + } + } + + public long getExpireAfterMillis() + { + return this.expireAfterMillis; + } + + public void setMaxEntries(long maxEntries) + { + if (enabled && this.maxEntries != maxEntries) + { + synchronized (this) + { + if (this.maxEntries == maxEntries) + { + return; + } + this.maxEntries = maxEntries; + cache = initCache(cache); + } + } + } + + public long getMaxEntries() + { + return this.maxEntries; + } + + /** + * Retrieve a value from the cache. Will call {@link LoadingCache#get(Object)} which will + * "load" the value if it's not present, thus populating the key. + * @param k key + * @return The current value of {@code K} if cached or loaded. + * + * See {@link LoadingCache#get(Object)} for possible exceptions. + */ + public V get(K k) + { + if (cache == null) + { + return loadFunction.apply(k); + } + return cache.get(k); + } + + /** + * Retrieve all cached entries. Will call {@link LoadingCache#asMap()} which does not trigger "load". + * @return a map of cached key-value pairs + */ + public Map<K, V> getAll() + { + if (cache == null) + { + return Collections.emptyMap(); + } + return Collections.unmodifiableMap(cache.asMap()); + } + + // We might need to add support for invalidating cache entries when we support removing auth entries from + // Cassandra auth tables through sidecar + public void invalidate(K k) + { + throw new UnsupportedOperationException("Invalidate functionality is not yet supported for AuthCache"); + } + + private LoadingCache<K, V> initCache(LoadingCache<K, V> existing) + { + if (!enabled) + return null; + + LoadingCache<K, V> updatedCache; + if (existing == null) + { + updatedCache = Caffeine.newBuilder() + // setting refreshAfterWrite and expireAfterWrite to same value makes sure no stale + // data is fetched after expire time + .refreshAfterWrite(expireAfterMillis, TimeUnit.MILLISECONDS) + .expireAfterWrite(expireAfterMillis, TimeUnit.MILLISECONDS) + .maximumSize(getMaxEntries()) + .build(loadFunction::apply); + } + else + { + updatedCache = cache; + // Always set as mandatory + cache.policy().expireAfterWrite().ifPresent(policy -> policy.setExpiresAfter(expireAfterMillis, TimeUnit.MILLISECONDS)); + cache.policy().eviction().ifPresent(policy -> policy.setMaximum(getMaxEntries())); + } + return updatedCache; + } + + public void warm() + { + if (cache == null) + { + LOGGER.warn("Cache {} not enabled, skipping pre-warming", name); Review Comment: Maybe just `info` ########## server/src/main/java/org/apache/cassandra/sidecar/accesscontrol/AuthCacheService.java: ########## @@ -0,0 +1,77 @@ +/* + * 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.accesscontrol; + +import java.util.HashSet; +import java.util.Set; + +import com.google.common.base.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import org.apache.cassandra.sidecar.config.SidecarConfiguration; + +/** + * {@link AuthCacheService} allows for easier maintenance of all {@link AuthCache}. Review Comment: ```suggestion * Manages all {@link AuthCache}. ``` Avoid "easier" as it does not compare to anything else. Hopefully, the simple sentence in the suggestion captures what this class does. ########## server/src/main/java/org/apache/cassandra/sidecar/accesscontrol/IdentityRoleCache.java: ########## @@ -0,0 +1,57 @@ +/* + * 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.accesscontrol; + +import org.apache.cassandra.sidecar.config.CacheConfiguration; +import org.apache.cassandra.sidecar.db.SystemAuthDatabaseAccessor; + +/** + * Caches entries from identity_to_role table. identity_to_role table maps valid certificate identity to Cassandra + * role the identity holds. identity_to_role table is created in Cassandra versions 5.x and above Review Comment: ```suggestion * Caches entries from system_auth.identity_to_role table. The table maps valid certificate identities to Cassandra * roles. identity_to_role table is available since Cassandra versions 5.0 ``` ########## server/src/main/java/org/apache/cassandra/sidecar/accesscontrol/IdentityRoleCache.java: ########## @@ -0,0 +1,57 @@ +/* + * 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.accesscontrol; + +import org.apache.cassandra.sidecar.config.CacheConfiguration; +import org.apache.cassandra.sidecar.db.SystemAuthDatabaseAccessor; + +/** + * Caches entries from identity_to_role table. identity_to_role table maps valid certificate identity to Cassandra + * role the identity holds. identity_to_role table is created in Cassandra versions 5.x and above + */ +public class IdentityRoleCache extends AuthCache<String, String> +{ + protected static final String NAME = "identity_to_role_cache"; + protected final SystemAuthDatabaseAccessor systemAuthDatabaseAccessor; + + public IdentityRoleCache(CacheConfiguration config, + SystemAuthDatabaseAccessor systemAuthDatabaseAccessor) + { + super(NAME, + config.enabled(), + systemAuthDatabaseAccessor::findRoleFromIdentity, + systemAuthDatabaseAccessor::findAllIdentityRoles, + config.warmingRetries(), + 2000, + config.expireAfterAccessMillis(), + config.maximumSize()); + this.systemAuthDatabaseAccessor = systemAuthDatabaseAccessor; + } + + public boolean contains(String identity) + { + + if (cache == null) + { + return false; + } + String role = get(identity); + return role != null && !role.isEmpty(); Review Comment: Should it be an invalid state if the retrieved `role` string is empty? ########## server/src/main/java/org/apache/cassandra/sidecar/accesscontrol/AuthCacheService.java: ########## @@ -0,0 +1,77 @@ +/* + * 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.accesscontrol; + +import java.util.HashSet; +import java.util.Set; + +import com.google.common.base.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import org.apache.cassandra.sidecar.config.SidecarConfiguration; + +/** + * {@link AuthCacheService} allows for easier maintenance of all {@link AuthCache}. + */ +@Singleton +public class AuthCacheService +{ + private static final Logger LOGGER = LoggerFactory.getLogger(AuthCacheService.class); + private final boolean enabled; + private final Set<AuthCache<?, ?>> caches = new HashSet<>(); Review Comment: Using `Set` here? How to retrieve a specific `AuthCache`? hmmm... looks like `AuthCacheService` does not support retrieval, which makes the service quite useless. It does not manage anything. How about removing the class? ########## server/src/main/java/org/apache/cassandra/sidecar/accesscontrol/AuthCacheService.java: ########## @@ -0,0 +1,77 @@ +/* + * 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.accesscontrol; + +import java.util.HashSet; +import java.util.Set; + +import com.google.common.base.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import org.apache.cassandra.sidecar.config.SidecarConfiguration; + +/** + * {@link AuthCacheService} allows for easier maintenance of all {@link AuthCache}. + */ +@Singleton +public class AuthCacheService +{ + private static final Logger LOGGER = LoggerFactory.getLogger(AuthCacheService.class); + private final boolean enabled; + private final Set<AuthCache<?, ?>> caches = new HashSet<>(); + + @Inject + public AuthCacheService(SidecarConfiguration sidecarConfiguration) + { + this.enabled = sidecarConfiguration.accessControlConfiguration().enabled(); + } + + public synchronized void register(AuthCache<?, ?> cache) + { + if (!enabled) + { + return; + } + Preconditions.checkNotNull(cache, "AuthCache can not be null"); + caches.add(cache); + } + + public synchronized void warmCaches() + { + if (!enabled) + { + LOGGER.warn("Access control is disabled in sidecar, hence skipping auth cache warming"); + return; + } + + LOGGER.info("Initializing bulk load of {} auth caches", caches.size()); + for (AuthCache<?, ?> cache : caches) + { + cache.warm(); + } + } + + public synchronized void invalidateCaches() + { + throw new UnsupportedOperationException("Currently invalidate of caches not supported"); + } Review Comment: remove the unsupported method ########## server/src/main/java/org/apache/cassandra/sidecar/config/CacheConfiguration.java: ########## @@ -41,4 +41,14 @@ default boolean enabled() { return true; } + + /** + * @return number of retries for cache warming + */ + int warmingRetries(); Review Comment: How about `warmUpRetries`? `warming` looks too similar to `warning` ########## server/src/main/java/org/apache/cassandra/sidecar/accesscontrol/authentication/MutualTlsAuthenticationHandler.java: ########## @@ -0,0 +1,85 @@ +/* + * 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.accesscontrol.authentication; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.ext.auth.User; +import io.vertx.ext.auth.authentication.CertificateCredentials; +import io.vertx.ext.auth.authentication.CredentialValidationException; +import io.vertx.ext.auth.mtls.MutualTlsAuthentication; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.HttpException; +import io.vertx.ext.web.handler.impl.AuthenticationHandlerImpl; +import org.apache.cassandra.sidecar.concurrent.ExecutorPools; + +/** + * Handler for verifying user certificates for Mutual TLS authentication. {@link MutualTlsAuthenticationHandler} can be + * chained with other {@link io.vertx.ext.web.handler.AuthenticationHandler} implementations. + */ +public class MutualTlsAuthenticationHandler extends AuthenticationHandlerImpl<MutualTlsAuthentication> +{ + private static final Logger LOGGER = LoggerFactory.getLogger(MutualTlsAuthenticationHandler.class); Review Comment: `LOGGER` not used ########## server/src/main/java/org/apache/cassandra/sidecar/db/schema/SystemAuthSchema.java: ########## @@ -0,0 +1,107 @@ +/* + * 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.db.schema; + +import com.datastax.driver.core.KeyspaceMetadata; +import com.datastax.driver.core.Metadata; +import com.datastax.driver.core.PreparedStatement; +import com.datastax.driver.core.Session; +import org.jetbrains.annotations.NotNull; + +/** + * Schema for getting information stored in system_auth keyspace. + */ +public class SystemAuthSchema extends TableSchema +{ + private static final String IDENTITY_TO_ROLE_TABLE = "identity_to_role"; + PreparedStatement selectRoleFromIdentity; + PreparedStatement getAllRolesAndIdentities; Review Comment: `private final`? ########## server/src/main/java/org/apache/cassandra/sidecar/db/schema/SystemAuthSchema.java: ########## @@ -0,0 +1,107 @@ +/* + * 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.db.schema; + +import com.datastax.driver.core.KeyspaceMetadata; +import com.datastax.driver.core.Metadata; +import com.datastax.driver.core.PreparedStatement; +import com.datastax.driver.core.Session; +import org.jetbrains.annotations.NotNull; + +/** + * Schema for getting information stored in system_auth keyspace. + */ +public class SystemAuthSchema extends TableSchema Review Comment: Can you use `org.apache.cassandra.sidecar.adapters.base.db.schema.ConnectedClientsSchema` as an example? It is similar to the use case here. ########## server/src/main/java/org/apache/cassandra/sidecar/db/SystemAuthDatabaseAccessor.java: ########## @@ -0,0 +1,89 @@ +/* + * 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.db; + +import java.util.HashMap; +import java.util.Map; + +import com.datastax.driver.core.BoundStatement; +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Row; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import org.apache.cassandra.sidecar.common.server.CQLSessionProvider; +import org.apache.cassandra.sidecar.db.schema.SystemAuthSchema; + +/** + * Database Accessor that queries cassandra to get information maintained under system_auth keyspace. + */ +@Singleton +public class SystemAuthDatabaseAccessor extends DatabaseAccessor<SystemAuthSchema> +{ + @Inject + public SystemAuthDatabaseAccessor(SystemAuthSchema systemAuthSchema, + CQLSessionProvider sessionProvider) + { + super(systemAuthSchema, sessionProvider); + } + + /** + * Queries Cassandra for the role associated with given identity. + * + * @param identity Identity of user extracted + * @return the role associated with the given identity in Cassandra + */ + public String findRoleFromIdentity(String identity) + { + if (!tableSchemaPrepared()) + { + throw new IllegalStateException("SystemAuthSchema was not prepared, values can not be retrieved from table"); + } + BoundStatement statement = tableSchema.selectRoleFromIdentity() + .bind(identity); + ResultSet result = execute(statement); + return result.one().getString("role"); + } + + /** + * Queries Cassandra for all rows in identity_to_role table + * + * @return - {@code List<Row>} containing each row in the identity to roles table + */ + public Map<String, String> findAllIdentityRoles() Review Comment: ```suggestion public Map<String, String> findAllIdentitiesToRoles() ``` Adds the word "to" back. And use plural form consistently for both words. ########## server/src/main/java/org/apache/cassandra/sidecar/config/AccessControlConfiguration.java: ########## @@ -0,0 +1,47 @@ +/* + * 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.config; + +import java.util.Set; + +/** + * Configuration stored for controlling user access to Sidecar. + */ +public interface AccessControlConfiguration +{ + /** + * @return whether access control is enabled, if yes requests need to be authenticated/authorized before allowed Review Comment: `authenticated/authorized` ==> `authenticated and authorized` ########## server/src/main/java/org/apache/cassandra/sidecar/accesscontrol/authentication/MutualTlsAuthenticationHandler.java: ########## @@ -0,0 +1,85 @@ +/* + * 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.accesscontrol.authentication; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.ext.auth.User; +import io.vertx.ext.auth.authentication.CertificateCredentials; +import io.vertx.ext.auth.authentication.CredentialValidationException; +import io.vertx.ext.auth.mtls.MutualTlsAuthentication; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.HttpException; +import io.vertx.ext.web.handler.impl.AuthenticationHandlerImpl; +import org.apache.cassandra.sidecar.concurrent.ExecutorPools; + +/** + * Handler for verifying user certificates for Mutual TLS authentication. {@link MutualTlsAuthenticationHandler} can be + * chained with other {@link io.vertx.ext.web.handler.AuthenticationHandler} implementations. + */ +public class MutualTlsAuthenticationHandler extends AuthenticationHandlerImpl<MutualTlsAuthentication> +{ + private static final Logger LOGGER = LoggerFactory.getLogger(MutualTlsAuthenticationHandler.class); + private final ExecutorPools executorPools; + + public MutualTlsAuthenticationHandler(MutualTlsAuthentication authProvider, + ExecutorPools executorPools) + { + super(authProvider); + this.executorPools = executorPools; + } + + @Override + public void authenticate(RoutingContext ctx, Handler<AsyncResult<User>> handler) + { + if (!ctx.request().isSSL()) + { + ctx.response().setStatusCode(HttpResponseStatus.BAD_REQUEST.code()).end(); + return; + } + + CertificateCredentials certificateCredentials = CertificateCredentials.fromHttpRequest(ctx.request()); + executorPools + .service() + .executeBlocking(promise -> { + authProvider.authenticate(certificateCredentials) + .onSuccess(promise::complete) + .onFailure(cause -> { + if (cause instanceof CredentialValidationException) + { + // If credentials are invalid, reject the promise with a BAD_REQUEST error + promise.fail(new HttpException(HttpResponseStatus.BAD_REQUEST.code(), + "Error validating credentials passed")); + } + else + { + // In case of other failures, reject the promise with an UNAUTHORIZED error + promise.fail(new HttpException(HttpResponseStatus.UNAUTHORIZED.code())); + } + }); + }) + .onSuccess(result -> handler.handle(Future.succeededFuture((User) result))) + .onFailure(cause -> handler.handle(Future.failedFuture(cause))); Review Comment: ```suggestion authProvider.authenticate(certificateCredentials) .onSuccess(result -> handler.handle(Future.succeededFuture(result))) .onFailure(cause -> ctx.fail(new HttpException(HttpResponseStatus.UNAUTHORIZED.code(), cause))); ``` 1. `authProvider.authenticate` is already async. No need to dispatch to another executor pool again. 2. All exceptions should be mapped to `UNAUTHORIZED`, IMO. Invalid cert is not a bad request. 3. Do not call the next handler when auth failed. Simply fail the request instead. ########## server/src/main/java/org/apache/cassandra/sidecar/config/yaml/MutualTlsAuthenticatorConfigurationImpl.java: ########## @@ -0,0 +1,86 @@ +/* + * 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.config.yaml; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.vertx.ext.auth.mtls.impl.CertificateValidatorImpl; +import io.vertx.ext.auth.mtls.impl.SpiffeIdentityExtractor; +import org.apache.cassandra.sidecar.config.MutualTlsAuthenticatorConfiguration; + +/** + * Encapsulates configuration needed for creating {@link io.vertx.ext.auth.mtls.impl.MutualTlsAuthenticationImpl} + * authentication provider. Review Comment: ```suggestion * {@inheritDoc} ``` ########## server/src/main/java/org/apache/cassandra/sidecar/config/yaml/AccessControlConfigurationImpl.java: ########## @@ -0,0 +1,107 @@ +/* + * 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.config.yaml; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.cassandra.sidecar.config.AccessControlConfiguration; +import org.apache.cassandra.sidecar.config.AuthenticatorsConfiguration; +import org.apache.cassandra.sidecar.config.CacheConfiguration; + +/** + * Encapsulates configuration needed for creating authenticators in Sidecar. Review Comment: ```suggestion * {@inheritDoc} ``` It is clearer to inherit the doc from interface, rather than writing a new version of it. ########## server/src/main/java/org/apache/cassandra/sidecar/db/schema/SystemAuthSchema.java: ########## @@ -0,0 +1,107 @@ +/* + * 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.db.schema; + +import com.datastax.driver.core.KeyspaceMetadata; +import com.datastax.driver.core.Metadata; +import com.datastax.driver.core.PreparedStatement; +import com.datastax.driver.core.Session; +import org.jetbrains.annotations.NotNull; + +/** + * Schema for getting information stored in system_auth keyspace. + */ +public class SystemAuthSchema extends TableSchema +{ + private static final String IDENTITY_TO_ROLE_TABLE = "identity_to_role"; + PreparedStatement selectRoleFromIdentity; + PreparedStatement getAllRolesAndIdentities; + + protected String keyspaceName() + { + return "system_auth"; + } + + @Override + protected void prepareStatements(@NotNull Session session) + { + selectRoleFromIdentity = prepare(selectRoleFromIdentity, + session, + CqlLiterals.selectRoleFromIdentity()); + + getAllRolesAndIdentities = prepare(getAllRolesAndIdentities, + session, + CqlLiterals.getAllRolesAndIdentities()); + } + + protected String tableName() + { + throw new UnsupportedOperationException("SystemAuthSchema supports reading information from multiple " + + "tables in system_auth keyspace"); Review Comment: There is only one table declared in the class. no? ########## server/src/main/java/org/apache/cassandra/sidecar/db/schema/SystemAuthSchema.java: ########## @@ -0,0 +1,107 @@ +/* + * 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.db.schema; + +import com.datastax.driver.core.KeyspaceMetadata; +import com.datastax.driver.core.Metadata; +import com.datastax.driver.core.PreparedStatement; +import com.datastax.driver.core.Session; +import org.jetbrains.annotations.NotNull; + +/** + * Schema for getting information stored in system_auth keyspace. + */ +public class SystemAuthSchema extends TableSchema +{ + private static final String IDENTITY_TO_ROLE_TABLE = "identity_to_role"; + PreparedStatement selectRoleFromIdentity; + PreparedStatement getAllRolesAndIdentities; + + protected String keyspaceName() + { + return "system_auth"; + } + + @Override + protected void prepareStatements(@NotNull Session session) + { + selectRoleFromIdentity = prepare(selectRoleFromIdentity, + session, + CqlLiterals.selectRoleFromIdentity()); + + getAllRolesAndIdentities = prepare(getAllRolesAndIdentities, + session, + CqlLiterals.getAllRolesAndIdentities()); + } + + protected String tableName() + { + throw new UnsupportedOperationException("SystemAuthSchema supports reading information from multiple " + + "tables in system_auth keyspace"); + } + + @Override + protected boolean exists(@NotNull Metadata metadata) + { + // check tables exists before preparing + KeyspaceMetadata keyspaceMetadata = metadata.getKeyspace(keyspaceName()); + // identity_to_role table exists in Cassandra versions starting 5.x + return keyspaceMetadata != null && keyspaceMetadata.getTable(IDENTITY_TO_ROLE_TABLE) != null; + } + + @Override + protected String createSchemaStatement() + { + return ""; + } Review Comment: It should never attempt to create system table. ########## server/src/main/java/org/apache/cassandra/sidecar/db/schema/SystemAuthSchema.java: ########## @@ -0,0 +1,107 @@ +/* + * 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.db.schema; + +import com.datastax.driver.core.KeyspaceMetadata; +import com.datastax.driver.core.Metadata; +import com.datastax.driver.core.PreparedStatement; +import com.datastax.driver.core.Session; +import org.jetbrains.annotations.NotNull; + +/** + * Schema for getting information stored in system_auth keyspace. + */ +public class SystemAuthSchema extends TableSchema +{ + private static final String IDENTITY_TO_ROLE_TABLE = "identity_to_role"; + PreparedStatement selectRoleFromIdentity; + PreparedStatement getAllRolesAndIdentities; + + protected String keyspaceName() + { + return "system_auth"; + } + + @Override + protected void prepareStatements(@NotNull Session session) + { + selectRoleFromIdentity = prepare(selectRoleFromIdentity, + session, + CqlLiterals.selectRoleFromIdentity()); + + getAllRolesAndIdentities = prepare(getAllRolesAndIdentities, + session, + CqlLiterals.getAllRolesAndIdentities()); + } + + protected String tableName() + { + throw new UnsupportedOperationException("SystemAuthSchema supports reading information from multiple " + + "tables in system_auth keyspace"); + } + + @Override + protected boolean exists(@NotNull Metadata metadata) + { + // check tables exists before preparing + KeyspaceMetadata keyspaceMetadata = metadata.getKeyspace(keyspaceName()); + // identity_to_role table exists in Cassandra versions starting 5.x + return keyspaceMetadata != null && keyspaceMetadata.getTable(IDENTITY_TO_ROLE_TABLE) != null; Review Comment: note that if the method returns false, it will try to create the table, which is not desirable. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]

