Files
scylladb/audit/audit_rule.cc
Andrzej Jackowski 65dd103f74 audit: add rule matching and sink helpers
Rule matching must be shared between the preprocessed cache
and the fallback path to avoid divergent semantics.

Introduce audit_sink enum and audit_sink_set bitmask for
routing. Match categories via bitmask, tables and roles via
fnmatch with extended globs. AUTH/ADMIN/DCL bypass table
matching. Empty category or role lists match nothing. Empty
keyspace (e.g. cross-table batches) bypasses table matching
for table-scoped categories. Convert validated sink names
to an audit_sink_set bitmask for routing.

Refs SCYLLADB-1430
2026-05-20 06:55:15 +02:00

211 lines
7.1 KiB
C++

/*
* Copyright (C) 2026-present ScyllaDB
*/
/*
* SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.1
*/
#include "audit/audit_rule.hh"
#include "audit/audit.hh"
#include "utils/rjson.hh"
#include <fmt/ranges.h>
#include <fnmatch.h>
#include <seastar/core/on_internal_error.hh>
namespace audit {
sstring category_to_string(statement_category category) {
switch (category) {
case statement_category::QUERY: return "QUERY";
case statement_category::DML: return "DML";
case statement_category::DDL: return "DDL";
case statement_category::DCL: return "DCL";
case statement_category::AUTH: return "AUTH";
case statement_category::ADMIN: return "ADMIN";
}
return "";
}
static statement_category string_to_category(std::string_view s) {
if (s == "QUERY") return statement_category::QUERY;
if (s == "DML") return statement_category::DML;
if (s == "DDL") return statement_category::DDL;
if (s == "DCL") return statement_category::DCL;
if (s == "AUTH") return statement_category::AUTH;
if (s == "ADMIN") return statement_category::ADMIN;
throw audit_exception(fmt::format(
"Bad configuration: invalid category '{}' in audit rule", s));
}
namespace {
rjson::value string_vec_to_json(const std::vector<sstring>& vec) {
rjson::value arr = rjson::empty_array();
for (const auto& s : vec) {
rjson::push_back(arr, rjson::from_string(s));
}
return arr;
}
std::vector<sstring> json_array_to_string_vec(const rjson::value& arr, const sstring& field_name) {
if (!arr.IsArray()) {
throw audit_exception(fmt::format(
"Bad configuration: '{}' must be a JSON array", field_name));
}
std::vector<sstring> result;
for (const auto& elem : arr.GetArray()) {
if (!elem.IsString()) {
throw audit_exception(fmt::format(
"Bad configuration: '{}' array elements must be strings", field_name));
}
result.emplace_back(rjson::to_string_view(elem));
}
return result;
}
} // anonymous namespace
category_set parse_categories(const std::vector<sstring>& categories) {
category_set result;
for (const auto& cat : categories) {
result.set(string_to_category(cat));
}
return result;
}
void validate_audit_rule(const audit_rule& rule) {
// Sinks: must be non-empty, each must be "table" or "syslog"
if (rule.sinks.empty()) {
throw audit_exception("Bad configuration: 'sinks' must be non-empty in audit rule");
}
for (const auto& sink : rule.sinks) {
if (sink != "table" && sink != "syslog") {
throw audit_exception(fmt::format(
"Bad configuration: invalid sink '{}' in audit rule (must be 'table' or 'syslog')", sink));
}
}
}
std::vector<audit_rule> parse_audit_rules_from_json(const sstring& json_str) {
if (json_str.empty()) {
return {};
}
rjson::value parsed;
try {
parsed = rjson::parse(json_str);
} catch (const rjson::error& e) {
throw audit_exception(fmt::format(
"Bad configuration: failed to parse audit_rules JSON: {}", e.what()));
}
if (!parsed.IsArray()) {
throw audit_exception("Bad configuration: audit_rules must be a JSON array");
}
std::vector<audit_rule> rules;
for (const auto& elem : parsed.GetArray()) {
if (!elem.IsObject()) {
throw audit_exception("Bad configuration: each audit rule must be a JSON object");
}
for (const auto& field : audit_rule_required_fields) {
if (!rjson::find(elem, field)) {
throw audit_exception(fmt::format(
"Bad configuration: audit rule missing required field '{}'", field));
}
}
audit_rule rule;
rule.sinks = json_array_to_string_vec(*rjson::find(elem, "sinks"), "sinks");
rule.categories = parse_categories(json_array_to_string_vec(*rjson::find(elem, "categories"), "categories"));
rule.qualified_table_names = json_array_to_string_vec(*rjson::find(elem, "qualified_table_names"), "qualified_table_names");
rule.roles = json_array_to_string_vec(*rjson::find(elem, "roles"), "roles");
validate_audit_rule(rule);
rules.push_back(std::move(rule));
}
return rules;
}
sstring audit_rules_to_json_string(const std::vector<audit_rule>& rules) {
rjson::value arr = rjson::empty_array();
for (const auto& rule : rules) {
rjson::value obj = rjson::empty_object();
rjson::add_with_string_name(obj, "sinks", string_vec_to_json(rule.sinks));
rjson::value cat_arr = rjson::empty_array();
for (auto cat : rule.categories) {
rjson::push_back(cat_arr, rjson::from_string(category_to_string(cat)));
}
rjson::add_with_string_name(obj, "categories", std::move(cat_arr));
rjson::add_with_string_name(obj, "qualified_table_names", string_vec_to_json(rule.qualified_table_names));
rjson::add_with_string_name(obj, "roles", string_vec_to_json(rule.roles));
rjson::push_back(arr, std::move(obj));
}
return rjson::print(arr);
}
bool matches_category(const audit_rule& rule, statement_category category) {
return rule.categories.contains(category);
}
bool matches_table(const audit_rule& rule, std::string_view keyspace, std::string_view table) {
return matches_qualified_table(rule, qualified_table_name(keyspace, table));
}
static bool matches_any_pattern(const std::vector<sstring>& patterns, std::string_view value) {
if (patterns.empty()) {
return false;
}
sstring value_s(value);
for (const auto& pattern : patterns) {
int rc = fnmatch(pattern.c_str(), value_s.c_str(), FNM_EXTMATCH);
if (rc == 0) {
return true;
}
if (rc != FNM_NOMATCH) {
logger.warn("Audit: fnmatch error for pattern '{}' against value '{}' (rc={})",
pattern, value, rc);
}
}
return false;
}
bool matches_qualified_table(const audit_rule& rule, std::string_view qualified_table_name) {
return matches_any_pattern(rule.qualified_table_names, qualified_table_name);
}
bool matches_role(const audit_rule& rule, std::string_view role) {
return matches_any_pattern(rule.roles, role);
}
audit_sink_set rule_sinks(const audit_rule& rule) {
audit_sink_set result;
for (const auto& s : rule.sinks) {
if (s == "table") {
result.set(audit_sink::table);
} else if (s == "syslog") {
result.set(audit_sink::syslog);
} else {
// Should never happen — validate_audit_rule() rejects unknown sinks.
on_internal_error(logger, fmt::format("Unknown audit sink '{}' in validated rule", s));
}
}
return result;
}
bool matches_rule(const audit_rule& rule, statement_category category,
std::string_view keyspace, std::string_view table,
std::string_view role) {
return matches_category(rule, category)
&& (is_table_scoped_category(category) ? (keyspace.empty() || matches_table(rule, keyspace, table)) : true)
&& matches_role(rule, role);
}
} // namespace audit