letopis 1.4.0 → 1.4.1
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.en.md +10 -5
- package/AGENT-CHEATSHEET.md +14 -9
- package/CHANGELOG.md +159 -0
- package/README.en.md +119 -52
- package/README.md +107 -47
- package/dist/acl.d.ts +11 -5
- package/dist/admin.d.ts +30 -14
- package/dist/admin.js +42 -16
- package/dist/auth.d.ts +61 -17
- package/dist/auth.js +16 -2
- package/dist/cache.d.ts +5 -1
- package/dist/chain.d.ts +91 -15
- package/dist/chain.js +47 -4
- package/dist/cli.js +6 -0
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +5 -5
- package/dist/import.js +7 -0
- package/dist/index.d.ts +74 -45
- package/dist/index.js +12 -5
- package/dist/load.d.ts +10 -3
- package/dist/load.js +19 -4
- package/dist/mcp.d.ts +20 -11
- package/dist/mcp.js +4 -4
- package/dist/model.d.ts +4 -1
- package/dist/ops.d.ts +58 -18
- package/dist/ops.js +58 -18
- package/dist/project.d.ts +30 -11
- package/dist/project.js +30 -4
- package/dist/registry.d.ts +4 -1
- package/dist/registry.js +4 -1
- package/dist/sql.d.ts +4 -1
- package/dist/sql.js +12 -4
- package/dist/tx.js +10 -1
- package/dist/types.d.ts +14 -7
- package/dist/types.js +13 -6
- package/dist/up.d.ts +48 -25
- package/dist/up.js +89 -41
- package/dist/validate.d.ts +9 -2
- package/dist/validate.js +19 -3
- package/dist/watch.d.ts +17 -5
- package/dist/watch.js +40 -8
- package/dist/write.d.ts +3 -2
- package/dist/write.js +29 -10
- package/package.json +1 -1
- package/sql/10-core.sql +6 -1
- package/sql/60-write.sql +38 -18
- package/sql/80-auth.sql +39 -2
- package/sql/88-derived.sql +1 -0
- package/sql/90-time.sql +15 -5
- package/sql/95-seed.system.sql +6 -3
- package/sql/99-revision.sql +1 -1
- package/sql/migrate/0007-secret-account-fk.sql +12 -0
package/sql/60-write.sql
CHANGED
|
@@ -1217,7 +1217,8 @@ begin
|
|
|
1217
1217
|
select n.id, n.rev, n.class, n.cv, n.tenant, n.owner, n.links, n.data, n.tags, n.at, n.author, n.agent, n.op, n.reason, n.moved, n.prev_hash
|
|
1218
1218
|
from added n
|
|
1219
1219
|
join (select k.class, k.tenant from (select distinct a.class, a.tenant from added a) k
|
|
1220
|
-
where @SCHEMA@.history_on(k.class, k.tenant)) h on h.class = n.class and h.tenant = n.tenant
|
|
1220
|
+
where @SCHEMA@.history_on(k.class, k.tenant)) h on h.class = n.class and h.tenant = n.tenant
|
|
1221
|
+
order by n.at, n.id; -- порядок seq — порядок входа, а не плана соединения
|
|
1221
1222
|
-- сигнал — по паре «класс, арендатор»
|
|
1222
1223
|
select array_agg(k.class), array_agg(k.tenant) into sc, st from (select distinct a.class, a.tenant from added a) k;
|
|
1223
1224
|
perform @SCHEMA@.signal(sc, st);
|
|
@@ -1273,7 +1274,8 @@ begin
|
|
|
1273
1274
|
from new_rows n
|
|
1274
1275
|
join old_rows o on o.id = n.id and o.rev <> n.rev
|
|
1275
1276
|
join (select k.class, k.tenant from (select distinct a.class, a.tenant from new_rows a) k
|
|
1276
|
-
where @SCHEMA@.history_on(k.class, k.tenant)) h on h.class = n.class and h.tenant = n.tenant
|
|
1277
|
+
where @SCHEMA@.history_on(k.class, k.tenant)) h on h.class = n.class and h.tenant = n.tenant
|
|
1278
|
+
order by n.id; -- порядок seq не зависит от плана: правка многих строк пишет версии в порядке id
|
|
1277
1279
|
-- сигнал — по парам нового и прежнего класса: при смене класса без ключа ответы прежнего класса в
|
|
1278
1280
|
-- кэше других процессов устарели бы (в 1.0.1 сигнал шёл только по новому)
|
|
1279
1281
|
select array_agg(k.class), array_agg(k.tenant) into sc, st
|
|
@@ -1385,6 +1387,24 @@ begin
|
|
|
1385
1387
|
return null;
|
|
1386
1388
|
end $$;
|
|
1387
1389
|
|
|
1390
|
+
-- Перед удалением строки Account (ревизия 7, решение владельца). Строки secret аккаунта — сессии, хэши паролей и ключей,
|
|
1391
|
+
-- секреты TOTP, внешние учётные записи — уносит каскад внешнего ключа secret.account в конце оператора, раньше триггера
|
|
1392
|
+
-- после него, поэтому сессии гаснут с сигналом session:<хэш> здесь: процессы сбрасывают кэш ответов этих сессий.
|
|
1393
|
+
-- Срабатывает для всех ролей, в том числе владельца: аккаунты удаляют и функции владельца — каскад trg_entity_ad и
|
|
1394
|
+
-- account_purge. Строка Account у System не удаляется: ей принадлежат служебные строки secret (KEY — ключ прав и
|
|
1395
|
+
-- сигналов, LOCK — строки-замки, PROJECTOR — чекпоинты проекторов), которые каскад стёр бы, а без них установка не пишет.
|
|
1396
|
+
create or replace function @SCHEMA@.trg_entity_bd_account() returns trigger
|
|
1397
|
+
language plpgsql security definer
|
|
1398
|
+
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
1399
|
+
as $$
|
|
1400
|
+
begin
|
|
1401
|
+
if OLD.id = '@SYSTEM@'::uuid then
|
|
1402
|
+
perform @SCHEMA@.lt_fail('acl_denied', 'аккаунт System не удаляется: ему принадлежат служебные строки установки');
|
|
1403
|
+
end if;
|
|
1404
|
+
perform @SCHEMA@.auth_kill(OLD.id, null);
|
|
1405
|
+
return OLD;
|
|
1406
|
+
end $$;
|
|
1407
|
+
|
|
1388
1408
|
-- Действие со строкой, которая ссылается на удаляемые id (p_ids), по правилам её концов (p_ends):
|
|
1389
1409
|
-- cascade сильнее restrict, restrict сильнее unset; null — строка на них не ссылается. Общее правило
|
|
1390
1410
|
-- для триггера удаления и превью delete_preview() (этап 4, п. 6): превью не расходится с удалением.
|
|
@@ -1456,9 +1476,7 @@ declare
|
|
|
1456
1476
|
dplan jsonb;
|
|
1457
1477
|
wplan jsonb;
|
|
1458
1478
|
denied text;
|
|
1459
|
-
accs uuid[];
|
|
1460
1479
|
mems jsonb;
|
|
1461
|
-
lvl_accs uuid[];
|
|
1462
1480
|
lvl_mems jsonb;
|
|
1463
1481
|
drows jsonb;
|
|
1464
1482
|
own uuid[];
|
|
@@ -1526,19 +1544,21 @@ begin
|
|
|
1526
1544
|
perform @SCHEMA@.agg_mark(g.tenant, @SCHEMA@.agg_of(g.data->'resolved'))
|
|
1527
1545
|
from gone g where g.class = 'Class' and g.data->'resolved' ? 'aggregate';
|
|
1528
1546
|
v_author := coalesce(c.author, '@SYSTEM@'::uuid);
|
|
1529
|
-
-- надгробия: последний снимок следующей версией с видом delete — одной вставкой на
|
|
1547
|
+
-- надгробия: последний снимок следующей версией с видом delete — одной вставкой на уровень, в порядке id: без
|
|
1548
|
+
-- order by порядок seq (а с ним итог delete() и время at) зависел от плана соединения и менялся от прогона к
|
|
1549
|
+
-- прогону; время now_ts() (volatile) планировщик вычисляет уже после сортировки
|
|
1530
1550
|
insert into @SCHEMA@.log (id, rev, class, cv, tenant, owner, links, data, tags, at, author, agent, op, reason, moved, prev_hash, changed)
|
|
1531
1551
|
select g.id, g.rev + 1, g.class, g.cv, g.tenant, g.owner, g.links, g.data, g.tags,
|
|
1532
1552
|
greatest(@SCHEMA@.now_ts(), g.at + interval '1 microsecond'), v_author, c.agent, 'delete', v_reason, null, g.hash, '[]'::jsonb
|
|
1533
1553
|
from gone g
|
|
1534
1554
|
join (select d.class, d.tenant from (select distinct x.class, x.tenant from gone x) d
|
|
1535
|
-
where @SCHEMA@.history_on(d.class, d.tenant)) h on h.class = g.class and h.tenant = g.tenant
|
|
1555
|
+
where @SCHEMA@.history_on(d.class, d.tenant)) h on h.class = g.class and h.tenant = g.tenant
|
|
1556
|
+
order by g.id;
|
|
1536
1557
|
-- пары «класс, арендатор» для сигнала — строкой «класс арендатор» (пробела нет ни в имени, ни в uuid)
|
|
1537
1558
|
select coalesce(array_agg(g.id), '{}'), coalesce(array_agg(distinct g.class), '{}'),
|
|
1538
1559
|
coalesce(array_agg(distinct g.class || ' ' || g.tenant::text), '{}'),
|
|
1539
|
-
coalesce(array_agg(g.id) filter (where g.class = 'Account'), '{}'),
|
|
1540
1560
|
coalesce(jsonb_agg(jsonb_build_object('tenant', g.tenant, 'user', g.links->>'User')) filter (where g.class = 'member'), '[]'::jsonb)
|
|
1541
|
-
into lvl, classes, pairs,
|
|
1561
|
+
into lvl, classes, pairs, mems from gone g;
|
|
1542
1562
|
all_ids := lvl;
|
|
1543
1563
|
while cardinality(lvl) > 0 loop
|
|
1544
1564
|
-- кто ссылается на уровень: действие по правилам концов (общее с превью)
|
|
@@ -1566,20 +1586,19 @@ begin
|
|
|
1566
1586
|
from d
|
|
1567
1587
|
join (select dd.class, dd.tenant from (select distinct x.class, x.tenant from d x) dd
|
|
1568
1588
|
where @SCHEMA@.history_on(dd.class, dd.tenant)) h on h.class = d.class and h.tenant = d.tenant
|
|
1589
|
+
order by d.id -- надгробия уровня в порядке id, как и на уровне 0
|
|
1569
1590
|
)
|
|
1570
1591
|
select coalesce(array_agg(d.id), '{}'), coalesce(array_agg(distinct d.class), '{}'),
|
|
1571
1592
|
coalesce(array_agg(distinct d.class || ' ' || d.tenant::text), '{}'),
|
|
1572
1593
|
jsonb_agg(to_jsonb(d)) filter (where not @SCHEMA@.acl_row_ok(dplan, d.class, d.id, d.owner, d.tenant, d.tags, d.data, d.links)),
|
|
1573
|
-
coalesce(array_agg(d.id) filter (where d.class = 'Account'), '{}'),
|
|
1574
1594
|
coalesce(jsonb_agg(jsonb_build_object('tenant', d.tenant, 'user', d.links->>'User')) filter (where d.class = 'member'), '[]'::jsonb)
|
|
1575
|
-
into lvl, lvl_classes, lvl_pairs, drows,
|
|
1595
|
+
into lvl, lvl_classes, lvl_pairs, drows, lvl_mems from d;
|
|
1576
1596
|
-- класс отказавшей строки называется, только если актор её видит (ревью безопасности №3)
|
|
1577
1597
|
if drows is not null then
|
|
1578
1598
|
denied := @SCHEMA@.visible_class(drows);
|
|
1579
1599
|
perform @SCHEMA@.lt_fail('acl_denied', case when denied is not null then format('нет права DELETE на класс %s в каскаде удаления', denied)
|
|
1580
1600
|
else 'нет права DELETE на строки в каскаде удаления' end);
|
|
1581
1601
|
end if;
|
|
1582
|
-
accs := accs || lvl_accs;
|
|
1583
1602
|
mems := mems || lvl_mems;
|
|
1584
1603
|
classes := classes || lvl_classes;
|
|
1585
1604
|
pairs := pairs || lvl_pairs;
|
|
@@ -1605,14 +1624,11 @@ begin
|
|
|
1605
1624
|
end loop;
|
|
1606
1625
|
-- выход из членства (и удаление аккаунта, которое снимает его членства): строки арендатора, которыми
|
|
1607
1626
|
-- владел участник, переходят арендатору (§2.10) — новой версией
|
|
1608
|
-
for r in select distinct (m->>'tenant')::uuid as tenant, (m->>'user')::uuid as usr from jsonb_array_elements(mems) m loop
|
|
1627
|
+
for r in select distinct (m->>'tenant')::uuid as tenant, (m->>'user')::uuid as usr from jsonb_array_elements(mems) m order by 1, 2 loop
|
|
1609
1628
|
perform @SCHEMA@.owner_to_tenant(r.tenant, r.usr);
|
|
1610
1629
|
end loop;
|
|
1611
|
-
-- удалённый
|
|
1612
|
-
|
|
1613
|
-
perform @SCHEMA@.auth_kill(r.acc, null);
|
|
1614
|
-
update @SCHEMA@.secret x set deleted_at = clock_timestamp() where x.account = r.acc and x.deleted_at is null;
|
|
1615
|
-
end loop;
|
|
1630
|
+
-- удалённый аккаунт (ревизия 7): сессии погасил с сигналом триггер до удаления (trg_entity_bd_account), его строки
|
|
1631
|
+
-- secret стёр каскад внешнего ключа secret.account — идентификаторы входа свободны
|
|
1616
1632
|
perform @SCHEMA@.signal(array_agg(split_part(x.p, ' ', 1)), array_agg(split_part(x.p, ' ', 2)::uuid))
|
|
1617
1633
|
from (select distinct p from unnest(pairs) p) x;
|
|
1618
1634
|
if classes && array['rule', 'Resource', 'member', 'Account', 'Class'] then
|
|
@@ -1700,6 +1716,9 @@ create or replace trigger entity_au after update on @SCHEMA@.entity referencing
|
|
|
1700
1716
|
for each statement when (current_user <> '@OWNER_NAME@') execute function @SCHEMA@.trg_entity_au();
|
|
1701
1717
|
create or replace trigger entity_bd before delete on @SCHEMA@.entity
|
|
1702
1718
|
for each statement when (current_user <> '@OWNER_NAME@') execute function @SCHEMA@.trg_entity_bd();
|
|
1719
|
+
-- удаление аккаунта — для всех ролей, в том числе владельца (каскад trg_entity_ad, account_purge): ревизия 7
|
|
1720
|
+
create or replace trigger entity_bd_account before delete on @SCHEMA@.entity
|
|
1721
|
+
for each row when (OLD.class = 'Account') execute function @SCHEMA@.trg_entity_bd_account();
|
|
1703
1722
|
create or replace trigger entity_ad after delete on @SCHEMA@.entity referencing old table as gone
|
|
1704
1723
|
for each statement when (current_user <> '@OWNER_NAME@') execute function @SCHEMA@.trg_entity_ad();
|
|
1705
1724
|
-- в режиме репликации (session_replication_role = replica) триггеры тоже работают
|
|
@@ -1708,6 +1727,7 @@ alter table @SCHEMA@.entity enable always trigger entity_bu;
|
|
|
1708
1727
|
alter table @SCHEMA@.entity enable always trigger entity_ai;
|
|
1709
1728
|
alter table @SCHEMA@.entity enable always trigger entity_au;
|
|
1710
1729
|
alter table @SCHEMA@.entity enable always trigger entity_bd;
|
|
1730
|
+
alter table @SCHEMA@.entity enable always trigger entity_bd_account;
|
|
1711
1731
|
alter table @SCHEMA@.entity enable always trigger entity_ad;
|
|
1712
1732
|
|
|
1713
1733
|
-- ========================= смена класса с ключом =========================
|
|
@@ -2030,7 +2050,7 @@ begin
|
|
|
2030
2050
|
'incoming_conflicts(uuid, text, uuid)', 'visible_detail(jsonb, uuid)', 'visible_class(jsonb)', 'class_change_safe(jsonb, jsonb)',
|
|
2031
2051
|
'family_conflicts(text[], uuid, boolean)', 'reserved_names()', 'class_acl_admit(text, uuid)', 'class_rows_admit(text, uuid, text[], text)', 'class_has_history(text, uuid)', 'class_orphan_history(text, uuid)', 'class_admit(jsonb, uuid)',
|
|
2032
2052
|
'trg_entity_bi()', 'trg_entity_bu()', 'trg_entity_ai()', 'trg_entity_au()', 'history_on(text, uuid)', 'signal_token(text, text, bytea)', 'signal_raw(text[])', 'signal(text[], uuid[])', 'class_evolve(@SCHEMA@.entity, @SCHEMA@.entity)',
|
|
2033
|
-
'trg_entity_bd()', 'trg_entity_ad()', 'owner_to_tenant(uuid, uuid)', 'ref_action(jsonb, jsonb, uuid[])', 'delete_preview(uuid[])', 'merge(jsonb, jsonb)', 'inc(jsonb, text[], numeric, numeric)', 'links_patch(jsonb, jsonb)'] loop
|
|
2053
|
+
'trg_entity_bd()', 'trg_entity_bd_account()', 'trg_entity_ad()', 'owner_to_tenant(uuid, uuid)', 'ref_action(jsonb, jsonb, uuid[])', 'delete_preview(uuid[])', 'merge(jsonb, jsonb)', 'inc(jsonb, text[], numeric, numeric)', 'links_patch(jsonb, jsonb)'] loop
|
|
2034
2054
|
execute format('alter function @SCHEMA@.%s owner to @OWNER@', f);
|
|
2035
2055
|
execute format('revoke all on function @SCHEMA@.%s from public', f);
|
|
2036
2056
|
end loop;
|
package/sql/80-auth.sql
CHANGED
|
@@ -795,6 +795,43 @@ begin
|
|
|
795
795
|
where m.class = 'member' and m.links->>'User' = c.account::text and @SCHEMA@.auth_enabled(m.tenant);
|
|
796
796
|
end $$;
|
|
797
797
|
|
|
798
|
+
-- Сведения о сессии по токену (db.auth.session): сервис, у которого есть токен пользователя (например, из
|
|
799
|
+
-- cookie), узнаёт, чья это сессия, жива ли она и в каком арендаторе работает, не подключаясь по этому токену.
|
|
800
|
+
-- Право auth.session, как у issue_session. Неверный, погашенный или истёкший токен и отключённый аккаунт — null:
|
|
801
|
+
-- причина не различается, как в ctx(). Сессия ищется только среди SESSION: ключ API или код с тем же sha256 не
|
|
802
|
+
-- подойдут. member — проверка членства из ctx() без исключения: членство в рабочем арендаторе цело и арендатор
|
|
803
|
+
-- включён; собственный арендатор — всегда true.
|
|
804
|
+
create or replace function @SCHEMA@.auth_session_info(p_token text) returns jsonb
|
|
805
|
+
language plpgsql stable security definer
|
|
806
|
+
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
807
|
+
as $$
|
|
808
|
+
declare
|
|
809
|
+
c record;
|
|
810
|
+
s record;
|
|
811
|
+
t uuid;
|
|
812
|
+
begin
|
|
813
|
+
select * into c from @SCHEMA@.ctx();
|
|
814
|
+
perform @SCHEMA@.auth_session_required(c.account);
|
|
815
|
+
perform @SCHEMA@.acl_require('auth.session');
|
|
816
|
+
select x.account, x.meta, x.expires_at into s from @SCHEMA@.secret x
|
|
817
|
+
where x.kind = 'SESSION' and x.deleted_at is null and x.material = sha256(convert_to(coalesce(p_token, ''), 'UTF8'))
|
|
818
|
+
and (x.expires_at is null or x.expires_at > clock_timestamp());
|
|
819
|
+
if s.account is null or not @SCHEMA@.auth_enabled(s.account) then
|
|
820
|
+
return null;
|
|
821
|
+
end if;
|
|
822
|
+
t := coalesce((s.meta->>'tenant')::uuid, s.account);
|
|
823
|
+
return jsonb_build_object(
|
|
824
|
+
'account', s.account,
|
|
825
|
+
'tenant', t,
|
|
826
|
+
'member', t = s.account or (
|
|
827
|
+
exists (select 1 from @SCHEMA@.entity x
|
|
828
|
+
where x.id = @SCHEMA@.v5_id(t, 'member', array[s.account::text, t::text]) and x.class = 'member' and x.tenant = t)
|
|
829
|
+
and @SCHEMA@.auth_enabled(t)),
|
|
830
|
+
'expiresAt', s.expires_at,
|
|
831
|
+
'loginAt', s.meta->'login_at',
|
|
832
|
+
'methods', coalesce(s.meta->'methods', '[]'::jsonb));
|
|
833
|
+
end $$;
|
|
834
|
+
|
|
798
835
|
do $$
|
|
799
836
|
declare f text;
|
|
800
837
|
begin
|
|
@@ -807,7 +844,7 @@ begin
|
|
|
807
844
|
'login_otp(text, text, text)', 'totp_code(bytea, bigint)', 'auth_enroll_totp(uuid)', 'auth_verify_totp(uuid, text, int)',
|
|
808
845
|
'auth_totp_enabled(uuid)', 'auth_link(uuid, text, text, jsonb, boolean)', 'auth_lookup(text, text, boolean)',
|
|
809
846
|
'auth_credentials(uuid)', 'auth_revoke_credential(uuid, boolean)', 'auth_set_flags(uuid, boolean, boolean)',
|
|
810
|
-
'auth_refresh(interval)', 'auth_logout()', 'auth_logout_all(uuid)', 'auth_switch(uuid)', 'auth_tenants()'] loop
|
|
847
|
+
'auth_refresh(interval)', 'auth_logout()', 'auth_logout_all(uuid)', 'auth_switch(uuid)', 'auth_tenants()', 'auth_session_info(text)'] loop
|
|
811
848
|
execute format('alter function @SCHEMA@.%s owner to @OWNER@', f);
|
|
812
849
|
execute format('revoke all on function @SCHEMA@.%s from public', f);
|
|
813
850
|
end loop;
|
|
@@ -824,4 +861,4 @@ grant execute on function
|
|
|
824
861
|
@SCHEMA@.auth_totp_enabled(uuid), @SCHEMA@.auth_link(uuid, text, text, jsonb, boolean), @SCHEMA@.auth_lookup(text, text, boolean),
|
|
825
862
|
@SCHEMA@.auth_credentials(uuid), @SCHEMA@.auth_revoke_credential(uuid, boolean), @SCHEMA@.auth_set_flags(uuid, boolean, boolean),
|
|
826
863
|
@SCHEMA@.auth_refresh(interval), @SCHEMA@.auth_logout(), @SCHEMA@.auth_logout_all(uuid), @SCHEMA@.auth_switch(uuid),
|
|
827
|
-
@SCHEMA@.auth_tenants() to @APP@;
|
|
864
|
+
@SCHEMA@.auth_tenants(), @SCHEMA@.auth_session_info(text) to @APP@;
|
package/sql/88-derived.sql
CHANGED
|
@@ -272,6 +272,7 @@ begin
|
|
|
272
272
|
from g
|
|
273
273
|
join (select d.class, d.tenant from (select distinct y.class, y.tenant from g y) d
|
|
274
274
|
where @SCHEMA@.history_on(d.class, d.tenant)) h on h.class = g.class and h.tenant = g.tenant
|
|
275
|
+
order by g.id -- надгробия в порядке id, как в trg_entity_ad
|
|
275
276
|
)
|
|
276
277
|
select count(*)::int, coalesce(array_agg(distinct g.class || ' ' || g.tenant::text), '{}') into n, pairs from g;
|
|
277
278
|
if n > 0 then
|
package/sql/90-time.sql
CHANGED
|
@@ -622,7 +622,7 @@ end $$;
|
|
|
622
622
|
-- последнего включённого владельца. Данные и журнал арендатора-цели стираются физически; в чужих
|
|
623
623
|
-- арендаторах членства снимаются (надгробия), владение строками переходит арендаторам (§2.10);
|
|
624
624
|
-- остались ссылки на аккаунт — delete_restricted. Строка Account удаляется с надгробием (история
|
|
625
|
-
-- аккаунтов — аудит), сессии
|
|
625
|
+
-- аккаунтов — аудит), сессии гаснут с сигналом, строки secret аккаунта стирает каскад внешнего ключа.
|
|
626
626
|
create or replace function @SCHEMA@.account_purge(p_account uuid) returns jsonb
|
|
627
627
|
language plpgsql volatile security definer
|
|
628
628
|
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
@@ -658,6 +658,14 @@ begin
|
|
|
658
658
|
if a.id is null or p_account = '@SYSTEM@'::uuid then
|
|
659
659
|
perform @SCHEMA@.lt_fail('target_not_found', 'аккаунт не найден');
|
|
660
660
|
end if;
|
|
661
|
+
-- владелец стирает арендатора, в котором работает: его членство уйдёт вместе с данными, а надгробия и
|
|
662
|
+
-- передача владения ниже перечитывают ctx() — остаток операции идёт в личном арендаторе вызывающего,
|
|
663
|
+
-- и его сессия больше не указывает на стёртого арендатора (иначе tenant_denied и откат всего вызова)
|
|
664
|
+
if c.tenant = p_account then
|
|
665
|
+
perform set_config('letopis.tenant', c.account::text, true);
|
|
666
|
+
update @SCHEMA@.secret s set meta = s.meta - 'tenant'
|
|
667
|
+
where s.id = c.session and s.kind = 'SESSION' and s.meta->>'tenant' = p_account::text;
|
|
668
|
+
end if;
|
|
661
669
|
-- последний владелец: в каждом чужом арендаторе, где цель — владелец, должен остаться другой
|
|
662
670
|
for t in select x.tenant from @SCHEMA@.entity x
|
|
663
671
|
where x.class = 'member' and x.links->>'User' = p_account::text and x.tenant <> p_account
|
|
@@ -679,7 +687,7 @@ begin
|
|
|
679
687
|
perform @SCHEMA@.tombstone(m);
|
|
680
688
|
n_mem := n_mem + 1;
|
|
681
689
|
end loop;
|
|
682
|
-
for t in select distinct x.tenant from @SCHEMA@.entity x where x.owner = p_account and x.owner <> x.tenant loop
|
|
690
|
+
for t in select distinct x.tenant from @SCHEMA@.entity x where x.owner = p_account and x.owner <> x.tenant order by x.tenant loop
|
|
683
691
|
n_own := n_own + @SCHEMA@.owner_to_tenant(t.tenant, p_account);
|
|
684
692
|
end loop;
|
|
685
693
|
select coalesce(jsonb_agg(jsonb_build_object('id', x.id, 'class', x.class, 'tenant', x.tenant)), '[]'::jsonb) into blockers
|
|
@@ -687,10 +695,10 @@ begin
|
|
|
687
695
|
if blockers <> '[]'::jsonb then
|
|
688
696
|
perform @SCHEMA@.lt_fail('delete_restricted', 'на аккаунт ссылаются другие строки', @SCHEMA@.visible_detail(blockers, c.tenant));
|
|
689
697
|
end if;
|
|
698
|
+
-- сессии аккаунта гасит с сигналом триггер до удаления (trg_entity_bd_account, срабатывает и для владельца), а его
|
|
699
|
+
-- строки secret — сессии и креды — стирает каскад внешнего ключа secret.account (ревизия 7)
|
|
690
700
|
delete from @SCHEMA@.entity x where x.id = p_account;
|
|
691
701
|
perform @SCHEMA@.tombstone(a);
|
|
692
|
-
perform @SCHEMA@.auth_kill(p_account, null);
|
|
693
|
-
delete from @SCHEMA@.secret s where s.account = p_account;
|
|
694
702
|
-- данные арендатора-цели — его парами; Account и member — классы прав, их сигнал сбрасывает всё
|
|
695
703
|
perform @SCHEMA@.signal(classes || array['Account', 'member'],
|
|
696
704
|
array(select p_account from unnest(classes)) || array['@SYSTEM@'::uuid, '@SYSTEM@'::uuid]);
|
|
@@ -897,7 +905,9 @@ begin
|
|
|
897
905
|
with d as (delete from @SCHEMA@.secret x where x.kind = 'SESSION'
|
|
898
906
|
and (x.deleted_at is not null or x.expires_at <= clock_timestamp()) returning 1)
|
|
899
907
|
select count(*)::int into s from d;
|
|
900
|
-
|
|
908
|
+
-- коды всех видов, которые выпускает auth_issue_otp: OTP и OTP_<имя> (тот же шаблон вида), — использованные,
|
|
909
|
+
-- заменённые, исчерпавшие попытки (deleted_at) и истёкшие
|
|
910
|
+
with d as (delete from @SCHEMA@.secret x where x.kind ~ '^OTP(_[A-Z0-9]{1,27})?$'
|
|
901
911
|
and (x.deleted_at is not null or x.expires_at <= clock_timestamp()) returning 1)
|
|
902
912
|
select count(*)::int into o from d;
|
|
903
913
|
return jsonb_build_object('sessions', s, 'codes', o, 'queue', pg_notification_queue_usage())
|
package/sql/95-seed.system.sql
CHANGED
|
@@ -78,6 +78,12 @@ declare
|
|
|
78
78
|
o_signals uuid;
|
|
79
79
|
op text;
|
|
80
80
|
begin
|
|
81
|
+
-- аккаунт System — раньше строки ключа: служебные строки secret (KEY, LOCK, PROJECTOR) принадлежат ему, а внешний
|
|
82
|
+
-- ключ secret.account (ревизия 7) требует строку аккаунта в entity. Счётчик прав sys_create повышает, только когда
|
|
83
|
+
-- строка ключа уже есть, — на свежей установке и после reset({ all }) ей неоткуда взяться, и начальный v — 0
|
|
84
|
+
perform @SCHEMA@.sys_create('@SYSTEM@'::uuid, 'Account', '@SYSTEM@'::uuid, '{}',
|
|
85
|
+
'{"name": "System", "categories": ["System"], "enabled": true}');
|
|
86
|
+
|
|
81
87
|
-- Ключ подписи пропуска прав и счётчик изменений прав (50-acl.sql: acl_token, acl_bump); от ключа же
|
|
82
88
|
-- выводится ключ сигналов. Режим сигналов (up({ signals })) хранится в meta этой строки: установщик
|
|
83
89
|
-- читает его перед накатом, поэтому повторный up() без опции режим не меняет, а reset({ all }) кладёт
|
|
@@ -87,9 +93,6 @@ begin
|
|
|
87
93
|
on conflict (id) do update set meta = k.meta || '{"signals": "@SIGNALS@"}'
|
|
88
94
|
where k.meta->>'signals' is distinct from '@SIGNALS@';
|
|
89
95
|
|
|
90
|
-
perform @SCHEMA@.sys_create('@SYSTEM@'::uuid, 'Account', '@SYSTEM@'::uuid, '{}',
|
|
91
|
-
'{"name": "System", "categories": ["System"], "enabled": true}');
|
|
92
|
-
|
|
93
96
|
-- Строка Class, описывающая саму себя, и системные классы. Полная метасхема и развёрнутое
|
|
94
97
|
-- описание resolved — в 40-class.sql (этап 1, п. 6).
|
|
95
98
|
perform @SCHEMA@.sys_create(@SCHEMA@.v5_id('@SYSTEM@'::uuid, 'Class', array['Class']), 'Class', '@SYSTEM@'::uuid, '{}',
|
package/sql/99-revision.sql
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
-- letopis 1.0 — штамп ревизии движка (план, §2.2, этап 1, п. 11): последний оператор наката.
|
|
2
2
|
-- Номер совпадает с DDL_REVISION в lib/src/types.ts (проверяет check-docs). С 1.4 в штампе ещё версия пакета и хэш
|
|
3
3
|
-- файлов ядра (engine, sql): connect() предупреждает, если функции схемы ставила другая версия движка той же ревизии.
|
|
4
|
-
comment on schema @SCHEMA@ is 'letopis ddl_revision=
|
|
4
|
+
comment on schema @SCHEMA@ is 'letopis ddl_revision=7 engine=@ENGINE_VERSION@ sql=@ENGINE_SQL@';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- Ревизия 7 (1.4.1-alpha.0, решение владельца): строки secret принадлежат аккаунту — строке entity. Удалённый аккаунт
|
|
2
|
+
-- уносит свои строки каскадом внешнего ключа: сессии, хэши паролей и ключей, секреты TOTP, внешние учётные записи. До
|
|
3
|
+
-- ревизии 7 удаление аккаунта лишь помечало их deleted_at, а maintain() убирает только сессии и коды OTP — остальное
|
|
4
|
+
-- хранилось вечно. Сначала стираются строки, чьих аккаунтов в entity уже нет, затем ставится ключ (как в 10-core.sql).
|
|
5
|
+
delete from @SCHEMA@.secret s where not exists (select 1 from @SCHEMA@.entity e where e.id = s.account);
|
|
6
|
+
do $$
|
|
7
|
+
begin
|
|
8
|
+
if not exists (select 1 from pg_constraint where conrelid = '@SCHEMA@.secret'::regclass and conname = 'secret_account_fkey') then
|
|
9
|
+
alter table @SCHEMA@.secret add constraint secret_account_fkey
|
|
10
|
+
foreign key (account) references @SCHEMA@.entity (id) on delete cascade;
|
|
11
|
+
end if;
|
|
12
|
+
end $$;
|