+18


![dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)

![autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)




FFXN
GitHub
yyh
盐粒 Yanli
autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Tianle
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Yunlu Wen
zyssyz123
Claude Opus 4.7
chariri
Asuka Minato
Copilot Autofix powered by AI
Nian
非法操作
Carmen Fernández Ruiz
wangxiaolei
QuantumGhost
L1nSn0w
Evan
Escape0707
Jingyi
Amr Sherif
ZHOU ZHICHEN
unknown
JzoNg
Xiyuan Chen
-LAN-
107bba0116
Signed-off-by: dependabot[bot] <[email protected]> Signed-off-by: EvanYao826 <[email protected]> Co-authored-by: yyh <[email protected]> Co-authored-by: 盐粒 Yanli <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Tianle <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Yunlu Wen <[email protected]> Co-authored-by: zyssyz123 <[email protected]> Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]> Co-authored-by: chariri <[email protected]> Co-authored-by: Asuka Minato <[email protected]> Co-authored-by: Copilot Autofix powered by AI <[email protected]> Co-authored-by: Nian <[email protected]> Co-authored-by: 非法操作 <[email protected]> Co-authored-by: Carmen Fernández Ruiz <[email protected]> Co-authored-by: wangxiaolei <[email protected]> Co-authored-by: QuantumGhost <[email protected]> Co-authored-by: L1nSn0w <[email protected]> Co-authored-by: Evan <[email protected]> Co-authored-by: Escape0707 <[email protected]> Co-authored-by: Jingyi <[email protected]> Co-authored-by: Amr Sherif <[email protected]> Co-authored-by: ZHOU ZHICHEN <[email protected]> Co-authored-by: unknown <[email protected]> Co-authored-by: JzoNg <[email protected]> Co-authored-by: Xiyuan Chen <[email protected]> Co-authored-by: -LAN- <[email protected]>
78 lines
2.6 KiB
Python
78 lines
2.6 KiB
Python
"""Workspace role gate.
|
|
|
|
Layered on top of `validate_bearer` + `accept_subjects(SubjectType.ACCOUNT)`
|
|
for routes whose access depends on the caller's `TenantAccountJoin.role`
|
|
in the workspace named by the `workspace_id` path parameter.
|
|
|
|
Usage::
|
|
|
|
@openapi_ns.route("/workspaces/<string:workspace_id>/members")
|
|
class Members(Resource):
|
|
@validate_bearer(accept=ACCEPT_USER_ANY)
|
|
@accept_subjects(SubjectType.ACCOUNT)
|
|
@require_workspace_role() # any member
|
|
def get(self, workspace_id: str): ...
|
|
|
|
@validate_bearer(accept=ACCEPT_USER_ANY)
|
|
@accept_subjects(SubjectType.ACCOUNT)
|
|
@require_workspace_role(TenantAccountRole.OWNER, TenantAccountRole.ADMIN)
|
|
def post(self, workspace_id: str): ...
|
|
|
|
Non-member callers get 404 (matching `GET /openapi/v1/workspaces/<id>`)
|
|
so workspace IDs do not leak across tenants. A member without one of the
|
|
allowed roles gets 403.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
from functools import wraps
|
|
from typing import TypeVar
|
|
|
|
from werkzeug.exceptions import Forbidden, NotFound
|
|
|
|
from extensions.ext_database import db
|
|
from libs.oauth_bearer import try_get_auth_ctx
|
|
from models.account import TenantAccountRole
|
|
from services.account_service import TenantService
|
|
|
|
F = TypeVar("F", bound=Callable[..., object])
|
|
|
|
|
|
def require_workspace_role(*allowed_roles: TenantAccountRole) -> Callable[[F], F]:
|
|
"""Gate a route on the caller's role in ``workspace_id``.
|
|
|
|
Pass no roles to require only membership. Pass one or more roles to
|
|
require the caller's role be in that set.
|
|
"""
|
|
|
|
allowed = frozenset(allowed_roles)
|
|
|
|
def deco(fn: F) -> F:
|
|
@wraps(fn)
|
|
def wrapper(*args: object, **kwargs: object) -> object:
|
|
ctx = try_get_auth_ctx()
|
|
if ctx is None or ctx.account_id is None:
|
|
raise RuntimeError(
|
|
"require_workspace_role called without account-bearer context; "
|
|
"stack validate_bearer + accept_subjects(SubjectType.ACCOUNT) above it"
|
|
)
|
|
|
|
workspace_id = kwargs.get("workspace_id")
|
|
if not workspace_id:
|
|
raise RuntimeError("require_workspace_role expects a 'workspace_id' route parameter")
|
|
|
|
role = TenantService.get_account_role_in_tenant(db.session, str(ctx.account_id), str(workspace_id))
|
|
|
|
if role is None:
|
|
raise NotFound("workspace not found")
|
|
|
|
if allowed and role not in allowed:
|
|
raise Forbidden("insufficient workspace role")
|
|
|
|
return fn(*args, **kwargs)
|
|
|
|
return wrapper # type: ignore[return-value]
|
|
|
|
return deco
|