Skip to content

fix(auth): workspace owner can generate token via read_write token (#958) - #1054

Merged
oeway merged 1 commit into
mainfrom
fix/owner-generate-token-readwrite
Aug 19, 2026
Merged

fix(auth): workspace owner can generate token via read_write token (#958)#1054
oeway merged 1 commit into
mainfrom
fix/owner-generate-token-readwrite

Conversation

@oeway

@oeway oeway commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes the remaining half of #958. A workspace owner cannot generate a token
for their own workspace when the token they are acting through only grants
read_write — they hit PermissionError("Only admin can generate token."). In
practice this blocks a normal user from starting their own server-app: apps.start
calls workspace.generate_token, and Svamp's stored/derived token for the user's
own workspace is a child read_write token.

#959 (already merged) fixed the scope-shadowing half — get_permission now
returns the strongest of * and the specific-workspace grant. This PR fixes the
ownership-recognition half so the two together let an owner start their own
server-apps without an admin token.

Root cause

generate_token() only ran its owner-elevation fallback when the resolved
permission was falsy:

maximum_permission = user_info.get_permission(allowed_workspace)
if not maximum_permission:                     # <-- only None triggers the fallback
    workspace_info = await self.load_workspace_info(allowed_workspace)
    if workspace_info.owned_by(user_info):
        maximum_permission = UserPermission.admin
    else:
        raise PermissionError("You do not have any permission ...")
if maximum_permission != UserPermission.admin:
    raise PermissionError("Only admin can generate token.")

An owner acting through a read_write token has maximum_permission == read_write,
skips the fallback entirely, and hits "Only admin can generate token."

There is a second, related gap. A child token minted via generate_token() gets
a fresh random id and carries parent = <original user id>, but owned_by() only
matched on id/email being present in the workspace owners list. A personal
workspace ws-user-<uid> does not necessarily materialise the email in owners, so
owned_by() returned False for the user's own child token even though it is the
same user.

Changes

  1. WorkspaceInfo.owned_by() also recognises a user's personal workspace by
    construction — ws-user-<user.id>, and ws-user-<user.parent> for a child token.
    The parent chain is assigned server-side inside generate_token() and is not
    settable by a caller, so this cannot be forged. This makes ownership recognition
    consistent with how personal workspaces are keyed, independent of what is
    materialised in owners.

  2. generate_token() runs the ownership fallback whenever the resolved
    permission is below admin (not only when it is None). A non-owner with no
    permission at all still gets the original "no permission" error; a non-owner with
    read_write still gets "Only admin can generate token."

Test

Adds test_owner_can_generate_token_with_read_write_token: an owner connects,
mints a read_write token, reconnects with it, and mints a further token for their
own workspace — which raised "Only admin can generate token" before this change.

Notes

This is currently applied in production as a runtime monkeypatch (the amun-ai
platform's hypha-fix958 launcher, "owner-elevation" patch); merging this lets us
drop that patch. No behavior change for non-owners.
🤖 Generated with Claude Code

)

generate_token()'s owner-elevation fallback only ran when the resolved
permission was falsy, so a workspace owner acting through a read_write
token — e.g. a child token minted (and scoped) for their own workspace —
fell through to "Only admin can generate token" and could not start their
own server-apps (apps.start -> ws.generate_token).

Two changes:

- WorkspaceInfo.owned_by() now also recognises a user's personal
  workspace by construction ("ws-user-<uid>", or "ws-user-<parent>" for a
  child token). owned_by() previously matched only on id/email being
  present in the owners list, which a child token (random id, no email in
  owners) fails, even though it is the same user. The parent chain is
  assigned server-side in generate_token(), so it cannot be forged.

- generate_token() runs the ownership fallback whenever the resolved
  permission is below admin (not only when it is None), elevating an owner
  to admin so a read_write owner can mint tokens for their own workspace.
  Non-owners with no permission still get the original error.

Companion to #959 (get_permission strongest-of, already merged): together
they let a normal workspace owner start their own server-apps without an
admin token.

Adds a regression test (owner mints a token through a read_write token).
@github-actions github-actions Bot added the bug Something isn't working label Aug 19, 2026
@oeway
oeway merged commit e817c0a into main Aug 19, 2026
6 checks passed
@oeway
oeway deleted the fix/owner-generate-token-readwrite branch August 19, 2026 04:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant