Files
scylladb/audit/audit_rule.hh
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

135 lines
5.2 KiB
C++

/*
* Copyright (C) 2026-present ScyllaDB
*/
/*
* SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.1
*/
#pragma once
#include "seastarx.hh"
#include "enum_set.hh"
#include <seastar/core/sstring.hh>
#include <fmt/format.h>
#include <fmt/ranges.h>
#include <algorithm>
#include <array>
#include <string_view>
#include <vector>
namespace audit {
enum class statement_category {
QUERY, DML, DDL, DCL, AUTH, ADMIN
};
using category_set = enum_set<super_enum<statement_category, statement_category::QUERY,
statement_category::DML,
statement_category::DDL,
statement_category::DCL,
statement_category::AUTH,
statement_category::ADMIN>>;
sstring category_to_string(statement_category category);
/// Required field names for an audit rule (used by both JSON and YAML parsers).
inline constexpr std::array<const char*, 4> audit_rule_required_fields = {
"sinks", "categories", "qualified_table_names", "roles"
};
struct audit_rule {
std::vector<sstring> sinks;
category_set categories;
std::vector<sstring> qualified_table_names;
std::vector<sstring> roles;
bool operator==(const audit_rule& other) const {
return sinks == other.sinks
&& categories.mask() == other.categories.mask()
&& qualified_table_names == other.qualified_table_names
&& roles == other.roles;
}
};
std::vector<audit_rule> parse_audit_rules_from_json(const sstring& json_str);
sstring audit_rules_to_json_string(const std::vector<audit_rule>& rules);
category_set parse_categories(const std::vector<sstring>& categories);
void validate_audit_rule(const audit_rule& rule);
/// Formats a "keyspace.table" qualified table name from separate components.
inline sstring qualified_table_name(std::string_view keyspace, std::string_view table) {
sstring result(sstring::initialized_later(), keyspace.size() + 1 + table.size());
auto it = result.begin();
it = std::copy(keyspace.begin(), keyspace.end(), it);
*it++ = '.';
std::copy(table.begin(), table.end(), it);
return result;
}
enum class audit_sink {
table,
syslog,
};
using audit_sink_set = enum_set<super_enum<audit_sink, audit_sink::table, audit_sink::syslog>>;
/// Returns true if the category is table-scoped (DML, DDL, QUERY).
/// Table-independent categories (AUTH, ADMIN, DCL) bypass the table filter
/// because they represent operations that have no meaningful keyspace/table.
inline bool is_table_scoped_category(statement_category category) {
return category == statement_category::DML
|| category == statement_category::DDL
|| category == statement_category::QUERY;
}
/// Returns true if the given category matches any of the rule's categories (bitmask check).
/// Empty categories set matches nothing.
bool matches_category(const audit_rule& rule, statement_category category);
/// Returns true if the given keyspace.table matches any of the rule's qualified_table_names
/// patterns (uses fnmatch with FNM_EXTMATCH for extended glob support).
/// Empty qualified_table_names list matches nothing.
bool matches_table(const audit_rule& rule, std::string_view keyspace, std::string_view table);
/// Same as matches_table but takes a pre-formed "keyspace.table" string.
bool matches_qualified_table(const audit_rule& rule, std::string_view qualified_table_name);
/// Returns true if the given role name matches any of the rule's role patterns
/// (uses fnmatch with FNM_EXTMATCH for extended glob support: @(), !(), +(), *(), ?()).
/// Empty roles list matches nothing.
bool matches_role(const audit_rule& rule, std::string_view role);
/// Returns the set of sinks declared by this rule as a bitmap.
audit_sink_set rule_sinks(const audit_rule& rule);
/// Returns true if a statement with the given attributes matches this rule.
/// A rule matches when all three filters (categories, tables, roles) match.
/// For table-independent categories (AUTH, ADMIN, DCL), the table filter is
/// bypassed — these operations have no meaningful keyspace/table context.
/// Sink filtering is handled separately via rule_sinks().
bool matches_rule(const audit_rule& rule, statement_category category,
std::string_view keyspace, std::string_view table,
std::string_view role);
} // namespace audit
template<>
struct fmt::formatter<audit::audit_rule> {
constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); }
auto format(const audit::audit_rule& rule, fmt::format_context& ctx) const {
auto out = fmt::format_to(ctx.out(), "audit_rule{{sinks=[{}], categories=[", fmt::join(rule.sinks, ","));
bool first = true;
for (auto cat : rule.categories) {
if (!first) { out = fmt::format_to(out, ","); }
out = fmt::format_to(out, "{}", audit::category_to_string(cat));
first = false;
}
return fmt::format_to(out, "], qualified_table_names=[{}], roles=[{}]}}",
fmt::join(rule.qualified_table_names, ","), fmt::join(rule.roles, ","));
}
};