Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CASSSIDECAR-161: Add RBAC Authorization support in Sidecar #165

Open
wants to merge 4 commits into
base: trunk
Choose a base branch
from

Conversation

sarankk
Copy link
Contributor

@sarankk sarankk commented Dec 16, 2024

No description provided.

Copy link
Contributor

@nvharikrishna nvharikrishna left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed it partially. Will continue to review.

*/
default Authorization toAuthorization()
{
return toAuthorization(null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does null as a resource represent? All resources or no resources?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When resource is not set, resource matching is not done. So all resources. But in sidecar, we require resource set for all endpoints. This method is to get the Authorization and set resource at endpoint level like here https://github.com/sarankk/cassandra-sidecar/blob/1d808bc2f2b5c4569099f13954f38019713d09be/server/src/main/java/org/apache/cassandra/sidecar/routes/snapshots/CreateSnapshotHandler.java#L73

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code in the specified link is using the Authorization toAuthorization(String resource); method. It it used only in test cases. Is it required? If yes, then it is good to have expected behavior in the documentation comment.

String role = identityToRoleCache.get(identity);
Boolean isSuperuser = superUserCache.isSuperUser(role);
// Cassandra superusers have admin privileges. For fallback, sidecar configured admin identities are honored
return isSuperuser || adminIdentities.contains(identity);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the superUserCache doesn't have the given role, then it returns null, right? Unboxing null Boolean result NPE.

How does SuperUserCache behaves if the role is null?

Copy link
Contributor Author

@sarankk sarankk Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, will fix this. When role is null we should throw, for RBAC we need role to retrieve authorizations.

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;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is needed, what about extracting the : to its own static variable as well? And maybe refactoring the rest of params?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do that we might have to touch all the other path params. I didn't want to do that.

*/
static Action fromName(@NotNull String name)
{
boolean isWildCard = name.equals(WILDCARD_TOKEN) || name.contains(WILDCARD_PART_DIVIDER_TOKEN);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we want to support partial wildcards? Things like: foo*?

Also, don't quite love that the static variables are part of WildcardAction. If we are going to share them between an interface and one of its implementations, we should extract them to somewhere else.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

foo* is not supported instead we support foo:*. Sure I can add this method to a helper class instead

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have tests for "deny access" scenarios?

Copy link
Contributor Author

@sarankk sarankk Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we do cover them inside RoleBasedAuthorizationIntegrationTest

/**
* Cassandra actions allowed.
*/
public class CassandraActions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cassandra calls these things as permissions. https://cassandra.apache.org/doc/5.0/cassandra/developing/cql/security.html#cql-permissions. Can it be renamed as CassandraPermissions?

Copy link
Contributor Author

@sarankk sarankk Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, since we call SidecarActions named it such for consistency

import static org.apache.cassandra.sidecar.acl.authorization.WildcardAction.WILDCARD_TOKEN;

/**
* Represents an action that can be granted to a user on a resource or across resources.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally it is the permissions granted on a resource but not action. Actions that can be performed on a resource are defined by the resource itself. Granting permission is the authorization to do something. I think you mean it to be a Permission rather than an Action. If yes, can this class be renamed as Permission?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, here I am just breaking down the permission string into 2 parts <action_allowed>:<action_target> and this entire permission string is allowed for a resource. For e.g. UPLOAD:SSTABLE permission allows UPLOAD action on SSTABLE target for resource data/sample_keyspace/sample_table

Copy link
Contributor Author

@sarankk sarankk Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you mean targets of a resource should be combined with the resource itself, it makes sense to me. But then we would be storing resource string differently in Sidecar's role_permissions_v1 table, currently in Cassandra's table we only have data/<ks>/<table>, they do not have permissions listed for snapshot, SSTables

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the observation from the StandardAction and WildcardAction which are constructing PermissionBasedAuthorization & WildcardPermissionBasedAuthorization which are accepting permission as the constructor parameter. For consistency I feel it is apt to call it as Permission.


public Set<Authorization> getAuthorizations(String role)
{
return get(UNIQUE_CACHE_ENTRY).get(role);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get(UNIQUE_CACHE_ENTRY) - trying to fetch entry from cache. I think we need a null check here (in case the cache fails to load entries).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch thanks, updated here

@sarankk
Copy link
Contributor Author

sarankk commented Dec 20, 2024

Thanks for the review @nvharikrishna and @bbotella appreciate it, addressed your comments.

Co-authored-by: Raymond Welgosh <[email protected]>
Co-authored-by: Saranya Krishnakumar <[email protected]>
Copy link
Contributor

@nvharikrishna nvharikrishna left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few more comments.

*/
default Authorization toAuthorization()
{
return toAuthorization(null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code in the specified link is using the Authorization toAuthorization(String resource); method. It it used only in test cases. Is it required? If yes, then it is good to have expected behavior in the documentation comment.

import static org.apache.cassandra.sidecar.acl.authorization.WildcardAction.WILDCARD_TOKEN;

/**
* Represents an action that can be granted to a user on a resource or across resources.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the observation from the StandardAction and WildcardAction which are constructing PermissionBasedAuthorization & WildcardPermissionBasedAuthorization which are accepting permission as the constructor parameter. For consistency I feel it is apt to call it as Permission.


route.handler(authorizationHandler);
}
handlers.forEach(route::handler);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't allow to add a blocking handler. Can it allow adding a blocking handler?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what blocking handler are we talking about? Router in vertx allows taking in only these Handler<RoutingContext>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We generally do not use blockingHandler to register handler. Dispatching blocking actions to executor pool provides better flexibility.

private BiConsumer<RoutingContext, AuthorizationContext> routeGenericVariableConsumer()
{
return (routingCtx, authZContext) -> {
if (routingCtx.pathParams().containsKey(KEYSPACE))
Copy link
Contributor

@nvharikrishna nvharikrishna Dec 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to select/pass only KEYSPACE & TABLE? What if we copy all path params?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment we are only checking authorizations for data resource, we can add them later when they are required I feel.

Copy link

@bbotella bbotella left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for addressing so many comments! Just a couple of nits, but it's a +1 from my end (nb).

super(NAME,
vertx,
executorPools,
k -> loadAuthorizations(systemAuthDatabaseAccessor,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking on some kind of lazy load wrapper, but on second thoughts, that's probably overkill as this is not going to be executed that often.

}

private void verifyAccess(VertxTestContext context, Checkpoint checkpoint, HttpMethod method,
String testRoute, Path clientKeystorePath, boolean expectForbidden)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Please consider changing the flag meaning to a expectAllowed. I know this may be personal preference, but in my head, passing a true "should" mean that the access should be allowed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use this method to check both access allowed cases and access denied cases. Hence named it verifyAccess

Copy link
Contributor

@yifan-c yifan-c left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

submit what I have so far.

{
// when entries in cache are not found, null is returned. We can not add null in Map
Map<String, Set<Authorization>> roleAuthorizations
= Optional.ofNullable(systemAuthDatabaseAccessor.getAllRolesAndPermissions()).orElse(Collections.emptyMap());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

systemAuthDatabaseAccessor.getAllRolesAndPermissions() does not return null. Optional.ofNullable is unnecessary.

if (isSidecarSchemaEnabled)
{
Map<String, Set<Authorization>> sidecarAuthorizations
= Optional.ofNullable(sidecarPermissionsDatabaseAccessor.getAllRolesAndPermissions()).orElse(Collections.emptyMap());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, Optional.ofNullable is unnecessary.

Map<String, Set<Authorization>> roleAuthorizations
= Optional.ofNullable(systemAuthDatabaseAccessor.getAllRolesAndPermissions()).orElse(Collections.emptyMap());

if (isSidecarSchemaEnabled)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this condition is incomplete. You want to check org.apache.cassandra.sidecar.db.schema.SidecarSchema#isInitialized instead, before querying from the sidecar table.

super(NAME,
vertx,
executorPools,
k -> loadAuthorizations(systemAuthDatabaseAccessor,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the input k from the loadFunction is unused? It looks like that both loadFunction and bulkLoadFunction do the bulk load of all authorization. The same Set<Authorization> is duplicated in the cache.


if (identities.isEmpty())
{
return Future.failedFuture(new HttpException(HttpResponseStatus.FORBIDDEN.code(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Hari. HttpException is not the proper exception for this domain. Ideally, HttpException is only thrown from request handlers.
A domain specific exception is preferable here.

public class SidecarPermissions
{
// cassandra cluster related permissions
public static final Permission VIEW_CLUSTER = new WildcardPermission("VIEW:CLUSTER");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VIEW:CLUSTER reads very general and vague. Can you make it more precise?
What does "CLUSTER" mean here?

// SSTable related permissions
public static final Permission UPLOAD_SSTABLE = new WildcardPermission("UPLOAD:SSTABLE");
public static final Permission IMPORT_SSTABLE = new WildcardPermission("IMPORT:SSTABLE");
public static final Permission STREAM_SSTABLE = new WildcardPermission("STREAM:SSTABLE");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about using READ to replace both VIEW and STREAM? Do we need to distinguish between those 2 read operations?

Comment on lines +55 to +56
public static final Permission UPDATE_RESTORE_JOB = new WildcardPermission("UPDATE:RESTORE_JOB");
public static final Permission ABORT_RESTORE_JOB = new WildcardPermission("ABORT:RESTORE_JOB");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about EDIT instead of UPDATE and DELETE instead of ABORT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will change to DELETE, I wanted to reuse action names if possible e.g. UPDATE:SSTABLE any reason for preferring EDIT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The action names are reused. The suggestion is applied to all actions.

I was thinking from the perspective of HTTP methods. Given that it is web service, all interactions are performed via the REST APIs (i.e. http method + resource). I think we should model the permissions from it.

DELETE is directly from the delete method. EDIT is for patch method (as it partially update the entity). Then, UPDATE is for put method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, will update it.

Comment on lines +65 to +67
// cassandra data related actions
public static final Permission VIEW_SCHEMA = new WildcardPermission("VIEW:SCHEMA");
public static final Permission VIEW_TOPOLOGY = new WildcardPermission("VIEW:TOPOLOGY");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SCHEMA and TOPOLOGY is different from CLUSTER? (I do not like CLUSTER, but still curious about the difference here)

public static final Permission VIEW_CDC = new WildcardPermission("VIEW:CDC");

// sidecar operation related permissions
public static final Permission VIEW_TASKS = new WildcardPermission("VIEW:TASKS");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TASKS is too general. It is OPERATIONAL_JOBS

Copy link
Contributor

@yifan-c yifan-c left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good stuff.
Besides the inline comments, I do have one more question. How do operators configures the permissions in the sidecar table?

* A handler that provides ring information for a specific keyspace for the Cassandra cluster
*/
@Singleton
public class KeyspaceRingHandler extends AbstractHandler<Name> implements AccessProtected
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you either drop this class and only have RingHandler? or extend from RingHandler to remove code duplication?

* The {@link KeyspaceSchemaHandler} class handles schema request for a keyspace
*/
@Singleton
public class KeyspaceSchemaHandler extends AbstractHandler<Name> implements AccessProtected
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, please drop this class or extend from SchemaHandler.

Comment on lines +43 to +47
public Set<Authorization> requiredAuthorizations()
{
String resource = VariableAwareResource.DATA_WITH_KEYSPACE.resource();
return ImmutableSet.of(SidecarPermissions.VIEW_CLUSTER.toAuthorization(resource));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method seems to be the only reason that this class exists. Since the requiredAuthorizations is configured statically, maybe extending from RingHandler and only overriding requiredAuthorizations is a more viable option.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have already tried it, it is not possible. since input we take is different between 2 classes Name vs Void

String resource = VariableAwareResource.DATA_WITH_KEYSPACE_TABLE.resource();
Authorization createRestore = SidecarActions.CREATE_RESTORE.toAuthorization(resource);
Authorization viewRestore = SidecarActions.VIEW_RESTORE.toAuthorization(resource);
OrAuthorization createOrView
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any overlap between CREATE and VIEW? For this endpoint, I think only view is required. What is the reasoning of allowing a user with CREATE only permission to view the progress?

Authorization viewRestore = SidecarPermissions.VIEW_RESTORE_JOB.toAuthorization(resource);
OrAuthorization createOrView
= new OrAuthorizationImpl().addAuthorization(createRestore).addAuthorization(viewRestore);
return ImmutableSet.of(createOrView);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, why allow create?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought if someone has create permission for restore job, then they should be able to view summary. I can remove create from update handler but for view it makes sense?

void unrecognizedAuthorizationProviderSet()
{
assertThatThrownBy(() -> configureServer("config/sidecar_unrecognized_authorizer.yaml"))
.hasCauseInstanceOf(RuntimeException.class)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can it throw a dedicated exception instead of RuntimeException?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants