-
Notifications
You must be signed in to change notification settings - Fork 36
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
base: trunk
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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.
server-common/src/main/java/org/apache/cassandra/sidecar/db/schema/AbstractSchema.java
Show resolved
Hide resolved
*/ | ||
default Authorization toAuthorization() | ||
{ | ||
return toAuthorization(null); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/Action.java
Outdated
Show resolved
Hide resolved
server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/AllowAllAuthorization.java
Show resolved
Hide resolved
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
.../main/java/org/apache/cassandra/sidecar/acl/authorization/AllowAllAuthorizationProvider.java
Show resolved
Hide resolved
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
...n/java/org/apache/cassandra/sidecar/common/server/exceptions/SchemaUnavailableException.java
Show resolved
Hide resolved
server-common/src/main/java/org/apache/cassandra/sidecar/db/schema/AbstractSchema.java
Show resolved
Hide resolved
*/ | ||
static Action fromName(@NotNull String name) | ||
{ | ||
boolean isWildCard = name.equals(WILDCARD_TOKEN) || name.contains(WILDCARD_PART_DIVIDER_TOKEN); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
server/src/main/java/org/apache/cassandra/sidecar/db/SystemAuthDatabaseAccessor.java
Show resolved
Hide resolved
server/src/main/java/org/apache/cassandra/sidecar/routes/AccessProtectedRouteBuilder.java
Outdated
Show resolved
Hide resolved
server/src/main/java/org/apache/cassandra/sidecar/routes/RingHandler.java
Show resolved
Hide resolved
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
server/src/test/java/org/apache/cassandra/sidecar/acl/authorization/ActionTest.java
Outdated
Show resolved
Hide resolved
/** | ||
* Cassandra actions allowed. | ||
*/ | ||
public class CassandraActions |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
...er/src/main/java/org/apache/cassandra/sidecar/acl/authorization/RoleAuthorizationsCache.java
Outdated
Show resolved
Hide resolved
server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/StandardAction.java
Outdated
Show resolved
Hide resolved
server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/StandardAction.java
Outdated
Show resolved
Hide resolved
Thanks for the review @nvharikrishna and @bbotella appreciate it, addressed your comments. |
3d0bf1c
to
bd34d9e
Compare
Co-authored-by: Raymond Welgosh <[email protected]> Co-authored-by: Saranya Krishnakumar <[email protected]>
bd34d9e
to
d88b2db
Compare
There was a problem hiding this 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); |
There was a problem hiding this comment.
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.
server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/Action.java
Outdated
Show resolved
Hide resolved
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. |
There was a problem hiding this comment.
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.
server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/AdminIdentityResolver.java
Outdated
Show resolved
Hide resolved
server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/AdminIdentityResolver.java
Outdated
Show resolved
Hide resolved
server/src/main/java/org/apache/cassandra/sidecar/db/schema/SystemAuthSchema.java
Show resolved
Hide resolved
server/src/main/java/org/apache/cassandra/sidecar/db/schema/SystemAuthSchema.java
Show resolved
Hide resolved
|
||
route.handler(authorizationHandler); | ||
} | ||
handlers.forEach(route::handler); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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.
server/src/main/java/org/apache/cassandra/sidecar/routes/AccessProtectedRouteBuilder.java
Outdated
Show resolved
Hide resolved
private BiConsumer<RoutingContext, AuthorizationContext> routeGenericVariableConsumer() | ||
{ | ||
return (routingCtx, authZContext) -> { | ||
if (routingCtx.pathParams().containsKey(KEYSPACE)) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this 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, |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this 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()); |
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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(), |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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?
public static final Permission UPDATE_RESTORE_JOB = new WildcardPermission("UPDATE:RESTORE_JOB"); | ||
public static final Permission ABORT_RESTORE_JOB = new WildcardPermission("ABORT:RESTORE_JOB"); |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
// cassandra data related actions | ||
public static final Permission VIEW_SCHEMA = new WildcardPermission("VIEW:SCHEMA"); | ||
public static final Permission VIEW_TOPOLOGY = new WildcardPermission("VIEW:TOPOLOGY"); |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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
There was a problem hiding this 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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
.
public Set<Authorization> requiredAuthorizations() | ||
{ | ||
String resource = VariableAwareResource.DATA_WITH_KEYSPACE.resource(); | ||
return ImmutableSet.of(SidecarPermissions.VIEW_CLUSTER.toAuthorization(resource)); | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same, why allow create
?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
?
No description provided.