From 22e1532bb269ba685bb2eb6df830c2657081e48c Mon Sep 17 00:00:00 2001 From: Mergen Imeev <imeevma@tarantool.org> Date: Thu, 24 Aug 2023 11:42:10 +0300 Subject: [PATCH] config: introduce audit options This patch introduces all audit options. Closes #8861 NO_DOC=Was already described before. --- .../unreleased/gh-8861-audit-options.md | 3 + src/box/lua/config/applier/box_cfg.lua | 26 ++++- src/box/lua/config/applier/mkdir.lua | 6 + src/box/lua/config/instance_config.lua | 109 +++++++++++++++++- .../cluster_config_schema_test.lua | 14 ++- test/config-luatest/config_test.lua | 36 ++++++ test/config-luatest/helpers.lua | 6 +- .../instance_config_schema_test.lua | 70 ++++++++++- 8 files changed, 257 insertions(+), 13 deletions(-) create mode 100644 changelogs/unreleased/gh-8861-audit-options.md diff --git a/changelogs/unreleased/gh-8861-audit-options.md b/changelogs/unreleased/gh-8861-audit-options.md new file mode 100644 index 0000000000..1668e2bc12 --- /dev/null +++ b/changelogs/unreleased/gh-8861-audit-options.md @@ -0,0 +1,3 @@ +## feature/config + +* All audit options are now supported (gh-8861). diff --git a/src/box/lua/config/applier/box_cfg.lua b/src/box/lua/config/applier/box_cfg.lua index 54c91ee56d..ae78ece5d8 100644 --- a/src/box/lua/config/applier/box_cfg.lua +++ b/src/box/lua/config/applier/box_cfg.lua @@ -43,9 +43,8 @@ local function peer_uris(configdata) return uris end -local function log_destination(configdata) - local log = configdata:get('log', {use_default = true}) - if log.to == 'stderr' then +local function log_destination(log) + if log.to == 'stderr' or log.to == 'devnull' then return box.NULL elseif log.to == 'file' then return ('file:%s'):format(log.file) @@ -137,7 +136,24 @@ local function apply(config) -- `log.nonblock`, `log.level`, `log.format`, 'log.modules' -- options are marked with the `box_cfg` annotations and so -- they're already added to `box_cfg`. - box_cfg.log = log_destination(configdata) + local cfg_log = configdata:get('log', {use_default = true}) + box_cfg.log = log_destination(cfg_log) + + -- Construct audit logger destination and audit filter (box_cfg.audit_log + -- and audit_filter). + -- + -- `audit_log.nonblock` and 'audit_log.filter' options are marked with the + -- `box_cfg` annotations and so they're already added to `box_cfg`. + local audit_log = configdata:get('audit_log', {use_default = true}) + if audit_log ~= nil and next(audit_log) ~= nil then + box_cfg.audit_log = log_destination(audit_log) + if audit_log.filter ~= nil then + assert(type(audit_log.filter) == 'table') + box_cfg.audit_filter = table.concat(audit_log.filter, ',') + else + box_cfg.audit_filter = 'compatibility' + end + end local failover = configdata:get('replication.failover', {use_default = true}) @@ -225,6 +241,8 @@ local function apply(config) return w.schema.box_cfg, w.data end):tomap() box_cfg_nondynamic.log = box_cfg.log + box_cfg_nondynamic.audit_log = box_cfg.audit_log + box_cfg_nondynamic.audit_filter = box_cfg.audit_filter for k, v in pairs(box_cfg_nondynamic) do if v ~= box.cfg[k] then local warning = 'box_cfg.apply: non-dynamic option '..k.. diff --git a/src/box/lua/config/applier/mkdir.lua b/src/box/lua/config/applier/mkdir.lua index 5bcf011d55..f53d9fd2fb 100644 --- a/src/box/lua/config/applier/mkdir.lua +++ b/src/box/lua/config/applier/mkdir.lua @@ -78,6 +78,12 @@ local function apply(config) local prefix = ('mkdir.apply[%s]'):format('log.file') safe_mkdir(prefix, fio.dirname(log.file), work_dir) end + + local audit_log = configdata:get('audit_log', {use_default = true}) + if audit_log ~= nil and audit_log.to == 'file' then + local prefix = ('mkdir.apply[%s]'):format('audit_log.file') + safe_mkdir(prefix, fio.dirname(audit_log.file), work_dir) + end end return { diff --git a/src/box/lua/config/instance_config.lua b/src/box/lua/config/instance_config.lua index 427f98cd1b..e3881119ce 100644 --- a/src/box/lua/config/instance_config.lua +++ b/src/box/lua/config/instance_config.lua @@ -1694,7 +1694,114 @@ return schema.new('instance_config', schema.record({ end end, }), - }) + }), + audit_log = enterprise_edition(schema.record({ + -- The same as the destination for the logger, audit logger destination + -- is handled separately in the box_cfg applier, so there are no + -- explicit box_cfg and box_cfg_nondynamic annotations. + -- + -- The reason is that there is no direct-no-transform + -- mapping from, say, `audit_log.file` to `box_cfg.audit_log`. + -- The applier should add the `file:` prefix. + to = enterprise_edition(schema.enum({ + 'devnull', + 'file', + 'pipe', + 'syslog', + }, { + default = 'devnull', + })), + file = enterprise_edition(schema.scalar({ + type = 'string', + -- The mk_parent_dir annotation is not present here, + -- because otherwise the directory would be created + -- unconditionally. Instead, mkdir applier creates it + -- if audit_log.to is 'file'. + default = 'var/log/{{ instance_name }}/audit.log', + })), + pipe = enterprise_edition(schema.scalar({ + type = 'string', + default = box.NULL, + })), + syslog = schema.record({ + identity = enterprise_edition(schema.scalar({ + type = 'string', + default = 'tarantool', + })), + facility = enterprise_edition(schema.scalar({ + type = 'string', + default = 'local7', + })), + server = enterprise_edition(schema.scalar({ + type = 'string', + -- The logger tries /dev/log and then + -- /var/run/syslog if no server is provided. + default = box.NULL, + })), + }), + nonblock = enterprise_edition(schema.scalar({ + type = 'boolean', + box_cfg = 'audit_nonblock', + box_cfg_nondynamic = true, + default = false, + })), + format = enterprise_edition(schema.enum({ + 'plain', + 'json', + 'csv', + }, { + box_cfg = 'audit_format', + box_cfg_nondynamic = true, + default = 'json', + })), + -- The reason for the absence of the box_cfg and box_cfg_nondynamic + -- annotations is that this setting needs to be converted to a string + -- before being set to 'audit_filter'. This will be done in box_cfg + -- applier. + -- + -- TODO: Add a validation and a default value. Currently, the audit_log + -- validation can catch the setting of the option in the CE, but adding + -- its own validation seems more appropriate. + filter = schema.set({ + -- Events. + "audit_enable", + "custom", + "auth_ok", + "auth_fail", + "disconnect", + "user_create", + "user_drop", + "role_create", + "role_drop", + "user_enable", + "user_disable", + "user_grant_rights", + "user_revoke_rights", + "role_grant_rights", + "role_revoke_rights", + "password_change", + "access_denied", + "eval", + "call", + "space_select", + "space_create", + "space_alter", + "space_drop", + "space_insert", + "space_replace", + "space_delete", + -- Groups of events. + "none", + "all", + "audit", + "auth", + "priv", + "ddl", + "dml", + "data_operations", + "compatibility", + }), + })), }, { -- This kind of validation cannot be implemented as the -- 'validate' annotation of a particular schema node. There diff --git a/test/config-luatest/cluster_config_schema_test.lua b/test/config-luatest/cluster_config_schema_test.lua index 7e655c83e4..8f0c9e715f 100644 --- a/test/config-luatest/cluster_config_schema_test.lua +++ b/test/config-luatest/cluster_config_schema_test.lua @@ -270,7 +270,19 @@ g.test_defaults = function() sched_ref_quota = 300, shard_index = "bucket_id", sync_timeout = 1, - } + }, + audit_log = is_enterprise and { + file = "var/log/{{ instance_name }}/audit.log", + format = "json", + nonblock = false, + pipe = box.NULL, + syslog = { + facility = "local7", + identity = "tarantool", + server = box.NULL + }, + to = "devnull", + } or nil, } local res = cluster_config:apply_default({}) t.assert_equals(res, exp) diff --git a/test/config-luatest/config_test.lua b/test/config-luatest/config_test.lua index a81ee9756c..483e961b2a 100644 --- a/test/config-luatest/config_test.lua +++ b/test/config-luatest/config_test.lua @@ -2,6 +2,7 @@ local t = require('luatest') local server = require('test.luatest_helpers.server') local cluster_config = require('internal.config.cluster_config') local configdata = require('internal.config.configdata') +local helpers = require('test.config-luatest.helpers') local treegen = require('test.treegen') local justrun = require('test.justrun') local json = require('json') @@ -779,3 +780,38 @@ g.test_metrics_options = function() t.assert_equals(box.cfg.metrics.labels, {foo = 'bar'}) end) end + +g.test_audit_options = function() + t.tarantool.skip_if_not_enterprise() + local dir = treegen.prepare_directory(g, {}, {}) + + local events = { + 'audit_enable', 'custom', 'auth_ok', 'auth_fail', 'disconnect', + 'user_create', 'user_drop', 'role_create', 'role_drop', 'user_enable', + 'user_disable', 'user_grant_rights', 'user_revoke_rights', + 'role_grant_rights', 'role_revoke_rights', 'password_change', + 'access_denied', 'eval', 'call', 'space_select', 'space_create', + 'space_alter', 'space_drop', 'space_insert', 'space_replace', + 'space_delete', 'none', 'all', 'audit', 'auth', 'priv', 'ddl', 'dml', + 'data_operations', 'compatibility' + } + + local verify = function(events) + t.assert_equals(box.cfg.audit_log, nil) + t.assert_equals(box.cfg.audit_nonblock, true) + t.assert_equals(box.cfg.audit_format, 'csv') + t.assert_equals(box.cfg.audit_filter, table.concat(events, ",")) + end + + helpers.success_case(g, { + dir = dir, + options = { + ['audit_log.to'] = 'devnull', + ['audit_log.nonblock'] = true, + ['audit_log.format'] = 'csv', + ['audit_log.filter'] = events, + }, + verify = verify, + verify_args = {events} + }) +end diff --git a/test/config-luatest/helpers.lua b/test/config-luatest/helpers.lua index 4f6cefb821..534a21bcf5 100644 --- a/test/config-luatest/helpers.lua +++ b/test/config-luatest/helpers.lua @@ -148,12 +148,16 @@ end -- -- Function to run on the started server to verify some -- invariants. +-- +-- * opts.verify_args +-- +-- Arguments for the verify function. local function success_case(g, opts) local verify = assert(opts.verify) local prepared = prepare_case(g, opts) g.server = server:new(prepared.server) g.server:start() - g.server:exec(verify) + g.server:exec(verify, opts.verify_args) return prepared end diff --git a/test/config-luatest/instance_config_schema_test.lua b/test/config-luatest/instance_config_schema_test.lua index 589a6e32ce..61341862a2 100644 --- a/test/config-luatest/instance_config_schema_test.lua +++ b/test/config-luatest/instance_config_schema_test.lua @@ -980,6 +980,8 @@ g.test_box_cfg_coverage = function() cluster_name = true, log = true, metrics = true, + audit_log = true, + audit_filter = true, -- Controlled by the leader and database.mode options, -- handled by the box_cfg applier. @@ -994,12 +996,6 @@ g.test_box_cfg_coverage = function() -- Moved to the CLI options (see gh-8876). force_recovery = true, - - -- TODO: Will be added in the scope of gh-8861. - audit_log = true, - audit_nonblock = true, - audit_format = true, - audit_filter = true, } -- There are options, where defaults are changed deliberately. @@ -1007,6 +1003,7 @@ g.test_box_cfg_coverage = function() -- box.cfg.log_nonblock is set to nil by default, but -- actually it means false. log_nonblock = true, + audit_nonblock = true, -- Adjusted to use {{ instance_name }}. custom_proc_title = true, @@ -1292,3 +1289,64 @@ g.test_sharding = function() local res = instance_config:apply_default({}).sharding t.assert_equals(res, exp) end + +g.test_audit_unavailable = function() + t.tarantool.skip_if_enterprise() + local iconfig = { + audit_log = { + to = 'file', + }, + } + local err = '[instance_config] audit_log.to: This configuration '.. + 'parameter is available only in Tarantool Enterprise Edition' + t.assert_error_msg_equals(err, function() + instance_config:validate(iconfig) + end) + + iconfig = { + audit_log = { + filter = {'all'}, + }, + } + err = '[instance_config] audit_log: This configuration parameter is '.. + 'available only in Tarantool Enterprise Edition' + t.assert_error_msg_equals(err, function() + instance_config:validate(iconfig) + end) +end + +g.test_audit_available = function() + t.tarantool.skip_if_not_enterprise() + local iconfig = { + audit_log = { + to = 'file', + file = 'one', + pipe = 'two', + syslog = { + identity = 'three', + facility = 'four', + server = 'five', + }, + nonblock = true, + format = 'plain', + filter = {'all', 'none'} + }, + } + instance_config:validate(iconfig) + validate_fields(iconfig.audit_log, instance_config.schema.fields.audit_log) + + local exp = { + file = "var/log/{{ instance_name }}/audit.log", + format = "json", + nonblock = false, + pipe = box.NULL, + syslog = { + facility = "local7", + identity = "tarantool", + server = box.NULL + }, + to = "devnull", + } + local res = instance_config:apply_default({}).audit_log + t.assert_equals(res, exp) +end -- GitLab