ADR 0156 — Потолок самопонижения также при удалении участника проекта
Статус
Accepted. Закрывает пробел, который
ADR 0127 объявил
письменно и закрепил в тесте. Не отменяет и не пересматривает его: оба потолка того ADR
остаются в точности такими, как есть, а меняется набор ДВЕРЕЙ, через которые
оценивается потолок 2. Соответствует BRB-001, P1 безопасности из реестра
сопровождающего.
Контекст
ADR 0127 поставил два потолка на путь ЗАПИСИ project_members — никто
не понижает owner workspace, никто не понижает самого себя — и намеренно
решил не покрывать третье движение, записав причину и цену
в разделе «Остаётся возможным, с именем»:
самопонижение через УДАЛЕНИЕ.
RemoveProjectMemberUseCaseНЕ получил потолка (…) Предпосылка, что «удаление всегда безвредно», ложна, и она записана здесь, чтобы её не открывали заново. Оба случая ЗАКРЕПЛЕНЫ в тесте (…), включая тот, что остаётся открытым, чтобы следующий человек знал, что это решение, а не забывчивость. Его закрытие стоит сигнатурыRemoveProjectMemberUseCase.execute, которую сегодня вызываетlist-projects-and-members.use-case.spec.tsс двумя аргументами.
Этот ADR и есть тот следующий человек, и предсказанная цена оказалась ровно взысканной.
Дефект, измеренный. RemoveProjectMemberUseCase.execute получал
(projectId, userId) — не получал актора — а controller
(@Delete(':projectId/members/:userId'), @RequireRole('maintainer')) вызывал
его с двумя параметрами маршрута, без @CurrentUser(). Поскольку
ResolveEffectiveRoleUseCase.forProject — это projectRole ?? workspaceRole
(RN-471), удаление строки проекта не удаляет
роль: оно ЗАМЕНЯЕТ эффективную роль вторым членом. Когда роль
workspace меньше, итоговый эффект — то самое понижение, которое потолок 2 существует, чтобы
предотвратить, с той же необратимостью — вернуться значит POST :projectId/members,
который требует maintainer, только что оставленного. Без какой-либо роли в workspace
падение — к доступу НИКАКОМУ.
Случай воспроизводился в тесте со времён ADR 0127, с именем, говорящим, что
он документировал: «e a remoção SEGUE podendo rebaixar quem a chamou, quando o
workspace não segura — lacuna declarada no ADR 0127». maintainer, чья
власть происходила от строки проекта, viewer в workspace, удалял собственную
строку и падал до viewer без пути назад через экран.
Почему это то же правило, а не новое. Потолок 2 сравнивает эффективную роль
СЕГОДНЯ с эффективной ролью ПОСЛЕ и никогда не нуждался в знании того, по какому маршруту пришла
вторая. В add роль-после — роль из тела; при удалении — роль
workspace. Написать вторую линейку для второй двери значило бы сделать четвёртую копию
сравнения, которое уже живёт в domain/iam/tetos-de-rebaixamento.ts, — а копия
линейки — это режим отказа, который этот репозиторий преследует во многих
местах (генерируемый список областей, roleAtLeast с одним и тем же именем на обеих
сторонах, caminhoDeWorkspaceLocalValido, используемый повторно вместо переписывания).
Решение
1. Удаление начинает получать актора и отказывать в итоговом
самопонижении. RemoveProjectMemberUseCase.execute становится
(projectId, atorId, alvoId) — в том же порядке, что и AddProjectMemberUseCase, — и
бросает ForbiddenException (403), когда удаление строки САМОГО вызывающего
оставило бы его с ролью меньше сегодняшней. Controller передаёт @CurrentUser(),
как add уже делал.
2. Правило — это повторно используемый потолок 2, с ролью workspace вместо запрошенной
роли. В тот же доменный модуль входит remocaoEhAutoRebaixamento, и она
не реализует заново никакого сравнения: делегирует ehAutoRebaixamento, передавая
papelPedidoNoProjeto: papelDoAtorNoWorkspace. Она существует, вместо того чтобы сценарий
использования вызывал ehAutoRebaixamento напрямую, из-за ОДНОГО случая, который сигнатура другой
не умеет выразить, — актор без роли в workspace, где роль-после —
«никакой», которая не является Role. Этот случай — максимальное понижение, и он отклоняется.
3. Потолок 1 НЕ получает пары, и её отсутствие — решение. Удаление строки
проекта того, кто является owner WORKSPACE, может только ПОВЫСИТЬ его эффективную роль или
сохранить её: owner — вершина ROLE_ORDER, поэтому не существует роли workspace,
для которой падение было бы падением. Более чем безвредно, это движение — ЕДИНСТВЕННЫЙ
способ отменить ограничение, которое потолок 1 из ADR 0127 не даёт создать, —
owner workspace, у которого уже была строка viewer в проекте (записанная до
того ADR или прямой записью в базу), снова получает доступ к собственному
проекту именно так. Потолок там отклонял бы только безвредные движения и
сделал бы постоянным состояние, которое ADR 0127 был создан устранить. Отсутствие
записано в коде, рядом с функцией, и закреплено в тесте.
4. Без порога, как и потолок 2. Отказ — «самому себе», а не «ниже
maintainer», и объявленная цена ADR 0127 идёт вместе с ним: самоудаление, которое
опускает с owner проекта до maintainer workspace, обратимо и
ТОЖЕ ОТКЛОНЯЕТСЯ. Оно остаётся доступным другому maintainer. Этот случай проходил
раньше и теперь отклоняется — это единственное изменение поведения, которое этот ADR
вносит в безвредное движение, и это тот же обмен, который ADR 0127 уже
принял для add: клауза без числа против исключения, которое нужно
объяснять каждый раз.
5. Сообщение СОБСТВЕННОЕ, не как у add. Тот, кто нажал «удалить», не просил
менять никакую роль, и фраза о «понижении самого себя» не
объяснила бы, что произошло. MENSAGEM_TETO_AUTO_REBAIXAMENTO_POR_REMOCAO называет
механизм: без строки проекта роль падает до роли workspace, а возврат
требует оставленной роли.
6. Описание OpenAPI начинает описывать код. Описание маршрута говорило
лишь, что «тот, у кого есть роль в workspace, продолжает видеть проект по наследованию», —
правда наполовину, и недостающая половина — причина этого решения:
наследуемая роль может быть МЕНЬШЕ строки. openapi.json и api-types.generated.ts выходят из
генератора.
Что этот ADR явно отвергает
- Потолок
ownerworkspace при удалении. См. пункт 3. Это не забытая симметрия: не существует удаления, которое понизило быownerworkspace. - Вторую линейку сравнения ролей. См. пункт 2. Сравнение по-прежнему живёт в одном месте.
- Трогать
ResolveEffectiveRoleUseCase.forProject.projectRole ?? workspaceRoleостаётся байт в байт, в обоих направлениях, — это возможность, от устранения которой ADR 0127 отказался, и ничто здесь не открывает вопрос заново. - Распространять потолок на workspace.
POST workspaces/:workspaceId/members— это upsert без какого-либо потолка, как ADR 0127 уже объявлял, — а удаления там даже нет: уWorkspacesControllerнет@Deleteучастника (измерено), так что эквивалентное движение недостижимо по HTTP в этой области. Другая область, отдельное решение. - Блокировать удаление ДРУГОГО человека, даже когда он теряет роль. Это
привычная законная возможность — та же, что ограничивает
developerдоviewerв чувствительном проекте, — и она действует для удаления так же, как для записи. - Gate на экране. См. Consequences.
Последствия
Одно безвредное движение отклоняется вместе с остальными, и это уже принятая цена. Самоудаление
того, у кого owner в проекте и maintainer в workspace, было разрешено и становится
403. Оно обратимо другим maintainer, а тест, закреплявший разрешение,
теперь закрепляет отказ, с именем, говорящим, что это цена потолка без порога.
Кто хочет выйти из списка в такой ситуации, просит другого maintainer — тот
же исход, который потолок 2 уже давал через add.
Экран по-прежнему предлагает кнопку, которую api отклоняет, и отказ отображается.
У MembersSection.handleRemove уже есть try/catch с mensagemDaApi
(RN-471), так что фраза api доходит до toast
без единой строки web. В отличие от потолка 1, этот gate ВЫЧИСЛИМ на клиенте
— userIdDaSessao() и эффективная роль там есть, — но вычислить только его
дало бы кнопку, честную об одном отказе и молчащую о другом, а это
ровно тот полу-gate, который ADR 0127 отверг. Это остаётся для PR web, который
решит проблему целиком.
Два дополнительных запроса на каждое удаление, даже когда цель — другой человек:
forProject актора (который повторяет findById) и forWorkspace. Это цена, которую
ADR 0127 уже принял для add, на административном маршруте, а
альтернатива — if (atorId === alvoId) перед чтениями — повторила бы в
сценарии использования клаузу, которую чистая функция существует, чтобы содержать.
Сигнатура изменилась, и её вызывали два теста. У execute теперь три аргумента;
list-projects-and-members.use-case.spec.ts и
tetos-de-rebaixamento.use-case.spec.ts обновлены. Другого
вызывающего нет: конструктор получил ResolveEffectiveRoleUseCase, который уже был
зарегистрирован в iam-use-cases.module.ts.
Тест пробела стал тестом потолка. Блок, который ADR 0127 оставил, закрепляя дыру («a remoção SEGUE podendo rebaixar quem a chamou»), был ИНВЕРТИРОВАН, а не удалён, и имя хранит происхождение. Это способ, которым этот репозиторий уже пользуется, чтобы не терять доказательство того, почему что-то существовало.
Остаётся возможным, с именем: понизить другого человека удалением его строки;
само-ПОВЫШЕНИЕ (без изменений); и вся область workspace, чей POST
участника по-прежнему без потолка.