Files
qbit-secured-data/docs/ddl/secured-data-postgres.sql
T
james.maesandClaude Opus 5.5 0963b45467 feat: classification, policy, tier resolver and access log (phase b)
Tables secured_data_classification, _policy, _reveal_grant, _break_glass
and _access_log; SecuredDataClassifications; the tier resolver with
break-glass, table features, policy rows, channel caps and code defaults;
the asynchronous access log; enumeration guardrails (rate limit, miss and
distinct-value alerts, minimum entropy). Builds on QQQ RC.5 with marked
stand-ins for the unreleased fieldaccess API and a reflective bridge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:03:57 -05:00

96 lines
3.5 KiB
SQL

-- PostgreSQL 15+ (NULLS NOT DISTINCT makes the table-wide classification unique).
-- Extra value-free fingerprint column supports enumeration detection across restarts/nodes.
CREATE TABLE secured_data_classification (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
table_name VARCHAR(255) NOT NULL,
field_name VARCHAR(255),
classification VARCHAR(255) NOT NULL,
mask_style VARCHAR(255),
low_cardinality BOOLEAN,
notes TEXT,
create_date TIMESTAMP WITH TIME ZONE,
modify_date TIMESTAMP WITH TIME ZONE,
modified_by VARCHAR(255),
CONSTRAINT secured_data_classification_target UNIQUE NULLS NOT DISTINCT (table_name, field_name)
);
CREATE TABLE secured_data_policy (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR(255) NOT NULL,
permission_name VARCHAR(255) NOT NULL,
classification VARCHAR(255) NOT NULL,
table_name VARCHAR(255),
tier VARCHAR(255) NOT NULL,
reveal_minutes INTEGER DEFAULT 5,
daily_reveal_cap INTEGER,
reason_options TEXT,
allow_free_text_reason BOOLEAN DEFAULT true,
require_free_text BOOLEAN DEFAULT false,
client_channel VARCHAR(255),
exact_match_per_hour INTEGER DEFAULT 20,
miss_alert_consecutive INTEGER DEFAULT 5,
distinct_value_alert INTEGER DEFAULT 15,
min_entropy_enabled BOOLEAN DEFAULT true,
is_active BOOLEAN DEFAULT true,
create_date TIMESTAMP WITH TIME ZONE,
modify_date TIMESTAMP WITH TIME ZONE,
modified_by VARCHAR(255),
CHECK (reveal_minutes BETWEEN 1 AND 60),
CHECK (exact_match_per_hour >= 0 AND miss_alert_consecutive >= 0 AND distinct_value_alert >= 0)
);
CREATE TABLE secured_data_reveal_grant (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
user_id VARCHAR(255) NOT NULL,
user_name VARCHAR(255),
table_name VARCHAR(255) NOT NULL,
record_id VARCHAR(255) NOT NULL,
field_name VARCHAR(255) NOT NULL,
reason VARCHAR(255),
reason_text TEXT,
granted_at TIMESTAMP WITH TIME ZONE NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
access_log_id INTEGER NOT NULL
);
CREATE TABLE secured_data_break_glass (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
user_id VARCHAR(255) NOT NULL,
user_name VARCHAR(255),
reason VARCHAR(255),
reason_text TEXT,
classification_ceiling VARCHAR(255) DEFAULT 'PHI',
table_name VARCHAR(255),
started_at TIMESTAMP WITH TIME ZONE NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
ended_at TIMESTAMP WITH TIME ZONE,
ended_by VARCHAR(255),
status VARCHAR(255) DEFAULT 'ACTIVE'
);
CREATE TABLE secured_data_access_log (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
event_type VARCHAR(255) NOT NULL,
user_id VARCHAR(255),
user_name VARCHAR(255),
table_name VARCHAR(255) NOT NULL,
record_id TEXT,
field_name TEXT,
record_count INTEGER,
action_name VARCHAR(255),
reason VARCHAR(255),
reason_text TEXT,
client_channel VARCHAR(255),
outcome VARCHAR(255),
anomaly_flag BOOLEAN DEFAULT false,
break_glass_id INTEGER,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
search_value_fingerprints TEXT
);
CREATE INDEX secured_data_policy_lookup ON secured_data_policy (is_active, permission_name, classification, table_name);
CREATE INDEX secured_data_break_glass_lookup ON secured_data_break_glass (user_id, status, expires_at);
CREATE INDEX secured_data_reveal_lookup ON secured_data_reveal_grant (user_id, table_name, field_name, record_id, expires_at);
CREATE INDEX secured_data_access_lookup ON secured_data_access_log (user_id, table_name, field_name, event_type, created_at);