у записи появился владелец: чужую больше не отдают
- колонка `owner` связью с `users` в обеих коллекциях новым шагом схемы `202608140001`; чтение задачи сужено владельцем, и чужая, ничья и несуществующая дают один ответ; правило просмотра файлов сужено им же - приём по HTTP берёт владельца из сессии, а предъявителя без учётной записи пользователя отвергает до чтения тела: позже пришлось бы убирать уложенный файл, а уборки файлов сервис не умеет. Выборка воркера владельцем не сужается - удаление учётной записи с записями отвергается стражем, и вешает его сама сборка хранилища: сборка, забывшая его позвать, теряла защиту молча
This commit is contained in:
@@ -80,17 +80,49 @@ func (repo *TranscriptJobRepository) Save(job *entity.TranscribeJob, holder stri
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *TranscriptJobRepository) GetByID(id string) (*entity.TranscribeJob, error) {
|
||||
// GetByID отдаёт задачу, только если её владелец — ownerID.
|
||||
//
|
||||
// Чужая задача, задача без владельца и несуществующая дают одну и ту же ошибку:
|
||||
// по разнице ответов иначе перебирается список заведённых задач, а
|
||||
// идентификатор задачи и есть то, что разграничение прячет.
|
||||
//
|
||||
// Пустой ownerID отсекается **до** чтения и не совпадает ни с чем: иначе
|
||||
// вызывающий без учётной записи получил бы ровно множество задач без владельца,
|
||||
// то есть все записи бота.
|
||||
//
|
||||
// Владелец сверяется тем же чтением, каким берётся состояние, а не отдельным
|
||||
// запросом: между двумя чтениями задача успевает измениться, и ответ перестаёт
|
||||
// быть функцией от того, что с ней произошло.
|
||||
func (repo *TranscriptJobRepository) GetByID(id, ownerID string) (*entity.TranscribeJob, error) {
|
||||
if ownerID == "" {
|
||||
return nil, &contract.JobNotFoundError{Message: "job not found"}
|
||||
}
|
||||
|
||||
record, err := repo.app.FindRecordById(migrations.JobsCollection, id)
|
||||
if err != nil {
|
||||
// «Такой записи нет» переводится в доменную ошибку **здесь**, у
|
||||
// источника, как велит конвенция об ошибках. Иначе три исхода, которые
|
||||
// разграничение обязано сделать неразличимыми, разъезжаются: чужая и
|
||||
// ничья задачи дают доменную ошибку, а несуществующая — отказ базы,
|
||||
// неотличимый от настоящей аварии хранилища. Держалась бы эта
|
||||
// неразличимость только тем, что транспорт кладёт в `404` любую ошибку,
|
||||
// — то есть ровно тем расхождением, которое конвенция велит закрыть.
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, &contract.JobNotFoundError{Message: "job not found"}
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get transcribe job: %w", err)
|
||||
}
|
||||
|
||||
if record.GetString("owner") != ownerID {
|
||||
return nil, &contract.JobNotFoundError{Message: "job not found"}
|
||||
}
|
||||
|
||||
return recordToJob(record), nil
|
||||
}
|
||||
|
||||
// Колонки, которые читает захват. Список нужен запросу дословно: `RETURNING *`
|
||||
// отдал бы и порядок, зависящий от схемы.
|
||||
const acquireColumns = `id, state, source, file, error_text, acquisition_id, ` +
|
||||
const acquireColumns = `id, state, owner, source, file, error_text, acquisition_id, ` +
|
||||
`acquire_time, delay_time, attempts, recognition_op_id, transcription_text, ` +
|
||||
`tg_chat_id, tg_reply_message_id, created, updated`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user