gabriel / musehub public
patch security/131-wire-write-authz #1 / 1
AI Agent gabriel · 3 days ago · Sep 9, 2026 · Diff

security: close wire-protocol write-authorization gap (#131)

push/unpack-mpack, push/mpack-presign, releases, tags, and version-tags all depended only on require_valid_token -- proof of *some* valid signed identity -- with no check that the signer had write access to the specific repo being written to. Any registered identity could push to any repo, public or private, regardless of collaborator status.

Add _assert_writable (musehub/api/routes/wire.py), mirroring the existing owner-or-write/admin-collaborator check already used correctly by repair-object/snapshot/commit and by issues.py/proposals.py's _guard_repo_owner. Unlike _assert_readable, visibility never widens write access -- a public repo still requires owner or write/admin collaborator status to push directly.

Regression suite (tests/test_authz_wire_push.py) proves the gap first (AUTHZ_01/02 from the issue) then confirms the fix: owner and accepted write/admin collaborators can still push; non-collaborators, read-only collaborators, and pending (not yet accepted) collaborators are rejected with 403 on every affected endpoint.

sha256:fcce6888011c181054887be7e797ccf323705c409aeb4030c1a376d557146cc7 sha
+61 ~6 symbols
sha256:ade791fdb5857503bb1f57e69a79713a7bd24f89b53321145bd795a00f60c0c9 snapshot
+61
symbols added
~6
symbols modified
0
dead code introduced
Semantic Changes 67 symbols
+ docs/
+ src/
+ tests/
+ tools/
~ tests/test_authz_wire_push.py .py 60 symbols added
+ _ATTACKER variable variable _ATTACKER L52–52
+ _ATTACKER_CLAIMS variable variable _ATTACKER_CLAIMS L71–71
+ _ATTACKER_IDENTITY_ID variable variable _ATTACKER_IDENTITY_ID L53–53
+ _HEAD variable variable _HEAD L63–63
+ _MPACK_BYTES variable variable _MPACK_BYTES L61–61
+ _MPACK_KEY variable variable _MPACK_KEY L62–62
+ _OWNER variable variable _OWNER L50–50
+ _OWNER_CLAIMS variable variable _OWNER_CLAIMS L70–70
+ _OWNER_IDENTITY_ID variable variable _OWNER_IDENTITY_ID L51–51
+ _PENDING_COLLAB variable variable _PENDING_COLLAB L58–58
+ _PENDING_COLLAB_CLAIMS variable variable _PENDING_COLLAB_CLAIMS L74–74
+ _PENDING_COLLAB_IDENTITY_ID variable variable _PENDING_COLLAB_IDENTITY_ID L59–59
+ _READ_COLLAB variable variable _READ_COLLAB L56–56
+ _READ_COLLAB_CLAIMS variable variable _READ_COLLAB_CLAIMS L73–73
+ _READ_COLLAB_IDENTITY_ID variable variable _READ_COLLAB_IDENTITY_ID L57–57
+ _WRITE_COLLAB variable variable _WRITE_COLLAB L54–54
+ _WRITE_COLLAB_CLAIMS variable variable _WRITE_COLLAB_CLAIMS L72–72
+ _WRITE_COLLAB_IDENTITY_ID variable variable _WRITE_COLLAB_IDENTITY_ID L55–55
+ _add_collaborator function async_function _add_collaborator L108–128
+ _claims function function _claims L66–67
+ _make_client function async_function _make_client L86–92
+ _make_repo function async_function _make_repo L95–105
+ _presign_body function function _presign_body L152–153
+ _unpack_body function function _unpack_body L145–149
+ db function async_function db L82–83
+ ASGITransport import import ASGITransport L30–30
+ AsyncClient import import AsyncClient L30–30
+ AsyncSession import import AsyncSession L31–31
+ MSignContext import import MSignContext L36–36
+ MusehubCollaborator import import MusehubCollaborator L38–38
+ MusehubRepo import import MusehubRepo L39–39
+ annotations import import annotations L21–21
+ app import import app L41–41
+ blob_id import import blob_id L34–34
+ build_wire_mpack import import build_wire_mpack L33–33
+ compute_collaborator_id import import compute_collaborator_id L37–37
+ compute_identity_id import import compute_identity_id L37–37
+ create_repo import import create_repo L42–42
+ datetime import import datetime L25–25
+ fake_id import import fake_id L34–34
+ get_db import import get_db L40–40
+ logging import import logging L23–23
+ msgpack import import msgpack L27–27
+ pytest import import pytest L28–28
+ pytest_asyncio import import pytest_asyncio L29–29
+ require_valid_token import import require_valid_token L35–35
+ timezone import import timezone L25–25
+ logger variable variable logger L48–48
+ mock_backend function async_function mock_backend L132–142
+ test_accepted_write_collaborator_can_push function async_function test_accepted_write_collaborator_can_push L212–226
+ test_authz01_private_repo_non_collaborator_push_rejected function async_function test_authz01_private_repo_non_collaborator_push_rejected L161–171
+ test_authz02_public_repo_non_collaborator_push_rejected function async_function test_authz02_public_repo_non_collaborator_push_rejected L179–190
+ test_create_release_rejects_non_collaborator function async_function test_create_release_rejects_non_collaborator L285–299
+ test_owner_can_push function async_function test_owner_can_push L198–208
+ test_pending_write_collaborator_cannot_push function async_function test_pending_write_collaborator_cannot_push L248–263
+ test_presign_rejects_non_collaborator function async_function test_presign_rejects_non_collaborator L271–281
+ test_push_tags_rejects_non_collaborator function async_function test_push_tags_rejects_non_collaborator L303–317
+ test_push_version_tags_rejects_non_collaborator function async_function test_push_version_tags_rejects_non_collaborator L321–335
+ test_read_only_collaborator_cannot_push function async_function test_read_only_collaborator_cannot_push L230–244
+ utc_now_iso function function utc_now_iso L45–46
~ musehub/api/routes/wire.py .py 1 symbol added, 6 symbols modified
← Older Oldest on security/131-wire-write-authz
All commits
Newer → Latest on security/131-wire-write-authz

0 comments

No comments yet. Be the first to start the discussion.

To add a comment, use the Muse CLI: muse hub commit comment sha256:fcce6888011c181054887be7e797ccf323705c409aeb4030c1a376d557146cc7 --body "your comment"