feat: implement T-019 reliable event ingress
Harness governance / validate (pull_request) Has been cancelled
Harness governance / validate (pull_request) Has been cancelled
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
-- Bell v5 Brain event ingress identity bindings and durable receipts.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bell.event_ingress_bindings (
|
||||
producer_id text NOT NULL,
|
||||
tenant_id bigint NOT NULL,
|
||||
site_id bigint NOT NULL,
|
||||
device_id bigint NOT NULL,
|
||||
logical_tenant_id text NOT NULL,
|
||||
logical_site_id text NOT NULL,
|
||||
logical_device_id text NOT NULL,
|
||||
logical_area_id text NOT NULL,
|
||||
modality text NOT NULL,
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
PRIMARY KEY (producer_id, tenant_id, site_id, device_id),
|
||||
CONSTRAINT bell_ingress_binding_site_fk
|
||||
FOREIGN KEY (logical_tenant_id, logical_site_id)
|
||||
REFERENCES bell.sites(tenant_id, id),
|
||||
CONSTRAINT bell_ingress_binding_area_fk
|
||||
FOREIGN KEY (logical_tenant_id, logical_area_id)
|
||||
REFERENCES bell.areas(tenant_id, id),
|
||||
CONSTRAINT bell_ingress_binding_device_fk
|
||||
FOREIGN KEY (logical_tenant_id, logical_site_id, logical_device_id)
|
||||
REFERENCES sense.devices(tenant_id, site_id, id),
|
||||
CONSTRAINT bell_ingress_binding_numeric_ids CHECK (
|
||||
tenant_id >= 1 AND site_id >= 1 AND device_id >= 1
|
||||
),
|
||||
CONSTRAINT bell_ingress_binding_producer CHECK (
|
||||
char_length(producer_id) BETWEEN 1 AND 64
|
||||
AND producer_id ~ '^[A-Za-z0-9][A-Za-z0-9._-]*$'
|
||||
),
|
||||
CONSTRAINT bell_ingress_binding_logical_ids CHECK (
|
||||
btrim(logical_tenant_id) <> '' AND btrim(logical_site_id) <> ''
|
||||
AND btrim(logical_device_id) <> '' AND btrim(logical_area_id) <> ''
|
||||
),
|
||||
CONSTRAINT bell_ingress_binding_modality CHECK (
|
||||
modality IN ('video', 'radar', 'contact', 'button', 'wearable', 'other')
|
||||
)
|
||||
);
|
||||
ALTER TABLE bell.event_ingress_bindings OWNER TO bell_app;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS bell_event_ingress_binding_numeric_idx
|
||||
ON bell.event_ingress_bindings(tenant_id, site_id, device_id)
|
||||
WHERE enabled;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bell.event_ingress_receipts (
|
||||
producer_id text NOT NULL,
|
||||
source_event_id text NOT NULL,
|
||||
candidate_hash bytea NOT NULL,
|
||||
event_id text NOT NULL REFERENCES bell.events(id),
|
||||
received_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
PRIMARY KEY (producer_id, source_event_id),
|
||||
CONSTRAINT bell_ingress_receipt_producer CHECK (
|
||||
char_length(producer_id) BETWEEN 1 AND 64
|
||||
AND producer_id ~ '^[A-Za-z0-9][A-Za-z0-9._-]*$'
|
||||
),
|
||||
CONSTRAINT bell_ingress_receipt_source CHECK (
|
||||
source_event_id ~ '^[A-Za-z0-9_-]{1,128}$'
|
||||
),
|
||||
CONSTRAINT bell_ingress_receipt_hash CHECK (octet_length(candidate_hash) = 32),
|
||||
CONSTRAINT bell_ingress_receipt_event_id CHECK (
|
||||
event_id ~ '^evt_[0-9A-HJKMNP-TV-Z]{26}$'
|
||||
)
|
||||
);
|
||||
ALTER TABLE bell.event_ingress_receipts OWNER TO bell_app;
|
||||
|
||||
DROP TRIGGER IF EXISTS bell_event_ingress_receipts_immutable
|
||||
ON bell.event_ingress_receipts;
|
||||
CREATE TRIGGER bell_event_ingress_receipts_immutable
|
||||
BEFORE UPDATE OR DELETE ON bell.event_ingress_receipts
|
||||
FOR EACH ROW EXECUTE FUNCTION bell.reject_immutable_change();
|
||||
|
||||
CREATE INDEX IF NOT EXISTS bell_event_ingress_receipt_event_idx
|
||||
ON bell.event_ingress_receipts(event_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bell.event_ingress_nonces (
|
||||
key_id text NOT NULL,
|
||||
nonce text NOT NULL,
|
||||
request_hash bytea NOT NULL,
|
||||
response_status integer NOT NULL,
|
||||
response_body jsonb NOT NULL,
|
||||
received_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
expires_at timestamptz NOT NULL,
|
||||
PRIMARY KEY (key_id, nonce),
|
||||
CONSTRAINT bell_ingress_nonce_key CHECK (
|
||||
char_length(key_id) BETWEEN 1 AND 64
|
||||
AND key_id ~ '^[A-Za-z0-9][A-Za-z0-9._-]*$'
|
||||
),
|
||||
CONSTRAINT bell_ingress_nonce_value CHECK (
|
||||
char_length(nonce) BETWEEN 22 AND 64
|
||||
AND nonce ~ '^[A-Za-z0-9_-]+$'
|
||||
),
|
||||
CONSTRAINT bell_ingress_nonce_hash CHECK (octet_length(request_hash) = 32),
|
||||
CONSTRAINT bell_ingress_nonce_status CHECK (response_status IN (200, 201)),
|
||||
CONSTRAINT bell_ingress_nonce_body CHECK (jsonb_typeof(response_body) = 'object'),
|
||||
CONSTRAINT bell_ingress_nonce_ttl CHECK (
|
||||
expires_at >= received_at + interval '10 minutes'
|
||||
AND expires_at <= received_at + interval '11 minutes'
|
||||
)
|
||||
);
|
||||
ALTER TABLE bell.event_ingress_nonces OWNER TO bell_app;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS bell_event_ingress_nonce_expiry_idx
|
||||
ON bell.event_ingress_nonces(expires_at, key_id, nonce);
|
||||
|
||||
INSERT INTO bell.schema_migrations(version) VALUES (5)
|
||||
ON CONFLICT (version) DO NOTHING;
|
||||
@@ -0,0 +1,25 @@
|
||||
-- Bell runtime may read controlled identity bindings, append events/source
|
||||
-- receipts and maintain only short-lived nonce receipts.
|
||||
|
||||
REVOKE ALL ON TABLE bell.event_ingress_bindings,
|
||||
bell.event_ingress_receipts, bell.event_ingress_nonces FROM PUBLIC;
|
||||
REVOKE ALL ON TABLE bell.event_ingress_bindings,
|
||||
bell.event_ingress_receipts, bell.event_ingress_nonces FROM bell_runtime;
|
||||
|
||||
GRANT SELECT ON TABLE bell.event_ingress_bindings TO bell_runtime;
|
||||
GRANT SELECT, INSERT ON TABLE bell.event_ingress_receipts TO bell_runtime;
|
||||
GRANT SELECT, INSERT, DELETE ON TABLE bell.event_ingress_nonces TO bell_runtime;
|
||||
|
||||
-- Column grants deliberately exclude endpoint_ref, credential_ref,
|
||||
-- profile_token and path_name.
|
||||
GRANT USAGE ON SCHEMA sense TO bell_runtime;
|
||||
REVOKE ALL ON TABLE sense.devices FROM bell_runtime;
|
||||
GRANT SELECT (id, tenant_id, site_id, area_id, modality)
|
||||
ON TABLE sense.devices TO bell_runtime;
|
||||
GRANT SELECT (tenant_id, id, deleted_at)
|
||||
ON TABLE bell.sites TO bell_runtime;
|
||||
GRANT SELECT (tenant_id, site_id, id, capture_policy, deleted_at)
|
||||
ON TABLE bell.areas TO bell_runtime;
|
||||
|
||||
REVOKE ALL ON TABLE bell.event_ingress_bindings,
|
||||
bell.event_ingress_receipts, bell.event_ingress_nonces FROM sense_app;
|
||||
@@ -1,6 +1,6 @@
|
||||
# YoVision PostgreSQL 初始化
|
||||
|
||||
本目录实现 T-009~T-012、T-015~T-016 的 PostgreSQL `17.10` schema。SQL 必须按文件名前缀顺序执行:`001`~`004` 创建 NOLOGIN 权限角色、Bell/Sense 初始对象和配额权限;`005`~`007` 增量增加 Area 与本地审计;`008`~`009` 增加 Control API 状态;`010`~`011` 增加调和 fencing、MediaMTX Path 历史归属、孤儿报告/受控处置结果;`012`~`013` 增加 Bell 不可变事件、append-only outcome 和独立 `bell_runtime` 最小权限;`014`~`015` 增加审计 Outbox relay fencing、Bell 全局审计事实、短期防重收据和双方最小权限。全部 SQL 可重放。对象 owner/迁移角色为 `bell_app`/`sense_app`;应用登录角色及密码由部署环境或密钥系统创建,Sense 登录加入 `sense_app`,Bell 运行登录只加入 `bell_runtime`,仓库不保存登录凭据。
|
||||
本目录实现 T-009~T-012、T-015~T-016、T-019 的 PostgreSQL `17.10` schema。SQL 必须按文件名前缀顺序执行:`001`~`004` 创建 NOLOGIN 权限角色、Bell/Sense 初始对象和配额权限;`005`~`007` 增量增加 Area 与本地审计;`008`~`009` 增加 Control API 状态;`010`~`011` 增加调和 fencing、MediaMTX Path 历史归属、孤儿报告/受控处置结果;`012`~`013` 增加 Bell 不可变事件、append-only outcome 和独立 `bell_runtime` 最小权限;`014`~`015` 增加审计 relay;`016`~`017` 增加 Brain 事件身份绑定、永久来源收据、短期 nonce 和最小权限。全部 SQL 可重放。对象 owner/迁移角色为 `bell_app`/`sense_app`;应用登录角色及密码由部署环境或密钥系统创建,Sense 登录加入 `sense_app`,Bell 运行登录只加入 `bell_runtime`,仓库不保存登录凭据。
|
||||
|
||||
生产/共享实例必须由管理员先备份并在 YoVision 专用数据库中执行。Sense 进程不会用高权限自动建库或建角色。示例只使用私有环境变量,不把实际 DSN 写入脚本或日志:
|
||||
|
||||
@@ -23,6 +23,7 @@ Get-ChildItem deploy/postgres/[0-9][0-9][0-9]_*.sql |
|
||||
- `sense.device_operation_outbox` 是本地持久化队列,Bell 全局审计真相只写入 `bell.audit_events`。Sense 只领取/确认本地行,不获得 Bell 表权限;Bell 只通过签名 HTTP ingress 收取,不读取 Outbox。
|
||||
- `bell.audit_events` 与事件事实一样不可更新/删除;`bell.audit_relay_receipts` 只为 10 分钟 nonce 幂等窗口保留,Bell runtime 仅可在这张限定表中查询、插入和清理过期记录。
|
||||
- `bell.events` 与 `bell.event_outcomes` 由 `bell_app` 拥有;`bell_runtime` 只获得 `SELECT/INSERT`,没有 owner、`UPDATE`、`DELETE` 或 `TRUNCATE` 权限,数据库 trigger 再拒绝 owner 路径的意外事实改写。
|
||||
- `bell.event_ingress_bindings` 由管理员维护并引用当前 Bell Site/Area 和 Sense Device;Bell runtime 只读绑定及 Sense 设备的逻辑 ID/Area/modality 列,不可读取 endpoint、credential、profile token 或 MediaMTX path。`event_ingress_receipts` 永久只追加;只有 `event_ingress_nonces` 可按 10 分钟 TTL 清理。
|
||||
- `sense.control_idempotency_receipts` 不保存原始 Idempotency-Key,只保存 scope/request SHA-256 和脱敏响应快照;`batch_operations`/items 只保存逻辑 ID、状态和稳定错误,不保存连接秘密。
|
||||
- 调和与孤儿租约使用 PostgreSQL `clock_timestamp()` 和 fencing token;过期 worker 不能提交完成/失败或扫描报告。`media_path_ownership`、扫描和处置表不保存 endpoint、credential 或 source URI;数据库约束禁止为 `unowned` finding 写删除结果。
|
||||
- PUBLIC 对两个业务 schema 的表和函数没有权限。
|
||||
|
||||
@@ -71,7 +71,7 @@ BEGIN
|
||||
OR NOT has_table_privilege('sense_app', 'sense.device_operation_outbox', 'DELETE') THEN
|
||||
RAISE EXCEPTION 'sense_app lacks access to its local audit Outbox';
|
||||
END IF;
|
||||
IF (SELECT max(version) FROM bell.schema_migrations) <> 4
|
||||
IF (SELECT max(version) FROM bell.schema_migrations) <> 5
|
||||
OR (SELECT max(version) FROM sense.schema_migrations) <> 6 THEN
|
||||
RAISE EXCEPTION 'schema migration version drift';
|
||||
END IF;
|
||||
@@ -228,3 +228,29 @@ BEGIN
|
||||
END IF;
|
||||
END
|
||||
$audit_relay$;
|
||||
|
||||
DO $event_ingress$
|
||||
BEGIN
|
||||
IF NOT has_table_privilege('yovision_t015_bell', 'bell.event_ingress_bindings', 'SELECT')
|
||||
OR has_table_privilege('yovision_t015_bell', 'bell.event_ingress_bindings', 'INSERT,UPDATE,DELETE,TRUNCATE')
|
||||
OR NOT has_table_privilege('yovision_t015_bell', 'bell.event_ingress_receipts', 'SELECT,INSERT')
|
||||
OR has_table_privilege('yovision_t015_bell', 'bell.event_ingress_receipts', 'UPDATE,DELETE,TRUNCATE')
|
||||
OR NOT has_table_privilege('yovision_t015_bell', 'bell.event_ingress_nonces', 'SELECT,INSERT,DELETE')
|
||||
OR has_table_privilege('yovision_t015_bell', 'bell.event_ingress_nonces', 'UPDATE,TRUNCATE') THEN
|
||||
RAISE EXCEPTION 'Bell runtime violates event ingress privileges';
|
||||
END IF;
|
||||
IF NOT has_column_privilege('yovision_t015_bell', 'sense.devices', 'id', 'SELECT')
|
||||
OR NOT has_column_privilege('yovision_t015_bell', 'sense.devices', 'area_id', 'SELECT')
|
||||
OR has_column_privilege('yovision_t015_bell', 'sense.devices', 'endpoint_ref', 'SELECT')
|
||||
OR has_column_privilege('yovision_t015_bell', 'sense.devices', 'credential_ref', 'SELECT')
|
||||
OR has_column_privilege('yovision_t015_bell', 'sense.devices', 'profile_token', 'SELECT')
|
||||
OR has_column_privilege('yovision_t015_bell', 'sense.devices', 'path_name', 'SELECT') THEN
|
||||
RAISE EXCEPTION 'Bell runtime Sense device projection privilege drift';
|
||||
END IF;
|
||||
IF has_table_privilege('public', 'bell.event_ingress_bindings', 'SELECT,INSERT,UPDATE,DELETE,TRUNCATE')
|
||||
OR has_table_privilege('public', 'bell.event_ingress_receipts', 'SELECT,INSERT,UPDATE,DELETE,TRUNCATE')
|
||||
OR has_table_privilege('public', 'bell.event_ingress_nonces', 'SELECT,INSERT,UPDATE,DELETE,TRUNCATE') THEN
|
||||
RAISE EXCEPTION 'Bell event ingress tables leaked to PUBLIC';
|
||||
END IF;
|
||||
END
|
||||
$event_ingress$;
|
||||
|
||||
Reference in New Issue
Block a user