nvharikrishna commented on code in PR #165: URL: https://github.com/apache/cassandra-sidecar/pull/165#discussion_r1912410128
########## server/src/main/java/org/apache/cassandra/sidecar/db/schema/SidecarRolePermissionsSchema.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.schema; + +import com.datastax.driver.core.PreparedStatement; +import com.datastax.driver.core.Session; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import org.apache.cassandra.sidecar.config.SchemaKeyspaceConfiguration; +import org.apache.cassandra.sidecar.config.SidecarConfiguration; +import org.jetbrains.annotations.NotNull; + +/** + * sidecar_internal.role_permissions_v1 table holds custom sidecar permissions that are not stored in Cassandra. + * Permissions are stored against resource. + */ +@Singleton +public class SidecarRolePermissionsSchema extends TableSchema +{ + private static final String ROLE_PERMISSIONS_TABLE = "role_permissions_v1"; + + private final SchemaKeyspaceConfiguration keyspaceConfig; + + private PreparedStatement getAllRolesAndPermissions; + + @Inject + public SidecarRolePermissionsSchema(SidecarConfiguration sidecarConfiguration) + { + this.keyspaceConfig = sidecarConfiguration.serviceConfiguration().schemaKeyspaceConfiguration(); + } + + @Override + protected String tableName() + { + return ROLE_PERMISSIONS_TABLE; + } + + @Override + protected String keyspaceName() + { + return keyspaceConfig.keyspace(); + } + + @Override + protected void prepareStatements(@NotNull Session session) + { + getAllRolesAndPermissions = prepare(getAllRolesAndPermissions, session, CqlLiterals.getAllRolesAndPermissions(keyspaceConfig)); + } + + @Override + protected String createSchemaStatement() + { + return String.format("CREATE TABLE IF NOT EXISTS %s.%s (" + + "role text," + + "resource text," + + "permissions set<text>," + + "PRIMARY KEY(role, resource))", + keyspaceConfig.keyspace(), ROLE_PERMISSIONS_TABLE); + } + + public PreparedStatement getAllRolesAndPermissions() + { + return getAllRolesAndPermissions; + } + + private static class CqlLiterals Review Comment: This private static class is holding only one static method. Any reason to have this class? Can the static method of this class can be part of its parent? ########## server/src/main/java/org/apache/cassandra/sidecar/routes/AccessProtectedRouteBuilder.java: ########## @@ -0,0 +1,216 @@ +/* + * 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.routes; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; + +import io.vertx.core.Handler; +import io.vertx.core.http.HttpMethod; +import io.vertx.ext.auth.authorization.AndAuthorization; +import io.vertx.ext.auth.authorization.Authorization; +import io.vertx.ext.auth.authorization.AuthorizationContext; +import io.vertx.ext.auth.authorization.AuthorizationProvider; +import io.vertx.ext.web.Route; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.BodyHandler; +import org.apache.cassandra.sidecar.acl.authorization.AdminIdentityResolver; +import org.apache.cassandra.sidecar.acl.authorization.AuthorizationWithAdminBypassHandler; +import org.apache.cassandra.sidecar.common.utils.Preconditions; +import org.apache.cassandra.sidecar.config.AccessControlConfiguration; +import org.apache.cassandra.sidecar.exceptions.ConfigurationException; + +import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.KEYSPACE; +import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.TABLE; + +/** + * Builder for building authorized routes + */ +public class AccessProtectedRouteBuilder +{ + private final AccessControlConfiguration accessControlConfiguration; + private final AuthorizationProvider authorizationProvider; + private final AdminIdentityResolver adminIdentityResolver; + + private Router router; + private HttpMethod method; + private String endpoint; + private boolean setBodyHandler; + private final List<Handler<RoutingContext>> handlers = new ArrayList<>(); + + public AccessProtectedRouteBuilder(AccessControlConfiguration accessControlConfiguration, + AuthorizationProvider authorizationProvider, + AdminIdentityResolver adminIdentityResolver) + { + this.accessControlConfiguration = accessControlConfiguration; + this.authorizationProvider = authorizationProvider; + this.adminIdentityResolver = adminIdentityResolver; + } + + /** + * Sets {@link Router} authorized route is built for + * + * @param router Router authorized route is built for + * @return a reference to {@link AccessProtectedRouteBuilder} for chaining + */ + public AccessProtectedRouteBuilder router(Router router) + { + this.router = router; + return this; + } + + /** + * Sets {@link HttpMethod} for route + * + * @param method HttpMethod set for route + * @return a reference to {@link AccessProtectedRouteBuilder} for chaining + */ + public AccessProtectedRouteBuilder method(HttpMethod method) + { + this.method = method; + return this; + } + + /** + * Sets path for route + * + * @param endpoint REST path for route + * @return a reference to {@link AccessProtectedRouteBuilder} for chaining + */ + public AccessProtectedRouteBuilder endpoint(String endpoint) + { + this.endpoint = endpoint; + return this; + } + + /** + * Sets if BodyHandler should be created for the route. + * + * @param setBodyHandler boolean flag indicating if route requires BodyHandler + * @return a reference to {@link AccessProtectedRouteBuilder} for chaining + */ + public AccessProtectedRouteBuilder setBodyHandler(Boolean setBodyHandler) + { + this.setBodyHandler = setBodyHandler; + return this; + } + + /** + * Adds handler to handler chain of route. Handlers are ordered, they are called in order they are set in chain. + * + * @param handler handler for route + * @return a reference to {@link AccessProtectedRouteBuilder} for chaining + */ + public AccessProtectedRouteBuilder handler(Handler<RoutingContext> handler) + { + this.handlers.add(handler); + return this; + } + + /** + * Builds an authorized route. Adds {@link io.vertx.ext.web.handler.AuthorizationHandler} at top of handler + * chain if access control is enabled. + */ + public void build() + { + Preconditions.checkArgument(router != null, "Router must be set"); + Preconditions.checkArgument(method != null, "Http method must be set"); + Preconditions.checkArgument(endpoint != null && !endpoint.isEmpty(), "Endpoint must be set"); + Preconditions.checkArgument(!handlers.isEmpty(), "Handler chain can not be empty"); + + Route route = router.route(method, endpoint); + + // BodyHandler should be at index 0 in handler chain + if (setBodyHandler) + { + route.handler(BodyHandler.create()); + } + + if (accessControlConfiguration.enabled()) + { + // authorization handler added before route specific handler chain + AuthorizationWithAdminBypassHandler authorizationHandler + = new AuthorizationWithAdminBypassHandler(adminIdentityResolver, requiredAuthorization()); + authorizationHandler.addAuthorizationProvider(authorizationProvider); + authorizationHandler.variableConsumer(routeGenericVariableConsumer()); + + route.handler(authorizationHandler); + } + handlers.forEach(route::handler); + } + + private Authorization requiredAuthorization() + { + Set<Authorization> requiredAuthorizations = handlers + .stream() + .filter(handler -> handler instanceof AccessProtected) + .map(handler -> (AccessProtected) handler) + .flatMap(handler -> handler.requiredAuthorizations().stream()) + .collect(Collectors.toSet()); + if (requiredAuthorizations.isEmpty()) + { + throw new ConfigurationException("Authorized route must have authorizations declared"); + } + AndAuthorization andAuthorization = AndAuthorization.create(); + requiredAuthorizations.forEach(andAuthorization::addAuthorization); + return andAuthorization; + } + + private BiConsumer<RoutingContext, AuthorizationContext> routeGenericVariableConsumer() + { + return (routingCtx, authZContext) -> { + if (routingCtx.pathParams().containsKey(KEYSPACE)) + { + authZContext.variables().add(KEYSPACE, routingCtx.pathParam(KEYSPACE)); + } + if (routingCtx.pathParams().containsKey(TABLE)) + { + authZContext.variables().add(TABLE, routingCtx.pathParam(TABLE)); + } + + // TODO remove this hack once VariableAwareExpression bug is fixed + // VariableAwareExpression in vertx-auth-common package has a bug during String.substring() call, hence + // we cannot set resources that do not end in curly braces (e.g. data/keyspace/*) in + // PermissionBasedAuthorizationImpl or WildcardPermissionBasedAuthorizationImpl. hence we treat + // TABLE_WILDCARD as a variable and set it always. This hack allows to read + // resource level permissions that could be set for all tables through data/<keyspace_name>/* + authZContext.variables().add("TABLE_WILDCARD", "*"); + }; + } + + /** + * Creates an instance of {@link AccessProtectedRouteBuilder} for building authorized route. + * + * @param accessControlConfiguration config + * @param authorizationProvider AuthorizationProvider for retrieving user authorizations + * @param adminIdentityResolver AdminIdentityResolver for identifying admin identities + * + * @return instance of {@link AccessProtectedRouteBuilder} + */ + public static AccessProtectedRouteBuilder instance(AccessControlConfiguration accessControlConfiguration, Review Comment: Constructor of AccessProtectedRouteBuilder is public and looks like this method isn't used anywhere. Can it be deleted? ########## server/src/main/java/org/apache/cassandra/sidecar/db/schema/SidecarRolePermissionsSchema.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.schema; + +import com.datastax.driver.core.PreparedStatement; +import com.datastax.driver.core.Session; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import org.apache.cassandra.sidecar.config.SchemaKeyspaceConfiguration; +import org.apache.cassandra.sidecar.config.SidecarConfiguration; +import org.jetbrains.annotations.NotNull; + +/** + * sidecar_internal.role_permissions_v1 table holds custom sidecar permissions that are not stored in Cassandra. + * Permissions are stored against resource. Review Comment: Permissions are stored against both role and resource right (both role and resource are part of primary key)? ########## server/src/main/java/org/apache/cassandra/sidecar/db/schema/SidecarRolePermissionsSchema.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.schema; + +import com.datastax.driver.core.PreparedStatement; +import com.datastax.driver.core.Session; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import org.apache.cassandra.sidecar.config.SchemaKeyspaceConfiguration; +import org.apache.cassandra.sidecar.config.SidecarConfiguration; +import org.jetbrains.annotations.NotNull; + +/** + * sidecar_internal.role_permissions_v1 table holds custom sidecar permissions that are not stored in Cassandra. + * Permissions are stored against resource. + */ +@Singleton +public class SidecarRolePermissionsSchema extends TableSchema +{ + private static final String ROLE_PERMISSIONS_TABLE = "role_permissions_v1"; + + private final SchemaKeyspaceConfiguration keyspaceConfig; + + private PreparedStatement getAllRolesAndPermissions; + + @Inject + public SidecarRolePermissionsSchema(SidecarConfiguration sidecarConfiguration) + { + this.keyspaceConfig = sidecarConfiguration.serviceConfiguration().schemaKeyspaceConfiguration(); + } + + @Override + protected String tableName() + { + return ROLE_PERMISSIONS_TABLE; + } + + @Override + protected String keyspaceName() + { + return keyspaceConfig.keyspace(); + } + + @Override + protected void prepareStatements(@NotNull Session session) + { + getAllRolesAndPermissions = prepare(getAllRolesAndPermissions, session, CqlLiterals.getAllRolesAndPermissions(keyspaceConfig)); Review Comment: These are three `getAllRolesAndPermissions` causing some confusion. Can you consider renaming them? ########## server/src/main/java/org/apache/cassandra/sidecar/db/schema/SystemAuthSchema.java: ########## @@ -32,8 +32,19 @@ public class SystemAuthSchema extends CassandraSystemTableSchema { private static final String IDENTITY_TO_ROLE_TABLE = "identity_to_role"; + + private static final String SELECT_ROLE_FROM_IDENTITY + = "SELECT role FROM system_auth.identity_to_role WHERE identity = ?"; + private static final String GET_ALL_ROLES_AND_IDENTITIES = "SELECT * FROM system_auth.identity_to_role"; + private static final String GET_SUPER_USER_STATUS = "SELECT * FROM system_auth.roles WHERE role = ?"; Review Comment: Looks like only `is_superuser` is required for this case. Shall we select only this column instead of `*`? ########## server/src/main/java/org/apache/cassandra/sidecar/routes/KeyspaceRingHandler.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.routes; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.net.SocketAddress; +import io.vertx.ext.auth.authorization.Authorization; +import io.vertx.ext.web.RoutingContext; +import org.apache.cassandra.sidecar.acl.authorization.SidecarPermissions; +import org.apache.cassandra.sidecar.acl.authorization.VariableAwareResource; +import org.apache.cassandra.sidecar.common.server.StorageOperations; +import org.apache.cassandra.sidecar.common.server.data.Name; +import org.apache.cassandra.sidecar.concurrent.ExecutorPools; +import org.apache.cassandra.sidecar.utils.CassandraInputValidator; +import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher; + +import static org.apache.cassandra.sidecar.utils.HttpExceptions.wrapHttpException; + +/** + * A handler that provides ring information for the Cassandra cluster + */ +@Singleton +public class KeyspaceRingHandler extends AbstractHandler<Name> implements AccessProtected +{ + @Inject + public KeyspaceRingHandler(InstanceMetadataFetcher metadataFetcher, + CassandraInputValidator validator, + ExecutorPools executorPools) + { + super(metadataFetcher, executorPools, validator); + } + + @Override + public Set<Authorization> requiredAuthorizations() + { + List<String> eligibleResources = VariableAwareResource.DATA_WITH_KEYSPACE.expandedResources(); + return Collections.singleton(SidecarPermissions.READ_RING.toAuthorization(eligibleResources)); Review Comment: Doesn't it need authorization/permission for keyspace? ########## server/src/main/java/org/apache/cassandra/sidecar/routes/RingHandler.java: ########## @@ -1,101 +1,38 @@ -/* - * 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.routes; -import org.apache.commons.lang3.StringUtils; +import java.util.Collections; +import java.util.Set; import com.google.inject.Inject; import com.google.inject.Singleton; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.vertx.core.http.HttpServerRequest; -import io.vertx.core.net.SocketAddress; +import io.vertx.ext.auth.authorization.Authorization; import io.vertx.ext.web.RoutingContext; -import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate; -import org.apache.cassandra.sidecar.common.server.StorageOperations; +import org.apache.cassandra.sidecar.acl.authorization.SidecarPermissions; +import org.apache.cassandra.sidecar.acl.authorization.VariableAwareResource; import org.apache.cassandra.sidecar.common.server.data.Name; import org.apache.cassandra.sidecar.concurrent.ExecutorPools; import org.apache.cassandra.sidecar.utils.CassandraInputValidator; import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher; -import static org.apache.cassandra.sidecar.utils.HttpExceptions.cassandraServiceUnavailable; -import static org.apache.cassandra.sidecar.utils.HttpExceptions.wrapHttpException; - /** - * A handler that provides ring information for the Cassandra cluster + * A handler that provides ring information for a specific keyspace for the Cassandra cluster Review Comment: > ring information for a specific keyspace for the Cassandra cluster Needs to be updated. Kespayce specific ring information is handled by KeyspaceRingHandler right? ########## server/src/main/java/org/apache/cassandra/sidecar/routes/KeyspaceSchemaHandler.java: ########## @@ -0,0 +1,154 @@ +/* + * 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.routes; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import com.datastax.driver.core.KeyspaceMetadata; +import com.datastax.driver.core.Metadata; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.Future; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.net.SocketAddress; +import io.vertx.ext.auth.authorization.Authorization; +import io.vertx.ext.auth.authorization.OrAuthorization; +import io.vertx.ext.web.RoutingContext; +import org.apache.cassandra.sidecar.acl.authorization.CassandraPermissions; +import org.apache.cassandra.sidecar.acl.authorization.SidecarPermissions; +import org.apache.cassandra.sidecar.acl.authorization.VariableAwareResource; +import org.apache.cassandra.sidecar.common.response.SchemaResponse; +import org.apache.cassandra.sidecar.common.server.data.Name; +import org.apache.cassandra.sidecar.concurrent.ExecutorPools; +import org.apache.cassandra.sidecar.utils.CassandraInputValidator; +import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher; +import org.apache.cassandra.sidecar.utils.MetadataUtils; + +import static org.apache.cassandra.sidecar.utils.HttpExceptions.wrapHttpException; + +/** + * The {@link SchemaHandler} class handles schema requests Review Comment: Needs to be updated (it says SchemaHandler) ########## server/src/test/java/org/apache/cassandra/sidecar/routes/AccessProtectedRouteBuilderTest.java: ########## @@ -0,0 +1,53 @@ +/* + * 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.routes; + +import org.junit.jupiter.api.Test; + +import io.vertx.core.http.HttpMethod; +import io.vertx.ext.auth.authorization.AuthorizationProvider; +import io.vertx.ext.web.Router; +import org.apache.cassandra.sidecar.acl.authorization.AdminIdentityResolver; +import org.apache.cassandra.sidecar.config.AccessControlConfiguration; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; + +/** + * Test for {@link AccessProtectedRouteBuilder} + */ +public class AccessProtectedRouteBuilderTest Review Comment: Can you add 1) a positive test and 2) build AcessProtectedRouteBuilder without any AccessProtected handler? -- 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]

