Unverified Commit 61b283ae authored by Lemon-miaow's avatar Lemon-miaow
Browse files

feat(auth): add Owner authentication source settings

Manage Yggdrasil providers from the panel using durable platform settings, protected identity namespaces and atomic revisions. Apply changes to subsequent logins and profile lookups without restarting. Return operator-host logouts to the login method selection page.
parent 2c98e8b2
Loading
Loading
Loading
Loading
+2 −0
Changes for README.md: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -74,6 +74,8 @@ curl -fsSL https://raw.githubusercontent.com/FelisMC/Felis/main/deploy/bootstrap

* **设置向导**:先完成面板访问方式与存储配置,再由主机管理员创建首位 Owner,终端会给出一次性网页设置链接(30 分钟内有效)。用浏览器打开链接、登记邮箱并创建通行密钥,即可进入面板,无需启动 Minecraft。角色关联可稍后在“账户”页选择认证源、输入角色名或 UUID 并确认;普通玩家继续通过游戏绑定码验证身份。未完成网页登录或链接过期时,再次执行 `sudo felis setup` 会提供新链接;已设置登录凭证的账号不会被重置。登录服或大厅故障不会阻止面板初始化。“账户”页会解释世界树(Yggdrasil)认证、显示进服地址与登录服/大厅所需的 Java 版客户端版本;安装器会把实际构建版本写入 `[velocity].game_version`,自定义镜像需自行填写,未配置时不会猜测版本。标准世界树接口可直接查询角色;非标准 `hasJoined` 地址可通过 `[[auth_source]].api_url` 指定认证站 API 根地址,游戏绑定码仍可作为替代方式。安装器仅在交互式终端中自动启动向导;输出重定向至日志或经由 cloud-init 安装时,请在安装结束后执行 `sudo felis setup`。设置 `FELIS_NO_SETUP=1` 时,安装器在输出摘要后直接结束。

* **认证源管理**:Owner 可在左侧“平台 → 认证源”添加、编辑、排序、停用第三方 Yggdrasil 认证站,并测试认证接口。保存后立即用于下一次登录和角色查询,无需重启;面板配置优先于安装配置。已保存的永久标识不能改名或删除,以保留玩家 UUID 与账号绑定;不再使用的源可停用。Mojang 始终优先验证。Nano 继续使用 TOML 配置。

* **支持的系统**:CentOS Stream 9(aarch64)已在实机上验证;Ubuntu 24.04(x86_64)在每次推送时由 CI 执行全新安装、重复安装、升级及上述安装命令(参见 [运维手册 §1](docs/operations.md#1-supported-hosts))。

* **安装前检查**:安装器在修改主机之前检查内存、磁盘、端口、网段冲突、已有的 Kubernetes 及外网连通性。发现问题时一次性列出全部问题并退出,主机保持原状(检查项参见 [运维手册 §1](docs/operations.md#1-supported-hosts))。
+2 −1
Changes for cmd/felis/api.go: 2 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -475,7 +475,8 @@ func cmdAPI(args []string, stdout, stderr io.Writer) int {
	// [[auth_source]] is configured, so an empty list has to mean a Mojang-only relay, the
	// same as under `felis nano`. A nil list would 204 every login, premium ones included.
	a.AuthSources = authSourcesFromConfig(cfg.AuthSources)
	fmt.Fprintf(stderr, "felis api: hasJoined multiplexer active — Mojang + %d third-party source(s)\n", len(cfg.AuthSources))
	a.AuthSourceSettings = &api.AuthSourceSettings{Repo: repo, Defaults: a.AuthSources}
	fmt.Fprintf(stderr, "felis api: hasJoined multiplexer active — Mojang + %d default third-party source(s); panel settings take precedence\n", len(cfg.AuthSources))

	// Passkey (WebAuthn) verifier (spec §14, Phase 6). One relying party spans BOTH
	// web faces: the RP id is the panel hostname (console.<root>), and because that is
+131 −0
Changes for docs/openapi.yaml: 131 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -311,6 +311,42 @@ components:
              output: { type: string }

  schemas:
    AuthSourceConfig:
      type: object
      additionalProperties: false
      required: [tag, prefix, url, api_url, enabled]
      properties:
        tag:
          type: string
          description: Permanent UUID namespace; saved tags cannot be renamed or removed. mojang is reserved.
        prefix:
          type: string
          pattern: '^[A-Za-z0-9]{1,4}$'
          description: Unique case-insensitive display prefix.
        url:
          type: string
          description: hasJoined endpoint; HTTPS required except for localhost or private literal IP addresses. No query or fragment.
        api_url:
          type: string
          description: Optional Yggdrasil API base for role lookup; empty infers it from the standard hasJoined suffix.
        enabled: { type: boolean }

    AuthSourcesSettings:
      type: object
      required: [sources, revision, managed]
      properties:
        sources:
          type: array
          maxItems: 32
          description: Third-party sources in priority order; built-in Mojang always precedes them and is immutable.
          items: { $ref: '#/components/schemas/AuthSourceConfig' }
        revision:
          type: string
          description: Opaque revision to send unchanged when saving; stale or concurrent writes return 409.
        managed:
          type: boolean
          description: True when stored in platform_settings; false while using installation TOML defaults.

    Error:
      type: object
      description: Uniform error envelope emitted by every handler (internal/api/errors.go).
@@ -4229,6 +4265,101 @@ paths:
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/settings/auth-sources:
    get:
      tags: [account]
      operationId: getAuthSources
      summary: Read authentication sources (Owner).
      x-felis-face: [external]
      x-felis-tier: owner
      security: [{ sessionCookie: [] }]
      responses:
        '200':
          description: Current third-party sources and their revision.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AuthSourcesSettings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }
    put:
      tags: [account]
      operationId: setAuthSources
      summary: Save authentication sources (Owner, fresh reauthentication).
      description: >-
        Atomically persists an override in platform_settings. It applies to the next
        login and role lookup on every API replica without restarting; existing
        players stay online. Tags identify permanent UUID namespaces; retain every
        saved tag and disable unwanted sources. Mojang remains built-in and trusted,
        while configured sources always remain third-party. Nano remains TOML-only.
      x-felis-face: [external]
      x-felis-tier: owner
      security: [{ sessionCookie: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [sources, revision]
              properties:
                sources:
                  type: array
                  maxItems: 32
                  items: { $ref: '#/components/schemas/AuthSourceConfig' }
                revision: { type: string }
      responses:
        '200':
          description: Saved configuration and new revision.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AuthSourcesSettings' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409':
          description: auth_sources_changed or auth_source_tag_locked; reload instead of overwriting another Owner's changes.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }

  /api/v1/settings/auth-sources/test:
    post:
      tags: [account]
      operationId: testAuthSource
      summary: Probe a hasJoined endpoint (Owner).
      description: >-
        Checks an unsaved source with a fresh random serverId. A healthy endpoint
        returns 204 for a session that never joined. Uses a five-second timeout,
        verified TLS and no redirects. This tests connectivity and hasJoined
        behavior, not launcher login or profile lookup. Does not save configuration.
      x-felis-face: [external]
      x-felis-tier: owner
      security: [{ sessionCookie: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AuthSourceConfig' }
      responses:
        '200':
          description: Probe result; non-204 status has ok=false.
          content:
            application/json:
              schema:
                type: object
                required: [ok, status, elapsed_ms]
                properties:
                  ok: { type: boolean }
                  status: { type: integer }
                  elapsed_ms: { type: integer, format: int64 }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }

  /api/v1/updates/window:
    get:
      tags: [admin-updates]
+29 −0
Changes for docs/operations.md: 29 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -874,3 +874,32 @@ the moved domain and left `check` clean; moving back restored every surface
on every run, so a host upgraded from one can show that line once with the file already
on the names; `sudo systemctl restart felis-velocity` clears it. The installer now leaves
the file alone when its content is the same.

## 7. Authentication sources

On the operator console, the Owner's **Platform → Authentication sources** page
(`/admin/auth-sources`) manages third-party Yggdrasil providers. It imports the
installation's `[[auth_source]]` list on first use. Save stores the ordered list
in `platform_settings.auth_sources`; that override then takes precedence over TOML
and is read by every full-API replica for the next game login and role lookup.
No restart is required, and existing players stay connected. Nano continues to
use its TOML list. A database read failure refuses new authentication rather than
falling back to an obsolete or disabled provider.

Mojang remains enabled and first, retaining official UUIDs. Every third-party
provider uses a permanent tag as its UUID namespace; saved or imported tags cannot
be renamed or removed. Disable a provider to stop accepting its logins, or enable
it again to restore the same identities. Changing a provider's endpoint changes
who verifies identities in that namespace; keep it pointed at the same trusted
service. Prefixes are 1–4 letters/digits and must be unique regardless of case.

Set the full `hasJoined` URL. Standard paths infer the profile-query API root;
nonstandard paths need an explicit API root for role-name/UUID lookup. HTTPS is
required, except for localhost or literal private IPs; query strings and fragments
are rejected. Launchers must authenticate with the same provider. **Test connection**
probes an unused session and expects HTTP 204; it does not save, verify launcher
configuration, or test the profile-query API.

Saving requires Owner access on the operator host and recent reauthentication for
a local session. A revision conflict preserves the draft; discard it and reload
before editing the newer configuration.
+4 −0
Changes for internal/api/api.go: 4 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -202,6 +202,7 @@ type API struct {
	// cmd/felis always wires at least the Mojang source through authSourcesFromConfig.
	// Consumed by handleHasJoined (handlers_hasjoined.go).
	AuthSources        []AuthSource
	AuthSourceSettings *AuthSourceSettings

	// AuthDoorLimit bounds how often one client address may call the public
	// pre-session auth doors (ratelimit.go). MailLimit bounds all mail the API
@@ -651,6 +652,9 @@ func (a *API) externalAPIRoutes() []apiRoute {
		// Staff can designate their own game identity after panel setup. Players
		// retain the in-game proof flow above.
		{Method: "GET", Pattern: "/api/v1/account/link/sources", Admin: true, h: a.handleLinkSources},
		{Method: "GET", Pattern: "/api/v1/settings/auth-sources", Owner: true, Admin: true, h: a.handleGetAuthSources},
		{Method: "PUT", Pattern: "/api/v1/settings/auth-sources", Owner: true, Admin: true, h: a.handleSetAuthSources},
		{Method: "POST", Pattern: "/api/v1/settings/auth-sources/test", Owner: true, Admin: true, h: a.handleTestAuthSource},
		{Method: "GET", Pattern: "/api/v1/account/link/profile", Admin: true, h: a.handleLookupProfile},
		{Method: "POST", Pattern: "/api/v1/account/link/profile", Admin: true, h: a.handleLinkProfile},
		// Email verification (spec §B2 onboarding), web side: /start mints+delivers a
Loading