Packages

macula

0.44.0
7.0.0 6.0.0 5.2.2 5.2.1 5.2.0 5.1.0 5.0.0 4.8.0 4.7.1 4.7.0 4.6.0 4.5.0 4.4.10 4.4.9 4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.1 4.3.0 4.2.9 4.2.8 4.2.7 4.2.6 4.2.5 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.1 4.1.0 4.0.0 3.16.0 3.15.3 3.15.2 3.15.1 3.14.0 3.13.0 3.12.1 3.12.0 3.11.1 3.11.0 3.10.3 3.10.2 3.10.1 3.9.0 3.8.0 3.7.0 3.5.0 3.4.0 3.3.0 3.2.0 3.1.0 3.0.0 2.1.1 2.1.0 2.0.0 1.5.2 1.5.1 1.4.30 1.4.29 1.4.28 1.4.27 1.4.26 1.4.25 1.4.24 1.4.23 1.4.22 1.4.21 1.4.20 1.4.19 1.4.18 1.4.17 1.4.16 1.4.15 1.4.14 1.4.13 1.4.11 1.4.10 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.1 1.3.0 1.2.0 1.1.0 1.0.10 1.0.9 1.0.8 1.0.7 1.0.6 1.0.5 1.0.4 1.0.3 1.0.2 1.0.1 1.0.0 0.48.6 0.48.5 0.48.4 0.48.3 0.48.2 0.48.1 0.48.0 0.47.1 0.47.0 0.46.3 0.46.1 0.46.0 0.45.3 0.45.2 0.45.1 0.45.0 0.44.2 0.44.1 0.44.0 0.43.3 0.43.2 0.43.1 0.43.0 0.42.9 0.42.8 0.42.7 0.42.6 0.42.5 0.42.4 0.42.3 0.42.2 0.42.1 0.42.0 0.41.1 0.41.0 0.40.1 0.40.0 0.39.9 0.39.8 0.39.7 0.39.6 0.39.5 0.39.4 0.39.3 0.39.2 0.39.1 0.39.0 0.38.8 0.38.7 0.38.6 0.38.5 0.38.4 0.38.3 0.38.2 0.38.1 0.38.0 0.37.7 0.37.6 0.37.5 0.37.4 0.37.3 0.37.2 0.37.1 0.37.0 0.36.6 0.36.5 0.36.4 0.36.3 0.36.2 0.36.1 0.36.0 0.35.4 0.35.3 0.35.2 0.35.1 0.35.0 0.34.1 0.34.0 0.33.1 0.33.0 0.32.5 0.32.4 0.32.3 0.32.2 0.32.1 0.32.0 0.31.9 0.31.8 0.31.7 0.31.6 0.31.5 0.31.4 0.31.3 0.31.2 0.31.1 0.31.0 0.30.10 0.30.9 0.30.8 0.30.7 0.30.6 0.30.5 0.30.4 0.30.3 0.30.2 0.30.1 0.30.0 0.29.0 0.28.3 0.28.2 0.28.1 0.28.0 0.27.1 0.27.0 0.26.1 0.26.0 0.25.6 0.25.5 0.25.4 0.25.3 0.25.2 0.25.1 0.25.0 0.24.6 0.24.5 0.24.4 0.24.3 0.24.2 0.24.1 0.24.0 0.23.3 0.23.2 0.23.1 0.23.0 0.22.12 0.22.11 0.22.10 0.22.9 0.22.8 0.22.7 0.22.6 0.22.5 0.22.4 0.22.3 0.22.2 0.22.1 0.22.0 0.21.7 0.21.6 0.21.5 0.21.4 0.21.2 0.21.1 0.21.0 0.20.25 0.20.24 0.20.23 0.20.22 0.20.21 0.20.20 0.20.19 0.20.18 0.20.17 0.20.16 0.20.15 0.20.14 0.20.13 0.20.12 0.20.11 0.20.10 0.20.9 0.20.8 0.20.7 0.20.6 0.20.5 0.20.3 0.20.2 0.20.1 0.20.0 0.19.2 0.19.1 0.19.0 0.18.1 0.18.0 0.17.4 0.17.3 0.17.2 0.17.1 0.17.0 0.16.6 0.16.5 0.16.4 0.16.3 0.16.2 0.16.1 0.16.0 0.15.1 0.15.0 0.14.3 0.14.2 0.14.1 0.14.0 0.12.6 0.12.5 0.12.3 0.11.3 0.10.2 0.10.1 0.10.0 0.9.2 0.9.1 0.9.0 0.8.25 0.8.24 0.8.23 0.8.22 0.8.21 0.8.20 0.8.19 0.8.18 0.8.17 0.8.16 0.8.15 0.8.14 0.8.13 0.8.12 0.8.11 0.8.10 0.8.9 0.8.8 0.8.7 0.8.6 0.8.5 0.8.4 0.8.3 0.8.2 0.8.1 0.8.0 0.7.30 0.7.29 0.7.28 0.7.27 0.7.26 0.7.25 0.7.24 0.7.23 0.7.22 0.7.21 0.7.20 0.7.19 0.7.18 0.7.17 0.7.16 0.7.15 0.7.14 0.7.13 0.7.12 0.7.11 0.7.10 0.7.9 0.7.8 0.7.7 0.7.6 0.7.5 0.7.4 0.7.3 0.7.2 0.7.1 0.7.0 0.6.7 0.6.6 0.6.5 0.6.4 0.6.3 0.6.2 0.6.1 0.6.0 0.5.0 0.4.4 0.4.3 0.4.2 0.4.1 0.4.0 0.3.4 0.3.3 0.3.2 0.3.1

Macula HTTP/3 Mesh SDK — connect, subscribe, publish, call, advertise

Current section

Files

Jump to
macula src macula_authorization_audit.erl
Raw

src/macula_authorization_audit.erl

%% @doc Authorization Audit Logging Module.
%%
%% Provides comprehensive audit logging for all authorization decisions in the
%% Macula mesh. Uses telemetry for real-time metrics and optionally stores
%% recent entries in ETS for debugging and analysis.
%%
%% == Telemetry Events ==
%%
%% - `[macula, authorization, allowed]' - Authorization succeeded
%% - `[macula, authorization, denied]' - Authorization denied
%% - `[macula, authorization, error]' - Authorization check error
%%
%% == Event Metadata ==
%%
%% All events include:
%% - `operation' - The operation type (call, publish, subscribe, announce)
%% - `caller' - The caller's DID
%% - `resource' - The topic or procedure
%% - `timestamp' - Unix timestamp
%%
%% Denied events also include:
%% - `reason' - Why authorization failed
%%
%% == Usage ==
%%
%% Log an authorized operation:
%%
%% `macula_authorization_audit:log_authorized(call, CallerDID, Procedure).'
%%
%% Log a denied operation:
%%
%% `macula_authorization_audit:log_denied(publish, CallerDID, Topic, unauthorized).'
%%
%% Query recent audit entries (for debugging):
%%
%% `Entries = macula_authorization_audit:get_recent(100).'
%%
%% == Performance ==
%%
%% Designed for sub-millisecond overhead:
%% - Telemetry events are synchronous but fast
%% - ETS writes are non-blocking
%% - Periodic cleanup prevents unbounded growth
%%
%% @author Claude
-module(macula_authorization_audit).
-behaviour(gen_server).
%% API
-export([
start_link/0,
start_link/1,
stop/0,
stop/1
]).
%% Audit logging
-export([
log_authorized/3,
log_authorized/4,
log_denied/4,
log_denied/5,
log_error/4,
log_error/5
]).
%% Query API
-export([
get_recent/1,
get_recent/2,
get_by_caller/2,
get_by_caller/3,
get_by_resource/2,
get_by_resource/3,
get_stats/0,
get_stats/1,
clear/0,
clear/1
]).
%% Configuration
-export([
set_retention/1,
set_retention/2,
set_max_entries/1,
set_max_entries/2,
is_enabled/0,
is_enabled/1,
enable/0,
enable/1,
disable/0,
disable/1
]).
%% gen_server callbacks
-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2
]).
%%====================================================================
%% Types
%%====================================================================
-type operation() :: call | publish | subscribe | announce | atom().
-type did() :: binary().
-type resource() :: binary().
-type reason() :: unauthorized
| invalid_ucan
| expired_ucan
| revoked_ucan
| insufficient_capability
| invalid_did
| namespace_mismatch
| atom().
-type audit_entry() :: #{
id := binary(),
timestamp := integer(),
operation := operation(),
caller := did(),
resource := resource(),
result := allowed | denied | error,
reason => reason(),
metadata => map()
}.
-type opts() :: #{
retention_seconds => pos_integer(),
max_entries => pos_integer(),
enabled => boolean(),
cleanup_interval => pos_integer()
}.
-export_type([audit_entry/0, opts/0]).
%%====================================================================
%% Macros
%%====================================================================
-define(DEFAULT_SERVER, ?MODULE).
-define(AUDIT_TABLE, macula_authorization_audit_log).
-define(DEFAULT_RETENTION, 3600). %% 1 hour
-define(DEFAULT_MAX_ENTRIES, 10000). %% 10k entries
-define(DEFAULT_CLEANUP_INTERVAL, 60000). %% 1 minute
%%====================================================================
%% State
%%====================================================================
-record(state, {
enabled = true :: boolean(),
retention_seconds = ?DEFAULT_RETENTION :: pos_integer(),
max_entries = ?DEFAULT_MAX_ENTRIES :: pos_integer(),
cleanup_interval = ?DEFAULT_CLEANUP_INTERVAL :: pos_integer(),
stats = #{} :: map()
}).
%%====================================================================
%% API - Start/Stop
%%====================================================================
%% @doc Start the audit server with default name.
-spec start_link() -> {ok, pid()} | {error, term()}.
start_link() ->
start_link(#{}).
%% @doc Start the audit server with options.
-spec start_link(opts()) -> {ok, pid()} | {error, term()}.
start_link(Opts) ->
gen_server:start_link({local, ?DEFAULT_SERVER}, ?MODULE, Opts, []).
%% @doc Stop the default audit server.
-spec stop() -> ok.
stop() ->
stop(?DEFAULT_SERVER).
%% @doc Stop a specific audit server.
-spec stop(pid() | atom()) -> ok.
stop(ServerRef) ->
gen_server:stop(ServerRef).
%%====================================================================
%% API - Audit Logging
%%====================================================================
%% @doc Log an authorized operation.
-spec log_authorized(operation(), did(), resource()) -> ok.
log_authorized(Operation, CallerDID, Resource) ->
log_authorized(?DEFAULT_SERVER, Operation, CallerDID, Resource).
%% @doc Log an authorized operation to specific server.
-spec log_authorized(pid() | atom(), operation(), did(), resource()) -> ok.
log_authorized(ServerRef, Operation, CallerDID, Resource) ->
Now = erlang:system_time(second),
%% Emit telemetry event (always, even if ETS storage disabled)
telemetry:execute(
[macula, authorization, allowed],
#{count => 1, duration_ms => 0},
#{operation => Operation, caller => CallerDID, resource => Resource, timestamp => Now}
),
%% Store in ETS if enabled
gen_server:cast(ServerRef, {log, allowed, Operation, CallerDID, Resource, undefined, #{}, Now}).
%% @doc Log a denied operation.
-spec log_denied(operation(), did(), resource(), reason()) -> ok.
log_denied(Operation, CallerDID, Resource, Reason) ->
log_denied(?DEFAULT_SERVER, Operation, CallerDID, Resource, Reason).
%% @doc Log a denied operation to specific server.
-spec log_denied(pid() | atom(), operation(), did(), resource(), reason()) -> ok.
log_denied(ServerRef, Operation, CallerDID, Resource, Reason) ->
Now = erlang:system_time(second),
%% Emit telemetry event
telemetry:execute(
[macula, authorization, denied],
#{count => 1},
#{operation => Operation, caller => CallerDID, resource => Resource,
reason => Reason, timestamp => Now}
),
%% Store in ETS if enabled
gen_server:cast(ServerRef, {log, denied, Operation, CallerDID, Resource, Reason, #{}, Now}).
%% @doc Log an error during authorization check.
-spec log_error(operation(), did(), resource(), term()) -> ok.
log_error(Operation, CallerDID, Resource, Error) ->
log_error(?DEFAULT_SERVER, Operation, CallerDID, Resource, Error).
%% @doc Log an error to specific server.
-spec log_error(pid() | atom(), operation(), did(), resource(), term()) -> ok.
log_error(ServerRef, Operation, CallerDID, Resource, Error) ->
Now = erlang:system_time(second),
%% Emit telemetry event
telemetry:execute(
[macula, authorization, error],
#{count => 1},
#{operation => Operation, caller => CallerDID, resource => Resource,
error => Error, timestamp => Now}
),
%% Store in ETS if enabled
gen_server:cast(ServerRef, {log, error, Operation, CallerDID, Resource, Error, #{}, Now}).
%%====================================================================
%% API - Query
%%====================================================================
%% @doc Get recent audit entries (most recent first).
-spec get_recent(pos_integer()) -> [audit_entry()].
get_recent(Limit) ->
get_recent(?DEFAULT_SERVER, Limit).
%% @doc Get recent audit entries from specific server.
-spec get_recent(pid() | atom(), pos_integer()) -> [audit_entry()].
get_recent(_ServerRef, Limit) ->
case ets:whereis(?AUDIT_TABLE) of
undefined -> [];
_Tid ->
All = [Entry || {_Id, Entry} <- ets:tab2list(?AUDIT_TABLE)],
Sorted = lists:sort(fun(A, B) ->
maps:get(timestamp, A) > maps:get(timestamp, B)
end, All),
lists:sublist(Sorted, Limit)
end.
%% @doc Get audit entries for a specific caller.
-spec get_by_caller(did(), pos_integer()) -> [audit_entry()].
get_by_caller(CallerDID, Limit) ->
get_by_caller(?DEFAULT_SERVER, CallerDID, Limit).
%% @doc Get audit entries for a specific caller from specific server.
-spec get_by_caller(pid() | atom(), did(), pos_integer()) -> [audit_entry()].
get_by_caller(_ServerRef, CallerDID, Limit) ->
case ets:whereis(?AUDIT_TABLE) of
undefined -> [];
_Tid ->
All = [Entry || {_Id, Entry} <- ets:tab2list(?AUDIT_TABLE)],
Matching = [E || E <- All, maps:get(caller, E) =:= CallerDID],
Sorted = lists:sort(fun(A, B) ->
maps:get(timestamp, A) > maps:get(timestamp, B)
end, Matching),
lists:sublist(Sorted, Limit)
end.
%% @doc Get audit entries for a specific resource.
-spec get_by_resource(resource(), pos_integer()) -> [audit_entry()].
get_by_resource(Resource, Limit) ->
get_by_resource(?DEFAULT_SERVER, Resource, Limit).
%% @doc Get audit entries for a specific resource from specific server.
-spec get_by_resource(pid() | atom(), resource(), pos_integer()) -> [audit_entry()].
get_by_resource(_ServerRef, Resource, Limit) ->
case ets:whereis(?AUDIT_TABLE) of
undefined -> [];
_Tid ->
All = [Entry || {_Id, Entry} <- ets:tab2list(?AUDIT_TABLE)],
Matching = [E || E <- All, maps:get(resource, E) =:= Resource],
Sorted = lists:sort(fun(A, B) ->
maps:get(timestamp, A) > maps:get(timestamp, B)
end, Matching),
lists:sublist(Sorted, Limit)
end.
%% @doc Get audit statistics.
-spec get_stats() -> map().
get_stats() ->
get_stats(?DEFAULT_SERVER).
%% @doc Get audit statistics from specific server.
-spec get_stats(pid() | atom()) -> map().
get_stats(ServerRef) ->
gen_server:call(ServerRef, get_stats).
%% @doc Clear all audit entries.
-spec clear() -> ok.
clear() ->
clear(?DEFAULT_SERVER).
%% @doc Clear all audit entries from specific server.
-spec clear(pid() | atom()) -> ok.
clear(ServerRef) ->
gen_server:call(ServerRef, clear).
%%====================================================================
%% API - Configuration
%%====================================================================
%% @doc Set retention period in seconds.
-spec set_retention(pos_integer()) -> ok.
set_retention(Seconds) ->
set_retention(?DEFAULT_SERVER, Seconds).
%% @doc Set retention period for specific server.
-spec set_retention(pid() | atom(), pos_integer()) -> ok.
set_retention(ServerRef, Seconds) ->
gen_server:call(ServerRef, {set_retention, Seconds}).
%% @doc Set maximum number of entries.
-spec set_max_entries(pos_integer()) -> ok.
set_max_entries(MaxEntries) ->
set_max_entries(?DEFAULT_SERVER, MaxEntries).
%% @doc Set maximum entries for specific server.
-spec set_max_entries(pid() | atom(), pos_integer()) -> ok.
set_max_entries(ServerRef, MaxEntries) ->
gen_server:call(ServerRef, {set_max_entries, MaxEntries}).
%% @doc Check if audit logging is enabled.
-spec is_enabled() -> boolean().
is_enabled() ->
is_enabled(?DEFAULT_SERVER).
%% @doc Check if audit logging is enabled for specific server.
-spec is_enabled(pid() | atom()) -> boolean().
is_enabled(ServerRef) ->
gen_server:call(ServerRef, is_enabled).
%% @doc Enable audit logging.
-spec enable() -> ok.
enable() ->
enable(?DEFAULT_SERVER).
%% @doc Enable audit logging for specific server.
-spec enable(pid() | atom()) -> ok.
enable(ServerRef) ->
gen_server:call(ServerRef, enable).
%% @doc Disable audit logging (telemetry still emits, ETS storage disabled).
-spec disable() -> ok.
disable() ->
disable(?DEFAULT_SERVER).
%% @doc Disable audit logging for specific server.
-spec disable(pid() | atom()) -> ok.
disable(ServerRef) ->
gen_server:call(ServerRef, disable).
%%====================================================================
%% gen_server Callbacks
%%====================================================================
%% @private
init(Opts) ->
%% Create ETS table for audit log storage
%% Stores {Id, Entry} tuples where Entry is a map
ets:new(?AUDIT_TABLE, [
named_table,
public,
set,
{read_concurrency, true},
{write_concurrency, true}
]),
State = #state{
enabled = maps:get(enabled, Opts, true),
retention_seconds = maps:get(retention_seconds, Opts, ?DEFAULT_RETENTION),
max_entries = maps:get(max_entries, Opts, ?DEFAULT_MAX_ENTRIES),
cleanup_interval = maps:get(cleanup_interval, Opts, ?DEFAULT_CLEANUP_INTERVAL),
stats = #{
allowed_count => 0,
denied_count => 0,
error_count => 0,
cleanup_count => 0
}
},
%% Schedule periodic cleanup
schedule_cleanup(State#state.cleanup_interval),
{ok, State}.
%% @private
handle_call(get_stats, _From, State = #state{stats = Stats}) ->
TableInfo = case ets:whereis(?AUDIT_TABLE) of
undefined -> #{table_size => 0, memory_bytes => 0};
_Tid -> #{
table_size => ets:info(?AUDIT_TABLE, size),
memory_bytes => ets:info(?AUDIT_TABLE, memory) * erlang:system_info(wordsize)
}
end,
FullStats = maps:merge(Stats, TableInfo),
FullStats2 = FullStats#{
enabled => State#state.enabled,
retention_seconds => State#state.retention_seconds,
max_entries => State#state.max_entries
},
{reply, FullStats2, State};
handle_call(clear, _From, State) ->
case ets:whereis(?AUDIT_TABLE) of
undefined -> ok;
_Tid -> ets:delete_all_objects(?AUDIT_TABLE)
end,
NewStats = #{
allowed_count => 0,
denied_count => 0,
error_count => 0,
cleanup_count => 0
},
{reply, ok, State#state{stats = NewStats}};
handle_call({set_retention, Seconds}, _From, State) ->
{reply, ok, State#state{retention_seconds = Seconds}};
handle_call({set_max_entries, MaxEntries}, _From, State) ->
{reply, ok, State#state{max_entries = MaxEntries}};
handle_call(is_enabled, _From, State = #state{enabled = Enabled}) ->
{reply, Enabled, State};
handle_call(enable, _From, State) ->
{reply, ok, State#state{enabled = true}};
handle_call(disable, _From, State) ->
{reply, ok, State#state{enabled = false}};
handle_call(_Request, _From, State) ->
{reply, {error, unknown_request}, State}.
%% @private
handle_cast({log, Result, _Operation, _CallerDID, _Resource, _Reason, _Metadata, _Timestamp},
State = #state{enabled = false}) ->
%% Logging disabled, just update stats
NewStats = update_stats(Result, State#state.stats),
{noreply, State#state{stats = NewStats}};
handle_cast({log, Result, Operation, CallerDID, Resource, Reason, Metadata, Timestamp},
State = #state{enabled = true, max_entries = MaxEntries}) ->
%% Create audit entry
EntryId = generate_entry_id(),
Entry = #{
id => EntryId,
timestamp => Timestamp,
operation => Operation,
caller => CallerDID,
resource => Resource,
result => Result,
reason => Reason,
metadata => Metadata
},
%% Store in ETS as {Id, Entry} tuple
case ets:whereis(?AUDIT_TABLE) of
undefined -> ok;
_Tid ->
ets:insert(?AUDIT_TABLE, {EntryId, Entry}),
%% Check if we need to evict old entries
CurrentSize = ets:info(?AUDIT_TABLE, size),
case CurrentSize > MaxEntries of
true -> evict_oldest(CurrentSize - MaxEntries);
false -> ok
end
end,
NewStats = update_stats(Result, State#state.stats),
{noreply, State#state{stats = NewStats}};
handle_cast(_Msg, State) ->
{noreply, State}.
%% @private
handle_info(cleanup, State = #state{retention_seconds = Retention, cleanup_interval = Interval}) ->
Expired = cleanup_expired(Retention),
NewStats = maps:update_with(cleanup_count, fun(C) -> C + Expired end, Expired, State#state.stats),
schedule_cleanup(Interval),
{noreply, State#state{stats = NewStats}};
handle_info(_Info, State) ->
{noreply, State}.
%% @private
terminate(_Reason, _State) ->
case ets:whereis(?AUDIT_TABLE) of
undefined -> ok;
_Tid -> ets:delete(?AUDIT_TABLE)
end,
ok.
%%====================================================================
%% Internal Functions
%%====================================================================
%% @private Generate unique entry ID
-spec generate_entry_id() -> binary().
generate_entry_id() ->
Timestamp = erlang:system_time(microsecond),
Random = rand:uniform(16#FFFFFFFF),
list_to_binary(io_lib:format("~16.16.0b-~8.16.0b", [Timestamp, Random])).
%% @private Update statistics
-spec update_stats(allowed | denied | error, map()) -> map().
update_stats(allowed, Stats) ->
maps:update_with(allowed_count, fun(C) -> C + 1 end, 1, Stats);
update_stats(denied, Stats) ->
maps:update_with(denied_count, fun(C) -> C + 1 end, 1, Stats);
update_stats(error, Stats) ->
maps:update_with(error_count, fun(C) -> C + 1 end, 1, Stats).
%% @private Schedule cleanup timer
-spec schedule_cleanup(pos_integer()) -> reference().
schedule_cleanup(Interval) ->
erlang:send_after(Interval, self(), cleanup).
%% @private Remove expired entries
-spec cleanup_expired(pos_integer()) -> non_neg_integer().
cleanup_expired(RetentionSeconds) ->
case ets:whereis(?AUDIT_TABLE) of
undefined -> 0;
_Tid ->
Cutoff = erlang:system_time(second) - RetentionSeconds,
All = ets:tab2list(?AUDIT_TABLE),
Expired = [Id || {Id, Entry} <- All, maps:get(timestamp, Entry) < Cutoff],
lists:foreach(fun(Id) -> ets:delete(?AUDIT_TABLE, Id) end, Expired),
length(Expired)
end.
%% @private Evict oldest entries when over capacity
-spec evict_oldest(pos_integer()) -> ok.
evict_oldest(Count) when Count =< 0 ->
ok;
evict_oldest(Count) ->
case ets:whereis(?AUDIT_TABLE) of
undefined -> ok;
_Tid ->
All = ets:tab2list(?AUDIT_TABLE),
Sorted = lists:sort(fun({_IdA, A}, {_IdB, B}) ->
maps:get(timestamp, A) < maps:get(timestamp, B)
end, All),
ToDelete = lists:sublist(Sorted, Count),
lists:foreach(fun({Id, _Entry}) -> ets:delete(?AUDIT_TABLE, Id) end, ToDelete),
ok
end.