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

admin: implement create role API #17089

Merged
merged 2 commits into from
Mar 18, 2024

Conversation

pgellert
Copy link
Contributor

This implements the create_role_handler which will allow the creation of
an empty role to which users can later be assigned to.

Closes https://github.com/redpanda-data/core-internal/issues/1104

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

Copy link
Member

@oleiman oleiman left a comment

Choose a reason for hiding this comment

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

Looks good. Several minor comments, mostly carry-over from POC I think.

FWIW - if you see something on the POC branch that looks very janky, it probably is. I won't be offended if you want to blow some of it up 🙂

src/v/redpanda/admin/security.cc Outdated Show resolved Hide resolved
src/v/redpanda/admin/security.cc Show resolved Hide resolved
tests/rptest/tests/rbac_test.py Outdated Show resolved Hide resolved
tests/rptest/tests/rbac_test.py Outdated Show resolved Hide resolved
src/v/redpanda/admin/security.cc Outdated Show resolved Hide resolved
@pgellert
Copy link
Contributor Author

Force-pushed to address the comments above

@pgellert pgellert requested a review from oleiman March 14, 2024 19:33
Copy link
Member

@oleiman oleiman left a comment

Choose a reason for hiding this comment

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

Couple things on the ducktape side and an error path I missed; otherwise looking good.

Probably a good idea to get one more set of eyes before merging :)

with expect_role_error(RoleErrorCode.MALFORMED_DEF):
self.superuser_admin.create_role(role=role_name0)
res = self.superuser_admin.create_role(role=self.role_name0)
created_role = res.json()['role']
assert created_role == self.role_name0, f"Incorrect create role response: {res.json()}"
Copy link
Member

Choose a reason for hiding this comment

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

Redundant with test_create_user, right?

def test_create_role(self):
res = self.superuser_admin.create_role(role=self.role_name0)
created_role = res.json()['role']
assert created_role == self.role_name0, f"Incorrect create role response: {res.json()}"
Copy link
Member

Choose a reason for hiding this comment

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

Meant to comment before - I think we'd be well served to test misuse of the API here, specifically the malformed body cases. Also idempotency.

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 call. Because I used parse_json_body, we return a 400 with a non-role-specific error if the body is missing / not well-formed json. I think that's fine and not worth the trouble changing (RPK/console should just send us well-formed requests anyway). But maybe we should call this out in the RFC. What do you think?

Copy link
Member

Choose a reason for hiding this comment

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

SGTM 👍

src/v/redpanda/admin/security.cc Outdated Show resolved Hide resolved
}

co_await throw_on_error(*req, err, model::controller_ntp);
co_return ss::json::json_return_type(j_res);
Copy link
Member

Choose a reason for hiding this comment

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

Something else I've been meaning to address - we intended to return 201 (Created) on this endpoint because REST... I don't know how easy that is. It's possible that the seastar machinery is clever enough to glean it from the swagger, but I'm not sure.

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 don't think it's smart enough, unfortunately. It's returning 200 (OK) at the moment.

I spent a bit of time trying to refactor this to allow setting a result code, but it's not straightforward as far as I can tell. We'd have to at least refactor register_route and replicate what seastar is doing for creating a json handler for this to work.

I'd be tempted to keep this returning 200 to be consistent with the other admin API endpoints (and to keep things simple), but let me know what you think.

Copy link
Member

Choose a reason for hiding this comment

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

Bummer. I'll add it my short list of minor API changes. Nothing for you to do, but I'll post an update on the channel today. I have a couple others, also arising from seastar::httpd implementation details

Copy link
Member

Choose a reason for hiding this comment

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

It can be done, but you have to be a bit more manual about it: https://github.com/redpanda-data/redpanda/blob/dev/src/v/pandaproxy/reply.h#L75-L82

Copy link
Member

Choose a reason for hiding this comment

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

Nice. We'd need access to the http_reply object, so we'd want to use register_route_raw_async to register the route IIUC

Copy link
Member

Choose a reason for hiding this comment

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

My mistake, I think this is the correct overload:

template<auth_level required_auth>
void register_route(
ss::httpd::path_description const& path, request_handler_fn handler) {
path.set(
_server._routes,
new handler_impl<required_auth>{*this, std::move(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.

Created an issue to make this change as a follow up: https://github.com/redpanda-data/core-internal/issues/1173

Copy link
Member

@oleiman oleiman left a comment

Choose a reason for hiding this comment

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

This is looking good to me. As noted elsewhere, let's wait for a spot check from someone not me and :shipit:

tests/rptest/tests/rbac_test.py Show resolved Hide resolved
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.

Looks pretty good

src/v/redpanda/admin/security.cc Outdated Show resolved Hide resolved
src/v/redpanda/admin/security.cc Outdated Show resolved Hide resolved
if (err == cluster::errc::role_exists) {
// Idempotency: if the empty role already exists,
// suppress the role_exists error and return success.
auto role = _controller->get_role_store().local().get(role_name);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
auto role = _controller->get_role_store().local().get(role_name);

Copy link
Member

Choose a reason for hiding this comment

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

This is now just duplicating the auto role = ... above.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, I removed this now. I was thinking that it might be worth shortcutting a controller log entry replication by checking the existence of the role before calling create_role. But it does simplify the code a bit if we don't double-check.

Comment on lines +543 to +527
// Idempotency: if the empty role already exists,
// suppress the role_exists error and return success.
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.

Isn't this checked above? Or are you gearing up for something more than an empty role?

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 think this is just double-checking to guard against a race condition with a _controller->get_security_frontend().local().create_role(...) on another node. It's mimicking the create_user_handler:

if (err == cluster::errc::user_exists) {
// Idempotency: if user is same as one that already exists,
// suppress the user_exists error and return success.
const auto& credentials_store
= _controller->get_credential_store().local();
std::optional<security::scram_credential> creds
= credentials_store.get<security::scram_credential>(username);
if (creds.has_value() && match_scram_credential(doc, creds.value())) {
co_return ss::json::json_return_type(ss::json::json_void());
}

}

co_await throw_on_error(*req, err, model::controller_ntp);
co_return ss::json::json_return_type(j_res);
Copy link
Member

Choose a reason for hiding this comment

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

It can be done, but you have to be a bit more manual about it: https://github.com/redpanda-data/redpanda/blob/dev/src/v/pandaproxy/reply.h#L75-L82

@pgellert
Copy link
Contributor Author

Force-pushed to simplify checking for existing roles based on feedback from @BenPope

BenPope
BenPope previously approved these changes Mar 18, 2024
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

Comment on lines 520 to 528
auto err
= co_await _controller->get_security_frontend().local().create_role(
role_name, security::role{}, model::timeout_clock::now() + 5s);

if (err == cluster::errc::role_exists) {
// Idempotency: if the empty role already exists,
// suppress the role_exists error and return success.
auto role = _controller->get_role_store().local().get(role_name);
if (role.has_value() && role.value().members().empty()) {
Copy link
Member

Choose a reason for hiding this comment

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

This is a nitpick, which seems a little clearer to me, and will help with the role is not empty.

Suggested change
auto err
= co_await _controller->get_security_frontend().local().create_role(
role_name, security::role{}, model::timeout_clock::now() + 5s);
if (err == cluster::errc::role_exists) {
// Idempotency: if the empty role already exists,
// suppress the role_exists error and return success.
auto role = _controller->get_role_store().local().get(role_name);
if (role.has_value() && role.value().members().empty()) {
security::role role{};
auto err
= co_await _controller->get_security_frontend().local().create_role(
role_name, role, model::timeout_clock::now() + 5s);
if (err == cluster::errc::role_exists) {
// Idempotency: if the empty role already exists,
// suppress the role_exists error and return success.
if (_controller->get_role_store().local().get(role_name) == role) {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice, I like this. Addressed

This implements the create_role_handler which will allow the creation of
an empty role to which users can later be assigned to.
This tests the create_role_handler of the admin API, testing that an
empty role can successfully be created. Additionally, it verifies that
the validation on incorrect role names works.
Copy link
Contributor

@michael-redpanda michael-redpanda left a comment

Choose a reason for hiding this comment

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

LGTM!

@pgellert pgellert merged commit 036ce1f into redpanda-data:dev Mar 18, 2024
17 checks passed
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