О сервисе
- Название
- TSOY ID
- Адрес
- https://id.tsoy.my
- Версия движка
- 4.43.1
- PHP
- 8.4.17
Название, логотип, цвет и текст подвала настраиваются в разделе «Настройки» — движок не навязывает своего имени.
TSOY ID — открытый OpenID-провайдер на PHP: один аккаунт на сеть сайтов, без Composer и Node. Лицензия MIT: можно использовать, дорабатывать и разворачивать у себя.
Правовые документы этой инсталляции. Редакция и дата изменения — на странице каждого.
4.43.1
Добавлено
- Заглушка в корне вместо голого «Not Found» от Apache. На боевой не доехал
.htaccess, и сайт стал отвечать ошибкой веб-сервера — без единой подсказки, что искать. Файлы при этом лежали на месте:/public/assets/css/app.cssоткрывался, а/assets/css/app.cssнет. Понять это можно было только сравнив два адреса руками.
Теперь корневой index.php объясняет причину: что распаковано, какие .htaccess на месте, включён ли mod_rewrite, и что делать — включая то, что FTP-клиенты по умолчанию не показывают и не отправляют файлы, начинающиеся с точки. Отвечает 503, а не 200: сайт не работает, и мониторинг обязан это видеть.
Выполняется ровно в одном случае — когда переписывание не действует. Работает — запрос к «/» переписывается в public/, и заглушка не срабатывает. Ничего не подключает: ни автозагрузку, ни конфиг, ни базу, — она обязана отработать, даже если не доехало вообще ничего, кроме неё.
4.43.0 · По группам
Исправлено
- Перенесённый с портала человек не попадал в сам портал. Перенос заводил аккаунт и записывал связь — и всё: доступ к сервису не выдавался. Человека не было ни в списке людей портала, ни в людях его организации (тот считается по доступам). Получалось, что мы забрали людей сервиса, а в самом сервисе их нет.
Теперь доступ выдаётся тому сервису, откуда человек и пришёл: портал — источник истины о том, кто им пользуется, и спрашивать его политику доступа тут незачем, эти люди уже внутри. Только если записи ещё нет: повторный перенос не снимает блокировку, поставленную администратором руками.
- Раздел ролей был одной бесконечной лентой. Все роли шли подряд, и на десяти сервисах со своими ролями это превращалось в несколько экранов, где системные — те, которыми управляют, — тонули между чужими.
Теперь «Роли системы» отдельной карточкой и всегда на виду, а роли каждого сервиса — своей свёрнутой группой с числом и ссылкой в его карточку. Замер на десяти сервисах по три роли: 2,3 экрана вместо 5,1, и разрыв растёт с каждым следующим сервисом. Колонка «Скоуп» убрана — её заменил заголовок группы, а уровень теперь показан со ступенью, а не голым числом.
Проверяемость
- В батарее: перенесённый человек получает активный доступ к своему сервису и попадает в его список людей; повторный перенос не снимает блокировку.
4.42.0 · Менеджер ролей
Последнее из отложенного: раздел ролей сервиса был наполовину сделан, а наполовину оставался рядом плашек.
Добавлено
- Роли сервиса — таблицей вместо ряда плашек. Плашки отвечали ровно на один вопрос — «какие роли есть». Не отвечали: какого они уровня, кому выданы, как завести новую и как убрать ненужную. Теперь видно всё: машинное имя, название, уровень со ступенью и сколько людей эту роль имеет.
- Завести роль можно из карточки сервиса. Раньше — только на общей странице ролей, то есть уйдя из карточки того самого сервиса, о котором и речь. Удалить — оттуда же, с возвратом обратно, а не в общий справочник.
- Уровень выводится из имени, а не спрашивается. По тому же правилу, что и у роли, приехавшей из портала: «editor» — 60, «moderator» — 55, незнакомое имя — 20. Так роль значит одно и то же независимо от того, кто её завёл — портал сообщением или человек руками. Прав в самом SSO роль сервиса не получает никогда.
Проверяемость
- В батарее: роль заводится из карточки, получает 55 по имени «moderator», не несёт прав SSO и удаляется оттуда же с возвратом в карточку.
- Замер раздела: четыре карточки одной ширины (904px), обе формы выровнены с полями на 100%, высоты только 32 и 40, боковой прокрутки нет.
4.41.0 · Кого пускаем
Три вещи, которые я откладывал и называл вслух. Теперь сделаны.
Добавлено
- Регистрацию можно закрыть. Раньше зарегистрироваться мог любой, у кого есть почта, и единственным ограничителем был список доменов — для закрытого контура этого мало. Переключатель в настройках регистрации; по умолчанию открыта, то есть поведение не меняется.
Закрытая дверь встречает отдельным экраном, а не отказом после четырёх заполненных полей: человеку нечего исправлять, и сказать об этом надо сразу. Проверка стоит и на отправке — форму присылают и мимо браузера.
- Ссылка-приглашение на регистрацию. Выдаётся в «Инструментах», живёт неделю, одноразовая. С указанной почтой работает только для неё: иначе одна пересланная ссылка открывает дверь кому угодно. Гасится после создания аккаунта, а не до, — иначе опечатка в форме сжигала бы ключ.
Не путать с приглашением в портал: то про доступ к сервису и живёт в его карточке, это — про появление аккаунта вообще.
- Роль нового аккаунта настраивается. Она была вписана в код пятью
Role::findBySlug('user')— в регистрации, соцвходе, импорте, переносе с портала и Admin API. «Поменять во всех пяти» рано или поздно означает «поменять в четырёх», поэтому теперь это одно место —Signup.
В списке только роли не старше «Пользователя»: раздать администратора выпадающим списком нельзя. Это не недоверие к владельцу, а защита от промаха мышью, который иначе раздаст права каждому следующему зарегистрировавшемуся.
- У переноса людей появились два состояния, которых не было. «Забирать некого» — портал отвечает, но людей у него нет: раньше на это рисовались три нуля и кнопка «Забрать людей», то есть предложение сделать несуществующее. «Все на месте» — новых нет, все узнаны: «0 будет заведено» рядом с большой кнопкой читалось как поломка, хотя это и есть нужный результат.
Проверяемость
- Закрытая регистрация не пускает ни экраном, ни отправкой формы; приглашение на чужой адрес не срабатывает, на свой — открывает и гаснет; новый аккаунт получает роль из настройки, а не «user» из кода.
4.40.2
Исправлено
- Кнопка «Скопировать» съедала пол-облака. Стояла в строку с текстом и отбирала у него ширину — сообщение, ради которого облако и появилось, оказывалось в узкой колонке. Теперь два значка 32×32 под текстом справа, а текст занимает всю ширину. Облако выложено сеткой: точка-маркер остаётся на своём месте в первой колонке.
- Кнопки фильтров стояли не в уровень с полями. У поиска по номеру инцидента подсказка жила внутри поля, а строка равняется по нижнему краю — «Найти» опускалась ровно на её высоту. Подсказка вынесена под форму, куда и относится: она про поиск целиком, а не про поле.
Заодно кнопки трёх полос фильтров переведены на слой полей и прижаты к правому краю: шесть фильтров в строку не влезают, и кнопка переносилась к левому краю второй строки, где выглядела случайной. Справа она читается как конец формы — поместилась строка или нет.
4.40.1
Исправлено
- Миграция 023 не применялась на MySQL: «Cannot execute queries while other unbuffered queries are active». Причина — моя заглушка
SELECT 1;в её.sql:exec()на SELECT оставляет открытый набор строк, и следующий запрос — тот самый PHP-шаг, заводящий роли, — падает. На SQLite этого не видно вовсе, поэтому батарея была зелёной.
Закрыто с трёх сторон: заглушка убрана; фрагмент из одних комментариев пропускается (миграции, у которой нечего менять в схеме, такой файл и положен); буферизация MySQL включена явно, и весь класс ошибки исчезает. Проверка не пускает отдельный SELECT в .sql миграции.
Добавлено
- У сообщения об ошибке появилась кнопка «Скопировать», и оно больше не исчезает само. Текст ошибки нужен целиком — его несут в поддержку и в переписку, — а на это была пара секунд, за которые надо было успеть выделить мышью бегущее облачко. Теперь ошибка ждёт, текст выделяется, рядом крестик. Успех и предупреждение исчезают как раньше: там копировать нечего.
Копирование с запасным путём: clipboard-API требует защищённого соединения, а установка по http — обычное дело на своём сервере.
4.40.0 · Прибрано
Исправлено
- Кнопка «Выберите файл» висела не по центру поля. Причина моя и общая: в v4.34 полям задали высоту, а у файлового поля внутри лежит кнопка — без выравнивания она прижималась к низу. Поле стало флексом, кнопка встала по центру везде сразу: импорт людей, загрузка карты ролей, всё остальное.
- Стрелка выпадающих списков стояла встык к рамке. Её рисовала ОС, и отступы поля на неё не действовали: слева текст с полем в 12px, справа стрелка в ноль. Теперь стрелка своя, с отступом 12px — как у переключателя организации, который поэтому и выглядел аккуратнее.
- «Подключить сервис» из быстрых действий вело в список порталов, а не в окно создания: обещало действие, приводило к оглавлению. Модалку теперь можно открыть ссылкой (
#открыть-wizard), без второго маршрута.
- Навигация повторяла себя трижды. «Порталы», имя сервиса и «Роли» были напечатаны по два раза в одном углу экрана: в крошках, в шапке колонки и в заголовке. Из крошек убран текущий раздел (он и так заголовок страницы), из шапки колонки — подпись, слово в слово повторяющая первую крошку.
- Карта ролей занимала пол-экрана над тем, ради чего в раздел заходят. Свёрнута в раскрывающийся блок с подписью «настроено соответствий: N». Связь с порталом — спросить о ролях, забрать людей — вынесена в свою карточку и осталась на виду: её трогают каждый день, а карту настраивают однажды.
Добавлено
- Отозванные токены Admin API можно убрать из списка. Отозванный не работает, но строку занимает, и через год список состоит из них. Рядом сказано прямо: показать ключ ещё раз нельзя, в базе лежит только хэш, — это не строгость ради строгости, а единственный способ хранить ключ так, чтобы его нельзя было достать ни из базы, ни из бэкапа.
- Журналы приложения и отправленные письма чистятся кнопкой. До этого — только по расписанию, а планировщик после установки настроен далеко не всегда. Сегодняшний журнал не трогается, ждущие и упавшие письма остаются: по ним видно, что чинить.
Проверяемость
- Замер по восьми разделам: 68 элементов управления, высоты только 32 и 40, стрелка у всех списков своя, боковой прокрутки нет.
4.39.0 · Матрица ролей
Исправлено
- Второе нажатие «продолжить» уводило в личный кабинет. Кнопка блокируется на время отправки и возвращается через пятнадцать секунд — если ответ шёл дольше, человек жал ещё раз. А запрос авторизации из сессии съеден первым нажатием, и второй POST не находил ничего:
/profile. Согласие получило тот же ключ возврата, что и вход, и смотрит на него не гася: повторное нажатие ведёт на портал, как и первое.
- Портал не узнавал об окончательном удалении. Событие
user.deletedрассылается по строкам доступа, аUser::purge()их сносит — рассылка стояла ПОСЛЕ удаления, и рассылать было уже некому. Уходил только вебхук, а мост слушает не его, а свой приёмник событий. Порядок исправлен в трёх местах: карточка человека, массовая чистка, слияние аккаунтов.
- Мост терял события. В сгенерированном
backchannel.phpнаuser.deletedстоял// TODO— событие приходило, подпись проверялась, и не происходило ничего. Со стороны SSO всё выглядело доставленным: 200 получен. Появилсяhooks.php— единственный файл, который портал обязан дописать; необработанное событие ложится вvar/sso-events.ndjsonи попадает в самопроверку. Молча пропасть событие больше не может.
Добавлено
- Справочник ролей
RoleCatalogи документ ROLES.md. Две семьи: роли управления несут права в SSO, роли портала — только метка в claimroles. Ничего не изобретено: лестница уровней уже содержалаeditor,moderator,publisher,viewer,guest— ролей на этих ступенях просто не было.
- Роли управления, которых не хватало (миграция 023):
auditor(90, только чтение),support(50,users.read+ вход от имени),org_auditor(50). Уровень поддержки — 50 намеренно: войти от имени можно только к тому, кто не выше тебя.
- Роль, о которой сообщил портал, заводится одной кнопкой — и сразу с уровнем.
blog_editorопознаётся как редактор и получает 60; незнакомое имя получает 20. Прав SSO приехавшая роль не получает никогда, как бы она ни называлась.
Проверяемость
- Двойное нажатие на согласии: оба ведут на портал.
- Порядок при удалении: проверка ловит перестановку рассылки после
purge(). - Справочник: уровни существуют на лестнице, права существуют, роль портала не несёт прав, приехавшее старшинство не выдаётся, ROLES.md и код описывают одни и те же роли.
4.38.0 · Вернись, откуда пришёл
Обещание SSO ровно одно: пришёл с сайта А — вернёшься на сайт А. Держалось оно на честном слове PHP-сессии и на терпении человека, ждущего чужой SMTP.
Исправлено
- Вход из сервиса ждал почту и вебхук.
Mailer::queue()клал письмо в очередь и ТУТ ЖЕ пытался отправить — TLS-рукопожатие и разговор с чужим SMTP, секунды.WebhookDispatcherтак же ходил HTTP-запросом к порталу. Оба вызываются на успешном входе, и человек ждал их, глядя на крутилку, — до редиректа, который и есть весь смысл происходящего.
Немедленная попытка сама по себе правильная (иначе письмо с кодом идёт до минуты по расписанию) — неправильно, что она стояла ПЕРЕД ответом. Появился Defer: на FPM ответ закрывается (fastcgi_finish_request), и человек уходит на портал, пока мы разговариваем с почтой. Где такого нет — работа делается в конце запроса, как раньше.
- Возврат на портал зависел от сессии. Запрос авторизации жил только в
$_SESSION, а сессия хрупкая: её роняет чужая настройка cookie, вторая вкладка, сборщик мусора при долгой возне с паролем. Упала — и человек оказывался в личном кабинете SSO, которого не просил.
Теперь запрос кладётся ещё и в базу, а одноразовый ключ едет в адресе и скрытым полем через вход, регистрацию и второй фактор. Сессия осталась первым источником, ключ подстраховывает. Проверено на самом злом случае: сессии нет вовсе — человек всё равно возвращается на портал.
- Список пользователей печатал каждую выдачу роли. Одна роль, выданная в трёх областях, давала три одинаковых плашки; у владельца выходила стена, из которой ничего не следует. Теперь по названию, две штуки и «+N», полный список — в подсказке и в карточке.
Добавлено
- Выдача ролей в карточке сервиса — тем же экраном, что у организации. Роль, приехавшая из портала, показывалась списком, и выдать её человеку было нечем: смысла в такой роли ноль. Экран общий (
admin/_role_grants), запрос «кому что выдано» переехал вRole::grantsInScope(Scope). Меняется только область: организация — во всех её сервисах, сервис — только в нём.
Проверяемость
- Проверено, что ключ возврата выдаётся, доезжает до формы, работает при начисто потерянной сессии и не работает второй раз.
- Проверено, что роль сервиса выдаётся и отзывается из его карточки.
- Проверка про пустую ячейку сверяла разметку слово в слово и сработала на добавлении
title=, хотя ячейка осталась пустой. Переписана на правило.
4.37.0 · Одна оболочка
Оболочка разделов одна на организацию, сервис, человека и здоровье, а внутри было кто во что горазд. Замер вместо глаза: на странице «Здоровья» стояли карточки 720px и 904px, а пять разделов рисовали таблицу вовсе без карточки.
Исправлено
- Карточки разной ширины на одной странице. Первая просила
mw-xl(720px), вторая —col-12; оболочка сетки не создаёт, поэтомуcol-12означал «во всю ширину», аmw-xl— «720». Два блока про одно и то же выглядели как из разных мест.
col-*вне сетки — класс, который ничего не делает. Убран из одиннадцати разделов: оболочка.bentoне открывает, и класс лишь притворялся, что чем-то управляет. Там, где сетка есть (обзор организации, карточка сервиса, действия над человеком), он остался и работает.
- Пять разделов рисовали таблицу голой: сессии и доступы человека, журнал и сервисы организации, доставка событий портала, выданные роли. У соседнего раздела та же таблица лежала в карточке — одна оболочка, а внутри то так, то эдак. Теперь содержимое раздела везде в карточке с заголовком.
- Форма выдачи доступа в карточке человека собиралась голым
<select>с кнопкой рядом: ширину брала от самого длинного названия портала и вставала не по линии. Переведена на слой полей, как везде.
- «Хозяйство» переименовано в «Проверки». Название отсылало к завхозу, а раздел отвечает на вопрос «что проверено и в каком оно состоянии».
- Два осиротевших шаблона удалены —
admin/health.phpиadmin/org_shell.phpостались от перехода на разделы-страницы в v4.29, и с тех пор их никто не рисовал. Ехали в архиве и путали при чтении.
Проверяемость
- Новые проверки: содержимое раздела лежит в карточке, а не голым;
col-*стоит только там, где есть.bento. Обе проверены на возвращённых поломках. - Замер после правки: 18 разделов организации, сервиса, человека и здоровья — ни одной таблицы вне карточки, ни одной горизонтальной прокрутки; ширины либо 904 в одну колонку, либо честные доли сетки.
4.36.0 · Кто кого пускает
Исправлено
- Раздел «Действия» над аккаунтом был ПОЛНОСТЬЮ невидим. Блокировка, ссылка для входа, сброс пароля, смена номера, вход от имени, удаление — ничего. Разметка осталась от карточки со вкладками: обёртка
.tab-paneспрятана в CSS, пока скрипт не повесит на неё.activeпо нажатию на вкладку. Вкладки убрали в v4.30 — разделы стали страницами, — а обёртка осталась, и включить её стало нечем. Страница отдавала 200, подпись раздела рисовалась, содержимого не было никогда.
- Профиль предлагал ВСЕ сервисы системы. В списке «ещё не подключён» шли все активные порталы, и человек в своём профиле видел чужие проекты — включая те, о которых знать ему незачем: личный блокнот владельца выглядел как приглашение. Спрашивается тот же флаг, что и витрина на главной; второй переключатель не заведён — вопрос один и тот же.
Флаг переименован в «Предлагать этот сервис»: он и раньше отвечал на этот вопрос, но назывался «Показывать на главной» и про профиль молчал.
Добавлено
- Доступ выдаётся и закрывается из карточки сервиса. Раздел «Люди» был списком для чтения: посмотреть, кто ходит, можно, а пустить или закрыть — нет. Обратная сторона отношения (выбрать сервис в карточке человека) была, и чтобы пустить пятерых в один сервис, приходилось открыть пять чужих карточек. Теперь в строке — «Открыть», «Закрыть», «Убрать», а внизу поле «дать доступ» по почте или логину. Аккаунт при этом не заводится: «дать доступ» и «создать человека» — разные намерения, и второе не должно случаться по опечатке.
Проверяемость
- Новая проверка ловит класс ошибки, а не случай: у каждого переключателя, который слушает скрипт, обязана быть разметка. У
[data-tab]её не осталось ни одной — механизм был мёртв, а всё, что на нём висело, невидимо. Проверена на возвращённых вкладках. - Парная к ней: класс, спрятанный вёрсткой безусловно, обязан где-то снова становиться видимым. Медиазапросы не в счёт — там «спрятать» это ответ на ширину экрана.
- По дороге найден и удалён второй мёртвый механизм:
[data-sso-popup-done]ждал разметки, которой нет ни в одном шаблоне, — закрытием попапа занимается страница возврата на стороне портала.
4.35.0 · Своя организация
Порядок работы, ради которого всё и затевалось, — «завёл организацию, завожу ей сервисы» — не работал. Стоило выбрать свою организацию, и подключить сервис становилось нечем.
Исправлено
- Кнопка «Подключить сервис» пропадала при выбранной организации. Её гасило условие
$scope === null— «список не сужен». Сужение списка про то, что человек СМОТРИТ, а не про то, что ему можно; и работа внутри организации — обычный случай, а не исключение. Право теперь спрашивается правом, и одним и тем же для кнопки и для проверки при отправке:ClientsController::mayCreate(). Разъехались они именно потому, что определялись в двух местах.
- Сервис всегда рождался ничьим.
create()не проставлялorg_idвовсе, и человек, заводивший сервис из своей организации, обязан был пойти в её раздел и присоединить — при том, что другого намерения у него быть не могло.
Добавлено
- Подключение сервиса прямо в разделе «Сервисы» организации — там, где его и ищут. Заведённый оттуда сервис сразу принадлежит этой организации. Мастер один на оба места (
admin/_client_wizard), а не скопирован.
- Владелец организации заводит сервисы сам. Право спрашивается в области организации (
Auth::can('clients.write', Scope::org($id))) — ради этого области и заводились. Сервис «ничей» по-прежнему заводит только администратор системы.
Проверяемость
- Проверка отставшей схемы поймала регрессию: до миграции 020 колонки
clients.org_idнет, и безусловная запись ломала подключение сервиса целиком. Колонка теперь спрашивается, а не считается данностью. - В батарее: в контексте организации кнопка на месте; заведённый сервис сразу принадлежит организации; в разделе «Сервисы» кнопка есть и мастер оттуда назначает именно эту организацию.