@team-semicolon/semicolony-cli 4.18.102 → 4.18.103
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundle.js +482 -478
- package/migrations/197_ops_dashboard_asset_credential.sql +140 -0
- package/package.json +1 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
-- 197_ops_dashboard_asset_credential.sql
|
|
2
|
+
--
|
|
3
|
+
-- SEMO Ops 대시보드가 팀 자산 게이트웨이를 호출하기 위한 디바이스 비의존 자격증명 발급기.
|
|
4
|
+
--
|
|
5
|
+
-- 배경: 게이트웨이의 `authorizeRead`(packages/kb-gateway/src/lib/workspace-assets.ts)는
|
|
6
|
+
-- `ctx.kind === 'worker'` + `workspace:assets-read` scope + 프로젝트별 grant 행을 요구한다.
|
|
7
|
+
-- 대시보드는 워커 디바이스가 아니고, 기존 내부 HMAC 경로는 `kind:'internal'` 이라 자산
|
|
8
|
+
-- 라우트에서 전부 403 이다. 그래서 대시보드 전용 principal 이 필요하다.
|
|
9
|
+
--
|
|
10
|
+
-- 왜 기존 발급기를 못 쓰나: `issue_worker_gateway_credential` 은 활성 `worker_devices` 행
|
|
11
|
+
-- (doctor pass + 24h 내 last_seen)을 `FOR UPDATE` 로 잡고, 없으면 P0002 로 실패한다.
|
|
12
|
+
-- 대시보드는 디바이스 등록·doctor 보고를 하는 주체가 아니므로 그 전제를 만족할 수 없다.
|
|
13
|
+
--
|
|
14
|
+
-- 왜 이게 안전한가: 자격증명 해석기(kb-gateway `tenant-credentials.ts`)는 `worker_devices` 를
|
|
15
|
+
-- 조회하지 않는다. `metadata.credential_kind='worker'` 와 grant 행만 본다. 즉 디바이스 요구는
|
|
16
|
+
-- 발급 측 정책이지 검증 측 불변식이 아니다. 이 함수는 그 정책만 다르게 두고 나머지 검증
|
|
17
|
+
-- (scope 화이트리스트, 토큰 형식, 만료 상한, grant 모양)은 기존 발급기와 동일하게 유지한다.
|
|
18
|
+
--
|
|
19
|
+
-- 경계: 실제 워커 디바이스와 절대 충돌하지 않도록, 같은 worker/device 이름의 `worker_devices`
|
|
20
|
+
-- 행이 존재하면 발급을 거부한다. 그리고 이 발급기는 `workspace:assets-read` 외의 scope 를
|
|
21
|
+
-- 허용하지 않는다 — 쓰기 승격은 기존 `grant_worker_asset_write()` 로만 한다(감사 amendment 경유).
|
|
22
|
+
--
|
|
23
|
+
-- Safety: additive only. Runner가 BEGIN/COMMIT으로 감싸므로 트랜잭션 구문을 넣지 않는다.
|
|
24
|
+
-- `semicolony.` 접두는 러너의 retargetSchemaSql 이 활성 스키마로 바꾼다(중앙 DB 에서는 semo_ops).
|
|
25
|
+
|
|
26
|
+
CREATE OR REPLACE FUNCTION semicolony.issue_ops_dashboard_gateway_credential(
|
|
27
|
+
p_team_tenant_id uuid,
|
|
28
|
+
p_token_hash text,
|
|
29
|
+
p_token_prefix text,
|
|
30
|
+
p_project_id text,
|
|
31
|
+
p_issued_by text,
|
|
32
|
+
p_expires_days integer
|
|
33
|
+
)
|
|
34
|
+
RETURNS uuid
|
|
35
|
+
LANGUAGE plpgsql
|
|
36
|
+
SECURITY DEFINER
|
|
37
|
+
SET search_path = pg_catalog
|
|
38
|
+
AS $function$
|
|
39
|
+
DECLARE
|
|
40
|
+
v_principal constant text := 'semo-ops-dashboard';
|
|
41
|
+
v_credential_id uuid;
|
|
42
|
+
v_previous_credential_id uuid;
|
|
43
|
+
BEGIN
|
|
44
|
+
IF p_token_hash IS NULL OR p_token_hash !~ '^[0-9a-f]{64}$' THEN
|
|
45
|
+
RAISE EXCEPTION 'invalid ops dashboard credential token hash' USING ERRCODE = '22023';
|
|
46
|
+
END IF;
|
|
47
|
+
IF p_token_prefix IS NULL
|
|
48
|
+
OR p_token_prefix !~ ('^wck_' || v_principal || '_[A-Za-z0-9_-]{6}$') THEN
|
|
49
|
+
RAISE EXCEPTION 'invalid ops dashboard credential token prefix' USING ERRCODE = '22023';
|
|
50
|
+
END IF;
|
|
51
|
+
IF p_project_id IS NULL OR p_project_id !~ '^[a-z][a-z0-9-]{0,99}$' THEN
|
|
52
|
+
RAISE EXCEPTION 'invalid project id' USING ERRCODE = '22023';
|
|
53
|
+
END IF;
|
|
54
|
+
IF p_issued_by IS NULL OR p_issued_by <> btrim(p_issued_by)
|
|
55
|
+
OR btrim(p_issued_by) = '' OR char_length(p_issued_by) > 128
|
|
56
|
+
OR p_issued_by ~ '[[:cntrl:]]' THEN
|
|
57
|
+
RAISE EXCEPTION 'invalid credential issuer' USING ERRCODE = '22023';
|
|
58
|
+
END IF;
|
|
59
|
+
IF p_expires_days IS NULL OR p_expires_days < 1 OR p_expires_days > 90 THEN
|
|
60
|
+
RAISE EXCEPTION 'credential expiry must be between 1 and 90 days' USING ERRCODE = '22023';
|
|
61
|
+
END IF;
|
|
62
|
+
|
|
63
|
+
-- 실제 워커 디바이스와의 이름 충돌을 구조적으로 차단한다. 충돌하면 이 함수가 발급한
|
|
64
|
+
-- 자격증명이 워커 회전 로직에 휩쓸리거나 그 반대가 될 수 있다.
|
|
65
|
+
IF EXISTS (
|
|
66
|
+
SELECT 1 FROM semicolony.worker_devices AS wd
|
|
67
|
+
WHERE wd.worker_id = v_principal OR wd.device_id = v_principal
|
|
68
|
+
) THEN
|
|
69
|
+
RAISE EXCEPTION 'a worker device already claims the ops dashboard principal'
|
|
70
|
+
USING ERRCODE = '23505';
|
|
71
|
+
END IF;
|
|
72
|
+
|
|
73
|
+
PERFORM t.id
|
|
74
|
+
FROM public.tenants AS t
|
|
75
|
+
WHERE t.id = p_team_tenant_id
|
|
76
|
+
AND t.slug = 'team-semicolon'
|
|
77
|
+
AND t.tenant_type = 'team'
|
|
78
|
+
AND coalesce(t.metadata ->> 'status', 'active') = 'active';
|
|
79
|
+
IF NOT FOUND THEN
|
|
80
|
+
RAISE EXCEPTION 'active team-semicolon tenant not found' USING ERRCODE = 'P0002';
|
|
81
|
+
END IF;
|
|
82
|
+
|
|
83
|
+
-- 회전: 이전 활성 대시보드 자격증명을 폐기한다. 워커 디바이스 행을 건드리지 않는다.
|
|
84
|
+
SELECT gc.id
|
|
85
|
+
INTO v_previous_credential_id
|
|
86
|
+
FROM semicolony.gateway_credentials AS gc
|
|
87
|
+
WHERE gc.status = 'active'
|
|
88
|
+
AND gc.metadata ->> 'principal_class' = 'ops-dashboard'
|
|
89
|
+
AND gc.metadata ->> 'worker_id' = v_principal
|
|
90
|
+
ORDER BY gc.issued_at DESC
|
|
91
|
+
LIMIT 1
|
|
92
|
+
FOR UPDATE;
|
|
93
|
+
|
|
94
|
+
INSERT INTO semicolony.gateway_credentials (
|
|
95
|
+
tenant_id, tenant_slug, token_hash, token_prefix, scopes, issued_by, expires_at, metadata
|
|
96
|
+
) VALUES (
|
|
97
|
+
p_team_tenant_id,
|
|
98
|
+
'team-semicolon',
|
|
99
|
+
p_token_hash,
|
|
100
|
+
p_token_prefix,
|
|
101
|
+
ARRAY['workspace:assets-read']::text[],
|
|
102
|
+
p_issued_by,
|
|
103
|
+
clock_timestamp() + make_interval(days => p_expires_days),
|
|
104
|
+
jsonb_build_object(
|
|
105
|
+
'credential_kind', 'worker',
|
|
106
|
+
'principal_class', 'ops-dashboard',
|
|
107
|
+
'worker_id', v_principal,
|
|
108
|
+
'device_id', v_principal,
|
|
109
|
+
'tenant_anchor', 'team-semicolon',
|
|
110
|
+
'lineage_status', 'active'
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
RETURNING id INTO v_credential_id;
|
|
114
|
+
|
|
115
|
+
-- assets-read grant 는 환경 바인딩이 없어야 한다(192 의 environment 계약).
|
|
116
|
+
INSERT INTO semicolony.workspace_projection_grants (
|
|
117
|
+
gateway_credential_id, project_id, capability, environments
|
|
118
|
+
) VALUES (
|
|
119
|
+
v_credential_id, p_project_id, 'assets-read', ARRAY[]::text[]
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
IF v_previous_credential_id IS NOT NULL THEN
|
|
123
|
+
UPDATE semicolony.gateway_credentials
|
|
124
|
+
SET status = 'revoked',
|
|
125
|
+
revoked_at = clock_timestamp(),
|
|
126
|
+
revoked_reason = 'ops dashboard credential rotated'
|
|
127
|
+
WHERE id = v_previous_credential_id
|
|
128
|
+
AND status = 'active';
|
|
129
|
+
END IF;
|
|
130
|
+
|
|
131
|
+
RETURN v_credential_id;
|
|
132
|
+
END;
|
|
133
|
+
$function$;
|
|
134
|
+
|
|
135
|
+
REVOKE ALL ON FUNCTION semicolony.issue_ops_dashboard_gateway_credential(
|
|
136
|
+
uuid, text, text, text, text, integer
|
|
137
|
+
) FROM PUBLIC, sc_app, sc_provider;
|
|
138
|
+
GRANT EXECUTE ON FUNCTION semicolony.issue_ops_dashboard_gateway_credential(
|
|
139
|
+
uuid, text, text, text, text, integer
|
|
140
|
+
) TO sc_app;
|