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

Role based authorization #17025

Merged
merged 12 commits into from
Mar 21, 2024
Merged

Role based authorization #17025

merged 12 commits into from
Mar 21, 2024

Conversation

oleiman
Copy link
Member

@oleiman oleiman commented Mar 13, 2024

This PR wires roles into the authorization flow in redpanda.

closes https://github.com/redpanda-data/core-internal/issues/1101

Preliminary benchmarks:

test iterations median mad min max allocs tasks inst
role_store_bench.get_member_roles 12917749 49.132ns 0.044ns 48.993ns 49.264ns 0.000 0.000 0.0
role_store_bench.get_member_roles_bare_query 13760304 49.146ns 0.118ns 48.996ns 49.708ns 0.000 0.000 0.0
role_store_bench.user_range_query 9672507 78.019ns 0.068ns 77.803ns 78.220ns 0.000 0.000 0.0
role_store_bench.user_range_query_bare_query 20385250 25.463ns 0.026ns 25.398ns 25.521ns 0.000 0.000 0.0
role_store_bench.remove_role 47 18.703us 658.064ns 18.023us 29.874us 0.000 0.000 0.0
role_store_bench.update_role 47 60.956us 255.915ns 60.310us 61.285us 525.704 0.000 0.0
role_store_bench.put_role 47 24.174us 211.787ns 23.914us 24.997us 1.696 0.000 0.0
role_store_bench.role_authz_512_roles_1Ki_members 28388 5.719us 13.480ns 5.630us 5.734us 4.032 0.000 0.0
role_store_bench.role_authz_256_roles_1Ki_members 31263 2.995us 2.483ns 2.990us 3.002us 4.036 0.000 0.0
role_store_bench.role_authz_128_roles_1Ki_members 34120 1.619us 0.803ns 1.617us 1.621us 3.918 0.000 0.0
role_store_bench.role_authz_64_roles_1Ki_members 34654 921.467ns 2.078ns 912.048ns 924.389ns 3.993 0.000 0.0
role_store_bench.role_authz_64_roles_1Ki_members_4_extra_bindings 32975 1.034us 1.012ns 1.031us 1.035us 3.988 0.000 0.0
role_store_bench.role_authz_64_roles_1Ki_members_8_extra_bindings 31677 1.040us 0.607ns 1.038us 1.044us 3.991 0.000 0.0
role_store_bench.role_authz_64_roles_1Ki_members_16_extra_bindings 28099 1.165us 1.988ns 1.156us 1.167us 3.993 0.000 0.0
role_store_bench.role_authz_64_roles_512_members 70935 924.544ns 4.125ns 915.173ns 928.669ns 3.976 0.000 0.0
role_store_bench.role_authz_64_roles_512_members_deny 72286 691.401ns 0.590ns 690.265ns 693.075ns 3.972 0.000 0.0
role_store_bench.role_authz_8_roles_1Ki_members 36066 299.003ns 0.320ns 298.146ns 299.322ns 4.028 0.000 0.0
role_store_bench.role_authz_empty_store 1774757 95.098ns 0.028ns 94.953ns 95.189ns 2.000 0.000 0.0

Backports Required

  • none - not a bug fix
  • none - this is a backport
  • none - issue does not exist in previous branches
  • none - papercut/not impactful enough to backport
  • v23.3.x
  • v23.2.x

Release Notes

  • none

@oleiman oleiman self-assigned this Mar 13, 2024
@oleiman oleiman force-pushed the rbac/authZ branch 2 times, most recently from 73d6cc8 to 789bfab Compare March 13, 2024 21:22
@oleiman
Copy link
Member Author

oleiman commented Mar 13, 2024

/dt

@oleiman
Copy link
Member Author

oleiman commented Mar 13, 2024

/dt

@vbotbuildovich

This comment was marked as outdated.

@oleiman
Copy link
Member Author

oleiman commented Mar 14, 2024

/dt

@oleiman oleiman force-pushed the rbac/authZ branch 3 times, most recently from 7df47a5 to 8f68758 Compare March 14, 2024 16:53
@oleiman oleiman marked this pull request as ready for review March 14, 2024 16:53
src/v/security/role.cc Outdated Show resolved Hide resolved
src/v/cluster/controller.cc Outdated Show resolved Hide resolved
src/v/security/acl.cc Outdated Show resolved Hide resolved
@oleiman
Copy link
Member Author

oleiman commented Mar 15, 2024

force push review comments and minor commit history cleanups

@oleiman
Copy link
Member Author

oleiman commented Mar 15, 2024

CI Failure:

  • storage_e2e_single_thread_rpunit

Copy link
Member

@dotnwat dotnwat left a comment

Choose a reason for hiding this comment

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

those changes lgtm

Copy link
Member

@BenPope BenPope left a comment

Choose a reason for hiding this comment

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

Looking good.

I wonder if it's worth reducing allocs by changing the interface away from:

auto acls = _store.find(type, resource_name());

src/v/security/role.cc Outdated Show resolved Hide resolved
src/v/security/acl.h Show resolved Hide resolved
src/v/security/authorizer.h Outdated Show resolved Hide resolved
src/v/security/acl.h Outdated Show resolved Hide resolved
@@ -185,19 +220,31 @@ std::vector<std::vector<acl_binding>> acl_store::remove_bindings(
continue;
}

// NOTE(oren): deleted bindings are held in the enclosing scope, so
// we can use a non-owning reference here to avoid copying.
ss::chunked_fifo<acl_principal_view> maybe_roles;
Copy link
Member

Choose a reason for hiding this comment

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

No preference here, more of a question of why not?

Suggested change
ss::chunked_fifo<acl_principal_view> maybe_roles;
chunked_vector<acl_principal_view> maybe_roles;

Copy link
Member Author

Choose a reason for hiding this comment

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

The ambient guidance I've heard is "use chunked_vector where you would use std::vector"; I don't need random access here, so I chose the fifo. That might be an overly legalistic interpretation of the heuristic though. Will peek inside and/or ask around.

Copy link
Member

Choose a reason for hiding this comment

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

I think either is fine. chunked_fifo has items_per_chunk = 128 regardless of sizeof(item). I don't think it will matter much here.

Copy link
Member

@BenPope BenPope Mar 15, 2024

Choose a reason for hiding this comment

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

Actually I'm not sure why we're collecting maybe_role inside of erase_if instead of returning true, is it to deal with dry_run?

Copy link
Member Author

@oleiman oleiman Mar 15, 2024

Choose a reason for hiding this comment

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

is it to deal with dry_run

Partly. Also the erase_if operates on the internal acl_entry container (not the acl_entry_set itself), so there's no obvious way to plug in the role_cache accounting. I agree it's yucky.

Maybe the right thing is to do a little refactoring here. There are plenty of tests to support that effort.

Copy link
Member Author

@oleiman oleiman Mar 15, 2024

Choose a reason for hiding this comment

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

still looking at a refactor, but I did switch to chunked_vector

src/v/security/acl_store.h Show resolved Hide resolved
src/v/security/authorizer.h Outdated Show resolved Hide resolved
src/v/security/authorizer.h Outdated Show resolved Hide resolved
src/v/security/tests/role_store_bench.cc Outdated Show resolved Hide resolved
src/v/security/tests/role_store_bench.cc Outdated Show resolved Hide resolved
@oleiman
Copy link
Member Author

oleiman commented Mar 15, 2024

force push code review comments, notably:

  • some enum safety (?)
  • potentially clarifying cleanup in authorizer::authorized
  • switched to a chunked_vector for tracking deleted roles bindings in remove_bindings

@oleiman
Copy link
Member Author

oleiman commented Mar 15, 2024

I wonder if it's worth reducing allocs by changing the interface away from:

auto acls = _store.find(type, resource_name());

Taking a look this afternoon 👍

@oleiman
Copy link
Member Author

oleiman commented Mar 15, 2024

I wonder if it's worth reducing allocs by changing the interface away from:
auto acls = _store.find(type, resource_name());

Taking a look this afternoon 👍

Actually I'm confused - @BenPope are you referring to the std::vector inside acl_matches?

@BenPope
Copy link
Member

BenPope commented Mar 15, 2024

I wonder if it's worth reducing allocs by changing the interface away from:
auto acls = _store.find(type, resource_name());

Taking a look this afternoon 👍

Actually I'm confused - @BenPope are you referring to the std::vector inside acl_matches?

The return of the vector of matches

@oleiman
Copy link
Member Author

oleiman commented Mar 17, 2024

force push moved some new code to a follow-on PR: #17152

src/v/security/authorizer.h Outdated Show resolved Hide resolved

auto member_roles =
[this, &principal]() -> security::role_store::roles_range {
if (principal.type() == security::principal_type::user) {
Copy link
Member

Choose a reason for hiding this comment

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

I think it makes sense to allow ephemeral users to have role, although other parts of the code don't allow this.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'd like to talk through this a bit. Ticket to follow up: https://github.com/redpanda-data/core-internal/issues/1175

src/v/security/authorizer.h Outdated Show resolved Hide resolved
src/v/security/authorizer.h Outdated Show resolved Hide resolved
src/v/security/authorizer.h Outdated Show resolved Hide resolved
src/v/security/authorizer.h Outdated Show resolved Hide resolved
src/v/security/authorizer.h Outdated Show resolved Hide resolved
@oleiman
Copy link
Member Author

oleiman commented Mar 18, 2024

force push to address CR comments. Shifting code around and collapsing a few lambdas in authorizer::authorized

@oleiman
Copy link
Member Author

oleiman commented Mar 18, 2024

force push correct move semantics to save a copy

Copy link
Contributor

@rockwotj rockwotj left a comment

Choose a reason for hiding this comment

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

I only got part way through, but wanted to leave my feedback. I am mostly asking for comments on new methods/classes that are added, I know this code is pretty lightly doc'd at the moment, but we can at least improve the new stuff.

src/v/security/role.cc Outdated Show resolved Hide resolved
src/v/security/acl.h Show resolved Hide resolved
src/v/security/acl.h Show resolved Hide resolved
src/v/security/authorizer.h Outdated Show resolved Hide resolved
src/v/security/role.h Show resolved Hide resolved
Helper functions for needed conversions between role_members and
acl_principal-like things.

Signed-off-by: Oren Leiman <oren.leiman@redpanda.com>
Non-allocating string_view-based principal-like-thing.

Also introduces acl_principal_base, which is needed for polymorphic usage at
various internal interfaces, and corresponding changes to those interfaces.

A side effect of this is that `acl_principal::name` now returns a string_view,
so we also introduce `acl_principal::adl_name` to preserve the original behavior
for ADL-based serialization.

Note that both acl_principal_view and (now) acl_principal are declared final,
which should allow devirtualization to take place in release mode. As a result,
this change should (and based on subsequent benchmarking does) not carry any
appreciable performance penalty.

Signed-off-by: Oren Leiman <oren.leiman@redpanda.com>
Signed-off-by: Oren Leiman <oren.leiman@redpanda.com>
@oleiman
Copy link
Member Author

oleiman commented Mar 20, 2024

force push modest header documentation improvements

Signed-off-by: Oren Leiman <oren.leiman@redpanda.com>
Non-allocating role-member-like thing for transparent role_store
lookups. Saves an allocation in the authorizer.

Adjusts role_store::roles_for_member to accept either one of these or
the usual role_member struct.

Signed-off-by: Oren Leiman <oren.leiman@redpanda.com>
Cache of role principal names on acl_entry_set to avoid linear-time
lookups for role principals that don't have any bindings. We can do
this because role bindings are strict (no name wildcards).

Also adds a special case so acl_principal.wildcard() returns false for role
type principals. This avoids matching wildcard users against wildcard roles
(which shouldn't exist).

Signed-off-by: Oren Leiman <oren.leiman@redpanda.com>
role_store::roles_for_member returns a boost::iterator_range into
the members store.

For convenience in security::authorizer, expose that type via an alias
in role_store's public interface.

Signed-off-by: Oren Leiman <oren.leiman@redpanda.com>
This does come at a cost, since we have to search the role_store to
confidently assert "no match". However, if the role_store is empty
(or if the authN'ed principal is not found in the store), this cost
should be on the order of a node_hash_map lookup (internal to
role_store::roles_for_member).

See subsequent benchmarks for detail.

Signed-off-by: Oren Leiman <oren.leiman@redpanda.com>
For easier filtering.

Signed-off-by: Oren Leiman <oren.leiman@redpanda.com>
Signed-off-by: Oren Leiman <oren.leiman@redpanda.com>
Single invocations of various role_store calls are more interesting
than many in a loop.

Signed-off-by: Oren Leiman <oren.leiman@redpanda.com>
Notably, the cost of a single authorized operation increases super-
logarithmically but sub-linearly with the number of roles in the store.

Performance degradation in the presence of many member entries is
marginal.

Signed-off-by: Oren Leiman <oren.leiman@redpanda.com>
@oleiman
Copy link
Member Author

oleiman commented Mar 20, 2024

force push authorizer takes a pointer rather than a reference.

@oleiman oleiman requested a review from rockwotj March 21, 2024 04:14
Copy link
Member

@BenPope BenPope left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Contributor

@rockwotj rockwotj left a comment

Choose a reason for hiding this comment

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

A few questions, LGTM

}
// ensure that elements won't outlive the corresponding bindings
// in enclosing scope.
maybe_roles.erase_to_end(maybe_roles.begin());
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this clear?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, my intention was to avoid releasing the memory, but it does look like this decays to fragmented_vector::clear

Copy link
Member Author

Choose a reason for hiding this comment

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

maybe a std::vector is actually a better choice here?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't have a good mental model of the limits here, but it feels safer to alloc a bit more other than crash (if this could grow to an oversized alloc).

Copy link
Member

Choose a reason for hiding this comment

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

Oh, good spot - I hadn't noticed that it took the path of clear.

Copy link
Member Author

Choose a reason for hiding this comment

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

don't have a good mental model of the limits here

Neither do I, but I'd expect the number of iterations to be quite small in the common case. So potential savings is marginal compared to risk of the unknown. Probably just leave it as is for now.

Comment on lines +304 to +305
"Role principal should be non-null and have 'role' type if "
"present");
Copy link
Contributor

Choose a reason for hiding this comment

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

Someday we should either upgrade absl to use the nullability helpers or just create a similar abstraction: https://github.com/abseil/abseil-cpp/blob/master/absl/base/nullability.h

Nothing to do here, just food for thought.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I'll have all the contracts and monads please and thanks. Anyway, good call out, I wasn't aware of that stuff at all. Very cool.

Comment on lines +386 to +392
// NOTE(oren): We know there isn't a match at this point, but I've left
// this as an opt_acl_match to preserve semantics, namely that this
// will return a non-authorized result irrespective of the
// allow_empty_matches flag. On the other hand, switching to an
// empty_match_result _should_ alter semantics but doesn't break any
// tests. Not clear whether this is a bug, intended behavior, a gap
// in test coverage, or a combination.
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 a followup ticket to track this down?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not yet, but I'll make one now. Left the comment there on the off-chance somebody would drop into the PR with ad hoc knowledge, but that didn't happen

Copy link
Member Author

Choose a reason for hiding this comment

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

Comment on lines +342 to +356
auto result = _role_store->roles_for_member(
security::role_member_view::from_principal(
principal))
| std::views::transform([](const auto& e) {
return role::to_principal_view(e);
})
| std::views::transform(
[&user, &check_access, perm](const auto& e) {
return check_access(perm, user, &e);
})
| std::views::filter(
[](const std::optional<auth_result>& r) {
return r.has_value();
})
| std::views::take(1);
Copy link
Contributor

Choose a reason for hiding this comment

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

🔥

@oleiman oleiman merged commit ae4d3b9 into redpanda-data:dev Mar 21, 2024
16 checks passed
@@ -17,17 +17,52 @@
#include <seastar/coroutine/maybe_yield.hh>

#include <absl/container/flat_hash_map.h>
#include <container/fragmented_vector.h>
Copy link
Member

Choose a reason for hiding this comment

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

@oleiman use "..." for internal includes

Copy link
Member Author

Choose a reason for hiding this comment

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

Good spot, thanks. Not sure how that slipped in.

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

Successfully merging this pull request may close these issues.

None yet

5 participants