diff --git a/src/v/redpanda/admin_server.cc b/src/v/redpanda/admin_server.cc index c114d9b1b829..b28e68d6c9d5 100644 --- a/src/v/redpanda/admin_server.cc +++ b/src/v/redpanda/admin_server.cc @@ -1666,8 +1666,26 @@ static bool match_scram_credential( } } +bool is_no_op_user_write( + security::credential_store& store, + security::credential_user username, + security::scram_credential credential) { + auto user_opt = store.get(username); + if (user_opt.has_value()) { + return user_opt.value() == credential; + } else { + return false; + } +} + ss::future admin_server::create_user_handler(std::unique_ptr req) { + if (need_redirect_to_leader(model::controller_ntp, _metadata_cache)) { + // In order that we can do a reliably ordered validation of + // the request (and drop no-op requests), run on controller leader; + throw co_await redirect_to_leader(*req, model::controller_ntp); + } + auto doc = parse_json_body(*req); auto credential = parse_scram_credential(doc); @@ -1679,6 +1697,15 @@ admin_server::create_user_handler(std::unique_ptr req) { auto username = security::credential_user(doc["username"].GetString()); + if (is_no_op_user_write( + _controller->get_credential_store().local(), username, credential)) { + vlog( + logger.debug, + "User {} already exists with matching credential", + username); + co_return ss::json::json_return_type(ss::json::json_void()); + } + auto err = co_await _controller->get_security_frontend().local().create_user( username, credential, model::timeout_clock::now() + 5s); @@ -1703,8 +1730,19 @@ admin_server::create_user_handler(std::unique_ptr req) { ss::future admin_server::delete_user_handler(std::unique_ptr req) { + if (need_redirect_to_leader(model::controller_ntp, _metadata_cache)) { + // In order that we can do a reliably ordered validation of + // the request (and drop no-op requests), run on controller leader; + throw co_await redirect_to_leader(*req, model::controller_ntp); + } + auto user = security::credential_user(req->param["user"]); + if (!_controller->get_credential_store().local().contains(user)) { + vlog(logger.debug, "User '{}' already gone during deletion", user); + co_return ss::json::json_return_type(ss::json::json_void()); + } + auto err = co_await _controller->get_security_frontend().local().delete_user( user, model::timeout_clock::now() + 5s); @@ -1719,12 +1757,27 @@ admin_server::delete_user_handler(std::unique_ptr req) { ss::future admin_server::update_user_handler(std::unique_ptr req) { + if (need_redirect_to_leader(model::controller_ntp, _metadata_cache)) { + // In order that we can do a reliably ordered validation of + // the request (and drop no-op requests), run on controller leader; + throw co_await redirect_to_leader(*req, model::controller_ntp); + } + auto user = security::credential_user(req->param["user"]); auto doc = parse_json_body(*req); auto credential = parse_scram_credential(doc); + if (is_no_op_user_write( + _controller->get_credential_store().local(), user, credential)) { + vlog( + logger.debug, + "User {} already exists with matching credential", + user); + co_return ss::json::json_return_type(ss::json::json_void()); + } + auto err = co_await _controller->get_security_frontend().local().update_user( user, credential, model::timeout_clock::now() + 5s);