Skip to content

Commit

Permalink
CASSSIDECAR-161: Add RBAC Authorization support in Sidecar (#165)
Browse files Browse the repository at this point in the history
Patch by Saranya Krishnakumar, Raymond Welgosh; reviewed by Bernardo Botella, Venkata Harikrishna Nukala, Yifan Cai, Francisco Guerrero for CASSSIDECAR-161

Co-authored-by: Raymond Welgosh <[email protected]>
Co-authored-by: Saranya Krishnakumar <[email protected]>
  • Loading branch information
sarankk and rwelgosh authored Jan 20, 2025
1 parent dc2a989 commit 5a19e34
Show file tree
Hide file tree
Showing 83 changed files with 4,326 additions and 418 deletions.
1 change: 1 addition & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
1.0.0
-----
* Add RBAC Authorization support in Sidecar (CASSSIDECAR-161)
* Standardize configuration for duration units (CASSSIDECAR-186)
* Adds sidecar endpoint for node decommissioning operation (CASSANDRASC-151)
* Rename field in ListCdcSegmentsResponse (CASSSIDECAR-184)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ public final class ApiEndpointsV1

public static final String NATIVE = "/native";
public static final String JMX = "/jmx";
public static final String KEYSPACE_PATH_PARAM = ":keyspace";
public static final String TABLE_PATH_PARAM = ":table";
public static final String KEYSPACE = "keyspace";
public static final String TABLE = "table";
public static final String KEYSPACE_PATH_PARAM = ":" + KEYSPACE;
public static final String TABLE_PATH_PARAM = ":" + TABLE;
public static final String SNAPSHOT_PATH_PARAM = ":snapshot";
public static final String COMPONENT_PATH_PARAM = ":component";
public static final String INDEX_PATH_PARAM = ":index";
Expand Down Expand Up @@ -126,6 +128,7 @@ public final class ApiEndpointsV1
public static final String LIST_OPERATIONAL_JOBS_ROUTE = API_V1 + CASSANDRA + OPERATIONAL_JOBS;
public static final String OPERATIONAL_JOB_ROUTE = API_V1 + CASSANDRA + PER_OPERATIONAL_JOB;
public static final String NODE_DECOMMISSION_ROUTE = API_V1 + CASSANDRA + "/operations/decommission";

private ApiEndpointsV1()
{
throw new IllegalStateException(getClass() + " is a constants container and shall not be instantiated");
Expand Down
10 changes: 10 additions & 0 deletions conf/sidecar.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,16 @@ access_control:
#
# other options are, io.vertx.ext.auth.mtls.impl.SpiffeIdentityExtractor.
certificate_identity_extractor: org.apache.cassandra.sidecar.acl.authentication.CassandraIdentityExtractor
authorizer:
# Authorization backend, implementing io.vertx.ext.auth.authorization.AuthorizationProvider; used to
# provide permissions a user holds.
# Out of the box, Cassandra Sidecar provides
# org.apache.cassandra.sidecar.acl.authorization.{AllowAllAuthorizationProvider, RoleBasedAuthorizationProvider}.
#
# - AllowAllAuthorizationProvider allows any action to any user - use it to disable authorization.
# - RoleBasedAuthorizationProvider validates role associated with authenticated user has permission
# for resource it accesses.
- class_name: org.apache.cassandra.sidecar.acl.authorization.AllowAllAuthorizationProvider
# Identities that are authenticated and authorized.
admin_identities:
# - spiffe://authorized/admin/identities
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ protected boolean initializeInternal(@NotNull Session session,
}

prepareStatements(session);
logger.debug("{} is initialized!", this.getClass().getSimpleName());
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
import org.apache.cassandra.sidecar.concurrent.TaskExecutorPool;
import org.apache.cassandra.sidecar.config.CacheConfiguration;
import org.apache.cassandra.sidecar.exceptions.SchemaUnavailableException;
import org.jetbrains.annotations.VisibleForTesting;

import static org.apache.cassandra.sidecar.server.SidecarServerEvents.ON_SIDECAR_SCHEMA_INITIALIZED;
Expand Down Expand Up @@ -163,6 +164,10 @@ protected void warmUp(int availableRetries)
{
cache.putAll(bulkLoadFunction.get());
}
catch (SchemaUnavailableException sue)
{
LOGGER.warn("system_auth schema is unavailable. Skip warming up cache", sue);
}
catch (Exception e)
{
LOGGER.warn("Unexpected error encountered during pre-warming of cache={} ", name, e);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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.acl.authorization;

import java.util.Set;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.apache.cassandra.sidecar.acl.IdentityToRoleCache;
import org.apache.cassandra.sidecar.config.SidecarConfiguration;

/**
* Evaluates if provided identity is an admin identity.
*/
@Singleton
public class AdminIdentityResolver
{
private final IdentityToRoleCache identityToRoleCache;
private final SuperUserCache superUserCache;
private final Set<String> adminIdentities;

@Inject
public AdminIdentityResolver(IdentityToRoleCache identityToRoleCache,
SuperUserCache superUserCache,
SidecarConfiguration sidecarConfiguration)
{
this.identityToRoleCache = identityToRoleCache;
this.superUserCache = superUserCache;
this.adminIdentities = sidecarConfiguration.accessControlConfiguration().adminIdentities();
}

public boolean isAdmin(String identity)
{
if (adminIdentities.contains(identity))
{
return true;
}

String role = identityToRoleCache.get(identity);
if (role == null)
{
return false;
}
// Cassandra superusers have admin privileges
return superUserCache.isSuperUser(role);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* 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.acl.authorization;

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.authorization.Authorization;
import io.vertx.ext.auth.authorization.AuthorizationContext;
import io.vertx.ext.auth.authorization.AuthorizationProvider;

/**
* {@link AuthorizationProvider} implementation to allow all requests regardless of authorizations user holds.
*/
public class AllowAllAuthorizationProvider implements AuthorizationProvider
{
final Authorization authorization;

public AllowAllAuthorizationProvider()
{
// Authorization that always allows
authorization = new Authorization()
{
@Override
public boolean match(AuthorizationContext context)
{
return true;
}

@Override
public boolean verify(Authorization authorization)
{
return true;
}
};
}

/**
* @return unique id representing {@code AllowAllAuthorizationProvider}
*/
@Override
public String getId()
{
return "AllowAll";
}

@Override
public void getAuthorizations(User user, Handler<AsyncResult<Void>> handler)
{
getAuthorizations(user).onComplete(handler);
}

@Override
public Future<Void> getAuthorizations(User user)
{
if (user == null)
{
return Future.failedFuture("User cannot be null");
}

user.authorizations().add(getId(), authorization);
return Future.succeededFuture();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* 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.acl.authorization;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.net.SocketAddress;
import io.vertx.ext.web.RoutingContext;
import org.apache.cassandra.sidecar.common.server.data.Name;
import org.apache.cassandra.sidecar.common.server.data.QualifiedTableName;
import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
import org.apache.cassandra.sidecar.routes.AbstractHandler;
import org.apache.cassandra.sidecar.routes.RoutingContextUtils;
import org.apache.cassandra.sidecar.snapshots.SnapshotPathBuilder;
import org.apache.cassandra.sidecar.utils.CassandraInputValidator;
import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;

/**
* Simple handler that extracts authorization parameters keyspace/table parameters from the path, validates them,
* and then adds them to the context.
*/
@Singleton
public class AuthorizationParameterValidateHandler extends AbstractHandler<QualifiedTableName>
{
private final SnapshotPathBuilder snapshotPathBuilder;

/**
* Constructs a handler with the provided {@code metadataFetcher}
*
* @param metadataFetcher the interface to retrieve instance metadata
* @param executorPools the executor pools for blocking executions
* @param validator a validator instance to validate Cassandra-specific input
*/
@Inject
protected AuthorizationParameterValidateHandler(InstanceMetadataFetcher metadataFetcher,
ExecutorPools executorPools,
CassandraInputValidator validator,
SnapshotPathBuilder snapshotPathBuilder)
{
super(metadataFetcher, executorPools, validator);
this.snapshotPathBuilder = snapshotPathBuilder;
}

@Override
protected void handleInternal(RoutingContext context,
HttpServerRequest httpRequest,
String host,
SocketAddress remoteAddress,
QualifiedTableName qualifiedTableName)
{
RoutingContextUtils.put(context, RoutingContextUtils.SC_QUALIFIED_TABLE_NAME, qualifiedTableName);
}

@Override
protected QualifiedTableName extractParamsOrThrow(RoutingContext context)
{
Name tableName = null;
String tableNameParam = context.pathParam(TABLE_PATH_PARAM);
if (tableNameParam != null)
{
// Remove the tableId for routes that have the tableId as part of the path parameter
tableName = validator.validateTableName(snapshotPathBuilder.maybeRemoveTableId(tableNameParam));
}
return new QualifiedTableName(keyspace(context, false), tableName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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.acl.authorization;

import java.util.List;

import io.netty.handler.codec.http.HttpResponseStatus;
import io.vertx.ext.auth.authorization.Authorization;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.HttpException;
import io.vertx.ext.web.handler.impl.AuthorizationHandlerImpl;

import static org.apache.cassandra.sidecar.utils.AuthUtils.extractIdentities;

/**
* Verifies user has required authorizations. Allows admin identities to bypass authorization checks.
*/
public class AuthorizationWithAdminBypassHandler extends AuthorizationHandlerImpl
{
private final AuthorizationParameterValidateHandler authZParameterValidateHandler;
private final AdminIdentityResolver adminIdentityResolver;

public AuthorizationWithAdminBypassHandler(AuthorizationParameterValidateHandler authZParameterValidateHandler,
AdminIdentityResolver adminIdentityResolver,
Authorization authorization)
{
super(authorization);
this.authZParameterValidateHandler = authZParameterValidateHandler;
this.adminIdentityResolver = adminIdentityResolver;
}

@Override
public void handle(RoutingContext ctx)
{
authZParameterValidateHandler.handle(ctx);
if (ctx.failed()) // failed due to validation
{
return;
}

List<String> identities = extractIdentities(ctx.user());
if (identities.isEmpty())
{
throw new HttpException(HttpResponseStatus.FORBIDDEN.code(), "Client identities are missing");
}

// Admin identities bypass route specific authorization checks
if (identities.stream().anyMatch(adminIdentityResolver::isAdmin))
{
ctx.next();
return;
}

super.handle(ctx);
}
}
Loading

0 comments on commit 5a19e34

Please sign in to comment.