refactor(access): 统一分层白名单权限语义

取消跨主体专属占用,按租户、用户组、用户、当前 API Key 和 scope 分层求交,并在任务落库前统一校验候选。\n\n增加旧 allow 规则归档清理迁移、脱敏审计工具和回滚运行手册,补齐主体隔离、deny 优先及列表与运行时一致性测试。
This commit is contained in:
2026-08-03 15:43:49 +08:00
parent c9393af43a
commit 7376d6fab6
20 changed files with 1490 additions and 513 deletions
@@ -0,0 +1,147 @@
CREATE TABLE IF NOT EXISTS gateway_access_rule_allow_archive (
migration_batch text NOT NULL,
rule_id uuid NOT NULL,
subject_type text NOT NULL,
subject_id uuid NOT NULL,
resource_type text NOT NULL,
resource_id uuid NOT NULL,
effect text NOT NULL,
priority integer NOT NULL,
min_permission_level integer NOT NULL,
conditions jsonb NOT NULL,
metadata jsonb NOT NULL,
status text NOT NULL,
rule_created_at timestamptz NOT NULL,
rule_updated_at timestamptz NOT NULL,
row_sha256 text NOT NULL CHECK (row_sha256 ~ '^[0-9a-f]{64}$'),
archived_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (migration_batch, rule_id)
);
CREATE INDEX IF NOT EXISTS idx_gateway_access_rule_allow_archive_subject
ON gateway_access_rule_allow_archive(migration_batch, subject_type, subject_id);
CREATE TABLE IF NOT EXISTS gateway_access_rule_migration_batches (
migration_batch text PRIMARY KEY,
allow_count bigint NOT NULL,
allow_sha256 text NOT NULL CHECK (allow_sha256 ~ '^[0-9a-f]{64}$'),
deny_count bigint NOT NULL,
deny_sha256 text NOT NULL CHECK (deny_sha256 ~ '^[0-9a-f]{64}$'),
archived_at timestamptz NOT NULL DEFAULT now()
);
-- Reads stay available while access-rule writes are paused for the atomic
-- snapshot, archive, and cleanup sequence.
LOCK TABLE gateway_access_rules IN SHARE ROW EXCLUSIVE MODE;
INSERT INTO gateway_access_rule_migration_batches (
migration_batch, allow_count, allow_sha256, deny_count, deny_sha256
)
SELECT
'0102_access_rule_allow_whitelist_semantics',
COUNT(*) FILTER (WHERE effect = 'allow'),
encode(digest(COALESCE(string_agg(
encode(digest(concat_ws(chr(31),
id::text, subject_type, subject_id::text, resource_type,
resource_id::text, effect, priority::text,
min_permission_level::text, conditions::text, metadata::text,
status, extract(epoch FROM created_at)::text,
extract(epoch FROM updated_at)::text
), 'sha256'), 'hex'), chr(10) ORDER BY id
) FILTER (WHERE effect = 'allow'), ''), 'sha256'), 'hex'),
COUNT(*) FILTER (WHERE effect = 'deny'),
encode(digest(COALESCE(string_agg(
encode(digest(concat_ws(chr(31),
id::text, subject_type, subject_id::text, resource_type,
resource_id::text, effect, priority::text,
min_permission_level::text, conditions::text, metadata::text,
status, extract(epoch FROM created_at)::text,
extract(epoch FROM updated_at)::text
), 'sha256'), 'hex'), chr(10) ORDER BY id
) FILTER (WHERE effect = 'deny'), ''), 'sha256'), 'hex')
FROM gateway_access_rules
ON CONFLICT (migration_batch) DO NOTHING;
INSERT INTO gateway_access_rule_allow_archive (
migration_batch, rule_id, subject_type, subject_id, resource_type,
resource_id, effect, priority, min_permission_level, conditions,
metadata, status, rule_created_at, rule_updated_at, row_sha256
)
SELECT
'0102_access_rule_allow_whitelist_semantics', id, subject_type, subject_id,
resource_type, resource_id, effect, priority, min_permission_level,
conditions, metadata, status, created_at, updated_at,
encode(digest(concat_ws(chr(31),
id::text, subject_type, subject_id::text, resource_type,
resource_id::text, effect, priority::text,
min_permission_level::text, conditions::text, metadata::text,
status, extract(epoch FROM created_at)::text,
extract(epoch FROM updated_at)::text
), 'sha256'), 'hex')
FROM gateway_access_rules
WHERE effect = 'allow'
ON CONFLICT (migration_batch, rule_id) DO NOTHING;
DO $$
DECLARE
expected_count bigint;
expected_sha256 text;
archived_count bigint;
archived_sha256 text;
BEGIN
SELECT allow_count, allow_sha256
INTO expected_count, expected_sha256
FROM gateway_access_rule_migration_batches
WHERE migration_batch = '0102_access_rule_allow_whitelist_semantics';
SELECT COUNT(*), encode(digest(COALESCE(string_agg(
row_sha256, chr(10) ORDER BY rule_id
), ''), 'sha256'), 'hex')
INTO archived_count, archived_sha256
FROM gateway_access_rule_allow_archive
WHERE migration_batch = '0102_access_rule_allow_whitelist_semantics';
IF archived_count <> expected_count OR archived_sha256 <> expected_sha256 THEN
RAISE EXCEPTION 'access-rule allow archive verification failed';
END IF;
END
$$;
DELETE FROM gateway_access_rules
WHERE effect = 'allow';
DO $$
DECLARE
expected_deny_count bigint;
expected_deny_sha256 text;
actual_allow_count bigint;
actual_deny_count bigint;
actual_deny_sha256 text;
BEGIN
SELECT deny_count, deny_sha256
INTO expected_deny_count, expected_deny_sha256
FROM gateway_access_rule_migration_batches
WHERE migration_batch = '0102_access_rule_allow_whitelist_semantics';
SELECT
COUNT(*) FILTER (WHERE effect = 'allow'),
COUNT(*) FILTER (WHERE effect = 'deny'),
encode(digest(COALESCE(string_agg(
encode(digest(concat_ws(chr(31),
id::text, subject_type, subject_id::text, resource_type,
resource_id::text, effect, priority::text,
min_permission_level::text, conditions::text, metadata::text,
status, extract(epoch FROM created_at)::text,
extract(epoch FROM updated_at)::text
), 'sha256'), 'hex'), chr(10) ORDER BY id
) FILTER (WHERE effect = 'deny'), ''), 'sha256'), 'hex')
INTO actual_allow_count, actual_deny_count, actual_deny_sha256
FROM gateway_access_rules;
IF actual_allow_count <> 0 OR
actual_deny_count <> expected_deny_count OR
actual_deny_sha256 <> expected_deny_sha256 THEN
RAISE EXCEPTION 'access-rule whitelist migration verification failed';
END IF;
END
$$;