diff --git a/DJANGO_USAGE.md b/DJANGO_USAGE.md new file mode 100644 index 0000000..9f1b20c --- /dev/null +++ b/DJANGO_USAGE.md @@ -0,0 +1,34 @@ +# Django Usage + +A middleware to add the current user to the transactions + +Add as low down as possible in the order so all the session, user security comes first. + +```python +from django.db import connection, transaction + + +class AuditLogUserMiddleware: + """ + Execute the request/response cycle in an atomic transaction. + Update the audit log with the current user. + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + if request.method in ["POST", "PUT", "PATCH", "DELETE"]: + with transaction.atomic(): + response = self.get_response(request) + if request.user.is_authenticated: + with connection.cursor() as cursor: + sql = """ + UPDATE audit.logged_actions + SET meta_fields = hstore('current_user', %s) + WHERE transaction_id = txid_current(); + """ + cursor.execute(sql, [str(request.user)]) + return response + return self.get_response(request) +``` \ No newline at end of file diff --git a/README.md b/README.md index 63403b8..30bfb81 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,10 @@ triggers. See: http://wiki.postgresql.org/wiki/Audit_trigger_91plus + +Run the following to get a list of tables to enable the logging on: + + SELECT 'SELECT audit.audit_table(''' || table_name || ''');' + FROM information_schema.tables + WHERE table_schema = 'public' + ORDER BY table_name; diff --git a/audit.sql b/audit.sql index 601d749..7326a25 100644 --- a/audit.sql +++ b/audit.sql @@ -4,10 +4,10 @@ -- This file should be generic and not depend on application roles or structures, -- as it's being listed here: -- --- https://wiki.postgresql.org/wiki/Audit_trigger_91plus +-- https://wiki.postgresql.org/wiki/Audit_trigger_91plus -- -- This trigger was originally based on --- http://wiki.postgresql.org/wiki/Audit_trigger +-- http://wiki.postgresql.org/wiki/Audit_trigger -- but has been completely rewritten. -- -- Should really be converted into a relocatable EXTENSION, with control and upgrade files. @@ -42,18 +42,19 @@ CREATE TABLE audit.logged_actions ( table_name text not null, relid oid not null, session_user_name text, - action_tstamp_tx TIMESTAMP WITH TIME ZONE NOT NULL, - action_tstamp_stm TIMESTAMP WITH TIME ZONE NOT NULL, - action_tstamp_clk TIMESTAMP WITH TIME ZONE NOT NULL, + action_tstamp_tx bigint NOT NULL, + action_tstamp_stm bigint NOT NULL, + action_tstamp_clk bigint NOT NULL, transaction_id bigint, application_name text, client_addr inet, client_port integer, client_query text, - action TEXT NOT NULL CHECK (action IN ('I','D','U', 'T')), + action char(1) NOT NULL CHECK (action IN ('I','D','U','T')), row_data hstore, changed_fields hstore, - statement_only boolean not null + statement_only boolean not null, + meta_fields hstore ); REVOKE ALL ON audit.logged_actions FROM public; @@ -64,9 +65,9 @@ COMMENT ON COLUMN audit.logged_actions.schema_name IS 'Database schema audited t COMMENT ON COLUMN audit.logged_actions.table_name IS 'Non-schema-qualified table name of table event occured in'; COMMENT ON COLUMN audit.logged_actions.relid IS 'Table OID. Changes with drop/create. Get with ''tablename''::regclass'; COMMENT ON COLUMN audit.logged_actions.session_user_name IS 'Login / session user whose statement caused the audited event'; -COMMENT ON COLUMN audit.logged_actions.action_tstamp_tx IS 'Transaction start timestamp for tx in which audited event occurred'; -COMMENT ON COLUMN audit.logged_actions.action_tstamp_stm IS 'Statement start timestamp for tx in which audited event occurred'; -COMMENT ON COLUMN audit.logged_actions.action_tstamp_clk IS 'Wall clock time at which audited event''s trigger call occurred'; +COMMENT ON COLUMN audit.logged_actions.action_tstamp_tx IS 'Transaction start epoch for tx in which audited event occurred'; +COMMENT ON COLUMN audit.logged_actions.action_tstamp_stm IS 'Statement start epoch for tx in which audited event occurred'; +COMMENT ON COLUMN audit.logged_actions.action_tstamp_clk IS 'Wall clock time epoch at which audited event''s trigger call occurred'; COMMENT ON COLUMN audit.logged_actions.transaction_id IS 'Identifier of transaction that made the change. May wrap, but unique paired with action_tstamp_tx.'; COMMENT ON COLUMN audit.logged_actions.client_addr IS 'IP address of client that issued query. Null for unix domain socket.'; COMMENT ON COLUMN audit.logged_actions.client_port IS 'Remote peer IP port address of client that issued query. Undefined for unix socket.'; @@ -89,29 +90,32 @@ DECLARE h_old hstore; h_new hstore; excluded_cols text[] = ARRAY[]::text[]; + precision int = 1000000; BEGIN IF TG_WHEN <> 'AFTER' THEN RAISE EXCEPTION 'audit.if_modified_func() may only run as an AFTER trigger'; END IF; audit_row = ROW( - nextval('audit.logged_actions_event_id_seq'), -- event_id - TG_TABLE_SCHEMA::text, -- schema_name - TG_TABLE_NAME::text, -- table_name - TG_RELID, -- relation OID for much quicker searches - session_user::text, -- session_user_name - current_timestamp, -- action_tstamp_tx - statement_timestamp(), -- action_tstamp_stm - clock_timestamp(), -- action_tstamp_clk - txid_current(), -- transaction ID - current_setting('application_name'), -- client application - inet_client_addr(), -- client_addr - inet_client_port(), -- client_port - current_query(), -- top-level query or queries (if multistatement) from client - substring(TG_OP,1,1), -- action - NULL, NULL, -- row_data, changed_fields - 'f' -- statement_only - ); + nextval('audit.logged_actions_event_id_seq'), -- event_id + TG_TABLE_SCHEMA::text, -- schema_name + TG_TABLE_NAME::text, -- table_name + TG_RELID, -- relation OID for much quicker searches + session_user::text, -- session_user_name + EXTRACT(EPOCH FROM current_timestamp) * precision, -- action_tstamp_tx + EXTRACT(EPOCH FROM statement_timestamp()) * precision, -- action_tstamp_stm + EXTRACT(EPOCH FROM clock_timestamp()) * precision, -- action_tstamp_clk + txid_current(), -- transaction ID + current_setting('application_name'), -- client application + inet_client_addr(), -- client_addr + inet_client_port(), -- client_port + current_query(), -- top-level query or queries (if multistatement) from client + substring(TG_OP,1,1), -- action + NULL, -- row_data + NULL, -- changed_fields + 'f', -- statement_only + NULL -- fields to store abritary info + ); IF NOT TG_ARGV[0]::boolean IS DISTINCT FROM 'f'::boolean THEN audit_row.client_query = NULL; diff --git a/audit_listen.sql b/audit_listen.sql new file mode 100644 index 0000000..eeccaec --- /dev/null +++ b/audit_listen.sql @@ -0,0 +1,20 @@ +CREATE OR REPLACE FUNCTION audit.notify_audit_logged_action_channel() + RETURNS TRIGGER AS $$ +DECLARE + payload json; +BEGIN + payload := json_build_object( + 'event_id', NEW.event_id, + 'schema_name', NEW.schema_name, + 'table_name', NEW.table_name, + 'action', NEW.action, + 'statement_only', NEW.statement_only + ); + PERFORM pg_notify('audit_logged_action', payload::text); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE TRIGGER trigger_notify_action_channel +AFTER INSERT ON audit.logged_actions +FOR EACH ROW EXECUTE FUNCTION audit.notify_audit_logged_action_channel(); \ No newline at end of file