ntopng/scripts/lua/modules/notifications/endpoints.lua
2024-01-12 11:44:18 +01:00

490 lines
19 KiB
Lua

--
-- (C) 2019-24 - ntop.org
--
local dirs = ntop.getDirs()
package.path = dirs.installdir .. "/scripts/lua/modules/?.lua;" .. package.path
package.path = dirs.installdir .. "/scripts/lua/modules/recipients/?.lua;" .. package.path
package.path = dirs.installdir .. "/scripts/lua/modules/toasts/?.lua;" .. package.path
local script_manager = require("script_manager")
local json = require "dkjson"
-- #################################################################
-- A key to access a hash table containing mappings between endpoint_conf_name and endpoint_key
-- endpoint_key s are defined inside ./endpoint lua files, such as email.lua
--
-- Example:
-- ntop_mail -> mail
-- customer_1_mail -> mail
-- crew_es -> elasticsearch
--
local ENDPOINT_CONFIG_TO_ENDPOINT_KEY = "ntopng.prefs.notification_endpoint.endpoint_conf_name_endpoint_key"
-- A key to access a hash table containing mappings, for each endpoint_key, between every endpoint_conf_name and endpoint_params
--
-- Example:
-- notification endpoint mail has two configurations, `ntop_mail` and `customer_1_mail`, so the resulting entry
-- ntopng.prefs.notification_endpoint.endpoint_key_mail.configs is as follows:
-- ntop_mail -> {smtmp_server_name: "...", etc}
-- customer_1_mail -> {smtp_server_name: "...", etc}
--
local ENDPOINT_CONFIGS_KEY = "ntopng.prefs.notification_endpoint.endpoint_key_%s.configs"
-- A key to atomically generate integer endpoint ids
local ENDPOINT_NEXT_ID_KEY = "ntopng.prefs.notification_endpoint.next_endpoint_id"
-- #################################################################
local endpoints = {}
-- #################################################################
-- Key where it's saved a boolean indicating if the first endpoint has been created
endpoints.FIRST_ENDPOINT_CREATED_CACHE_KEY = "ntopng.prefs.endpoint_hints.endpoint_created"
-- #################################################################
function endpoints.get_types(exclude_builtin)
local endpoint_types = {}
-- Currently, we load all the available alert endpoints
local available_endpoints = script_manager.getLoadedAlertEndpoints()
-- Then, we actually consider vaid types for the notification configs
-- only those modules that have their `endpoint_params` and `recipient_params`.
-- Eventually, when the migration between alert endpoints and generic notification endpoints
-- will be completed, all the available endpoints will have `endpoint_params` and `recipient_params`.
for _, endpoint in ipairs(available_endpoints) do
if endpoint.endpoint_params and endpoint.recipient_params and endpoint.endpoint_template and endpoint.recipient_template then
for _, k in pairs({"script_key", "template_name"}) do
if not endpoint.endpoint_template[k] or not endpoint.recipient_template[k] then
goto continue
end
end
-- See if only non-builtin endpoints have been requested (e.g., to populate UI fields)
if not exclude_builtin or not endpoint.builtin then
endpoint_types[endpoint.key] = endpoint
end
end
::continue::
end
return endpoint_types
end
-- #################################################################
-- @brief Check if an endpoint configuration identified with `endpoint_id` exists
-- @param endpoint_conf_name A string with the configuration name
-- @return true if the configuration exists, false otherwise
local function is_endpoint_config_existing(endpoint_conf_name)
local configs = endpoints.get_configs()
for _, conf in pairs(configs) do
if conf.endpoint_conf_name == endpoint_conf_name then
-- Another endpoint with the same name already existing
return true, conf
end
end
-- No other endpoint exists with the same name
return false
end
-- #################################################################
-- @brief Set a configuration along with its params. Configuration name and params must be already sanitized
-- @param endpoint_key A string with the notification endpoint key
-- @param endpoint_id An integer identifier of the endpoint
-- @param endpoint_conf_name A string with the configuration name
-- @param safe_params A table with endpoint configuration params already sanitized
-- @return nil
local function set_endpoint_config_params(endpoint_key, endpoint_id, endpoint_conf_name, safe_params)
if tonumber(endpoint_id) then
-- Format the integer identifier as a string representation of the same integer
-- This is necessary as subsequent Redis cache sets work with strings and not integers
endpoint_id = string.format("%d", endpoint_id)
else
-- backward compatibility: we can ignore the cast to id because the it's a string (old endpoint type id)
end
-- Write the endpoint conf_name and its key in a hash
ntop.setHashCache(ENDPOINT_CONFIG_TO_ENDPOINT_KEY, endpoint_id, endpoint_key)
-- Before storing the configuration as safe_params, we need to extend safe_params
-- to also include the endpoint configuration name.
-- This is necessary as endpoints are identified with integers in newer implementations
-- so the name must be stored in the configuration
safe_params["endpoint_conf_name"] = endpoint_conf_name
-- Endpoint config is the merge of safe_params plus the endpoint_conf_name
-- Write the endpoint config in another hash
local k = string.format(ENDPOINT_CONFIGS_KEY, endpoint_key)
ntop.setHashCache(k, endpoint_id, json.encode(safe_params))
end
-- #################################################################
-- @brief Read the configuration parameters of an existing configuration
-- @param endpoint_id An integer identifier of the endpoint
-- @return A table with two keys: endpoint_key and endpoint_params or nil if the configuration isn't found
local function read_endpoint_config_raw(endpoint_id)
if tonumber(endpoint_id) then
-- Subsequent Redis keys access work with strings so, the integer must be converted to its string representation
endpoint_id = string.format("%d", endpoint_id)
else
-- Old endpoint configs were strings, so there's nothing to do here
end
local endpoint_key = ntop.getHashCache(ENDPOINT_CONFIG_TO_ENDPOINT_KEY, endpoint_id)
local k = string.format(ENDPOINT_CONFIGS_KEY, endpoint_key)
-- Endpoint params are saved as JSON
local endpoint_params_json = ntop.getHashCache(k, endpoint_id)
-- Decode params as a table
local endpoint_params = json.decode(endpoint_params_json)
if endpoint_params and endpoint_params ~= '' then
return {
endpoint_key = endpoint_key,
endpoint_id = endpoint_id,
-- For backward compatibility, endpoint_id is returned as the endpoint_conf_name when not name is found inside endpoint_conf
endpoint_conf_name = endpoint_params.endpoint_conf_name or endpoint_id,
endpoint_params = endpoint_params
}
end
end
-- #################################################################
-- @brief coherence checks for the endpoint key
-- @param endpoint_key A string with the notification endpoint key
-- @return true if the coherence checks are ok, false otherwise
local function check_endpoint_key(endpoint_key)
if not endpoints.get_types()[endpoint_key] then
return false, {status = "failed", error = {type = "endpoint_not_existing"}}
end
return true
end
-- #################################################################
-- @brief coherence checks for the endpoint configuration name
-- @param endpoint_conf_name A string with the configuration name
-- @return true if the coherence checks are ok, false otherwise
local function check_endpoint_conf_name(endpoint_conf_name)
if not endpoint_conf_name or endpoint_conf_name == "" then
return false, {status = "failed", error = {type = "invalid_endpoint_conf_name"}}
end
return true
end
-- #################################################################
-- @brief coherence checks for the endpoint configuration parameters
-- @param endpoint_key A string with the notification endpoint key
-- @param endpoint_params A table with endpoint configuration params that will be possibly sanitized
-- @return false with a description of the error, or true, with a table containing sanitized configuration params.
local function check_endpoint_config_params(endpoint_key, endpoint_params)
if not endpoint_params or not type(endpoint_params) == "table" then
return false, {status = "failed", error = {type = "invalid_endpoint_params"}}
end
-- Create a safe_params table with only expected params
local endpoint = endpoints.get_types()[endpoint_key]
local safe_params = {}
-- So iterate across all expected params of the current endpoint
for _, param in ipairs(endpoint.endpoint_params) do
-- param is a lua table so we access its elements
local param_name = param["param_name"]
local optional = param["optional"]
if endpoint_params and endpoint_params[param_name] and not safe_params[param_name] then
safe_params[param_name] = endpoint_params[param_name]
elseif not optional then
return false, {status = "failed", error = {type = "missing_mandatory_param", missing_param = param_name}}
end
end
return true, {status = "OK", safe_params = safe_params, builtin = endpoint.builtin or false}
end
-- #################################################################
-- @brief Add a new configuration endpoint
-- @param endpoint_key A string with the notification endpoint key
-- @param endpoint_conf_name A string with the configuration name
-- @param endpoint_params A table with endpoint configuration params that will be possibly sanitized
-- @return A table with a key status which is either "OK" or "failed". When "failed", the table contains another key "error" with an indication of the issue
function endpoints.add_config(endpoint_key, endpoint_conf_name, endpoint_params)
local ok, status = check_endpoint_key(endpoint_key)
if not ok then
return status
end
ok, status = check_endpoint_conf_name(endpoint_conf_name)
if not ok then
return status
end
-- Is the config already existing?
local is_existing, existing_conf = is_endpoint_config_existing(endpoint_conf_name)
if is_existing then
return {
status = "failed",
endpoint_id = existing_conf.endpoint_id,
endpoint_conf_name = existing_conf.endpoint_conf_name,
error = {
type = "endpoint_config_already_existing",
endpoint_conf_name = existing_conf.endpoint_conf_name,
}
}
end
-- Are the submitted params those expected by the endpoint?
ok, status = check_endpoint_config_params(endpoint_key, endpoint_params)
if not ok then
return status
end
local safe_params = status["safe_params"]
if status.builtin then
-- If the endpoint is a builtin endpoint, a special boolean safe param builtin is added to the configuration
safe_params["builtin"] = true
else
if isEmptyString(ntop.getPref(endpoints.FIRST_ENDPOINT_CREATED_CACHE_KEY)) then
-- set a flag to indicate that an endpoint has been created
ntop.setPref(endpoints.FIRST_ENDPOINT_CREATED_CACHE_KEY, "1")
end
end
-- Set the config
-- Atomically generate and endpoint identificator
local endpoint_id = ntop.incrCache(ENDPOINT_NEXT_ID_KEY)
-- Save endpoint params, along with the newly generated identificator
set_endpoint_config_params(endpoint_key, endpoint_id, endpoint_conf_name, safe_params)
return {
status = "OK",
endpoint_id = endpoint_id -- This is the newly assigned enpoint it
}
end
-- #################################################################
-- @brief Edit the configuration parameters of an existing endpoint
-- @param endpoint_id An integer identifier of the endpoint
-- @param endpoint_conf_name A string with the configuration name
-- @param endpoint_params A table with endpoint configuration params that will be possibly sanitized
-- @return A table with a key status which is either "OK" or "failed". When "failed", the table contains another key "error" with an indication of the issue
function endpoints.edit_config(endpoint_id, endpoint_conf_name, endpoint_params)
local ok, status = check_endpoint_conf_name(endpoint_conf_name)
if not ok then
return status
end
-- TODO: remove when migration of edit_endpoint.lua is complete and passes the id
if tonumber(endpoint_id) then
endpoint_id = string.format("%d", endpoint_id)
else
-- backward compatibility: we can ignore the cast to id because the it's a string (old endpoint type id)
end
-- Is the config already existing?
local ec = read_endpoint_config_raw(endpoint_id)
if not ec then
return {status = "failed", error = {type = "endpoint_config_not_existing", endpoint_conf_name = endpoint_conf_name}}
end
-- Are the submitted params those expected by the endpoint?
ok, status = check_endpoint_config_params(ec["endpoint_key"], endpoint_params)
if not ok then
return status
end
local safe_params = status["safe_params"]
-- Overwrite the config
set_endpoint_config_params(ec["endpoint_key"], ec["endpoint_id"], endpoint_conf_name, safe_params)
return {status = "OK"}
end
-- #################################################################
-- @brief Delete the configuration parameters of an existing endpoint configuration
-- @param endpoint_id An integer identifier of the endpoint
-- @return A table with a key status which is either "OK" or "failed". When "failed", the table contains another key "error" with an indication of the issue
function endpoints.delete_config(endpoint_id)
-- TODO: remove when migration of edit_endpoint.lua is complete and passes the id
if tonumber(endpoint_id) then
endpoint_id = string.format("%d", endpoint_id)
end
-- Is the config already existing?
local ec = read_endpoint_config_raw(endpoint_id)
if not ec then
return {status = "failed", error = {type = "endpoint_config_not_existing", endpoint_conf_name = endpoint_id}}
end
-- Delete the all the recipients associated to this config recipients
local recipients = require "recipients"
recipients.delete_recipients_by_conf(endpoint_id)
-- Now delete the actual config
local k = string.format(ENDPOINT_CONFIGS_KEY, ec["endpoint_key"])
ntop.delHashCache(k, endpoint_id)
ntop.delHashCache(ENDPOINT_CONFIG_TO_ENDPOINT_KEY, endpoint_id)
return {status = "OK"}
end
-- #################################################################
-- @brief Retrieve the configuration parameters of an existing endpoint configuration
-- @param endpoint_id An integer identifier of the endpoint
-- @return A table with a key status which is either "OK" or "failed".
-- When "failed", the table contains another key "error" with an indication of the issue.
-- When "OK", the table contains "endpoint_conf_name", "endpoint_key", and "endpoint_conf" with the results
function endpoints.get_endpoint_config(endpoint_id)
-- Is the config already existing?
local ec = read_endpoint_config_raw(endpoint_id)
if not ec then
return {status = "failed", error = {type = "endpoint_config_not_existing", endpoint_conf_name = endpoint_id}}
end
-- Decode endpoint configuration params
-- NOTE: in newer implementations, configuration params also contain the endpoint name
-- in older implementations, the endpoint name was used also as endpoint id
local endpoint_conf = ec["endpoint_params"]
return {
status = "OK",
endpoint_id = endpoint_id,
-- For backward compatibility, endpoint_id is returned as the endpoint_conf_name when not name is found inside endpoint_conf
endpoint_conf_name = endpoint_conf["endpoint_conf_name"] or endpoint_id,
endpoint_key = ec["endpoint_key"],
endpoint_conf = endpoint_conf
}
end
-- #################################################################
-- @brief Retrieve all the available configurations and configuration params
-- @param exclude_builtin Whether to exclude builtin configs. Default is false.
-- @return A lua array with a as many elements as the number of existing configurations.
-- Each element is the result of `endpoints.get_endpoint_config`
function endpoints.get_configs(exclude_builtin)
local res = {}
for endpoint_key, endpoint in pairs(endpoints.get_types()) do
local k = string.format(ENDPOINT_CONFIGS_KEY, endpoint_key)
local all_configs = ntop.getHashAllCache(k) or {}
for endpoint_id, endpoint_params in pairs(all_configs) do
local ec = endpoints.get_endpoint_config(endpoint_id)
if not exclude_builtin or not ec.endpoint_conf.builtin then
res[#res + 1] = ec
end
end
end
return res
end
-- #################################################################
-- @brief Retrieve all the available configurations, configuration params, and associated recipients
-- @return A lua array with as many elements as the number of existing configurations.
-- Each element is the result of `endpoints.get_endpoint_config`
-- with an extra key `recipients`.
function endpoints.get_configs_with_recipients(include_stats)
local recipients = require "recipients"
local configs = endpoints.get_configs()
for _, conf in pairs(configs) do
conf["recipients"] = recipients.get_recipients_by_conf(conf.endpoint_id, include_stats)
end
return configs
end
-- #################################################################
-- @brief Clear all the existing endpoint configurations
-- @return Always return a table {status = "OK"}
function endpoints.reset_configs()
local all_configs = endpoints.get_configs()
for _, endpoint_params in pairs(all_configs) do
endpoints.delete_config(endpoint_params.endpoint_id)
end
return {status = "OK"}
end
-- #################################################################
-- @brief Restore a full set of configurations, exported with get_configs_with_recipients
-- including configuration params and associated recipients
function endpoints.add_configs_with_recipients(configs)
local recipients = require "recipients"
local rc = true
-- Restore Endpoints
for _, conf in ipairs(configs) do
local endpoint_key = conf.endpoint_key
local endpoint_conf_name = conf.endpoint_conf_name
local endpoint_params = conf.endpoint_conf
if endpoint_key and endpoint_conf_name and endpoint_params and conf.recipients and
not endpoint_params.builtin then
local ret = endpoints.add_config(endpoint_key, endpoint_conf_name, endpoint_params)
if not ret or not ret.endpoint_id then
rc = false
else
-- Restore Recipients
for _, recipient_conf in ipairs(conf.recipients) do
local endpoint_recipient_name = recipient_conf.recipient_name
local check_categories = recipient_conf.check_categories
local check_entities = recipient_conf.check_entities
local minimum_severity = recipient_conf.minimum_severity
local recipient_params = recipient_conf.recipient_params
ret = recipients.add_recipient(ret.endpoint_id, endpoint_recipient_name,
check_categories, check_entities, minimum_severity,
{}, -- Host pools - restore should take care of this automatically
{}, -- Interface pools - restore should take care of this automatically
recipient_params)
if not ret or not ret.status or ret.status ~= "OK" then
rc = false
end
end
end
end
end
return rc
end
-- #################################################################
return endpoints