mirror of
https://github.com/TencentCloud/Octop.git
synced 2026-10-20 06:19:17 +08:00
* feat: add per-user module permissions (RBAC) with admin bypass
Introduce a module-level permission system so non-admin users can be
scoped to a subset of dashboard modules. Adds a `permissions` TEXT column
on the `users` table (migration 006, SQLite + PostgreSQL), a central
permission catalog in `infra/users/permissions.py`, backend ACL checks
across routers, and dashboard gating via `RequirePermission` /
`ForbiddenPage` keyed by nav module. `admin` always bypasses every gate;
read access and chat/agent use are never gated.
* fix: gate connector install-cli by connectors permission (not admin-only)
The host CLI installer is a connectors-module operation and should be
gated by the `connectors` module permission, consistent with the rest of
the connector routes, instead of being admin-only.
The earlier RBAC change demoted this route from `current_admin` but the
test `test_install_cli_forbidden_for_non_admin` still failed because the
`create_user` test helper grants `BASELINE_PERMISSIONS` (which includes
`connectors`) by default, so the "non-admin" user actually held the
permission. Gate the route with `require_permission("connectors")` and
create the test user with `permissions=[]` so it genuinely lacks the
key and is rejected with 403.
---------
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
39 lines
1.1 KiB
Python
39 lines
1.1 KiB
Python
"""Unit tests for require_permission dependency."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from octop.api.deps import require_permission
|
|
from octop.infra.errors import ErrorCode, OctopError
|
|
from octop.infra.users.identity import Role, User
|
|
|
|
|
|
def _user(is_admin: bool, permissions: list[str] | None = None) -> User:
|
|
return User(
|
|
id=1,
|
|
username="u",
|
|
role=Role.ADMIN if is_admin else Role.USER,
|
|
display_name=None,
|
|
permissions=permissions or [],
|
|
)
|
|
|
|
|
|
def test_unknown_key_raises_at_construction() -> None:
|
|
with pytest.raises(RuntimeError):
|
|
require_permission("not_a_real_key")
|
|
|
|
|
|
async def test_dependency_denies_without_permission() -> None:
|
|
dep = require_permission("browser")
|
|
with pytest.raises(OctopError) as exc:
|
|
await dep(_user(False, []))
|
|
assert exc.value.code is ErrorCode.FORBIDDEN
|
|
assert exc.value.details.get("permission") == "browser"
|
|
|
|
|
|
async def test_dependency_allows_admin_and_holder() -> None:
|
|
dep = require_permission("browser")
|
|
assert await dep(_user(True)) is not None
|
|
assert await dep(_user(False, ["browser"])) is not None
|