@team-semicolon/semicolony-cli 4.18.91 → 4.18.93
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 +1188 -1187
- package/migrations/192_workspace_asset_management.sql +237 -0
- package/package.json +1 -1
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
-- 192_workspace_asset_management.sql
|
|
2
|
+
--
|
|
3
|
+
-- Add explicit, project-bound read/write capabilities to the existing
|
|
4
|
+
-- immutable workspace asset projection. Worker issuance continues to pass
|
|
5
|
+
-- through the hardened migration-170 issuer. The operator amendment function
|
|
6
|
+
-- repairs both halves together so a recently rotated read-less worker key can
|
|
7
|
+
-- be granted useful access without an ad-hoc table write.
|
|
8
|
+
--
|
|
9
|
+
-- Slack actors keep their exact Phase-6 credential scopes because the
|
|
10
|
+
-- decision recorder pins that array. Asset capabilities are derived from a
|
|
11
|
+
-- separate DB binding table by the Gateway resolver.
|
|
12
|
+
--
|
|
13
|
+
-- The migration runner owns BEGIN/COMMIT.
|
|
14
|
+
|
|
15
|
+
ALTER TABLE semicolony.workspace_projection_grants
|
|
16
|
+
DROP CONSTRAINT workspace_projection_grants_capability_check;
|
|
17
|
+
ALTER TABLE semicolony.workspace_projection_grants
|
|
18
|
+
DROP CONSTRAINT workspace_projection_grants_check;
|
|
19
|
+
ALTER TABLE semicolony.workspace_projection_grants
|
|
20
|
+
ADD CONSTRAINT workspace_projection_grants_capability_check
|
|
21
|
+
CHECK (capability IN ('credentials-read', 'assets-read', 'assets-write', 'skills-read'));
|
|
22
|
+
ALTER TABLE semicolony.workspace_projection_grants
|
|
23
|
+
ADD CONSTRAINT workspace_projection_grants_environment_contract_check
|
|
24
|
+
CHECK (
|
|
25
|
+
(capability = 'credentials-read' AND cardinality(environments) > 0)
|
|
26
|
+
OR (capability IN ('assets-read', 'assets-write', 'skills-read')
|
|
27
|
+
AND cardinality(environments) = 0)
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE OR REPLACE FUNCTION semicolony.issue_worker_asset_gateway_credential(
|
|
31
|
+
p_team_tenant_id uuid,
|
|
32
|
+
p_worker_id text,
|
|
33
|
+
p_device_id text,
|
|
34
|
+
p_token_hash text,
|
|
35
|
+
p_token_prefix text,
|
|
36
|
+
p_scopes text[],
|
|
37
|
+
p_issued_by text,
|
|
38
|
+
p_expires_days integer,
|
|
39
|
+
p_workspace_grants jsonb
|
|
40
|
+
)
|
|
41
|
+
RETURNS uuid
|
|
42
|
+
LANGUAGE plpgsql
|
|
43
|
+
SECURITY DEFINER
|
|
44
|
+
SET search_path = pg_catalog
|
|
45
|
+
AS $function$
|
|
46
|
+
DECLARE
|
|
47
|
+
v_credential_id uuid;
|
|
48
|
+
v_base_grants jsonb;
|
|
49
|
+
v_base_scopes text[];
|
|
50
|
+
v_write_count integer;
|
|
51
|
+
BEGIN
|
|
52
|
+
IF jsonb_typeof(p_workspace_grants) <> 'array' THEN
|
|
53
|
+
RAISE EXCEPTION 'workspace grants must be a JSON array' USING ERRCODE = '22023';
|
|
54
|
+
END IF;
|
|
55
|
+
SELECT count(*)::integer
|
|
56
|
+
INTO v_write_count
|
|
57
|
+
FROM jsonb_array_elements(p_workspace_grants) AS grant_row(value)
|
|
58
|
+
WHERE grant_row.value ->> 'capability' = 'assets-write';
|
|
59
|
+
IF v_write_count < 1 OR NOT ('workspace:assets-write' = ANY(p_scopes)) THEN
|
|
60
|
+
RAISE EXCEPTION 'asset write scope and grant are required' USING ERRCODE = '22023';
|
|
61
|
+
END IF;
|
|
62
|
+
IF cardinality(p_scopes) <> (
|
|
63
|
+
SELECT count(DISTINCT requested.scope)::integer FROM unnest(p_scopes) AS requested(scope)
|
|
64
|
+
) THEN
|
|
65
|
+
RAISE EXCEPTION 'worker credential scopes contain duplicates' USING ERRCODE = '22023';
|
|
66
|
+
END IF;
|
|
67
|
+
IF EXISTS (
|
|
68
|
+
SELECT 1
|
|
69
|
+
FROM jsonb_array_elements(p_workspace_grants) AS write_grant(value)
|
|
70
|
+
WHERE write_grant.value ->> 'capability' = 'assets-write'
|
|
71
|
+
AND (
|
|
72
|
+
jsonb_typeof(write_grant.value) <> 'object'
|
|
73
|
+
OR write_grant.value ->> 'projectId' !~ '^[a-z][a-z0-9-]{0,99}$'
|
|
74
|
+
OR write_grant.value -> 'environments' <> '[]'::jsonb
|
|
75
|
+
OR NOT EXISTS (
|
|
76
|
+
SELECT 1
|
|
77
|
+
FROM jsonb_array_elements(p_workspace_grants) AS read_grant(value)
|
|
78
|
+
WHERE read_grant.value ->> 'projectId' = write_grant.value ->> 'projectId'
|
|
79
|
+
AND read_grant.value ->> 'capability' = 'assets-read'
|
|
80
|
+
AND read_grant.value -> 'environments' = '[]'::jsonb
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
) THEN
|
|
84
|
+
RAISE EXCEPTION 'every asset write grant requires a paired read grant'
|
|
85
|
+
USING ERRCODE = '22023';
|
|
86
|
+
END IF;
|
|
87
|
+
|
|
88
|
+
SELECT coalesce(jsonb_agg(grant_row.value), '[]'::jsonb)
|
|
89
|
+
INTO v_base_grants
|
|
90
|
+
FROM jsonb_array_elements(p_workspace_grants) AS grant_row(value)
|
|
91
|
+
WHERE grant_row.value ->> 'capability' <> 'assets-write';
|
|
92
|
+
SELECT array_agg(scope ORDER BY ordinal)
|
|
93
|
+
INTO v_base_scopes
|
|
94
|
+
FROM unnest(p_scopes) WITH ORDINALITY AS requested(scope, ordinal)
|
|
95
|
+
WHERE scope <> 'workspace:assets-write';
|
|
96
|
+
|
|
97
|
+
v_credential_id := semicolony.issue_worker_gateway_credential(
|
|
98
|
+
p_team_tenant_id,
|
|
99
|
+
p_worker_id,
|
|
100
|
+
p_device_id,
|
|
101
|
+
p_token_hash,
|
|
102
|
+
p_token_prefix,
|
|
103
|
+
v_base_scopes,
|
|
104
|
+
p_issued_by,
|
|
105
|
+
p_expires_days,
|
|
106
|
+
v_base_grants
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
INSERT INTO semicolony.workspace_projection_grants (
|
|
110
|
+
gateway_credential_id, project_id, capability, environments
|
|
111
|
+
)
|
|
112
|
+
SELECT
|
|
113
|
+
v_credential_id,
|
|
114
|
+
grant_row.value ->> 'projectId',
|
|
115
|
+
'assets-write',
|
|
116
|
+
ARRAY[]::text[]
|
|
117
|
+
FROM jsonb_array_elements(p_workspace_grants) AS grant_row(value)
|
|
118
|
+
WHERE grant_row.value ->> 'capability' = 'assets-write';
|
|
119
|
+
|
|
120
|
+
UPDATE semicolony.gateway_credentials
|
|
121
|
+
SET scopes = p_scopes
|
|
122
|
+
WHERE id = v_credential_id;
|
|
123
|
+
|
|
124
|
+
RETURN v_credential_id;
|
|
125
|
+
END;
|
|
126
|
+
$function$;
|
|
127
|
+
|
|
128
|
+
REVOKE ALL ON FUNCTION semicolony.issue_worker_asset_gateway_credential(
|
|
129
|
+
uuid, text, text, text, text, text[], text, integer, jsonb
|
|
130
|
+
) FROM PUBLIC, sc_app, sc_provider;
|
|
131
|
+
GRANT EXECUTE ON FUNCTION semicolony.issue_worker_asset_gateway_credential(
|
|
132
|
+
uuid, text, text, text, text, text[], text, integer, jsonb
|
|
133
|
+
) TO sc_app;
|
|
134
|
+
|
|
135
|
+
CREATE TABLE semicolony.worker_agent_actor_capabilities (
|
|
136
|
+
installation_id text NOT NULL,
|
|
137
|
+
actor_id text NOT NULL,
|
|
138
|
+
capability text NOT NULL CHECK (capability IN ('assets:read', 'assets:write')),
|
|
139
|
+
status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'revoked')),
|
|
140
|
+
granted_by text NOT NULL DEFAULT 'migration-192',
|
|
141
|
+
granted_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
142
|
+
PRIMARY KEY (installation_id, actor_id, capability),
|
|
143
|
+
FOREIGN KEY (installation_id, actor_id)
|
|
144
|
+
REFERENCES semicolony.worker_agent_actor_bindings(installation_id, actor_id)
|
|
145
|
+
ON DELETE CASCADE
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
INSERT INTO semicolony.worker_agent_actor_capabilities (
|
|
149
|
+
installation_id, actor_id, capability
|
|
150
|
+
)
|
|
151
|
+
SELECT binding.installation_id, binding.actor_id, capability
|
|
152
|
+
FROM semicolony.worker_agent_actor_bindings AS binding
|
|
153
|
+
CROSS JOIN (VALUES ('assets:read'), ('assets:write')) AS capabilities(capability)
|
|
154
|
+
WHERE binding.status = 'active'
|
|
155
|
+
ON CONFLICT (installation_id, actor_id, capability) DO NOTHING;
|
|
156
|
+
|
|
157
|
+
REVOKE ALL ON TABLE semicolony.worker_agent_actor_capabilities
|
|
158
|
+
FROM PUBLIC, sc_app, sc_provider;
|
|
159
|
+
GRANT SELECT ON TABLE semicolony.worker_agent_actor_capabilities TO sc_app;
|
|
160
|
+
|
|
161
|
+
CREATE OR REPLACE FUNCTION semicolony.grant_worker_asset_write(
|
|
162
|
+
p_credential_id uuid,
|
|
163
|
+
p_project_id text,
|
|
164
|
+
p_granted_by text,
|
|
165
|
+
p_reason text
|
|
166
|
+
)
|
|
167
|
+
RETURNS uuid
|
|
168
|
+
LANGUAGE plpgsql
|
|
169
|
+
SECURITY DEFINER
|
|
170
|
+
SET search_path = pg_catalog
|
|
171
|
+
AS $function$
|
|
172
|
+
DECLARE
|
|
173
|
+
v_amendment_id uuid;
|
|
174
|
+
v_previous_scopes text[];
|
|
175
|
+
v_next_scopes text[];
|
|
176
|
+
BEGIN
|
|
177
|
+
IF p_credential_id IS NULL
|
|
178
|
+
OR p_project_id IS NULL OR p_project_id !~ '^[a-z][a-z0-9-]{0,99}$'
|
|
179
|
+
OR NULLIF(btrim(coalesce(p_granted_by, '')), '') IS NULL
|
|
180
|
+
OR NULLIF(btrim(coalesce(p_reason, '')), '') IS NULL THEN
|
|
181
|
+
RAISE EXCEPTION 'credential, project, grantor, and reason are required'
|
|
182
|
+
USING ERRCODE = '22023';
|
|
183
|
+
END IF;
|
|
184
|
+
|
|
185
|
+
SELECT gc.scopes
|
|
186
|
+
INTO v_previous_scopes
|
|
187
|
+
FROM semicolony.gateway_credentials AS gc
|
|
188
|
+
WHERE gc.id = p_credential_id
|
|
189
|
+
AND gc.status = 'active'
|
|
190
|
+
AND gc.revoked_at IS NULL
|
|
191
|
+
AND (gc.expires_at IS NULL OR gc.expires_at > clock_timestamp())
|
|
192
|
+
AND gc.metadata ->> 'credential_kind' = 'worker'
|
|
193
|
+
FOR UPDATE;
|
|
194
|
+
IF NOT FOUND THEN
|
|
195
|
+
RAISE EXCEPTION 'active worker credential not found' USING ERRCODE = 'P0002';
|
|
196
|
+
END IF;
|
|
197
|
+
INSERT INTO semicolony.workspace_projection_grants (
|
|
198
|
+
gateway_credential_id, project_id, capability, environments, status,
|
|
199
|
+
expires_at, metadata
|
|
200
|
+
)
|
|
201
|
+
SELECT
|
|
202
|
+
p_credential_id, p_project_id, capabilities.capability, ARRAY[]::text[], 'active',
|
|
203
|
+
NULL, jsonb_build_object('granted_by', btrim(p_granted_by), 'reason', btrim(p_reason))
|
|
204
|
+
FROM (VALUES ('assets-read'), ('assets-write')) AS capabilities(capability)
|
|
205
|
+
ON CONFLICT (gateway_credential_id, project_id, capability) DO UPDATE
|
|
206
|
+
SET status = 'active',
|
|
207
|
+
expires_at = NULL,
|
|
208
|
+
metadata = EXCLUDED.metadata;
|
|
209
|
+
|
|
210
|
+
v_next_scopes := v_previous_scopes;
|
|
211
|
+
IF NOT ('workspace:assets-read' = ANY(v_next_scopes)) THEN
|
|
212
|
+
v_next_scopes := array_append(v_next_scopes, 'workspace:assets-read');
|
|
213
|
+
END IF;
|
|
214
|
+
IF NOT ('workspace:assets-write' = ANY(v_next_scopes)) THEN
|
|
215
|
+
v_next_scopes := array_append(v_next_scopes, 'workspace:assets-write');
|
|
216
|
+
END IF;
|
|
217
|
+
|
|
218
|
+
INSERT INTO semicolony.gateway_credential_scope_amendments (
|
|
219
|
+
credential_id, previous_scopes, next_scopes, reason, amended_by
|
|
220
|
+
) VALUES (
|
|
221
|
+
p_credential_id, v_previous_scopes, v_next_scopes,
|
|
222
|
+
btrim(p_reason), btrim(p_granted_by)
|
|
223
|
+
)
|
|
224
|
+
RETURNING amendment_id INTO v_amendment_id;
|
|
225
|
+
|
|
226
|
+
UPDATE semicolony.gateway_credentials
|
|
227
|
+
SET scopes = v_next_scopes
|
|
228
|
+
WHERE id = p_credential_id;
|
|
229
|
+
|
|
230
|
+
RETURN v_amendment_id;
|
|
231
|
+
END;
|
|
232
|
+
$function$;
|
|
233
|
+
|
|
234
|
+
REVOKE ALL ON FUNCTION semicolony.grant_worker_asset_write(uuid, text, text, text)
|
|
235
|
+
FROM PUBLIC, sc_app, sc_provider;
|
|
236
|
+
GRANT EXECUTE ON FUNCTION semicolony.grant_worker_asset_write(uuid, text, text, text)
|
|
237
|
+
TO sc_app;
|