- пришедшего называет заголовок Remote-User от прокси, и верят ему только с адреса из перечня trusted_proxies; своего входа у сервиса не осталось — ни корня /auth, ни кук, ни срока сессии, ни секрета клиента в конфиге и в базе - учётная запись заводится первым обращением: EnsureUser в пакете хранилища, шаг схемы 202608220001 с колонкой provider_login и снятыми правилами users - cmd/oidcstub заменён на cmd/devtools с подкомандой proxy; заодно закрыт унаследованный DL3066 — пользователь образа назван числом
132 lines
7.5 KiB
Go
132 lines
7.5 KiB
Go
package pocketbase
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"github.com/pocketbase/pocketbase/core"
|
|
|
|
"git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase/migrations"
|
|
"git.vakhrushev.me/av/transcriber/internal/entity"
|
|
)
|
|
|
|
// BindPanelRules подчиняет правку записи в панели тем же правилам, что и правку
|
|
// из кода.
|
|
//
|
|
// Панель — вход в запись наравне с конвейером, а не окно просмотра: ради правки
|
|
// она и покупалась, остановленная запись возвращается в работу снятием признака.
|
|
// Но правка полем идёт мимо кода, который чистит служебные поля, и владелец,
|
|
// «вернувший запись в работу», получил бы запись с прежним признаком захвата
|
|
// (захвату она не выдастся до конца срока), с числом отказов на пределе
|
|
// (остановится от первого же отказа) и со старым временем входа в рубеж
|
|
// (остановится снова первым же захватом по пределу простоя). Узнать об этом ему
|
|
// неоткуда.
|
|
//
|
|
// Правило живёт **одним местом** — доменными `Resume` и `MoveToState`, — и хук
|
|
// зовёт именно их, а не повторяет перечень служебных полей колонками. Повтор
|
|
// перечня был бы вторым домом того же правила: новый сторож попал бы в домен и
|
|
// не попал в панель, и владелец «вернул бы запись в работу», а она снова выпала
|
|
// бы из выборки — молча.
|
|
//
|
|
// Хук стоит на правке **запросом**, а не на всяком сохранении записи. Модельное
|
|
// событие не различает, кто пишет, и срабатывало бы на каждом переходе
|
|
// конвейера: тогда пауза, поставленная шагом вместе со сменой рубежа, стиралась
|
|
// бы тем же сохранением, а число отказов остановленной записи — которое
|
|
// остановка хранит намеренно — приходило бы владельцу нулём.
|
|
func BindPanelRules(app core.App) {
|
|
bindProviderLoginIsImmutable(app)
|
|
|
|
app.OnRecordUpdateRequest(migrations.RecordsCollection).BindFunc(func(e *core.RecordRequestEvent) error {
|
|
original := e.Record.Original()
|
|
if original == nil {
|
|
return e.Next()
|
|
}
|
|
|
|
stateChanged := original.GetString("state") != e.Record.GetString("state")
|
|
// Снятие признака остановки — то самое движение, ради которого признак и
|
|
// заведён: запись возвращается в работу с сохранённого рубежа.
|
|
resumed := !original.GetDateTime("halted_at").IsZero() &&
|
|
e.Record.GetDateTime("halted_at").IsZero()
|
|
|
|
if !stateChanged && !resumed {
|
|
return e.Next()
|
|
}
|
|
|
|
// Запись читается уже с правкой человека: рубеж здесь тот, который он
|
|
// выбрал, а признак остановки — тот, который он снял или оставил.
|
|
record := recordToAudioRecord(e.Record)
|
|
switch {
|
|
case resumed:
|
|
record.Resume()
|
|
default:
|
|
record.MoveToState(record.State)
|
|
}
|
|
applyOwnedByPipeline(e.Record, record)
|
|
|
|
if err := e.Next(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if resumed {
|
|
// Перезапуск виден в журнале событий с указанием, что его сделал
|
|
// человек: иначе запись, вернувшаяся в работу, выглядела бы как
|
|
// запись, которая туда и не уходила.
|
|
//
|
|
// Строка пишется **после** сохранения: событие о правке, которая не
|
|
// прошла, соврало бы о состоянии записи. Отказ записи журнала саму
|
|
// правку не отменяет — журнал никем не читается ради решения.
|
|
event := &entity.RecordEvent{
|
|
RecordID: e.Record.Id,
|
|
Origin: entity.EventOriginHuman,
|
|
Step: "resume",
|
|
Outcome: entity.EventOutcomeResumed,
|
|
}
|
|
if err := NewRecordEventRepository(e.App).Append(event); err != nil {
|
|
e.App.Logger().Error("Failed to log record resume", "error", err, "record_id", e.Record.Id)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// bindProviderLoginIsImmutable запрещает менять ключ учётной записи после
|
|
// заведения.
|
|
//
|
|
// Ключ — логин человека у провайдера, и по нему сервис узнаёт пришедшего.
|
|
// Переписанный, он отдаёт весь архив прежнего владельца следующему, кто придёт
|
|
// с этим именем: владелец записи назначается один раз и не меняется, так что
|
|
// вернуть архив будет нечем. Молча — журнала событий у коллекции пользователей
|
|
// нет.
|
|
//
|
|
// Правила доступа коллекции закрывают этот путь **снаружи**, но не изнутри:
|
|
// панель работает суперпользователем и правила обходит по построению. Отсюда
|
|
// хук, и он вешается на **модельное** событие, а не на правку запросом — иначе
|
|
// панель осталась бы незакрытой, а закрывать её и есть весь смысл.
|
|
//
|
|
// Заведение проходит: событие правки на нём не срабатывает вовсе.
|
|
//
|
|
// Прежнее значение читается **из базы**, а не из снимка правящейся записи.
|
|
// Снимок у записи, только что заведённой в этом же процессе, пуст — он не
|
|
// обновляется сохранением, — и сторож, опирающийся на него, пропускал бы правку
|
|
// в зависимости от того, откуда вызывающий взял запись. Панель её загружает, и
|
|
// на ней сторож сработал бы; молчаливая же зависимость от способа получения —
|
|
// ровно тот класс, из-за которого правило и заводится.
|
|
func bindProviderLoginIsImmutable(app core.App) {
|
|
app.OnRecordUpdate(migrations.UsersCollection).BindFunc(func(e *core.RecordEvent) error {
|
|
stored, err := e.App.FindRecordById(migrations.UsersCollection, e.Record.Id)
|
|
if err != nil {
|
|
// Записи в базе нет — правки тоже нет: сохранение отвергнется само.
|
|
return e.Next()
|
|
}
|
|
|
|
was := stored.GetString(migrations.ProviderLoginField)
|
|
now := e.Record.GetString(migrations.ProviderLoginField)
|
|
|
|
if was != "" && was != now {
|
|
return errors.New("provider login is assigned once and never changes")
|
|
}
|
|
|
|
return e.Next()
|
|
})
|
|
}
|