# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. # SPDX-License-Identifier: AGPL-3.0 """Identity and role types for OpenViking multi-tenant HTTP Server.""" from __future__ import annotations from dataclasses import dataclass, field from enum import Enum from typing import TYPE_CHECKING, Any, List, Optional if TYPE_CHECKING: from openviking.storage.viking_fs import VikingFS from openviking_cli.session.user_id import UserIdentifier class Role(str): """Role type — supports built-in roles and custom plugin-defined roles. Built-in roles (root/admin/user) are available as class attributes. Custom roles can be registered via ``Role.register(name, rank)``. Role comparison is string-based, so ``Role.ROOT == "root"`` and ``Role("custom") == "custom"`` both work. """ ROOT = "root" ADMIN = "admin" USER = "user" # Privilege ranking for role-downgrade detection. # Higher rank => more privilege. _BUILTIN_RANK: dict[str, int] = { USER: 0, ADMIN: 1, ROOT: 2, } _CUSTOM_RANK: dict[str, int] = {} @classmethod def register(cls, name: str, rank: int) -> None: """Register a custom role with a privilege rank. Args: name: Role identifier string. rank: Privilege rank — higher = more privilege. """ cls._CUSTOM_RANK[name] = rank @property def rank(self) -> int: """Return the privilege rank for this role.""" return self._BUILTIN_RANK.get(self, self._CUSTOM_RANK.get(self, 0)) def __eq__(self, other: object) -> bool: if isinstance(other, Role): return str(self) == str(other) if isinstance(other, str): return str(self) == other return NotImplemented def __hash__(self) -> int: return hash(str(self)) class AuthMode(str, Enum): """Authentication modes for OpenViking server. Built-in modes. Custom modes are plain strings not in this enum. """ API_KEY = "api_key" TRUSTED = "trusted" DEV = "dev" @dataclass class ResolvedIdentity: """Output of auth middleware: raw identity resolved from API Key.""" role: Role account_id: Optional[str] = None user_id: Optional[str] = None # True when this identity was minted from an OAuth-issued bearer token; # downstream checks (e.g. ROOT-requires-explicit-tenant headers) can skip # rules that target raw API-key auth, since OAuth claims already pin # account/user. from_oauth: bool = False @dataclass class RequestContext: """Request-level context, flows through Router -> Service -> VikingFS.""" user: UserIdentifier role: Role # Request-level view filter for the current user's peers collection. This does # not change tenant/user identity or session ownership. actor_peer_id: Optional[str] = None # Mirrors ResolvedIdentity.from_oauth. Routes that mint OAuth state # (OTP issuance, oauth-verify) reject callers with from_oauth=True to # prevent a stolen access token from laundering itself into a long-lived # refresh-token chain. from_oauth: bool = False # Raw API key from the request — used by Connector to call back into OV # on behalf of the original user api_key: Optional[str] = field(default=None, repr=False) @property def account_id(self) -> str: return self.user.account_id @dataclass class ToolContext: """Tool-level context, containing request context and additional tool-specific information.""" viking_fs: VikingFS request_ctx: RequestContext default_search_uris: List[str] = field(default_factory=list) transaction_handle: Optional[Any] = None read_file_contents: Optional[Any] = None # 用于记录已读取的文件内容 page_id_map: Optional[Any] = None # PageIdMap for annotating read results @property def user(self): return self.request_ctx.user @property def role(self): return self.request_ctx.role @property def account_id(self) -> str: return self.request_ctx.user.account_id