mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:11:59 +00:00
访问密钥收尾:服务账号进授权不进岗位的测试、关停时写请求记录的测试,几处整理
- 补测试:应用授权收服务账号、岗位绑定的检查把它算成错;改派给服务账号 422;调度循环停下时把攒着的请求记录写进库 (去掉对应的代码回跑,测试抓得住) - 门禁里只给拥有者的运行统一按 auth/access.PRIVATE_ORIGINS 认,不再各写一份字面量 - 幂等的 once 只把普通异常记成被拒:请求被取消时记录停在处理中,过了时限同一个键能重新认领(同钩子) - 新建密钥的弹窗等服务账号取回来再说「还不能建」,不先闪一下
This commit is contained in:
@@ -72,6 +72,7 @@ async def _for_key(principal: Any, items: list[dict[str, Any]]) -> list[dict[str
|
||||
return items
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.auth.access import PRIVATE_ORIGINS
|
||||
from app.db.base import SessionLocal
|
||||
from app.db.models import App, Run
|
||||
|
||||
@@ -81,7 +82,7 @@ async def _for_key(principal: Any, items: list[dict[str, Any]]) -> list[dict[str
|
||||
personal = set((await session.execute(select(App.id).where(App.id.in_(app_ids or {""}),
|
||||
App.scope == "personal"))).scalars())
|
||||
private = set((await session.execute(select(Run.id).where(
|
||||
Run.id.in_(run_ids or {""}), Run.origin_kind.in_(("personal_app", "trial"))))).scalars())
|
||||
Run.id.in_(run_ids or {""}), Run.origin_kind.in_(PRIVATE_ORIGINS)))).scalars())
|
||||
return [i for i in items if i.get("app_id") not in personal and i.get("run_id") not in private
|
||||
and (principal.app_ids is None or i.get("app_id") in principal.app_ids)]
|
||||
|
||||
|
||||
@@ -397,8 +397,10 @@ async def _check_task(session: Any, principal: Any, task_id: str) -> None:
|
||||
elif principal.app_ids is not None:
|
||||
# 不属于应用的待办(画布、定时任务里的审批):限定了应用的密钥碰不到
|
||||
raise _forbidden(APP_FORBIDDEN_TEXT, KEY_APP_FORBIDDEN)
|
||||
from app.auth.access import PRIVATE_ORIGINS
|
||||
|
||||
run = await session.get(Run, task.run_id) if task.run_id else None
|
||||
if run is not None and (getattr(run, "origin_kind", None) or "") in ("personal_app", "trial"):
|
||||
if run is not None and (getattr(run, "origin_kind", None) or "") in PRIVATE_ORIGINS:
|
||||
raise _forbidden(APP_PERSONAL_TEXT, KEY_APP_PERSONAL)
|
||||
|
||||
|
||||
@@ -519,7 +521,9 @@ async def once(principal: Any, idem_key: str | None, *, route: str, request: Any
|
||||
return await replay(previous)
|
||||
try:
|
||||
out = await run(reclaimed)
|
||||
except BaseException as e:
|
||||
except Exception as e:
|
||||
# 被拒(表单不对、发起不了):记成 rejected,同一个键可以重试。请求被取消(客户端断开)不在这里收:记录停在
|
||||
# processing,过了 STALE_S 同一个键能重新认领(同钩子)
|
||||
await book.finish(owner, key, status="rejected", detail=(str(e) or type(e).__name__)[:500])
|
||||
raise
|
||||
note_run(out.get("run_id"))
|
||||
|
||||
@@ -112,13 +112,12 @@ class AuthMiddleware:
|
||||
await self.app(scope, receive, send)
|
||||
return
|
||||
from app.api.deps import reset_principal, set_principal
|
||||
from app.auth import current_mode, refusal
|
||||
from app.auth import current_mode, keys, refusal
|
||||
|
||||
# 登录设置在启动时读进缓存(main.py 的 _platform_startup),改设置的接口改完就刷新;这里不查库
|
||||
mode = current_mode()
|
||||
headers = _headers(scope)
|
||||
from app.auth import keys
|
||||
|
||||
# 带访问密钥的请求另走一条路:只按密钥认,不看 cookie、不查 CSRF(见模块说明)
|
||||
if (secret := keys.bearer(headers)) is not None:
|
||||
await self._with_key(scope, receive, send, secret, mode)
|
||||
return
|
||||
|
||||
@@ -287,6 +287,9 @@ async def test_service_accounts_are_never_assigned_tasks(world):
|
||||
_, secret = await auth_env.make_key(svc, scopes=("tasks.read", "tasks.respond"))
|
||||
r = await auth_env.keyed(secret).post(f"/api/tasks/{task.id}/respond", json={"decision": "approve"})
|
||||
assert r.json()["code"] == "key_scope_missing", "服务账号的密钥没有回复待办这一项"
|
||||
admin = await auth_env.login("root")
|
||||
r = await admin.post(f"/api/tasks/{task.id}/reassign", json={"to": [{"kind": "user", "name": "svc-portal"}]})
|
||||
assert r.status_code == 422 and r.json()["code"] == "reassign_service", "改派给服务账号拒收"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 封顶在使用者
|
||||
|
||||
@@ -339,3 +339,34 @@ async def test_service_accounts_are_marked_in_the_directory_and_kept_out_of_assi
|
||||
|
||||
assert set(await _members(s, "ops")) == {"ana"}, "候选池里不列服务账号"
|
||||
assert "svc-bi" not in (await review_pool(s)).users
|
||||
|
||||
|
||||
async def test_grants_take_service_accounts_but_slot_bindings_do_not():
|
||||
"""服务账号能拿应用授权(外部系统按授权读应用),岗位绑定里算错:派给它的待办没人能处理。"""
|
||||
from app.api.apps import grants_problem
|
||||
from app.apps.lint import _Issues, _lint_slot_principals
|
||||
|
||||
await auth_env.turn_on_local()
|
||||
await auth_env.make_service("svc-bi")
|
||||
async with SessionLocal() as s:
|
||||
wanted, problem = await grants_problem(s, [("user", "svc-bi", "observe")])
|
||||
assert problem is None and wanted == [("user", "svc-bi", "observe")]
|
||||
out = _Issues()
|
||||
await _lint_slot_principals(s, {"approver": {"label": "审批人"}},
|
||||
{"approver": [{"kind": "user", "name": "svc-bi"}]}, out)
|
||||
assert [(i["level"], i["code"], i["problem"]) for i in out.items] == [
|
||||
("error", "app.slot_principal_unknown", "service")]
|
||||
assert "服务账号" in out.items[0]["message"]
|
||||
|
||||
|
||||
async def test_scheduler_stop_flushes_the_request_buffer():
|
||||
"""关停时还攒在进程里的请求记录写进库(没起调度循环时也写)。"""
|
||||
from app.scheduler.loop import runner
|
||||
|
||||
await auth_env.turn_on_local()
|
||||
svc = await auth_env.make_service()
|
||||
row, secret = await auth_env.make_key(svc)
|
||||
await auth_env.keyed(secret).get("/api/auth/me")
|
||||
await runner.stop()
|
||||
async with SessionLocal() as s:
|
||||
assert [r.key_id for r in (await s.execute(select(AccessKeyRequest))).scalars()] == [row.id]
|
||||
|
||||
@@ -315,7 +315,8 @@ function KeyCreator({ settings, onClose, onIssued }: {
|
||||
onClose: () => void
|
||||
onIssued: (k: AccessKeyIssued) => void
|
||||
}) {
|
||||
const [services, setServices] = useState<ServiceAccountRow[]>([])
|
||||
// null:还在取(管理员才取);取到之后才判「还不能建」
|
||||
const [services, setServices] = useState<ServiceAccountRow[] | null>(settings.admin ? null : [])
|
||||
const [apps, setApps] = useState<AppSummary[] | null>(null)
|
||||
// 拥有者:'' 是我自己(个人访问密钥),否则是服务账号 id
|
||||
const [owner, setOwner] = useState<string | null>(settings.can_create_personal ? '' : null)
|
||||
@@ -364,13 +365,13 @@ function KeyCreator({ settings, onClose, onIssued }: {
|
||||
</button>
|
||||
</>}>
|
||||
<div className="space-y-3" data-access-key-form="">
|
||||
{owner === null && !services.length && <Notice tone="warn">{T.noOwner}</Notice>}
|
||||
{owner === null && services !== null && !services.length && <Notice tone="warn">{T.noOwner}</Notice>}
|
||||
{settings.admin && (
|
||||
<Field label={T.owner}>
|
||||
{(p) => (
|
||||
<select {...p} className="field" value={owner ?? ''} onChange={(e) => setOwner(e.target.value)} data-access-key-owner="">
|
||||
{settings.can_create_personal && <option value="">{T.ownerMe}</option>}
|
||||
{services.map((s) => <option key={s.id} value={s.id}>{T.ownerService(s.username)}</option>)}
|
||||
{(services ?? []).map((s) => <option key={s.id} value={s.id}>{T.ownerService(s.username)}</option>)}
|
||||
</select>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
Reference in New Issue
Block a user