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