-
Notifications
You must be signed in to change notification settings - Fork 709
[CORE-15019] Consider group membership for authz checks #29157
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
Changes from 8 commits
20d1a0b
cbc7c84
ee219e8
82d24e3
cf5bcea
eb27943
d03d1c7
8b2e920
f73689c
8d5b860
f3fb23e
372ba79
fd3ac57
728e57d
53d94c1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -166,9 +166,10 @@ auth_result authorizer::authorized( | |
| acl_operation operation, | ||
| const acl_principal& principal, | ||
| const acl_host& host, | ||
| superuser_required superuser_required) const { | ||
| superuser_required superuser_required, | ||
| const chunked_vector<acl_principal>& groups) const { | ||
| auth_result r = do_authorized( | ||
| resource_name, operation, principal, host, superuser_required); | ||
| resource_name, operation, principal, host, superuser_required, groups); | ||
| _probe->record_authz_result( | ||
| r.is_authorized() ? authz_result::allow | ||
| : r.empty_matches ? authz_result::empty | ||
|
|
@@ -182,7 +183,8 @@ auth_result authorizer::do_authorized( | |
| acl_operation operation, | ||
| const acl_principal& principal, | ||
| const acl_host& host, | ||
| superuser_required superuser_required) const { | ||
| superuser_required superuser_required, | ||
| const chunked_vector<acl_principal>&) const { | ||
| auto type = get_resource_type<T>(); | ||
| auto acls = store().find(type, resource_name()); | ||
|
|
||
|
|
@@ -205,30 +207,43 @@ auth_result authorizer::do_authorized( | |
| bool(_allow_empty_matches)); | ||
| } | ||
|
|
||
| chunked_vector<acl_principal_view> effective_principals; | ||
michael-redpanda marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const auto append_roles = [&effective_principals, | ||
| this](const acl_principal& p) { | ||
| std::ranges::copy( | ||
| _role_store->roles_for_member(role_member_view::from_principal(p)) | ||
| | std::views::transform( | ||
| [](const auto& r) { return role::to_principal_view(r); }), | ||
| std::back_inserter(effective_principals)); | ||
| }; | ||
|
|
||
| effective_principals.emplace_back(principal); | ||
|
|
||
| // Only users can be a member of roles, not ephemeral_users | ||
| if (principal.type() == principal_type::user) { | ||
| append_roles(principal); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: There is a potential performance regression here: The collection of roles is now performed eagerly, even if the match would have been on the principal rather than a role.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm that's a fair point... maybe the principal needs to be checked first, then groups, and then build the roles list... wdyt?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've gone ahead and filed CORE-15238 to come back to this - trying to get this into the QE teams hands asap for system testing and this PR goes a long way to getting them started. |
||
| } | ||
|
|
||
| auto check_access = | ||
| [this, &acls, &operation, &host, &resource_name]( | ||
| acl_permission perm, | ||
| const security::acl_principal& user, | ||
| std::optional<const security::acl_principal_base*> role | ||
| = std::nullopt) -> std::optional<auth_result> { | ||
| vassert( | ||
| !role | ||
| || (*role != nullptr && (*role)->type() == principal_type::role), | ||
| "Role principal should be non-null and have 'role' type if " | ||
| "present"); | ||
| const acl_principal_base& to_check = *role.value_or(&user); | ||
| const acl_principal& user, | ||
| acl_principal_view check_principal) -> std::optional<auth_result> { | ||
| bool is_allow = perm == acl_permission::allow; | ||
| std::optional<security::acl_match> entry; | ||
| if (is_allow) { | ||
| entry = acl_any_implied_ops_allowed( | ||
| acls, to_check, host, operation); | ||
| acls, check_principal, host, operation); | ||
| } else { | ||
| entry = acls.find(operation, to_check, host, perm); | ||
| entry = acls.find(operation, check_principal, host, perm); | ||
| } | ||
|
|
||
| if (!entry) { | ||
| return std::nullopt; | ||
| } | ||
| switch (to_check.type()) { | ||
|
|
||
| switch (check_principal.type()) { | ||
| case principal_type::user: | ||
| case principal_type::ephemeral_user: | ||
| return auth_result::acl_match( | ||
|
|
@@ -238,66 +253,29 @@ auth_result authorizer::do_authorized( | |
| case principal_type::group: | ||
| return auth_result::role_acl_match( | ||
| user, | ||
| security::role_name{to_check.name_view()}, | ||
| security::role_name{check_principal.name_view()}, | ||
| host, | ||
| operation, | ||
| resource_name, | ||
| is_allow, | ||
| *entry); | ||
| } | ||
| __builtin_unreachable(); | ||
| std::unreachable(); | ||
michael-redpanda marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }; | ||
|
|
||
| auto check_role_access = | ||
| [this, &principal, &check_access]( | ||
| acl_permission perm, | ||
| const acl_principal& user) -> std::optional<auth_result> { | ||
| switch (principal.type()) { | ||
| case security::principal_type::user: { | ||
| 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); | ||
| return (result.empty() ? std::nullopt : result.front()); | ||
| for (const auto& p : effective_principals) { | ||
| if (auto r = check_access(acl_permission::deny, principal, p); | ||
| r.has_value()) { | ||
| return *r; | ||
| } | ||
| case security::principal_type::ephemeral_user: | ||
| case security::principal_type::role: | ||
| // TODO(GBAC) - CORE-14895 | ||
| case security::principal_type::group: | ||
| return std::nullopt; | ||
| } | ||
| __builtin_unreachable(); | ||
| }; | ||
|
|
||
| if (auto result = check_access(acl_permission::deny, principal); | ||
| result.has_value()) { | ||
| return std::move(result).value(); | ||
| } | ||
|
|
||
| if (auto result = check_role_access(acl_permission::deny, principal); | ||
| result.has_value()) { | ||
| return std::move(result).value(); | ||
| } | ||
|
|
||
| if (auto result = check_access(acl_permission::allow, principal); | ||
| result.has_value()) { | ||
| return std::move(result).value(); | ||
| } | ||
|
|
||
| if (auto result = check_role_access(acl_permission::allow, principal); | ||
| result.has_value()) { | ||
| return std::move(result).value(); | ||
| for (const auto& p : effective_principals) { | ||
| if (auto r = check_access(acl_permission::allow, principal, p); | ||
| r.has_value()) { | ||
| return *r; | ||
| } | ||
| } | ||
|
|
||
| return auth_result::opt_acl_match( | ||
| principal, host, operation, resource_name, std::nullopt); | ||
| } | ||
|
|
@@ -307,42 +285,48 @@ template auth_result authorizer::authorized( | |
| acl_operation, | ||
| const acl_principal&, | ||
| const acl_host&, | ||
| superuser_required) const; | ||
| superuser_required, | ||
| const chunked_vector<acl_principal>&) const; | ||
|
|
||
| template auth_result authorizer::authorized( | ||
| const kafka::group_id&, | ||
| acl_operation, | ||
| const acl_principal&, | ||
| const acl_host&, | ||
| superuser_required) const; | ||
| superuser_required, | ||
| const chunked_vector<acl_principal>& groups) const; | ||
|
|
||
| template auth_result authorizer::authorized( | ||
| const security::acl_cluster_name&, | ||
| acl_operation, | ||
| const acl_principal&, | ||
| const acl_host&, | ||
| superuser_required) const; | ||
| superuser_required, | ||
| const chunked_vector<acl_principal>&) const; | ||
|
|
||
| template auth_result authorizer::authorized( | ||
| const kafka::transactional_id&, | ||
| acl_operation, | ||
| const acl_principal&, | ||
| const acl_host&, | ||
| superuser_required) const; | ||
| superuser_required, | ||
| const chunked_vector<acl_principal>&) const; | ||
|
|
||
| template auth_result authorizer::authorized( | ||
| const pandaproxy::schema_registry::subject&, | ||
| acl_operation, | ||
| const acl_principal&, | ||
| const acl_host&, | ||
| superuser_required) const; | ||
| superuser_required, | ||
| const chunked_vector<acl_principal>&) const; | ||
|
|
||
| template auth_result authorizer::authorized( | ||
| const pandaproxy::schema_registry::registry_resource&, | ||
| acl_operation, | ||
| const acl_principal&, | ||
| const acl_host&, | ||
| superuser_required) const; | ||
| superuser_required, | ||
| const chunked_vector<acl_principal>&) const; | ||
|
|
||
| std::optional<security::acl_match> authorizer::acl_any_implied_ops_allowed( | ||
| const acl_matches& acls, | ||
|
|
||
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.
Not a huge fan of the ordering here, I feel like it should be
principal, groups, ..., but this overload set is tricky.