letopis 1.4.2 → 3.0.0
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/AGENT-CHEATSHEET.md +516 -398
- package/CHANGELOG.md +739 -0
- package/LICENSE +75 -21
- package/LICENSE-COMMERCIAL.md +49 -0
- package/README.md +1312 -1190
- package/api-contract.json +381 -0
- package/dist/acl.d.ts +81 -1
- package/dist/acl.js +281 -13
- package/dist/admin.d.ts +11 -141
- package/dist/admin.js +28 -186
- package/dist/auth.d.ts +20 -5
- package/dist/auth.js +80 -21
- package/dist/cache.d.ts +8 -1
- package/dist/cache.js +30 -8
- package/dist/chain.d.ts +46 -48
- package/dist/chain.js +556 -223
- package/dist/cli.js +73 -96
- package/dist/demo/booking.d.ts +352 -36
- package/dist/describe.d.ts +47 -0
- package/dist/describe.js +195 -0
- package/dist/errors.d.ts +77 -13
- package/dist/errors.js +231 -30
- package/dist/index.d.ts +62 -74
- package/dist/index.js +312 -155
- package/dist/internal.d.ts +7 -0
- package/dist/jsonschema.js +18 -17
- package/dist/load.d.ts +29 -17
- package/dist/load.js +235 -190
- package/dist/log.d.ts +121 -0
- package/dist/log.js +633 -0
- package/dist/mcp.d.ts +5 -7
- package/dist/mcp.js +194 -225
- package/dist/messages/access.d.ts +90 -0
- package/dist/messages/access.js +113 -0
- package/dist/messages/api.d.ts +237 -0
- package/dist/messages/api.js +304 -0
- package/dist/messages/boot.d.ts +201 -0
- package/dist/messages/boot.js +256 -0
- package/dist/messages/codes.d.ts +172 -0
- package/dist/messages/codes.js +173 -0
- package/dist/messages/index.d.ts +98 -0
- package/dist/messages/index.js +121 -0
- package/dist/messages/log.d.ts +229 -0
- package/dist/messages/log.js +102 -0
- package/dist/messages/mcp.d.ts +306 -0
- package/dist/messages/mcp.js +387 -0
- package/dist/messages/model.d.ts +256 -0
- package/dist/messages/model.js +314 -0
- package/dist/messages/query.d.ts +141 -0
- package/dist/messages/query.js +171 -0
- package/dist/messages/sql-access.d.ts +70 -0
- package/dist/messages/sql-access.js +87 -0
- package/dist/messages/sql-core.d.ts +164 -0
- package/dist/messages/sql-core.js +198 -0
- package/dist/messages/sql-time.d.ts +86 -0
- package/dist/messages/sql-time.js +111 -0
- package/dist/messages/sql-write.d.ts +246 -0
- package/dist/messages/sql-write.js +270 -0
- package/dist/messages/time.d.ts +23 -0
- package/dist/messages/time.js +29 -0
- package/dist/messages/types.d.ts +27 -0
- package/dist/messages/types.js +5 -0
- package/dist/messages/write.d.ts +452 -0
- package/dist/messages/write.js +572 -0
- package/dist/model.d.ts +77 -60
- package/dist/model.js +43 -34
- package/dist/ops.d.ts +35 -0
- package/dist/ops.js +101 -0
- package/dist/pglite.d.ts +0 -9
- package/dist/pglite.js +3 -1
- package/dist/registry.d.ts +3 -19
- package/dist/registry.js +3 -16
- package/dist/signals.d.ts +1 -5
- package/dist/signals.js +1 -5
- package/dist/sql.d.ts +9 -1
- package/dist/sql.js +175 -107
- package/dist/sync.d.ts +10 -4
- package/dist/sync.js +90 -17
- package/dist/tx.d.ts +21 -2
- package/dist/tx.js +76 -13
- package/dist/typed.d.ts +252 -53
- package/dist/types.d.ts +93 -90
- package/dist/types.js +17 -14
- package/dist/up.d.ts +56 -35
- package/dist/up.js +229 -83
- package/dist/uuid.d.ts +8 -0
- package/dist/uuid.js +14 -6
- package/dist/validate.d.ts +13 -1
- package/dist/validate.js +82 -33
- package/dist/watch.d.ts +10 -16
- package/dist/watch.js +36 -77
- package/dist/write.d.ts +87 -17
- package/dist/write.js +441 -206
- package/llms.txt +48 -31
- package/package.json +13 -14
- package/sql/10-core.sql +7 -50
- package/sql/15-errors.sql +64 -10
- package/sql/20-context.sql +18 -9
- package/sql/30-validate.sql +79 -30
- package/sql/40-class.sql +41 -115
- package/sql/50-acl.sql +8 -13
- package/sql/60-write.sql +220 -676
- package/sql/70-read.sql +11 -13
- package/sql/80-auth.sql +109 -42
- package/sql/90-time.sql +108 -947
- package/sql/95-seed.system.sql +7 -14
- package/sql/99-revision.sql +6 -1
- package/sql/README.md +32 -24
- package/sql/migrate/0009-restore-rev.tenants-name.sql +10 -0
- package/sql/migrate/0010-impersonation-no-account.sql +5 -0
- package/sql/migrate/0011-key-lock.otp-enabled.sql +7 -0
- package/sql/migrate/README.md +18 -0
- package/AGENT-CHEATSHEET.en.md +0 -488
- package/MIGRATION.md +0 -190
- package/README.en.md +0 -2910
- package/dist/import.d.ts +0 -185
- package/dist/import.js +0 -820
- package/dist/project.d.ts +0 -359
- package/dist/project.js +0 -1258
- package/sql/75-history.sql +0 -436
- package/sql/88-derived.sql +0 -1236
- package/sql/migrate/0006-changed-paths.sql +0 -3
- package/sql/migrate/0007-secret-account-fk.sql +0 -12
package/sql/70-read.sql
CHANGED
|
@@ -17,8 +17,7 @@
|
|
|
17
17
|
--
|
|
18
18
|
-- Источник — текущее состояние (entity) или журнал: версии, действовавшие на момент p_at (срез
|
|
19
19
|
-- asOf), либо надгробия удалённых (p_dead, для withDeleted). Возвращается rev версии: строки
|
|
20
|
-
-- журнала читаются по паре (id, rev) через уникальный индекс log_id_rev.
|
|
21
|
-
-- (этап 6) — не версии объекта: на момент после них объекта в срезе нет.
|
|
20
|
+
-- журнала читаются по паре (id, rev) через уникальный индекс log_id_rev.
|
|
22
21
|
|
|
23
22
|
-- Цели концов строки: всех (p_roles is null) или только перечисленных ролей.
|
|
24
23
|
create or replace function @SCHEMA@.role_ids(p_links jsonb, p_roles text[]) returns uuid[]
|
|
@@ -55,8 +54,7 @@ as $$
|
|
|
55
54
|
select y.* from (select x.* from @SCHEMA@.log x where x.id = c.id offset 0) y
|
|
56
55
|
where y.at <= coalesce(p_at, 'infinity'::timestamptz)
|
|
57
56
|
order by y.rev desc limit 1) w
|
|
58
|
-
-- класс — у последней версии на момент, а не среди версий классов
|
|
59
|
-
-- не числится в прежнем классе (план 2.0, 7.1 №1)
|
|
57
|
+
-- класс — у последней версии на момент, а не среди версий классов шага (план 2.0, 7.1 №1)
|
|
60
58
|
where w.class = any (p_classes) and @SCHEMA@.link_ids(w.links) @> array[p_src]
|
|
61
59
|
$$;
|
|
62
60
|
|
|
@@ -123,21 +121,21 @@ begin
|
|
|
123
121
|
from @SCHEMA@.log l
|
|
124
122
|
where l.id = any (p_ids) and l.at <= coalesce(p_at, 'infinity'::timestamptz)
|
|
125
123
|
order by l.id, l.rev desc) v
|
|
126
|
-
where v.class = any (p_classes) and (v.op = 'delete') = p_dead
|
|
124
|
+
where v.class = any (p_classes) and (v.op = 'delete') = p_dead
|
|
127
125
|
and (p_data is null or v.data @> p_data) and (p_ends is null or @SCHEMA@.link_ids(v.links) @> p_ends)
|
|
128
126
|
and @SCHEMA@.row_visible(v.tenant, v.class, v.id, v.owner, v.tags, v.data, v.links, c_tenant, c_account, c_plan);
|
|
129
127
|
elsif p_ends is not null then
|
|
130
128
|
-- по целям концов — от версий, ссылавшихся на первую цель (GIN log_ends); арендатор — после пробы
|
|
131
129
|
return query select v.id, v.rev from @SCHEMA@.log_refs_at(p_classes, p_ends[1], p_at) v
|
|
132
130
|
where (v.tenant = any (v_tenants) or v.class = 'member')
|
|
133
|
-
and (v.op = 'delete') = p_dead
|
|
131
|
+
and (v.op = 'delete') = p_dead
|
|
134
132
|
and (p_data is null or v.data @> p_data) and @SCHEMA@.link_ids(v.links) @> p_ends
|
|
135
133
|
and @SCHEMA@.row_visible(v.tenant, v.class, v.id, v.owner, v.tags, v.data, v.links, c_tenant, c_account, c_plan);
|
|
136
134
|
else
|
|
137
|
-
-- арендатор id
|
|
135
|
+
-- арендатор id неизменен: условие до distinct on не меняет, какая
|
|
138
136
|
-- версия id последняя (индекс log_class_tenant_at)
|
|
139
137
|
-- кандидаты — по классу (индекс log_class_tenant_at), последняя версия id на момент — любого класса, и класс
|
|
140
|
-
-- проверяется у
|
|
138
|
+
-- проверяется у неё (план 2.0, 7.1 №1)
|
|
141
139
|
return query select v.id, v.rev from (
|
|
142
140
|
select distinct l.id from @SCHEMA@.log l
|
|
143
141
|
where l.class = any (p_classes) and l.tenant = any (v_tenants) and l.at <= coalesce(p_at, 'infinity'::timestamptz)) c
|
|
@@ -145,14 +143,14 @@ begin
|
|
|
145
143
|
select y.id, y.rev, y.op, y.class, y.tenant, y.owner, y.tags, y.data, y.links
|
|
146
144
|
from @SCHEMA@.log y where y.id = c.id and y.at <= coalesce(p_at, 'infinity'::timestamptz)
|
|
147
145
|
order by y.rev desc limit 1) v
|
|
148
|
-
where v.class = any (p_classes) and (v.op = 'delete') = p_dead
|
|
146
|
+
where v.class = any (p_classes) and (v.op = 'delete') = p_dead
|
|
149
147
|
and (p_data is null or v.data @> p_data) and (p_ends is null or @SCHEMA@.link_ids(v.links) @> p_ends)
|
|
150
148
|
and @SCHEMA@.row_visible(v.tenant, v.class, v.id, v.owner, v.tags, v.data, v.links, c_tenant, c_account, c_plan);
|
|
151
149
|
-- свои членства в других арендаторах: версии, ссылавшиеся на аккаунт (GIN log_ends)
|
|
152
150
|
if v_member then
|
|
153
151
|
return query select v.id, v.rev from @SCHEMA@.log_refs_at(array['member'], c_account, p_at) v
|
|
154
152
|
where not (v.tenant = any (v_tenants))
|
|
155
|
-
and (v.op = 'delete') = p_dead
|
|
153
|
+
and (v.op = 'delete') = p_dead
|
|
156
154
|
and (p_data is null or v.data @> p_data) and (p_ends is null or @SCHEMA@.link_ids(v.links) @> p_ends)
|
|
157
155
|
and @SCHEMA@.row_visible(v.tenant, v.class, v.id, v.owner, v.tags, v.data, v.links, c_tenant, c_account, c_plan);
|
|
158
156
|
end if;
|
|
@@ -193,7 +191,7 @@ begin
|
|
|
193
191
|
return query select s.src, v.id, v.rev
|
|
194
192
|
from (select distinct u.x as src from unnest(p_src) u(x)) s
|
|
195
193
|
cross join lateral @SCHEMA@.log_refs_at(p_classes, s.src, p_at) v
|
|
196
|
-
where (v.op = 'delete') = p_dead
|
|
194
|
+
where (v.op = 'delete') = p_dead
|
|
197
195
|
and (p_data is null or v.data @> p_data) and (p_ends is null or @SCHEMA@.link_ids(v.links) @> p_ends)
|
|
198
196
|
and (p_roles is null or @SCHEMA@.role_has(v.links, p_roles, s.src))
|
|
199
197
|
and @SCHEMA@.row_visible(v.tenant, v.class, v.id, v.owner, v.tags, v.data, v.links, c_tenant, c_account, c_plan);
|
|
@@ -243,14 +241,14 @@ begin
|
|
|
243
241
|
select s.src, v.id, v.rev, 1, array[s.src, v.id]
|
|
244
242
|
from (select distinct u.x as src from unnest(p_src) u(x)) s
|
|
245
243
|
cross join lateral @SCHEMA@.log_refs_at(p_classes, s.src, p_at) v
|
|
246
|
-
where v.op
|
|
244
|
+
where v.op <> 'delete'
|
|
247
245
|
and (p_roles is null or @SCHEMA@.role_has(v.links, p_roles, s.src))
|
|
248
246
|
and @SCHEMA@.row_visible(v.tenant, v.class, v.id, v.owner, v.tags, v.data, v.links, c_tenant, c_account, c_plan)
|
|
249
247
|
union all
|
|
250
248
|
select d.src, v.id, v.rev, d.depth + 1, d.seen || v.id
|
|
251
249
|
from d
|
|
252
250
|
cross join lateral @SCHEMA@.log_refs_at(p_classes, d.id, p_at) v
|
|
253
|
-
where d.depth < p_max and not (v.id = any (d.seen)) and v.op
|
|
251
|
+
where d.depth < p_max and not (v.id = any (d.seen)) and v.op <> 'delete'
|
|
254
252
|
and (p_roles is null or @SCHEMA@.role_has(v.links, p_roles, d.id))
|
|
255
253
|
and @SCHEMA@.row_visible(v.tenant, v.class, v.id, v.owner, v.tags, v.data, v.links, c_tenant, c_account, c_plan))
|
|
256
254
|
select distinct on (d.src, d.id) d.src, d.id, d.rev, d.depth from d order by d.src, d.id, d.depth;
|
package/sql/80-auth.sql
CHANGED
|
@@ -17,7 +17,7 @@ return interval '10 minutes';
|
|
|
17
17
|
|
|
18
18
|
create or replace function @SCHEMA@.auth_kind_ok(p_kind text) returns boolean
|
|
19
19
|
language sql immutable parallel safe
|
|
20
|
-
return p_kind ~ '^[A-Z][A-Z0-9_]{1,31}$' and p_kind not in ('SESSION', 'APIKEY', 'KEYSECRET', 'OTP', 'TOTP', 'KEY', '
|
|
20
|
+
return p_kind ~ '^[A-Z][A-Z0-9_]{1,31}$' and p_kind not in ('SESSION', 'APIKEY', 'KEYSECRET', 'OTP', 'TOTP', 'KEY', 'LOCK');
|
|
21
21
|
|
|
22
22
|
-- Действующая сессия обязательна (иначе no_session).
|
|
23
23
|
create or replace function @SCHEMA@.auth_session_required(p_account uuid) returns void
|
|
@@ -25,7 +25,7 @@ language plpgsql immutable
|
|
|
25
25
|
as $$
|
|
26
26
|
begin
|
|
27
27
|
if p_account is null then
|
|
28
|
-
perform @SCHEMA@.lt_fail('no_session'
|
|
28
|
+
perform @SCHEMA@.lt_fail('sql-access.no_session');
|
|
29
29
|
end if;
|
|
30
30
|
end $$;
|
|
31
31
|
|
|
@@ -42,7 +42,7 @@ declare
|
|
|
42
42
|
begin
|
|
43
43
|
select * into c from @SCHEMA@.ctx();
|
|
44
44
|
if c.account is null then
|
|
45
|
-
perform @SCHEMA@.lt_fail('no_session'
|
|
45
|
+
perform @SCHEMA@.lt_fail('sql-access.no_session');
|
|
46
46
|
end if;
|
|
47
47
|
if c.account = p_account and c.service is null then
|
|
48
48
|
select (s.meta->>'login_at')::timestamptz, coalesce(s.meta->'methods', '[]'::jsonb) into login_at, methods
|
|
@@ -52,17 +52,18 @@ begin
|
|
|
52
52
|
if login_at > clock_timestamp() - @SCHEMA@.auth_recent()
|
|
53
53
|
and exists (select 1 from jsonb_array_elements_text(methods) m where m not in ('APIKEY', 'KEYSECRET', 'SERVICE'))
|
|
54
54
|
and (methods ? 'TOTP' or not exists (select 1 from @SCHEMA@.secret t where t.account = p_account and t.kind = 'TOTP'
|
|
55
|
-
and t.deleted_at is null and coalesce((t.meta->>'confirmed')::boolean, false)
|
|
55
|
+
and t.deleted_at is null and coalesce((t.meta->>'confirmed')::boolean, false)
|
|
56
|
+
and coalesce((t.meta->>'enabled')::boolean, true))) then
|
|
56
57
|
return;
|
|
57
58
|
end if;
|
|
58
59
|
end if;
|
|
59
60
|
if not coalesce((@SCHEMA@.acl_api_for(c.tenant, c.categories, c.roles, 'auth.credential', true)->>'allow')::boolean, false) then
|
|
60
|
-
perform @SCHEMA@.lt_fail('
|
|
61
|
+
perform @SCHEMA@.lt_fail('sql-access.bind_denied');
|
|
61
62
|
end if;
|
|
62
63
|
-- креды System, сервисов и системных администраторов выдаёт только системный администратор: иначе
|
|
63
64
|
-- сервис привязал бы себе вход под ними (ревью этапа 5)
|
|
64
65
|
if not @SCHEMA@.auth_target_ok(p_account) and not @SCHEMA@.auth_is_sysadmin() then
|
|
65
|
-
perform @SCHEMA@.lt_fail('
|
|
66
|
+
perform @SCHEMA@.lt_fail('sql-access.bind_denied');
|
|
66
67
|
end if;
|
|
67
68
|
end $$;
|
|
68
69
|
|
|
@@ -77,7 +78,7 @@ declare
|
|
|
77
78
|
begin
|
|
78
79
|
select * into c from @SCHEMA@.ctx();
|
|
79
80
|
if p_account is distinct from c.account and not @SCHEMA@.auth_target_ok(p_account) and not @SCHEMA@.auth_is_sysadmin() then
|
|
80
|
-
perform @SCHEMA@.lt_fail('
|
|
81
|
+
perform @SCHEMA@.lt_fail('sql-access.self_or_sysadmin');
|
|
81
82
|
end if;
|
|
82
83
|
end $$;
|
|
83
84
|
|
|
@@ -203,7 +204,7 @@ begin
|
|
|
203
204
|
perform @SCHEMA@.auth_session_required(c.account);
|
|
204
205
|
perform @SCHEMA@.acl_require('auth.session');
|
|
205
206
|
if not @SCHEMA@.auth_target_ok(p_account) then
|
|
206
|
-
perform @SCHEMA@.lt_fail('
|
|
207
|
+
perform @SCHEMA@.lt_fail('sql-access.no_sessions_for_account');
|
|
207
208
|
end if;
|
|
208
209
|
return @SCHEMA@.new_session(p_account, p_ttl, jsonb_build_object('methods', to_jsonb(p_methods), 'service', c.account));
|
|
209
210
|
end $$;
|
|
@@ -248,14 +249,14 @@ begin
|
|
|
248
249
|
p_confirmed := null;
|
|
249
250
|
end if;
|
|
250
251
|
if not @SCHEMA@.auth_kind_ok(p_kind) or coalesce(p_ident, '') = '' or p_hash !~ '^scrypt\$' then
|
|
251
|
-
perform @SCHEMA@.lt_fail('
|
|
252
|
+
perform @SCHEMA@.lt_fail('sql-access.password_args');
|
|
252
253
|
end if;
|
|
253
254
|
select s.id, s.account into cur from @SCHEMA@.secret s
|
|
254
255
|
where s.kind = p_kind and s.deleted_at is null
|
|
255
256
|
and casefold(s.ident) collate pg_unicode_fast = casefold(p_ident) collate pg_unicode_fast
|
|
256
257
|
for update;
|
|
257
258
|
if cur.id is not null and cur.account <> p_account then
|
|
258
|
-
perform @SCHEMA@.lt_fail('
|
|
259
|
+
perform @SCHEMA@.lt_fail('sql-access.bind_denied');
|
|
259
260
|
end if;
|
|
260
261
|
if cur.id is not null then
|
|
261
262
|
update @SCHEMA@.secret s
|
|
@@ -270,7 +271,7 @@ begin
|
|
|
270
271
|
values (p_account, p_kind, p_ident, convert_to(p_hash, 'UTF8'), jsonb_build_object('confirmed', coalesce(p_confirmed, false)))
|
|
271
272
|
returning id into cid;
|
|
272
273
|
exception when unique_violation then
|
|
273
|
-
perform @SCHEMA@.lt_fail('
|
|
274
|
+
perform @SCHEMA@.lt_fail('sql-access.bind_denied');
|
|
274
275
|
end;
|
|
275
276
|
end if;
|
|
276
277
|
return cid;
|
|
@@ -308,7 +309,7 @@ begin
|
|
|
308
309
|
null; -- совпал префикс: новый ключ
|
|
309
310
|
end;
|
|
310
311
|
end loop;
|
|
311
|
-
perform @SCHEMA@.lt_raise('40001',
|
|
312
|
+
perform @SCHEMA@.lt_raise('40001','sql-access.apikey_prefix_conflict', '{}'::jsonb);
|
|
312
313
|
end $$;
|
|
313
314
|
|
|
314
315
|
-- Вход по ключу API для db.auth.verifyApiKey: { account, token } или null (login_apikey — для
|
|
@@ -326,7 +327,9 @@ begin
|
|
|
326
327
|
and s.material = sha256(convert_to(coalesce(p_key, ''), 'UTF8'))
|
|
327
328
|
and (s.expires_at is null or s.expires_at > clock_timestamp())
|
|
328
329
|
and coalesce((s.meta->>'enabled')::boolean, true)
|
|
329
|
-
and @SCHEMA@.auth_enabled(s.account)
|
|
330
|
+
and @SCHEMA@.auth_enabled(s.account)
|
|
331
|
+
-- строка ключа — под блокировкой до фиксации входа: отзыв ждёт её и гасит открытую здесь сессию
|
|
332
|
+
for share of s;
|
|
330
333
|
if k.account is null then
|
|
331
334
|
return null;
|
|
332
335
|
end if;
|
|
@@ -370,7 +373,9 @@ begin
|
|
|
370
373
|
and casefold(s.ident) collate pg_unicode_fast = casefold(p_key) collate pg_unicode_fast
|
|
371
374
|
and s.material = sha256(convert_to(coalesce(p_secret, ''), 'UTF8'))
|
|
372
375
|
and coalesce((s.meta->>'enabled')::boolean, true)
|
|
373
|
-
and @SCHEMA@.auth_enabled(s.account)
|
|
376
|
+
and @SCHEMA@.auth_enabled(s.account)
|
|
377
|
+
-- строка ключа — под блокировкой до фиксации входа: отзыв ждёт её и гасит открытую здесь сессию
|
|
378
|
+
for share of s;
|
|
374
379
|
if k.account is null then
|
|
375
380
|
return null;
|
|
376
381
|
end if;
|
|
@@ -390,13 +395,13 @@ declare
|
|
|
390
395
|
begin
|
|
391
396
|
perform @SCHEMA@.acl_require('auth.otp');
|
|
392
397
|
if p_kind !~ '^OTP(_[A-Z0-9]{1,27})?$' or coalesce(p_ident, '') = '' or p_digits not between 4 and 10 then
|
|
393
|
-
perform @SCHEMA@.lt_fail('
|
|
398
|
+
perform @SCHEMA@.lt_fail('sql-access.otp_args');
|
|
394
399
|
end if;
|
|
395
400
|
if not @SCHEMA@.auth_enabled(p_account) then
|
|
396
|
-
perform @SCHEMA@.lt_fail('
|
|
401
|
+
perform @SCHEMA@.lt_fail('sql-access.account_not_found');
|
|
397
402
|
end if;
|
|
398
403
|
if not @SCHEMA@.auth_target_ok(p_account) and not @SCHEMA@.auth_is_sysadmin() then
|
|
399
|
-
perform @SCHEMA@.lt_fail('
|
|
404
|
+
perform @SCHEMA@.lt_fail('sql-access.otp_sysadmin_only');
|
|
400
405
|
end if;
|
|
401
406
|
code := lpad((('x' || encode(@PGCRYPTO@.gen_random_bytes(7), 'hex'))::bit(56)::bigint % (10::bigint ^ p_digits)::bigint)::text, p_digits, '0');
|
|
402
407
|
update @SCHEMA@.secret s set deleted_at = clock_timestamp()
|
|
@@ -428,6 +433,7 @@ begin
|
|
|
428
433
|
from @SCHEMA@.secret x
|
|
429
434
|
where x.kind = p_kind and x.kind ~ '^OTP' and x.deleted_at is null
|
|
430
435
|
and casefold(x.ident) collate pg_unicode_fast = casefold(p_ident) collate pg_unicode_fast
|
|
436
|
+
and coalesce((x.meta->>'enabled')::boolean, true) -- отключённый код (setFlags) не принимается
|
|
431
437
|
for update;
|
|
432
438
|
if s.id is null then
|
|
433
439
|
return null;
|
|
@@ -507,6 +513,7 @@ begin
|
|
|
507
513
|
coalesce((x.meta->>'fails')::int, 0) as fails, (x.meta->>'locked_until')::timestamptz as locked_until into s
|
|
508
514
|
from @SCHEMA@.secret x
|
|
509
515
|
where x.account = p_account and x.kind = 'TOTP' and x.deleted_at is null
|
|
516
|
+
and coalesce((x.meta->>'enabled')::boolean, true) -- отключённый фактор (setFlags) не принимается
|
|
510
517
|
for update;
|
|
511
518
|
if s.id is null or s.locked_until > clock_timestamp() then
|
|
512
519
|
return false;
|
|
@@ -547,7 +554,7 @@ begin
|
|
|
547
554
|
perform @SCHEMA@.auth_guard(p_account);
|
|
548
555
|
end if;
|
|
549
556
|
return exists (select 1 from @SCHEMA@.secret s where s.account = p_account and s.kind = 'TOTP' and s.deleted_at is null
|
|
550
|
-
and coalesce((s.meta->>'confirmed')::boolean, false));
|
|
557
|
+
and coalesce((s.meta->>'confirmed')::boolean, false) and coalesce((s.meta->>'enabled')::boolean, true));
|
|
551
558
|
end $$;
|
|
552
559
|
|
|
553
560
|
-- Внешняя identity (TELEGRAM, GOOGLE, SSO-провайдер): токен проверяет сервис, здесь — связка.
|
|
@@ -564,7 +571,7 @@ begin
|
|
|
564
571
|
p_confirmed := false;
|
|
565
572
|
end if;
|
|
566
573
|
if not @SCHEMA@.auth_kind_ok(p_kind) or p_kind = 'PASSWORD' or coalesce(p_ident, '') = '' then
|
|
567
|
-
perform @SCHEMA@.lt_fail('
|
|
574
|
+
perform @SCHEMA@.lt_fail('sql-access.identity_args');
|
|
568
575
|
end if;
|
|
569
576
|
begin
|
|
570
577
|
insert into @SCHEMA@.secret (account, kind, ident, meta)
|
|
@@ -572,7 +579,7 @@ begin
|
|
|
572
579
|
|| jsonb_build_object('confirmed', coalesce(p_confirmed, false)))
|
|
573
580
|
returning id into cid;
|
|
574
581
|
exception when unique_violation then
|
|
575
|
-
perform @SCHEMA@.lt_fail('
|
|
582
|
+
perform @SCHEMA@.lt_fail('sql-access.bind_denied');
|
|
576
583
|
end;
|
|
577
584
|
return cid;
|
|
578
585
|
end $$;
|
|
@@ -617,7 +624,7 @@ begin
|
|
|
617
624
|
coalesce((s.meta->>'confirmed')::boolean, false), coalesce((s.meta->>'enabled')::boolean, true),
|
|
618
625
|
s.meta - 'confirmed' - 'enabled' - 'attempts' - 'last_step', s.created_at, s.expires_at
|
|
619
626
|
from @SCHEMA@.secret s
|
|
620
|
-
where s.account = p_account and s.kind not in ('SESSION', 'KEY', '
|
|
627
|
+
where s.account = p_account and s.kind not in ('SESSION', 'KEY', 'LOCK') and s.deleted_at is null
|
|
621
628
|
and (s.expires_at is null or s.expires_at > clock_timestamp())
|
|
622
629
|
order by s.created_at, s.id;
|
|
623
630
|
end $$;
|
|
@@ -639,7 +646,10 @@ begin
|
|
|
639
646
|
perform @SCHEMA@.auth_session_required(c.account);
|
|
640
647
|
select s.account, s.kind,
|
|
641
648
|
(s.expires_at is null or s.expires_at > clock_timestamp()) and coalesce((s.meta->>'enabled')::boolean, true) as live
|
|
642
|
-
into cr from @SCHEMA@.secret s where s.id = p_id and s.kind not in ('SESSION', 'KEY', '
|
|
649
|
+
into cr from @SCHEMA@.secret s where s.id = p_id and s.kind not in ('SESSION', 'KEY', 'LOCK') and s.deleted_at is null
|
|
650
|
+
-- строка креда — под блокировкой: вход по этому ключу, начатый раньше, отзыв дожидается (его сессию гасит
|
|
651
|
+
-- auth_kill_cred ниже), начатый позже — ждёт отзыва и ключа уже не находит (login_apikey, login_keysecret)
|
|
652
|
+
for update;
|
|
643
653
|
if cr.account is null then
|
|
644
654
|
return false;
|
|
645
655
|
end if;
|
|
@@ -655,7 +665,7 @@ begin
|
|
|
655
665
|
if not exists (select 1 from @SCHEMA@.secret s
|
|
656
666
|
where s.account = cr.account and s.kind in ('APIKEY', 'KEYSECRET') and s.deleted_at is null and s.id <> p_id
|
|
657
667
|
and (s.expires_at is null or s.expires_at > clock_timestamp()) and coalesce((s.meta->>'enabled')::boolean, true)) then
|
|
658
|
-
perform @SCHEMA@.lt_fail('
|
|
668
|
+
perform @SCHEMA@.lt_fail('sql-access.last_service_key');
|
|
659
669
|
end if;
|
|
660
670
|
end if;
|
|
661
671
|
update @SCHEMA@.secret s set deleted_at = clock_timestamp() where s.id = p_id and s.deleted_at is null;
|
|
@@ -677,17 +687,47 @@ set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
|
677
687
|
as $$
|
|
678
688
|
declare
|
|
679
689
|
acc uuid;
|
|
690
|
+
knd text;
|
|
691
|
+
live boolean;
|
|
680
692
|
begin
|
|
681
693
|
perform @SCHEMA@.acl_require('auth.credential');
|
|
682
|
-
|
|
694
|
+
-- строка креда — под блокировкой, как при отзыве (auth_revoke_credential): вход по этому ключу, начатый раньше,
|
|
695
|
+
-- отключение дожидается и гасит его сессию ниже; начатый позже — ждёт и видит enabled = false (3.0.0, ревизия 11)
|
|
696
|
+
select s.account, s.kind,
|
|
697
|
+
(s.expires_at is null or s.expires_at > clock_timestamp()) and coalesce((s.meta->>'enabled')::boolean, true)
|
|
698
|
+
into acc, knd, live
|
|
699
|
+
from @SCHEMA@.secret s where s.id = p_id and s.kind not in ('SESSION', 'KEY', 'LOCK') and s.deleted_at is null
|
|
700
|
+
for update;
|
|
683
701
|
if acc is null then
|
|
684
702
|
return false;
|
|
685
703
|
end if;
|
|
686
704
|
perform @SCHEMA@.auth_guard(acc);
|
|
705
|
+
if p_enabled is false and live and knd in ('APIKEY', 'KEYSECRET') and @SCHEMA@.auth_is_service(acc) then
|
|
706
|
+
-- последний действующий ключ сервиса не отключается, как и не отзывается
|
|
707
|
+
perform 1 from @SCHEMA@.secret s
|
|
708
|
+
where s.account = acc and s.kind in ('APIKEY', 'KEYSECRET') and s.deleted_at is null
|
|
709
|
+
for update;
|
|
710
|
+
if not exists (select 1 from @SCHEMA@.secret s
|
|
711
|
+
where s.account = acc and s.kind in ('APIKEY', 'KEYSECRET') and s.deleted_at is null and s.id <> p_id
|
|
712
|
+
and (s.expires_at is null or s.expires_at > clock_timestamp()) and coalesce((s.meta->>'enabled')::boolean, true)) then
|
|
713
|
+
perform @SCHEMA@.lt_fail('sql-access.last_service_key');
|
|
714
|
+
end if;
|
|
715
|
+
end if;
|
|
687
716
|
update @SCHEMA@.secret s
|
|
688
717
|
set meta = s.meta || jsonb_strip_nulls(jsonb_build_object('confirmed', p_confirmed, 'enabled', p_enabled))
|
|
689
718
|
where s.id = p_id;
|
|
690
|
-
|
|
719
|
+
if not found then
|
|
720
|
+
return false;
|
|
721
|
+
end if;
|
|
722
|
+
-- отключённый кред не должен оставлять действующих сессий: ключ — свои (auth_kill_cred), прочие — сессии аккаунта
|
|
723
|
+
if p_enabled is false then
|
|
724
|
+
if knd in ('APIKEY', 'KEYSECRET') then
|
|
725
|
+
perform @SCHEMA@.auth_kill_cred(acc, p_id, knd, null);
|
|
726
|
+
else
|
|
727
|
+
perform @SCHEMA@.auth_kill(acc, null);
|
|
728
|
+
end if;
|
|
729
|
+
end if;
|
|
730
|
+
return true;
|
|
691
731
|
end $$;
|
|
692
732
|
|
|
693
733
|
-- Продление текущей сессии (библиотека — не чаще раза в минуту, §2.9). Новый срок — от сейчас.
|
|
@@ -725,6 +765,31 @@ begin
|
|
|
725
765
|
return m is not null;
|
|
726
766
|
end $$;
|
|
727
767
|
|
|
768
|
+
-- Выход пользователя по токену его сессии (db.auth.revoke(token), план 2.1, A3): сервис, у которого есть токен
|
|
769
|
+
-- пользователя (например, из cookie), гасит эту сессию, не подключаясь по ней. Право auth.session, как у
|
|
770
|
+
-- auth_session_info. Сессия ищется только среди SESSION по sha256 токена; погашенная — сигнал session:… кэшу, как у
|
|
771
|
+
-- auth_logout. Неверный, погашенный или истёкший токен — false: причина не различается.
|
|
772
|
+
create or replace function @SCHEMA@.auth_logout_token(p_token text) returns boolean
|
|
773
|
+
language plpgsql volatile security definer
|
|
774
|
+
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
775
|
+
as $$
|
|
776
|
+
declare
|
|
777
|
+
c record;
|
|
778
|
+
m bytea;
|
|
779
|
+
begin
|
|
780
|
+
select * into c from @SCHEMA@.ctx();
|
|
781
|
+
perform @SCHEMA@.auth_session_required(c.account);
|
|
782
|
+
perform @SCHEMA@.acl_require('auth.session');
|
|
783
|
+
update @SCHEMA@.secret s set deleted_at = clock_timestamp()
|
|
784
|
+
where s.kind = 'SESSION' and s.deleted_at is null and s.material = sha256(convert_to(coalesce(p_token, ''), 'UTF8'))
|
|
785
|
+
and (s.expires_at is null or s.expires_at > clock_timestamp())
|
|
786
|
+
returning s.material into m;
|
|
787
|
+
if m is not null then
|
|
788
|
+
perform @SCHEMA@.signal_raw(array['session:' || left(encode(m, 'hex'), 16)]);
|
|
789
|
+
end if;
|
|
790
|
+
return m is not null;
|
|
791
|
+
end $$;
|
|
792
|
+
|
|
728
793
|
-- Выход везде: все сессии аккаунта (своего — сам; чужого — с правом auth.session).
|
|
729
794
|
create or replace function @SCHEMA@.auth_logout_all(p_account uuid default null) returns int
|
|
730
795
|
language plpgsql volatile security definer
|
|
@@ -756,20 +821,20 @@ declare
|
|
|
756
821
|
begin
|
|
757
822
|
if nullif(current_setting('letopis.account', true), '') is not null then
|
|
758
823
|
-- код из каталога базы (invalid_query в нём нет — lt_fail дал бы LT000 и сырую ошибку; план 2.0, 7.5 №3)
|
|
759
|
-
perform @SCHEMA@.lt_fail('
|
|
824
|
+
perform @SCHEMA@.lt_fail('sql-access.switch_impersonating');
|
|
760
825
|
end if;
|
|
761
826
|
select x.id, x.account into s from @SCHEMA@.secret x
|
|
762
827
|
where x.kind = 'SESSION' and x.deleted_at is null and x.material = sha256(convert_to(coalesce(tok, ''), 'UTF8'))
|
|
763
828
|
and (x.expires_at is null or x.expires_at > clock_timestamp())
|
|
764
829
|
for update;
|
|
765
830
|
if s.id is null or not @SCHEMA@.auth_enabled(s.account) then
|
|
766
|
-
perform @SCHEMA@.lt_fail('no_session'
|
|
831
|
+
perform @SCHEMA@.lt_fail('sql-access.no_session');
|
|
767
832
|
end if;
|
|
768
833
|
if p_tenant <> s.account and not (
|
|
769
834
|
exists (select 1 from @SCHEMA@.entity x
|
|
770
835
|
where x.id = @SCHEMA@.v5_id(p_tenant, 'member', array[s.account::text, p_tenant::text]) and x.class = 'member' and x.tenant = p_tenant)
|
|
771
836
|
and @SCHEMA@.auth_enabled(p_tenant)) then
|
|
772
|
-
perform @SCHEMA@.lt_fail('
|
|
837
|
+
perform @SCHEMA@.lt_fail('sql-access.no_membership');
|
|
773
838
|
end if;
|
|
774
839
|
update @SCHEMA@.secret x
|
|
775
840
|
set meta = case when p_tenant = s.account then x.meta - 'tenant' else x.meta || jsonb_build_object('tenant', p_tenant) end
|
|
@@ -777,8 +842,10 @@ begin
|
|
|
777
842
|
return p_tenant;
|
|
778
843
|
end $$;
|
|
779
844
|
|
|
780
|
-
-- Арендаторы, доступные аккаунту сессии: собственный и по членству (с ролями).
|
|
781
|
-
|
|
845
|
+
-- Арендаторы, доступные аккаунту сессии: собственный и по членству (с ролями). С ревизии 9 (план 2.1, A5) — ещё имя
|
|
846
|
+
-- аккаунта арендатора (строка Account в System: createTenant({ name }), у личного — имя пользователя) и признак
|
|
847
|
+
-- собственного арендатора personal; прежняя форма (tenant, roles) снимается миграцией 0009.
|
|
848
|
+
create or replace function @SCHEMA@.auth_tenants() returns table (tenant uuid, roles jsonb, name text, personal boolean)
|
|
782
849
|
language plpgsql stable security definer
|
|
783
850
|
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
784
851
|
as $$
|
|
@@ -789,11 +856,14 @@ begin
|
|
|
789
856
|
select * into c from @SCHEMA@.ctx();
|
|
790
857
|
perform @SCHEMA@.auth_session_required(c.account);
|
|
791
858
|
return query
|
|
792
|
-
select
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
859
|
+
select t.tenant, t.roles, a.data->>'name', t.personal
|
|
860
|
+
from (select c.account as tenant, '["owner"]'::jsonb as roles, true as personal, 0 as ord
|
|
861
|
+
union all
|
|
862
|
+
select m.tenant, coalesce(m.data->'roles', '[]'::jsonb), false, 1
|
|
863
|
+
from @SCHEMA@.entity m
|
|
864
|
+
where m.class = 'member' and m.links->>'User' = c.account::text and @SCHEMA@.auth_enabled(m.tenant)) t
|
|
865
|
+
left join @SCHEMA@.entity a on a.id = t.tenant and a.class = 'Account' and a.tenant = '@SYSTEM@'::uuid
|
|
866
|
+
order by t.ord, t.tenant;
|
|
797
867
|
end $$;
|
|
798
868
|
|
|
799
869
|
-- Сведения о сессии по токену (db.auth.session): сервис, у которого есть токен пользователя (например, из
|
|
@@ -845,15 +915,12 @@ begin
|
|
|
845
915
|
'login_otp(text, text, text)', 'totp_code(bytea, bigint)', 'auth_enroll_totp(uuid)', 'auth_verify_totp(uuid, text, int)',
|
|
846
916
|
'auth_totp_enabled(uuid)', 'auth_link(uuid, text, text, jsonb, boolean)', 'auth_lookup(text, text, boolean)',
|
|
847
917
|
'auth_credentials(uuid)', 'auth_revoke_credential(uuid, boolean)', 'auth_set_flags(uuid, boolean, boolean)',
|
|
848
|
-
'auth_refresh(interval)', 'auth_logout()', '
|
|
918
|
+
'auth_refresh(interval)', 'auth_logout()', 'auth_logout_token(text)', 'auth_logout_all(uuid)', 'auth_switch(uuid)', 'auth_tenants()',
|
|
919
|
+
'auth_session_info(text)'] loop
|
|
849
920
|
execute format('alter function @SCHEMA@.%s owner to @OWNER@', f);
|
|
850
921
|
execute format('revoke all on function @SCHEMA@.%s from public', f);
|
|
851
922
|
end loop;
|
|
852
923
|
end $$;
|
|
853
|
-
-- до этапа 5 выпуск сессии сервисом принимал два аргумента; ранние сигнатуры OTP этапа 5
|
|
854
|
-
drop function if exists @SCHEMA@.issue_session(uuid, interval);
|
|
855
|
-
drop function if exists @SCHEMA@.auth_issue_otp(uuid, text, text, interval, int);
|
|
856
|
-
drop function if exists @SCHEMA@.login_otp(text, text, text, int);
|
|
857
924
|
grant execute on function
|
|
858
925
|
@SCHEMA@.issue_session(uuid, interval, text[]), @SCHEMA@.auth_password_get(text, text),
|
|
859
926
|
@SCHEMA@.auth_set_password(uuid, text, text, text, boolean, boolean), @SCHEMA@.auth_issue_apikey(uuid, text, interval),
|
|
@@ -861,5 +928,5 @@ grant execute on function
|
|
|
861
928
|
@SCHEMA@.login_otp(text, text, text), @SCHEMA@.auth_enroll_totp(uuid), @SCHEMA@.auth_verify_totp(uuid, text, int),
|
|
862
929
|
@SCHEMA@.auth_totp_enabled(uuid), @SCHEMA@.auth_link(uuid, text, text, jsonb, boolean), @SCHEMA@.auth_lookup(text, text, boolean),
|
|
863
930
|
@SCHEMA@.auth_credentials(uuid), @SCHEMA@.auth_revoke_credential(uuid, boolean), @SCHEMA@.auth_set_flags(uuid, boolean, boolean),
|
|
864
|
-
@SCHEMA@.auth_refresh(interval), @SCHEMA@.auth_logout(), @SCHEMA@.
|
|
865
|
-
@SCHEMA@.auth_tenants(), @SCHEMA@.auth_session_info(text) to @APP@;
|
|
931
|
+
@SCHEMA@.auth_refresh(interval), @SCHEMA@.auth_logout(), @SCHEMA@.auth_logout_token(text), @SCHEMA@.auth_logout_all(uuid),
|
|
932
|
+
@SCHEMA@.auth_switch(uuid), @SCHEMA@.auth_tenants(), @SCHEMA@.auth_session_info(text) to @APP@;
|