Newer
Older
smart-home-server / docs / planning / gnexus-auth-integration.md
@Eugene Sukhodolskiy Eugene Sukhodolskiy 14 hours ago 22 KB Phase 0: gnexus-auth integration infrastructure

Техническое задание: Интеграция smart-home-serv с gnexus-auth

1. Цели

  1. Удалить локальную регистрацию/аутентификацию (таблицы users, sessions, profiles).
  2. Использовать gnexus-auth (auth.gnexus.space) как единый источник идентичности.
  3. Реализовать ролевую модель: superadmin, admin, user, guest.
  4. Реализовать permission-based ACL с возможностью урезания прав для конкретного пользователя админом/суперадмином.
  5. Поддержать группы пользователей, синхронизируемые с gnexus-auth.
  6. Обработать webhooks от gnexus-auth для актуализации ролей, прав, статусов.
  7. Минимальные изменения в существующем API устройств/скриптов/зон.

2. Архитектурные решения

2.1. Пакет gnexus/auth-client и HTTP-клиент

Пакет: gnexus/auth-client (репозиторий git.gnexus.space/root/gnexus-auth-client-php).

Подключение: через Composer repositories.type = vcs:

"repositories": [{
    "type": "vcs",
    "url": "https://git.gnexus.space/root/gnexus-auth-client-php.git"
}],
"require": {
    "gnexus/auth-client": "^0.1"
}

PSR-реализации: пакет требует только интерфейсы PSR-18/17/7 (psr/http-client, psr/http-factory, psr/http-message). Реализацию пишем сами — thin cURL-адаптер:

  • CurlHttpClient — PSR-18 ClientInterface
  • Psr7Factory — PSR-17 RequestFactoryInterface + StreamFactoryInterface
  • Psr7Request, Psr7Response, Psr7Stream — PSR-7 DTO
  • Ожидаемый объём ~350 строк, zero дополнительных Composer-зависимостей.
  • Расположение: server/SHServ/Integrations/GAuth/Http/.

2.2. Хранение токенов

Решение: PHP $_SESSION + опциональная таблица shserv_sessions для долгосрочного хранения refresh-токена.

Обоснование:

  • Access token живёт 15 минут — храним в $_SESSION.
  • Refresh token живёт 30 дней — при long-term «запомнить меня» сохраняем в shserv_sessions (cookie → session_id → refresh_token).
  • Это позволяет восстанавливать сессию после закрытия браузера без повторного OAuth-флоу.
  • При logout — отзываем refresh token через gnexus-auth и чистим локальную сессию.

2.3. Группы пользователей

Решение: синхронизировать с gnexus-auth через webhooks group.user_added / group.user_removed.

Обоснование:

  • Группы создаются/удаляются в админке gnexus-auth.
  • SHServ реплицирует структуру групп и membership в локальные таблицы.
  • Права групп применяются как union к правам пользователя.

2.4. Guest

Решение: локальная концепция SHServ (не system_role в gnexus-auth).

Обоснование:

  • В gnexus-auth три базовых system_role: superadmin, admin, user.
  • Guest = отсутствие авторизации в SHServ. Локально разрешаем минимальные permissions для guest.
  • Это не требует изменений в gnexus-auth и не создаёт фиктивных аккаунтов.

3. Модель данных (новые таблицы)

3.1. shserv_users — локальное отображение пользователей

CREATE TABLE shserv_users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    gauth_user_id VARCHAR(64) NOT NULL UNIQUE,
    email VARCHAR(255) NOT NULL,
    display_name VARCHAR(255),
    avatar_url VARCHAR(500),
    system_role VARCHAR(32) NOT NULL DEFAULT 'user',   -- superadmin | admin | user
    status VARCHAR(32) NOT NULL DEFAULT 'active',         -- active | blocked | archived
    max_role VARCHAR(32) NULL,                          -- роль, назначенная в gnexus-auth
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_gauth (gauth_user_id),
    INDEX idx_email (email)
);

3.2. shserv_roles — локальные роли (mirror + override)

CREATE TABLE shserv_roles (
    id INT AUTO_INCREMENT PRIMARY KEY,
    slug VARCHAR(32) NOT NULL UNIQUE,          -- superadmin | admin | user | guest
    name VARCHAR(64) NOT NULL,
    is_system TINYINT(1) NOT NULL DEFAULT 0,  -- system role из gnexus-auth
    default_permissions JSON,                  -- массив permission slugs
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

3.3. shserv_permissions — реестр прав

CREATE TABLE shserv_permissions (
    id INT AUTO_INCREMENT PRIMARY KEY,
    slug VARCHAR(64) NOT NULL UNIQUE,          -- devices.scan | devices.control | scripts.edit | areas.manage | admin.users | admin.roles
    name VARCHAR(128) NOT NULL,
    description TEXT,
    default_for_roles JSON,                    -- {"superadmin": true, "admin": true, "user": false, "guest": false}
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

3.4. shserv_user_permissions — персональные overrides

CREATE TABLE shserv_user_permissions (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    permission_slug VARCHAR(64) NOT NULL,
    granted TINYINT(1) NOT NULL DEFAULT 1,     -- 1 = разрешено, 0 = явно запрещено
    set_by_user_id INT NULL,                  -- кто установил override
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_user_perm (user_id, permission_slug),
    FOREIGN KEY (user_id) REFERENCES shserv_users(id) ON DELETE CASCADE
);

3.5. shserv_groups — группы (реплика из gnexus-auth)

CREATE TABLE shserv_groups (
    id INT AUTO_INCREMENT PRIMARY KEY,
    gauth_group_id VARCHAR(64) NOT NULL UNIQUE,
    slug VARCHAR(64) NOT NULL,
    name VARCHAR(128) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

3.6. shserv_group_members — membership

CREATE TABLE shserv_group_members (
    id INT AUTO_INCREMENT PRIMARY KEY,
    group_id INT NOT NULL,
    user_id INT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_membership (group_id, user_id),
    FOREIGN KEY (group_id) REFERENCES shserv_groups(id) ON DELETE CASCADE,
    FOREIGN KEY (user_id) REFERENCES shserv_users(id) ON DELETE CASCADE
);

3.7. shserv_group_permissions — права групп

CREATE TABLE shserv_group_permissions (
    id INT AUTO_INCREMENT PRIMARY KEY,
    group_id INT NOT NULL,
    permission_slug VARCHAR(64) NOT NULL,
    granted TINYINT(1) NOT NULL DEFAULT 1,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_group_perm (group_id, permission_slug),
    FOREIGN KEY (group_id) REFERENCES shserv_groups(id) ON DELETE CASCADE
);

3.8. shserv_sessions — локальные сессии (для refresh-токенов)

CREATE TABLE shserv_sessions (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    session_token VARCHAR(128) NOT NULL UNIQUE,
    refresh_token VARCHAR(255),
    access_token VARCHAR(255),
    expires_at TIMESTAMP NULL,
    ip_address VARCHAR(45),
    user_agent VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_token (session_token),
    FOREIGN KEY (user_id) REFERENCES shserv_users(id) ON DELETE CASCADE
);

4. Интеграционный слой (server/SHServ/Integrations/GAuth/)

Пакет gnexus/auth-client уже предоставляет: GAuthClient, GAuthConfig, HttpTokenEndpoint, HttpRuntimeUserProvider, HmacWebhookVerifier, JsonWebhookParser, PkceGenerator, весь DTO и exception набор. Мы реализуем только то, что пакет оставляет на усмотрение consuming-приложения:

Integrations/GAuth/
  Http/
    CurlHttpClient.php          -- PSR-18 ClientInterface (thin cURL)
    Psr7Factory.php             -- PSR-17 RequestFactory + StreamFactory
    Psr7Request.php             -- PSR-7 RequestInterface
    Psr7Response.php            -- PSR-7 ResponseInterface
    Psr7Stream.php              -- PSR-7 StreamInterface
  Store/
    SessionStateStore.php       -- StateStoreInterface (на $_SESSION)
    SessionPkceStore.php        -- PkceStoreInterface (на $_SESSION)
    DbTokenStore.php            -- TokenStoreInterface (на shserv_sessions)
  Webhook/
    WebhookRouter.php           -- route event.type → handler
    Handlers/
      UserHandler.php           -- user.* events
      RoleHandler.php           -- client.roles_changed
      PermissionHandler.php     -- client.permissions_changed
      GroupHandler.php          -- group.user_added / removed
      SessionHandler.php        -- auth.global_logout / session.revoked
  AuthService.php               -- high-level wrapper: login URL, callback, logout, me
  UserResolver.php              -- маппинг AuthenticatedUser → shserv_users
  PermissionResolver.php        -- resolve effective permissions

5. Ролевая модель и права

5.1. Роли

Роль Описание Источник
superadmin Полный доступ AuthenticatedUser->systemRole
admin Администрирование SHServ AuthenticatedUser->systemRole
user Обычный пользователь AuthenticatedUser->systemRole
guest Неавторизованный Локально (отсутствие auth)

Пакет gnexus/auth-client возвращает AuthenticatedUser с полями:

  • systemRole — глобальная роль в gnexus-auth (superadmin/admin/user).
  • clientAccessList — массив ClientAccess, каждый содержит clientId, roleIds[], permissionIds[] для конкретного Client.
  • Для SHServ берём clientAccessList где clientId === GAUTH_CLIENT_ID.

5.2. Права (starter set)

Пакет gnexus/auth-client возвращает permissionIds[] в ClientAccess (вместе с roleIds[]). Эти ID мапятся на локальные shserv_permissions.slug.

Starter set прав SHServ:

devices.view          -- просмотр списка устройств
devices.scan          -- сканирование сети
devices.control       -- управление устройствами
devices.setup         -- добавление новых устройств
devices.edit          -- редактирование устройств
devices.delete        -- удаление устройств
areas.view            -- просмотр зон
areas.manage          -- управление зонами
scripts.view          -- просмотр скриптов
scripts.edit          -- редактирование скриптов
scripts.run           -- запуск скриптов
firmware.view         -- просмотр прошивок
firmware.upload       -- загрузка прошивок
admin.users           -- управление пользователями
admin.roles           -- управление ролями/правами
admin.audit           -- просмотр аудита
settings.edit         -- изменение настроек системы

5.3. Алгоритм определения эффективных прав

effective_permissions(user) =
  union(
    permissions_from_role(user.system_role),           -- дефолт по роли
    permissions_from_groups(user.groups),                -- права групп
    user_overrides(user.id)                              -- персональные overrides
  )
  minus explicitly_denied(user.id)
  • superadmin всегда имеет все права (fast path).
  • admin / user / guest — через таблицы.
  • Override granted = 0 явно запрещает право, даже если оно есть у роли/группы.

5.4. Урезание прав

Админ/суперадмин через API POST /api/v1/admin/users/{id}/permissions может:

  • Добавить право (granted = 1) — даёт право, которого нет у роли.
  • Запретить право (granted = 0) — урезает право, имеющееся у роли.
  • Удалить запись — возвращает дефолт роли.

6. API (новые и изменённые endpoints)

6.1. Auth flow (OAuth-like)

GET  /auth/login     → редирект на gnexus-auth /oauth/authorize
GET  /auth/callback  → обмен code на tokens, создание/обновление local user, редирект в приложение
POST /auth/logout   → отзыв refresh token, очистка сессии
GET  /auth/me       → текущий пользователь + его effective permissions

6.2. Webhook endpoint

POST /webhooks/gnexus-auth
  • Verify HMAC-SHA256 через GAuthClient::verifyWebhook().
  • Parse через GAuthClient::parseWebhook().
  • Route на WebhookHandler по event.type.

6.3. Административные endpoints (требуют admin.users / admin.roles)

GET    /api/v1/admin/users
GET    /api/v1/admin/users/{id}
PATCH  /api/v1/admin/users/{id}/role
POST   /api/v1/admin/users/{id}/permissions      -- {permission_slug, granted}
DELETE /api/v1/admin/users/{id}/permissions/{slug}
GET    /api/v1/admin/roles
GET    /api/v1/admin/permissions
GET    /api/v1/admin/groups

6.4. Защита существующих endpoints

Все существующие /api/v1/* endpoints (кроме устройственных /about и /events/new) требуют авторизации.

Для Fury Router (нет middleware) — внедряем проверку в базовый контроллер:

  • Добавить AuthControllerTrait с методом require_auth() и require_permission($slug).
  • Вызывать в начале каждого защищённого метода контроллера.

6.5. Vue client API изменения

  • client.js — добавить Authorization: Bearer {access_token} header ко всем запросам.
  • При 401 от API — редирект на /auth/login.
  • Logout — вызов POST /auth/logout, затем очистка local storage.

7. Vue web client изменения

7.1. Auth store (src/stores/auth.js)

export const useAuthStore = defineStore('auth', () => {
  const user = ref(null);
  const permissions = ref([]);
  const isAuthenticated = computed(() => !!user.value);
  const hasPermission = (slug) => permissions.value.includes(slug);
  // ...
});

7.2. Login flow

  1. Пользователь нажимает «Войти» → window.location = '/auth/login'.
  2. gnexus-auth → callback → backend создаёт сессию → редирект на Vue app (/#/).
  3. Vue app при старте вызывает GET /auth/me → получает user + permissions.

7.3. UI адаптации

  • Условный рендеринг кнопок/меню по hasPermission().
  • Скрытие админ-разделов для non-admin.
  • Login/Logout кнопки в AppShell.

8. Обработка webhooks

8.1. Подписываемся на события

user.updated
user.blocked
user.unblocked
user.archived
user.restored
user.deleted
client.roles_changed
client.permissions_changed
client.access_granted
client.access_revoked
client.access_denied
group.user_added
group.user_removed
auth.global_logout
session.revoked

8.2. Логика обработки

Событие Действие
user.updated Обновить display_name, avatar_url, email в shserv_users
user.blocked status = 'blocked', очистить сессии
user.unblocked status = 'active'
user.deleted Удалить из shserv_users (cascade cleanup)
client.roles_changed Обновить system_role / client roles
client.permissions_changed Обновить shserv_user_permissions
group.user_added INSERT shserv_group_members
group.user_removed DELETE shserv_group_members
auth.global_logout Удалить все shserv_sessions для user
session.revoked Удалить конкретную сессию из shserv_sessions

9. Фазы реализации

Phase 0 — Инфраструктура (1–2 дня)

  • Добавить thin PSR-18/17 cURL адаптер (CurlHttpClient + Psr7Factory + DTO).
  • Подключить gnexus/auth-client через Composer vcs репозиторий (git.gnexus.space/root/gnexus-auth-client-php).
  • Создать миграции (8 новых таблиц).
  • Создать GAuthConfig и AuthService wrapper.
  • Настроить Client в gnexus-auth (client_id, secret, redirect_uri, webhook).
  • Проверить совместимость: пакет требует PHP ^8.3, текущий сервер — 8.5.6 ✓

Phase 1 — OAuth flow (2–3 дня)

  • GET /auth/loginGAuthClient::buildAuthorizationRequest(..., scopes: ['openid','email','profile','roles','permissions']) → редирект.
  • GET /auth/callbackexchangeAuthorizationCode(), fetchUser().
    • AuthenticatedUser содержит: userId, email, systemRole, status, profile, clientAccessListroleIds[], permissionIds[]).
    • Upsert в shserv_users, маппинг gauth_user_id.
  • POST /auth/logoutrevokeToken() + local cleanup.
  • GET /auth/me — текущий пользователь + effective permissions.
  • SessionStateStore + SessionPkceStore на PHP $_SESSION.
  • DbTokenStore для refresh-токена (связь session_tokenrefresh_token).

Phase 2 — Права и роли (2–3 дня)

  • Seed таблицы shserv_roles, shserv_permissions.
  • PermissionResolver — алгоритм union/minus.
  • AuthControllerTraitrequire_auth(), require_permission().
  • Защитить существующие API endpoints.
  • Админ endpoints для управления permissions.

Phase 3 — Webhooks (1–2 дня)

  • POST /webhooks/gnexus-auth endpoint.
  • Верификация: HmacWebhookVerifier::verify(rawBody, headers, secret) (из пакета).
  • Парсинг: JsonWebhookParser::parse(rawBody)WebhookEvent (из пакета).
  • WebhookRouter → event-specific handlers (UserHandler, RoleHandler, GroupHandler, SessionHandler).
  • Настроить webhook subscription в gnexus-auth.

Phase 4 — Vue client (2–3 дня)

  • Auth store (src/stores/auth.js) с user, permissions, isAuthenticated, hasPermission().
  • Bearer token injection в client.js / http.js.
  • Interceptor: при 401 от API → window.location = '/auth/login' (backend сделает redirect в gnexus-auth).
  • Lazy token refresh: при 401 от gnexus-auth runtime API или expiresAt прошёл — backend-endpoint POST /auth/refresh обновляет access token.
  • Conditional UI: скрытие кнопок/разделов по hasPermission().
  • Login/Logout в AppShell.

Phase 5 — Legacy web client (1 день)

  • Аналогичные изменения для webclient_legacy (если ещё используется в продакшене).

Phase 6 — Cleanup (1 день)

  • Удалить Example_AuthController.php, старые auth routes.
  • Архивировать legacy таблицы: users_legacy_users, sessions_legacy_sessions, profiles_legacy_profiles (на случай аудита).
  • Удалить SHServ/Entities/User.php, Session.php, Profile.php (или перенести в _legacy/).
  • Обновить docs/server-api.md и docs/architecture.md.
  • Smoke-test end-to-end.

10. Дополнительные решения и открытые вопросы

  1. Client credentials — хранить в .env:

    GAUTH_BASE_URL=https://auth.gnexus.space
    GAUTH_CLIENT_ID=shserv
    GAUTH_CLIENT_SECRET=...
    GAUTH_WEBHOOK_SECRET=...

    Пакет gnexus/auth-client читает их через GAuthConfig (передаются явно в конструктор).

  2. Redirect URI — регистрировать в gnexus-auth Client:

    • Prod: https://smarthome.gnexus.space/auth/callback
    • Dev: http://smart-home-serv.local/auth/callback
    • Local: http://localhost/auth/callback (если gnexus-auth разрешает local/dev origins).
  3. Token refresh — lazy refresh:

    • Access token храним в $_SESSION + expiresAt.
    • При 401 от runtime API или expiresAt прошёл — frontend вызывает POST /auth/refresh.
    • Backend берёт refresh token из DbTokenStore, делает GAuthClient::refreshToken(), обновляет сессию.
  4. Миграция старых пользователей — legacy таблицы (users, sessions, profiles) архивировать (RENAME TABLE users TO _legacy_users), не удалять.

  5. Аудит в SHServ — админ-действия (смена роли, override permission) логировать в shserv_audit:

    CREATE TABLE shserv_audit (
        id INT AUTO_INCREMENT PRIMARY KEY,
        actor_user_id INT NOT NULL,
        action VARCHAR(64) NOT NULL,
        target_type VARCHAR(32),
        target_id INT,
        old_value JSON,
        new_value JSON,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );
  6. Тестирование пакета — пакет gnexus/auth-client имеет unit tests (tests/Unit/). Перед интеграцией рекомендуется прогнать их:

    cd packages/auth-client && php vendor/bin/phpunit