letopis 2.0.0 → 3.0.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.md +491 -414
- package/CHANGELOG.md +361 -0
- package/LICENSE +75 -21
- package/LICENSE-COMMERCIAL.md +49 -0
- package/README.md +969 -210
- package/api-contract.json +385 -0
- package/dist/acl.d.ts +7 -2
- package/dist/acl.js +19 -2
- package/dist/admin.d.ts +7 -1
- package/dist/admin.js +16 -9
- package/dist/auth.d.ts +47 -11
- package/dist/auth.js +129 -15
- package/dist/chain.d.ts +52 -25
- package/dist/chain.js +445 -64
- package/dist/cli.js +72 -20
- package/dist/errors.d.ts +23 -6
- package/dist/errors.js +52 -13
- package/dist/index.d.ts +32 -15
- package/dist/index.js +209 -68
- package/dist/internal.d.ts +2 -0
- package/dist/load.d.ts +10 -6
- package/dist/load.js +30 -11
- package/dist/log.d.ts +121 -0
- package/dist/log.js +654 -0
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +34 -16
- package/dist/messages/access.d.ts +10 -0
- package/dist/messages/access.js +13 -0
- package/dist/messages/api.d.ts +76 -0
- package/dist/messages/api.js +109 -7
- package/dist/messages/boot.d.ts +5 -0
- package/dist/messages/boot.js +11 -4
- package/dist/messages/codes.js +4 -4
- package/dist/messages/index.d.ts +5 -2
- package/dist/messages/index.js +5 -2
- package/dist/messages/log.d.ts +232 -0
- package/dist/messages/log.js +105 -0
- package/dist/messages/mcp.js +7 -7
- package/dist/messages/model.d.ts +40 -1
- package/dist/messages/model.js +52 -3
- package/dist/messages/query.d.ts +10 -0
- package/dist/messages/query.js +13 -0
- package/dist/messages/sql-core.d.ts +15 -1
- package/dist/messages/sql-core.js +20 -3
- package/dist/messages/sql-time.d.ts +15 -0
- package/dist/messages/sql-time.js +23 -0
- package/dist/messages/write.d.ts +52 -0
- package/dist/messages/write.js +77 -6
- package/dist/model.d.ts +10 -2
- package/dist/model.js +28 -6
- package/dist/registry.d.ts +6 -0
- package/dist/registry.js +6 -0
- package/dist/sql.d.ts +2 -0
- package/dist/sql.js +77 -25
- package/dist/sync.d.ts +4 -5
- package/dist/sync.js +53 -4
- package/dist/tx.d.ts +10 -1
- package/dist/tx.js +44 -2
- package/dist/typed.d.ts +18 -14
- package/dist/types.d.ts +91 -8
- package/dist/types.js +8 -3
- package/dist/up.d.ts +8 -5
- package/dist/up.js +71 -23
- package/dist/uuid.d.ts +8 -0
- package/dist/uuid.js +9 -1
- package/dist/validate.js +24 -7
- package/dist/watch.d.ts +5 -0
- package/dist/watch.js +13 -0
- package/dist/write.d.ts +84 -3
- package/dist/write.js +427 -40
- package/llms.txt +38 -17
- package/package.json +12 -6
- package/sql/20-context.sql +11 -3
- package/sql/30-validate.sql +51 -10
- package/sql/40-class.sql +21 -1
- package/sql/60-write.sql +18 -6
- package/sql/80-auth.sql +88 -17
- package/sql/90-time.sql +34 -6
- package/sql/99-revision.sql +5 -1
- 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 +5 -1
package/llms.txt
CHANGED
|
@@ -1,30 +1,51 @@
|
|
|
1
1
|
# letopis
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> ИИ-агенту: код на letopis пишите по [AGENT-CHEATSHEET.md](AGENT-CHEATSHEET.md) — памятка лежит в пакете рядом с этим файлом (`node_modules/letopis/AGENT-CHEATSHEET.md`); прочитайте её целиком: «Правила для любой задачи», рабочий цикл и «Грабли».
|
|
4
|
+
>
|
|
5
|
+
> Хранилище версионированных объектов для Node.js на PostgreSQL 18, версия 3.0. Каждое изменение — версия в журнале (история, чтение на момент, подписка по курсору). Целостность, изоляцию арендаторов и права доступа проверяет сама база (триггеры и политики RLS), поэтому они действуют и для сырого SQL. Данные читают и пишут цепочками вида `db.Team(g).Item().rows()`.
|
|
6
|
+
|
|
7
|
+
Безопасность по умолчанию (одна строка — одно правило):
|
|
8
|
+
|
|
9
|
+
- Удаление — только по превью: `delete()` → показать → `delete({ confirm: true, expect: превью })`; `delete({ confirm: true })` без `expect` — `invalid_query` (`write.confirm_without_expect`).
|
|
10
|
+
- Восстановление — только по надгробиям: `restore({ expect: ответ delete() })`; `restore()` без `expect`, и по явному id тоже, — `invalid_query` (`write.restore_without_expect`).
|
|
11
|
+
- Массовое и без сверки — только явно и только в осознанных скриптах: `{ all: true }` у `update`, `delete({ confirm: true, all: true })`, `restore({ all: true })`.
|
|
12
|
+
- Арендатор — только из серверной сессии (`svc.auth.session(token)`), никогда из параметра клиента.
|
|
13
|
+
- Права по умолчанию запрещают; подключение в обход RLS — `bypass_rls`, пока нет `allowBypassRls: true`.
|
|
14
|
+
- `createTenant(admin, …)` — `admin` только пул `postgres()`, иначе `invalid_query` (`api.create_tenant_admin`).
|
|
15
|
+
- Журнал событий маскирует значения полей с паролями, токенами, ключами, секретами и кредами (и полей из `redact`), учётные данные в строке подключения и пароль в тексте SQL; значения параметров SQL, тексты ошибок PostgreSQL и поле `data` строк в подробностях ошибок не пишет. Значения фильтров чтения в `chain` пишутся как есть — персональные поля добавьте в `redact`.
|
|
4
16
|
|
|
5
17
|
Главное для кода на letopis:
|
|
6
18
|
|
|
7
|
-
-
|
|
8
|
-
- Схему ставит `up({ dsn, schema })` от
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
19
|
+
- Установка: `npm pkg set type=module && npm i letopis@3 zod postgres@3 && npm i -D tsx` (код приложения — ESM; `@3` — мажорная линия этой документации, без версии npm поставит последний выпуск; `postgres@3` — та же мажорная версия, что у letopis, иначе `createTenant` не проходит проверку типов). Node 22+, PostgreSQL 18+ с pgcrypto, база UTF8. Zod 4 нужен только `letopis/model`, `@modelcontextprotocol/server` — только `letopis/mcp`.
|
|
20
|
+
- Схему ставит `up({ dsn, schema, login, tenant })` от администратора (суперпользователь или роль с `CREATEROLE`, `CREATE` в базе и `CREATEDB`, если базы нет); роль приложения заводит `login`, её строка подключения — `r.login.dsn`. Ключ сервиса выдаёт только первый `up()` — храните его сами или передавайте свой `up({ serviceKey })`; `connect({ apiKey: null | '' })` — сразу `invalid_data` (`api.connect_empty_credential`), а `undefined` — подключение без сессии, поэтому ключ проверяйте до `connect()`; `up({ upgrade: true })` обновляет схему отставшей ревизии, `up({ fresh: true })` пересоздаёт её с потерей данных.
|
|
21
|
+
- Вход пользователя: одно подключение сервиса на процесс (`connect({ apiKey })`), `svc.auth.verifyPassword()` → `{ account, token }`, затем `svc.as(account, { tenant })` на запрос. Без `{ tenant }` сессия — в **личном** арендаторе пользователя (классов там нет, `invalid_class`); проверка — `db.tenant`. По погашенной сессии чтение отдаёт пустые ответы, запись — `no_session`, поэтому `svc.auth.session(token)` — в начале каждого запроса.
|
|
22
|
+
- Классы — хабы (объекты) и связи (отношения с концами): `hub()` и `link()` из `letopis/model`, в базу — `syncModels()` или `letopis sync` (в арендатора сессии). Имя роли конца одно на арендатора; роли `Group`, `Object`, `User`, `Tenant` заняты системными классами `rule` и `member` — конец с таким именем на другой класс — `invalid_class` (`model.end_role_system`).
|
|
23
|
+
- Уникальность и инварианты держит ключ, а не проверка в коде: у класса с ключом id — UUID v5 (`db.idOf`), второй такой `create()` — `exists`, и под гонкой. «Одна открытая запись на объект» — связь с ключом по концу (`key: ['Obj']`); `restore()` бизнес-правил не проверяет, поэтому и при возврате инвариант держит только ключ.
|
|
24
|
+
- Глаголы возвращают цепочку; терминал (`rows`, `first`, `ids`, `count`, …) исполняет весь план одной транзакцией. `create` только создаёт, `update` сливает патч и никогда не создаёт (`undefined` в патче — поле не меняется), `upsert` заменяет данные целиком (поле с `undefined` пропадает; только у классов с ключом).
|
|
25
|
+
- Удаление ровно показанного атомарно: превью сверяется под блокировкой каскада, расхождение — `conflict` (`write.preview_changed`) с новым превью в `err.detail.preview`. Целей нет: непустой `expect` — `conflict` с `err.detail.preview: []` («уже удалено или не найдено»), `expect: []` — пустой ответ.
|
|
26
|
+
- Возврат — по ответу `delete()` (или надгробиям `withDeleted()` по id): `restore({ expect })` — всё или отказ по смыслу (`write.restore_already`, `write.restore_id_taken` — `not_deleted`; `write.restore_stale` — `conflict`); `restore({ expect, partial: true })` — что можно, пропуски — строки `$action: 'skip'` с `$why`. Цель конца — первой, каскад — следующим шагом, оба — в одной `db.begin()` с `commit()`/`rollback()`.
|
|
27
|
+
- Гонка за прочитанное — `forUpdate()` в `db.begin()` (только с `rows`, `first`, `ids`), `update(patch, { rev })` (`conflict`) или ключ; счётчик — `update({ n: inc(k) })`. Фильтр `update()` по данным, концам, `tags()`, `owner()` перепроверяется под блокировкой строки: выведенную параллельной транзакцией из-под фильтра строку он не правит (из двух одновременных правок со `status: 'new'` проходит одна); фильтр только по id — пройдут обе. Форма транзакции — `const tr = await db.begin(); try { …; await tr.commit() } catch (e) { await tr.rollback(); throw e }`; `db.begin(fn)` — `invalid_query`.
|
|
28
|
+
- Строгий режим: `connect({ models })` или `connect<Models>()` дают строгие типы; неизвестное поле, класс или опция — отказ с похожими именами; `update()` без фильтра — `invalid_query` (все строки — `{ all: true }`); id не uuid — `invalid_query` до запроса; фильтр `undefined` — шаг без фильтра и предупреждение.
|
|
29
|
+
- Концы связи — не поля данных: из пути или слотом `.Роль.set(id)`; имя конца в данных — `invalid_data` (`write.link_in_data`).
|
|
30
|
+
- Модификаторы выборки (`sort`, `limit`, `offset`, `after`, `forUpdate`) — последнему шагу; `withDeleted()` — своему шагу. `versions()` — терминал; по пути — только живые, с удалёнными — `withDeleted()` на шаге или явный id.
|
|
31
|
+
- Ответы и ошибки: строка — `{ id, class, rev, links, data, … }`, превью — с `$action`, надгробие — `$deleted: true`; ошибка — `LetopisError` с `err.code`, `err.key`, `err.detail`, `err.hint`, у данных — `ValidationError` с `err.issues: [{ path, keyword, message }]`. Штатные отказы (`exists`, `conflict`, `not_deleted`, `target_not_found`, `tenant_denied`, `acl_denied`, `no_session`, …) — ответ человеку своими фразами по `err.key`/`err.code`; ошибки кода — 500.
|
|
32
|
+
- Логирование: `connect({ log, logLevel, redact })` и `up({ … })` — функция или объект-логгер (`console`, pino), порог `debug|info|warn|error|off` (по умолчанию `info`); без кода — `LETOPIS_LOG=info` (JSON-строки в stderr), CLI — `--log[=уровень]`. События `delete` и `restore` — что удалено и что не вернулось; внутри `db.begin()` событие значит «выполнено в транзакции», фиксацию доказывает `tx.commit` с тем же `tx`. Штатные отказы — событие `error` уровня `warn`, ошибки кода — уровня `error`.
|
|
33
|
+
- Обслуживание — `maintain()` (администраторское подключение, системный администратор или право `letopis.maintain`, иначе `acl_denied`); загрузка — `db.load(rows)` с администраторского подключения; подписка — `db.watch({ from, classes })` и `await sub.ready()` перед записью.
|
|
34
|
+
- MCP: `createMcpServer(await svc.as(пользователь, { tenant }))` из `letopis/mcp` и `letopis mcp` дают ИИ-агенту `letopis_model`, `letopis_query`, `letopis_history` и, с `write: true`, `letopis_write`; удаление подтверждает человек, удаляется ровно показанное превью.
|
|
35
|
+
- Системный администратор — член System с ролью `admin`, отдельный аккаунт: через сервис не входит (`verifyPassword`, `sessionFor`, `svc.as()` — `acl_denied`), входит ключом API и `auth.switch(System)`; `purgeAccount` стирает только собственного арендатора аккаунта — стираемые персональные данные держите в классе с `history: false` (README, «Аккаунты, арендаторы и членство», «Удалить аккаунт»).
|
|
36
|
+
- Лицензия — PolyForm Noncommercial 1.0.0 (SPDX `PolyForm-Noncommercial-1.0.0`): исходный код доступен, некоммерческое использование бесплатно, коммерческое — только по отдельной лицензии владельца (запрос — issue на https://github.com/alepri51/letopis/issues).
|
|
20
37
|
|
|
21
38
|
## Документы
|
|
22
39
|
|
|
23
|
-
- [
|
|
40
|
+
- [Памятка для агента](AGENT-CHEATSHEET.md): общие правила для любой задачи на letopis, рабочий цикл одной программой и грабли
|
|
24
41
|
- [Руководство](README.md): руководство по задачам, справочник API, ограничения
|
|
25
|
-
- [
|
|
42
|
+
- [Безопасность по умолчанию](README.md#безопасность-по-умолчанию): что отклоняется без сверки и как это сделать осознанно
|
|
43
|
+
- [Логирование](README.md#логирование): опции, уровни, `LETOPIS_LOG`, формат события, маскировка, список событий
|
|
44
|
+
- [Ошибки](README.md#15-ошибки): все коды и ключи — что значит, что делать и что сказать человеку
|
|
45
|
+
- Машинный каталог API и ошибок — `api-contract.json` в пакете (`node_modules/letopis/api-contract.json`)
|
|
26
46
|
|
|
27
47
|
## Дополнительно
|
|
28
48
|
|
|
29
|
-
- [Изменения по версиям](CHANGELOG.md):
|
|
49
|
+
- [Изменения по версиям](CHANGELOG.md): что изменилось при переходе на 3.0 и чем заменить снятое
|
|
50
|
+
- [Лицензия](LICENSE) и [коммерческое использование](LICENSE-COMMERCIAL.md): что можно без разрешения и как получить коммерческую лицензию
|
|
30
51
|
- Модель конкретной установки для агента — инструмент MCP `letopis_model`: классы, ключи, поля, концы и рабочие цепочки этой схемы
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letopis",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Letopis (летопись):
|
|
3
|
+
"version": "3.0.1",
|
|
4
|
+
"description": "Letopis (летопись): хранилище версионированных сущностей на PostgreSQL 18 — целостность, история, арендаторы и права проверяет сама база, поверх — цепочки через точку. ИИ-агенту — AGENT-CHEATSHEET.md и llms.txt в пакете; исходный код доступен (PolyForm Noncommercial 1.0.0)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"postgres",
|
|
7
7
|
"postgresql",
|
|
@@ -14,9 +14,12 @@
|
|
|
14
14
|
"jsonb",
|
|
15
15
|
"multi-tenant",
|
|
16
16
|
"acl",
|
|
17
|
-
"uuidv5"
|
|
17
|
+
"uuidv5",
|
|
18
|
+
"llms-txt",
|
|
19
|
+
"ai-agents",
|
|
20
|
+
"agent-cheatsheet"
|
|
18
21
|
],
|
|
19
|
-
"license": "
|
|
22
|
+
"license": "PolyForm-Noncommercial-1.0.0",
|
|
20
23
|
"repository": {
|
|
21
24
|
"type": "git",
|
|
22
25
|
"url": "git+https://github.com/alepri51/letopis.git",
|
|
@@ -53,12 +56,15 @@
|
|
|
53
56
|
"README.md",
|
|
54
57
|
"AGENT-CHEATSHEET.md",
|
|
55
58
|
"llms.txt",
|
|
59
|
+
"api-contract.json",
|
|
56
60
|
"CHANGELOG.md",
|
|
57
|
-
"LICENSE"
|
|
61
|
+
"LICENSE",
|
|
62
|
+
"LICENSE-COMMERCIAL.md"
|
|
58
63
|
],
|
|
59
64
|
"scripts": {
|
|
60
65
|
"build": "tsc -p tsconfig.build.json",
|
|
61
|
-
"typecheck": "tsc --noEmit",
|
|
66
|
+
"typecheck": "tsc -p tsconfig.build.json --noEmit",
|
|
67
|
+
"typecheck:all": "tsc --noEmit",
|
|
62
68
|
"test": "tsx --test --test-concurrency=1 --test-force-exit test/*.test.ts",
|
|
63
69
|
"check:docs": "node scripts/check-docs.mjs",
|
|
64
70
|
"check:package": "node scripts/check-package.mjs",
|
package/sql/20-context.sql
CHANGED
|
@@ -59,8 +59,13 @@ begin
|
|
|
59
59
|
perform @SCHEMA@.lt_fail('sql-core.no_impersonate_right');
|
|
60
60
|
end if;
|
|
61
61
|
select e.id, e.data into a from @SCHEMA@.entity e where e.id = imp::uuid and e.class = 'Account' and e.tenant = '@SYSTEM@'::uuid;
|
|
62
|
+
-- аккаунта нет (не заводили или удалён) — «не найден», а не «нельзя» (3.0): сервис с правом имперсонации отличит
|
|
63
|
+
-- опечатку в id от запрета
|
|
64
|
+
if a.id is null then
|
|
65
|
+
perform @SCHEMA@.lt_fail('sql-core.impersonation_no_account', jsonb_build_object('id', imp));
|
|
66
|
+
end if;
|
|
62
67
|
-- от имени System, сервисов, системных администраторов и отключённых аккаунтов действовать нельзя
|
|
63
|
-
if
|
|
68
|
+
if not coalesce((a.data->>'enabled')::boolean, true) or a.id = '@SYSTEM@'::uuid
|
|
64
69
|
or coalesce(a.data->'categories' ?| array['System', 'Service'], false)
|
|
65
70
|
or exists (select 1 from @SCHEMA@.entity x
|
|
66
71
|
where x.id = @SCHEMA@.v5_id('@SYSTEM@'::uuid, 'member', array[a.id::text, '@SYSTEM@']) and x.class = 'member') then
|
|
@@ -119,7 +124,7 @@ begin
|
|
|
119
124
|
end $$;
|
|
120
125
|
|
|
121
126
|
-- Вход по API-ключу (APIKEY): префикс ищется в ident, ключ сверяется по sha256. Сессия помнит ключ
|
|
122
|
-
-- (meta.cred): отзыв ключа гасит только его сессии.
|
|
127
|
+
-- (meta.cred): отзыв ключа гасит только его сессии. Вход и отзыв синхронизированы по строке ключа (for share).
|
|
123
128
|
create or replace function @SCHEMA@.login_apikey(p_key text) returns text
|
|
124
129
|
language plpgsql volatile security definer
|
|
125
130
|
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
@@ -135,7 +140,10 @@ begin
|
|
|
135
140
|
and s.material = sha256(convert_to(p_key, 'UTF8'))
|
|
136
141
|
and (s.expires_at is null or s.expires_at > clock_timestamp())
|
|
137
142
|
and coalesce((s.meta->>'enabled')::boolean, true)
|
|
138
|
-
and coalesce((a.data->>'enabled')::boolean, true)
|
|
143
|
+
and coalesce((a.data->>'enabled')::boolean, true)
|
|
144
|
+
-- строка ключа — под блокировкой до конца транзакции входа: отзыв (auth_revoke_credential) ждёт её
|
|
145
|
+
-- фиксации и гасит открытую здесь сессию, а вход после отзыва ключа не находит
|
|
146
|
+
for share of s;
|
|
139
147
|
if k.account is null then
|
|
140
148
|
-- DETAIL не было и до каталога (raise без detail): p_detail => 'null' (15-errors.sql)
|
|
141
149
|
perform @SCHEMA@.lt_fail('sql-core.bad_api_key', p_detail => 'null');
|
package/sql/30-validate.sql
CHANGED
|
@@ -351,6 +351,14 @@ begin
|
|
|
351
351
|
validator := ('strict ' || expr)::jsonpath;
|
|
352
352
|
end $$;
|
|
353
353
|
|
|
354
|
+
-- Допустимые значения перечисления для сообщения нарушения (план 2.1, A8): JSON через запятую, первые 20 — как у
|
|
355
|
+
-- валидатора на TypeScript (validate.ts, enumValues).
|
|
356
|
+
create or replace function @SCHEMA@.issue_values(p_values jsonb) returns text
|
|
357
|
+
language sql immutable
|
|
358
|
+
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
359
|
+
return (select string_agg(t.y::text, ', ' order by t.o) || case when jsonb_array_length(p_values) > 20 then ', …' else '' end
|
|
360
|
+
from jsonb_array_elements(p_values) with ordinality t(y, o) where t.o <= 20);
|
|
361
|
+
|
|
354
362
|
-- Проверка данных: общий вызов jsonb_path_match; при отказе — список нарушений по правилам.
|
|
355
363
|
-- Результат сравнивается через is true: ошибка внутри выражения даёт NULL, а не «да» (этап 1, п. 5).
|
|
356
364
|
-- Нарушение — { path, keyword, message, key, params }: message — русский текст по ключу каталога, рядом его key и
|
|
@@ -364,6 +372,7 @@ declare
|
|
|
364
372
|
ok boolean;
|
|
365
373
|
x jsonb;
|
|
366
374
|
p jsonb;
|
|
375
|
+
t text;
|
|
367
376
|
begin
|
|
368
377
|
ok := jsonb_path_match(p_data, p_validator, p_vars, true) is true;
|
|
369
378
|
if ok and not exists (select 1 from jsonb_array_elements(p_rules) r where r ? 'extra') then
|
|
@@ -373,10 +382,18 @@ begin
|
|
|
373
382
|
if e ? 'extra' then
|
|
374
383
|
for x in select * from jsonb_path_query(p_data, ('strict ' || (e->>'path_expr'))::jsonpath, '{}'::jsonb, true) loop
|
|
375
384
|
if (e->'extra'->>'kind') = 'length' and jsonb_typeof(x) = 'string' then
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
385
|
+
-- текст называет границу (letopis 2.1, второй раунд, A6) — как у валидатора на TypeScript (validate.ts)
|
|
386
|
+
t := null;
|
|
387
|
+
if char_length(x #>> '{}') < coalesce((e->'extra'->>'min')::int, 0) then
|
|
388
|
+
t := 'sql-core.issue_string_too_short';
|
|
389
|
+
p := jsonb_build_object('limit', (e->'extra'->>'min')::int);
|
|
390
|
+
elsif char_length(x #>> '{}') > coalesce((e->'extra'->>'max')::int, 2147483647) then
|
|
391
|
+
t := 'sql-core.issue_string_too_long';
|
|
392
|
+
p := jsonb_build_object('limit', (e->'extra'->>'max')::int);
|
|
393
|
+
end if;
|
|
394
|
+
if t is not null then
|
|
395
|
+
bad := bad || jsonb_build_object('path', e->'path', 'keyword', e->>'keyword', 'message', @SCHEMA@.lt_text(t, p),
|
|
396
|
+
'key', t, 'params', p);
|
|
380
397
|
end if;
|
|
381
398
|
elsif (e->'extra'->>'kind') = 'unique' and jsonb_typeof(x) = 'array' then
|
|
382
399
|
if (select count(*) from jsonb_array_elements(x)) <> (select count(distinct y) from jsonb_array_elements(x) y) then
|
|
@@ -397,16 +414,40 @@ begin
|
|
|
397
414
|
end if;
|
|
398
415
|
elsif (e->'extra'->>'kind') = 'enum' then
|
|
399
416
|
if not exists (select 1 from jsonb_array_elements(e->'extra'->'values') y where y = x) then
|
|
400
|
-
|
|
401
|
-
|
|
417
|
+
p := jsonb_build_object('values', @SCHEMA@.issue_values(e->'extra'->'values'));
|
|
418
|
+
bad := bad || jsonb_build_object('path', e->'path', 'keyword', e->>'keyword', 'message', @SCHEMA@.lt_text('sql-core.issue_enum', p),
|
|
419
|
+
'key', 'sql-core.issue_enum', 'params', p);
|
|
402
420
|
end if;
|
|
403
421
|
end if;
|
|
404
422
|
end loop;
|
|
405
423
|
elsif not ok then
|
|
406
424
|
if jsonb_path_match(p_data, ('strict !exists(' || (e->>'path_expr') || ' ? (!(' || (e->>'cond') || ')))')::jsonpath, e->'vars', true) is not true then
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
425
|
+
t := null;
|
|
426
|
+
if e->>'keyword' in ('enum', 'const') then
|
|
427
|
+
-- перечисление скаляров: значения — переменные правила v<N> (план 2.1, A8)
|
|
428
|
+
p := jsonb_build_object('values', @SCHEMA@.issue_values(
|
|
429
|
+
coalesce((select jsonb_agg(v.value order by (substr(v.key, 2))::int) from jsonb_each(e->'vars') v), '[]'::jsonb)));
|
|
430
|
+
bad := bad || jsonb_build_object('path', e->'path', 'keyword', e->>'keyword', 'message', @SCHEMA@.lt_text('sql-core.issue_enum', p),
|
|
431
|
+
'key', 'sql-core.issue_enum', 'params', p);
|
|
432
|
+
continue;
|
|
433
|
+
elsif e->>'keyword' = 'additionalProperties' then
|
|
434
|
+
-- лишние ключи: ключ, которого условие правила не принимает и в одиночку (план 2.1, A8)
|
|
435
|
+
select string_agg(z.k, ', ' order by z.k) into t from (
|
|
436
|
+
select distinct kv.key as k
|
|
437
|
+
from jsonb_path_query(p_data, ('strict ' || (e->>'path_expr'))::jsonpath, '{}'::jsonb, true) n(v)
|
|
438
|
+
cross join lateral jsonb_each(case when jsonb_typeof(n.v) = 'object' then n.v else '{}'::jsonb end) kv
|
|
439
|
+
where jsonb_path_exists(jsonb_build_object(kv.key, null), ('strict $ ? (' || (e->>'cond') || ')')::jsonpath, e->'vars', true) is not true
|
|
440
|
+
order by 1 limit 20) z;
|
|
441
|
+
end if;
|
|
442
|
+
if t is not null then
|
|
443
|
+
p := jsonb_build_object('keys', t);
|
|
444
|
+
bad := bad || jsonb_build_object('path', e->'path', 'keyword', e->>'keyword', 'message', @SCHEMA@.lt_text('sql-core.issue_additional', p),
|
|
445
|
+
'key', 'sql-core.issue_additional', 'params', p);
|
|
446
|
+
else
|
|
447
|
+
p := jsonb_build_object('keyword', e->>'keyword');
|
|
448
|
+
bad := bad || jsonb_build_object('path', e->'path', 'keyword', e->>'keyword', 'message', @SCHEMA@.lt_text('sql-core.issue_rule', p),
|
|
449
|
+
'key', 'sql-core.issue_rule', 'params', p);
|
|
450
|
+
end if;
|
|
410
451
|
end if;
|
|
411
452
|
end if;
|
|
412
453
|
end loop;
|
|
@@ -423,7 +464,7 @@ begin
|
|
|
423
464
|
foreach f in array array[
|
|
424
465
|
'uuid_v5(uuid, text)', 'v5_name(text, text[])', 'v5_id(uuid, text, text[])', 'jp_str(text)', 'jp_pattern(text)',
|
|
425
466
|
'jp_type_cond(text)', 'jp_prop(text, text)', 'jp_items(text)', 'jp_compile(jsonb, text, jsonb, boolean, int)', 'compile_validator(jsonb)',
|
|
426
|
-
'validate_data(jsonb, jsonpath, jsonb, jsonb)'] loop
|
|
467
|
+
'issue_values(jsonb)', 'validate_data(jsonb, jsonpath, jsonb, jsonb)'] loop
|
|
427
468
|
execute format('alter function @SCHEMA@.%s owner to @OWNER@', f);
|
|
428
469
|
execute format('revoke all on function @SCHEMA@.%s from public', f);
|
|
429
470
|
end loop;
|
package/sql/40-class.sql
CHANGED
|
@@ -209,12 +209,32 @@ begin
|
|
|
209
209
|
validator := c.validator;
|
|
210
210
|
end $$;
|
|
211
211
|
|
|
212
|
+
-- Нарушения данных строки класса по его описанию в базе, без записи (план 2.1, A8): валидатор на TypeScript не исполняет
|
|
213
|
+
-- шаблоны pattern (ревью, ReDoS), и у класса с ними библиотека берёт полный список нарушений здесь. Класс — свой
|
|
214
|
+
-- арендатора сессии или System, как у записи; умолчания подставляются, как перед проверкой при записи. Описание класса
|
|
215
|
+
-- сессия и так видит (реестр), поэтому ответ ничего нового не открывает. Нет класса — null.
|
|
216
|
+
create or replace function @SCHEMA@.validate_row(p_class text, p_data jsonb) returns jsonb
|
|
217
|
+
language plpgsql stable security definer
|
|
218
|
+
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
219
|
+
as $$
|
|
220
|
+
declare
|
|
221
|
+
cls @SCHEMA@.entity;
|
|
222
|
+
begin
|
|
223
|
+
cls := @SCHEMA@.class_row(p_class, @SCHEMA@.ctx_tenant());
|
|
224
|
+
if cls.id is null or cls.validator is null then
|
|
225
|
+
return null;
|
|
226
|
+
end if;
|
|
227
|
+
return @SCHEMA@.validate_data(cls.data->'resolved'->'rules', cls.validator, cls.data->'resolved'->'vars',
|
|
228
|
+
@SCHEMA@.apply_defaults(cls.data->'resolved'->'schema', p_data));
|
|
229
|
+
end $$;
|
|
230
|
+
|
|
212
231
|
do $$
|
|
213
232
|
declare f text;
|
|
214
233
|
begin
|
|
215
234
|
foreach f in array array['class_metaschema()', 'class_meta_validator()', 'class_row(text, uuid)',
|
|
216
|
-
'class_resolve(jsonb, uuid)', 'class_prepare(jsonb, uuid)'] loop
|
|
235
|
+
'class_resolve(jsonb, uuid)', 'class_prepare(jsonb, uuid)', 'validate_row(text, jsonb)'] loop
|
|
217
236
|
execute format('alter function @SCHEMA@.%s owner to @OWNER@', f);
|
|
218
237
|
execute format('revoke all on function @SCHEMA@.%s from public', f);
|
|
219
238
|
end loop;
|
|
220
239
|
end $$;
|
|
240
|
+
grant execute on function @SCHEMA@.validate_row(text, jsonb) to @APP@;
|
package/sql/60-write.sql
CHANGED
|
@@ -1378,12 +1378,15 @@ begin
|
|
|
1378
1378
|
return null;
|
|
1379
1379
|
end $$;
|
|
1380
1380
|
|
|
1381
|
-
--
|
|
1381
|
+
-- Предпросмотр удаления (этап 4, п. 6): что удалит delete этих строк — те же уровни и то же правило
|
|
1382
1382
|
-- ref_action(), что у триггера, но без изменений. Строки: action delete (цели — depth 0, каскад —
|
|
1383
1383
|
-- уровень), unset (конец будет снят) и restrict (мешает удалению). Функция владельца ищет по
|
|
1384
1384
|
-- индексу ends без RLS (§3 «Индексы под RLS»), поэтому и цели, и выдачу пропускает через
|
|
1385
1385
|
-- row_visible() — то же условие, что у политики чтения (как find_*): невидимых строк не раскрывает
|
|
1386
|
-
-- ни id, ни классом.
|
|
1386
|
+
-- ни id, ни классом. Невидимые строки замыкания (3.0.1) — в конце сводкой, по строке на действие (delete,
|
|
1387
|
+
-- unset, restrict; только непустые): id и class — null, action — 'hidden:<действие>', depth — их число. Вид
|
|
1388
|
+
-- результата прежний: тело ставит create or replace повторным накатом той же ревизии, а клиент, который
|
|
1389
|
+
-- соединяет выдачу с entity по id, сводок не видит.
|
|
1387
1390
|
create or replace function @SCHEMA@.delete_preview(p_ids uuid[]) returns table(id uuid, class text, action text, depth int)
|
|
1388
1391
|
language plpgsql stable security definer
|
|
1389
1392
|
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
@@ -1432,10 +1435,19 @@ begin
|
|
|
1432
1435
|
from (select x.id, @SCHEMA@.ref_action((@SCHEMA@.class_row(x.class, x.tenant)).data->'resolved'->'ends', x.links, all_ids) as a
|
|
1433
1436
|
from @SCHEMA@.entity x where x.ends && all_ids and not (x.id = any (all_ids))) r);
|
|
1434
1437
|
return query
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1438
|
+
with o as (
|
|
1439
|
+
select (j.v->>0)::uuid as id, e.class, j.v->>1 as action, (j.v->>2)::int as depth, j.ord,
|
|
1440
|
+
@SCHEMA@.row_visible(e.tenant, e.class, e.id, e.owner, e.tags, e.data, e.links, c_tenant, c_account, c_plan) as vis
|
|
1441
|
+
from jsonb_array_elements(out_rows) with ordinality j(v, ord)
|
|
1442
|
+
join @SCHEMA@.entity e on e.id = (j.v->>0)::uuid
|
|
1443
|
+
)
|
|
1444
|
+
select x.id, x.class, x.action, x.depth
|
|
1445
|
+
from (select o.id, o.class, o.action, o.depth, 0 as part, o.ord as k from o where o.vis
|
|
1446
|
+
union all
|
|
1447
|
+
select null::uuid, null::text, 'hidden:' || o.action, count(*)::int, 1,
|
|
1448
|
+
array_position(array['delete', 'unset', 'restrict'], o.action)::bigint
|
|
1449
|
+
from o where not o.vis group by o.action) x
|
|
1450
|
+
order by x.part, x.k;
|
|
1439
1451
|
end $$;
|
|
1440
1452
|
|
|
1441
1453
|
create or replace trigger entity_bi before insert on @SCHEMA@.entity
|
package/sql/80-auth.sql
CHANGED
|
@@ -52,7 +52,8 @@ 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;
|
|
@@ -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;
|
|
@@ -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-провайдер): токен проверяет сервис, здесь — связка.
|
|
@@ -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', 'LOCK') and s.deleted_at is null
|
|
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;
|
|
@@ -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
|
|
@@ -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,7 +915,8 @@ 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;
|
|
@@ -857,5 +928,5 @@ grant execute on function
|
|
|
857
928
|
@SCHEMA@.login_otp(text, text, text), @SCHEMA@.auth_enroll_totp(uuid), @SCHEMA@.auth_verify_totp(uuid, text, int),
|
|
858
929
|
@SCHEMA@.auth_totp_enabled(uuid), @SCHEMA@.auth_link(uuid, text, text, jsonb, boolean), @SCHEMA@.auth_lookup(text, text, boolean),
|
|
859
930
|
@SCHEMA@.auth_credentials(uuid), @SCHEMA@.auth_revoke_credential(uuid, boolean), @SCHEMA@.auth_set_flags(uuid, boolean, boolean),
|
|
860
|
-
@SCHEMA@.auth_refresh(interval), @SCHEMA@.auth_logout(), @SCHEMA@.
|
|
861
|
-
@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@;
|