@team-semicolon/semicolony-cli 4.18.59 → 4.18.60
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.
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
-- 170_worker_release_credential_scopes.sql
|
|
2
|
+
--
|
|
3
|
+
-- Migration 169 added the worker release Gateway surface, but the
|
|
4
|
+
-- owner-controlled issuance function from migration 167 still rejected the
|
|
5
|
+
-- corresponding read/report scopes. Replace only that function and preserve
|
|
6
|
+
-- its validation, atomic rotation/device binding, and privilege boundary.
|
|
7
|
+
--
|
|
8
|
+
-- The migration runner supplies BEGIN/COMMIT. All object references are
|
|
9
|
+
-- schema-qualified and the SECURITY DEFINER search path is fixed.
|
|
10
|
+
|
|
11
|
+
CREATE OR REPLACE FUNCTION semicolony.issue_worker_gateway_credential(
|
|
12
|
+
p_team_tenant_id uuid,
|
|
13
|
+
p_worker_id text,
|
|
14
|
+
p_device_id text,
|
|
15
|
+
p_token_hash text,
|
|
16
|
+
p_token_prefix text,
|
|
17
|
+
p_scopes text[],
|
|
18
|
+
p_issued_by text,
|
|
19
|
+
p_expires_days integer,
|
|
20
|
+
p_workspace_grants jsonb
|
|
21
|
+
)
|
|
22
|
+
RETURNS uuid
|
|
23
|
+
LANGUAGE plpgsql
|
|
24
|
+
SECURITY DEFINER
|
|
25
|
+
SET search_path = pg_catalog
|
|
26
|
+
AS $function$
|
|
27
|
+
DECLARE
|
|
28
|
+
v_credential_id uuid;
|
|
29
|
+
v_previous_credential_id uuid;
|
|
30
|
+
v_workspace_grants jsonb := coalesce(p_workspace_grants, '[]'::jsonb);
|
|
31
|
+
v_grant jsonb;
|
|
32
|
+
v_project_id text;
|
|
33
|
+
v_capability text;
|
|
34
|
+
v_environments text[];
|
|
35
|
+
v_grant_key text;
|
|
36
|
+
v_seen_grant_keys text[] := ARRAY[]::text[];
|
|
37
|
+
v_has_credential_grant boolean := false;
|
|
38
|
+
v_has_asset_grant boolean := false;
|
|
39
|
+
BEGIN
|
|
40
|
+
IF p_worker_id IS NULL OR p_worker_id !~ '^[a-z][a-z0-9-]{0,63}$'
|
|
41
|
+
OR p_device_id IS NULL OR p_device_id !~ '^[a-z][a-z0-9-]{0,99}$' THEN
|
|
42
|
+
RAISE EXCEPTION 'invalid worker/device binding' USING ERRCODE = '22023';
|
|
43
|
+
END IF;
|
|
44
|
+
IF p_token_hash IS NULL OR p_token_hash !~ '^[0-9a-f]{64}$' THEN
|
|
45
|
+
RAISE EXCEPTION 'invalid worker credential token hash' USING ERRCODE = '22023';
|
|
46
|
+
END IF;
|
|
47
|
+
IF p_token_prefix IS NULL
|
|
48
|
+
OR p_token_prefix !~ ('^wck_' || p_worker_id || '_[A-Za-z0-9_-]{6}$') THEN
|
|
49
|
+
RAISE EXCEPTION 'invalid worker credential token prefix' USING ERRCODE = '22023';
|
|
50
|
+
END IF;
|
|
51
|
+
IF p_issued_by IS NULL OR p_issued_by <> btrim(p_issued_by)
|
|
52
|
+
OR btrim(p_issued_by) = '' OR char_length(p_issued_by) > 128
|
|
53
|
+
OR p_issued_by ~ '[[:cntrl:]]' THEN
|
|
54
|
+
RAISE EXCEPTION 'invalid credential issuer' USING ERRCODE = '22023';
|
|
55
|
+
END IF;
|
|
56
|
+
IF p_expires_days IS NULL OR p_expires_days < 1 OR p_expires_days > 90 THEN
|
|
57
|
+
RAISE EXCEPTION 'worker credential expiry must be between 1 and 90 days' USING ERRCODE = '22023';
|
|
58
|
+
END IF;
|
|
59
|
+
IF p_scopes IS NULL OR cardinality(p_scopes) = 0
|
|
60
|
+
OR EXISTS (
|
|
61
|
+
SELECT 1
|
|
62
|
+
FROM unnest(p_scopes) AS requested(scope)
|
|
63
|
+
WHERE requested.scope IS NULL
|
|
64
|
+
OR requested.scope <> ALL (ARRAY[
|
|
65
|
+
'kb:read',
|
|
66
|
+
'skills:read',
|
|
67
|
+
'skills:write',
|
|
68
|
+
'workspace:credentials-read',
|
|
69
|
+
'workspace:assets-read',
|
|
70
|
+
'worker:release:read',
|
|
71
|
+
'worker:release:report'
|
|
72
|
+
]::text[])
|
|
73
|
+
)
|
|
74
|
+
OR cardinality(p_scopes) <> (
|
|
75
|
+
SELECT count(DISTINCT requested.scope)::integer FROM unnest(p_scopes) AS requested(scope)
|
|
76
|
+
)
|
|
77
|
+
OR ('skills:write' = ANY(p_scopes) AND NOT ('skills:read' = ANY(p_scopes))) THEN
|
|
78
|
+
RAISE EXCEPTION 'worker credential scopes are not permitted' USING ERRCODE = '22023';
|
|
79
|
+
END IF;
|
|
80
|
+
IF jsonb_typeof(v_workspace_grants) <> 'array' THEN
|
|
81
|
+
RAISE EXCEPTION 'workspace grants must be a JSON array' USING ERRCODE = '22023';
|
|
82
|
+
END IF;
|
|
83
|
+
|
|
84
|
+
FOR v_grant IN SELECT item.value FROM jsonb_array_elements(v_workspace_grants) AS item(value) LOOP
|
|
85
|
+
IF jsonb_typeof(v_grant) <> 'object'
|
|
86
|
+
OR EXISTS (
|
|
87
|
+
SELECT 1
|
|
88
|
+
FROM jsonb_object_keys(v_grant) AS object_key(key)
|
|
89
|
+
WHERE object_key.key NOT IN ('projectId', 'capability', 'environments')
|
|
90
|
+
) THEN
|
|
91
|
+
RAISE EXCEPTION 'invalid workspace grant shape' USING ERRCODE = '22023';
|
|
92
|
+
END IF;
|
|
93
|
+
v_project_id := v_grant ->> 'projectId';
|
|
94
|
+
v_capability := v_grant ->> 'capability';
|
|
95
|
+
IF v_project_id IS NULL OR v_project_id !~ '^[a-z][a-z0-9-]{0,99}$'
|
|
96
|
+
OR v_capability NOT IN ('credentials-read', 'assets-read')
|
|
97
|
+
OR jsonb_typeof(v_grant -> 'environments') <> 'array' THEN
|
|
98
|
+
RAISE EXCEPTION 'invalid workspace grant' USING ERRCODE = '22023';
|
|
99
|
+
END IF;
|
|
100
|
+
v_environments := ARRAY(
|
|
101
|
+
SELECT jsonb_array_elements_text(v_grant -> 'environments')
|
|
102
|
+
);
|
|
103
|
+
IF cardinality(v_environments) <> (
|
|
104
|
+
SELECT count(DISTINCT environment.value)::integer
|
|
105
|
+
FROM unnest(v_environments) AS environment(value)
|
|
106
|
+
) OR NOT (v_environments <@ ARRAY['dev', 'e2e']::text[])
|
|
107
|
+
OR (v_capability = 'credentials-read' AND cardinality(v_environments) = 0)
|
|
108
|
+
OR (v_capability = 'assets-read' AND cardinality(v_environments) <> 0) THEN
|
|
109
|
+
RAISE EXCEPTION 'invalid workspace grant environments' USING ERRCODE = '22023';
|
|
110
|
+
END IF;
|
|
111
|
+
v_grant_key := v_project_id || ':' || v_capability;
|
|
112
|
+
IF v_grant_key = ANY(v_seen_grant_keys) THEN
|
|
113
|
+
RAISE EXCEPTION 'duplicate workspace grant' USING ERRCODE = '22023';
|
|
114
|
+
END IF;
|
|
115
|
+
v_seen_grant_keys := array_append(v_seen_grant_keys, v_grant_key);
|
|
116
|
+
v_has_credential_grant := v_has_credential_grant OR v_capability = 'credentials-read';
|
|
117
|
+
v_has_asset_grant := v_has_asset_grant OR v_capability = 'assets-read';
|
|
118
|
+
END LOOP;
|
|
119
|
+
IF v_has_credential_grant <> ('workspace:credentials-read' = ANY(p_scopes))
|
|
120
|
+
OR v_has_asset_grant <> ('workspace:assets-read' = ANY(p_scopes)) THEN
|
|
121
|
+
RAISE EXCEPTION 'workspace scopes must exactly match workspace grants' USING ERRCODE = '22023';
|
|
122
|
+
END IF;
|
|
123
|
+
|
|
124
|
+
PERFORM t.id
|
|
125
|
+
FROM public.tenants AS t
|
|
126
|
+
WHERE t.id = p_team_tenant_id
|
|
127
|
+
AND t.slug = 'team-semicolon'
|
|
128
|
+
AND t.tenant_type = 'team'
|
|
129
|
+
AND coalesce(t.metadata ->> 'status', 'active') = 'active';
|
|
130
|
+
IF NOT FOUND THEN
|
|
131
|
+
RAISE EXCEPTION 'active team-semicolon tenant not found' USING ERRCODE = 'P0002';
|
|
132
|
+
END IF;
|
|
133
|
+
|
|
134
|
+
SELECT wd.gateway_credential_id
|
|
135
|
+
INTO v_previous_credential_id
|
|
136
|
+
FROM semicolony.worker_devices AS wd
|
|
137
|
+
WHERE wd.device_id = p_device_id
|
|
138
|
+
AND wd.worker_id = p_worker_id
|
|
139
|
+
AND wd.status = 'active'
|
|
140
|
+
AND wd.kb_access_mode = 'worker-gateway'
|
|
141
|
+
AND wd.last_doctor_status = 'pass'
|
|
142
|
+
AND wd.last_seen_at >= clock_timestamp() - interval '24 hours'
|
|
143
|
+
FOR UPDATE;
|
|
144
|
+
IF NOT FOUND THEN
|
|
145
|
+
RAISE EXCEPTION 'active worker device binding not found' USING ERRCODE = 'P0002';
|
|
146
|
+
END IF;
|
|
147
|
+
|
|
148
|
+
INSERT INTO semicolony.gateway_credentials (
|
|
149
|
+
tenant_id, tenant_slug, token_hash, token_prefix, scopes, issued_by, expires_at, metadata
|
|
150
|
+
) VALUES (
|
|
151
|
+
p_team_tenant_id,
|
|
152
|
+
'team-semicolon',
|
|
153
|
+
p_token_hash,
|
|
154
|
+
p_token_prefix,
|
|
155
|
+
p_scopes,
|
|
156
|
+
p_issued_by,
|
|
157
|
+
clock_timestamp() + make_interval(days => p_expires_days),
|
|
158
|
+
jsonb_build_object(
|
|
159
|
+
'credential_kind', 'worker',
|
|
160
|
+
'worker_id', p_worker_id,
|
|
161
|
+
'device_id', p_device_id,
|
|
162
|
+
'tenant_anchor', 'team-semicolon',
|
|
163
|
+
'lineage_status', 'active'
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
RETURNING id INTO v_credential_id;
|
|
167
|
+
|
|
168
|
+
INSERT INTO semicolony.workspace_projection_grants (
|
|
169
|
+
gateway_credential_id, project_id, capability, environments
|
|
170
|
+
)
|
|
171
|
+
SELECT
|
|
172
|
+
v_credential_id,
|
|
173
|
+
item.value ->> 'projectId',
|
|
174
|
+
item.value ->> 'capability',
|
|
175
|
+
ARRAY(SELECT jsonb_array_elements_text(item.value -> 'environments'))
|
|
176
|
+
FROM jsonb_array_elements(v_workspace_grants) AS item(value);
|
|
177
|
+
|
|
178
|
+
IF v_previous_credential_id IS NOT NULL THEN
|
|
179
|
+
UPDATE semicolony.gateway_credentials
|
|
180
|
+
SET status = 'revoked',
|
|
181
|
+
revoked_at = clock_timestamp(),
|
|
182
|
+
revoked_reason = 'worker credential rotated'
|
|
183
|
+
WHERE id = v_previous_credential_id
|
|
184
|
+
AND status = 'active';
|
|
185
|
+
END IF;
|
|
186
|
+
|
|
187
|
+
UPDATE semicolony.worker_devices
|
|
188
|
+
SET gateway_credential_id = v_credential_id
|
|
189
|
+
WHERE device_id = p_device_id
|
|
190
|
+
AND worker_id = p_worker_id;
|
|
191
|
+
|
|
192
|
+
RETURN v_credential_id;
|
|
193
|
+
END;
|
|
194
|
+
$function$;
|
|
195
|
+
|
|
196
|
+
REVOKE ALL ON FUNCTION semicolony.issue_worker_gateway_credential(
|
|
197
|
+
uuid, text, text, text, text, text[], text, integer, jsonb
|
|
198
|
+
) FROM PUBLIC, sc_app, sc_provider;
|
|
199
|
+
GRANT EXECUTE ON FUNCTION semicolony.issue_worker_gateway_credential(
|
|
200
|
+
uuid, text, text, text, text, text[], text, integer, jsonb
|
|
201
|
+
) TO sc_app;
|
package/migrations/README.md
CHANGED
|
@@ -16,6 +16,8 @@ SEMICOLONY CLI의 PostgreSQL 마이그레이션 파일들.
|
|
|
16
16
|
| `007_flatten_skill_names.sql` | Flat skill naming 마이그레이션 |
|
|
17
17
|
| `165_skill_release_retirements.sql` | 거버넌스 스킬 은퇴 프리미티브 (append-only 기록 + 뷰) |
|
|
18
18
|
| `166_lease_operations_retire.sql` | `skill_capability_leases` profile→operations CHECK 재생성: operate 프로필에 `skills.release.retire` 추가 |
|
|
19
|
+
| `169_worker_profile_releases.sql` | Immutable SEMO Ops worker profile releases and device rollout state |
|
|
20
|
+
| `170_worker_release_credential_scopes.sql` | Worker credential owner 함수의 허용 범위에 release read/report scope 추가 |
|
|
19
21
|
|
|
20
22
|
## 007: Flat Skill Naming
|
|
21
23
|
|