Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions roar/publish_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@ class PublishAuthContext:
ssh_auth_available: bool = False


def _scope_visibility(repo_scope: Any) -> str | None:
"""Resolve the visibility for a scope_request from the active repo scope.

``public`` -> ``"public"`` (attributed); ``anonymous`` -> ``None`` so the
caller omits ``scope_request`` and the server uses the legacy anonymous
public scope; everything else (``private``/``project``/unset) -> ``"private"``.
"""
if repo_scope is not None:
if repo_scope.mode == "public":
return "public"
if repo_scope.mode == "anonymous":
return None
return "private"


def load_publish_auth_context(
start_dir: str | Path | None = None,
*,
Expand Down Expand Up @@ -95,17 +110,22 @@ def load_publish_auth_context(
scope_request = {
"owner_id": binding["owner_id"],
"owner_type": binding["owner_type"],
"visibility": "private",
"visibility": _scope_visibility(repo_scope) or "private",
}
project_id = binding.get("project_id")
if project_id:
scope_request["project_id"] = project_id
elif not allow_public_without_binding:
scope_request = {
"owner_resolution": "current_user",
"owner_type": "user",
"visibility": "private",
}
# Server-authoritative owner resolution; visibility follows the active
# repo scope. The anonymous scope yields None here so we omit
# scope_request entirely (server uses the legacy anonymous public scope).
visibility = _scope_visibility(repo_scope)
if visibility is not None:
scope_request = {
"owner_resolution": "current_user",
"owner_type": "user",
"visibility": visibility,
}

return PublishAuthContext(
access_token=access_token,
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/test_publish_auth_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,38 @@ def test_private_publish_without_binding_uses_current_user_scope_request(
}


def test_public_scope_uses_current_user_public_scope_request(tmp_path: Path) -> None:
config_dir = tmp_path / ".roar"
config_dir.mkdir(parents=True)
(config_dir / "config.toml").write_text('[scope]\nmode = "public"\n', encoding="utf-8")

with (
patch("roar.publish_auth.load_auth_state", return_value=_auth_state()),
patch("roar.publish_auth._has_ssh_auth_credentials", return_value=False),
):
context = load_publish_auth_context(start_dir=tmp_path, allow_public_without_binding=False)

assert context.scope_request == {
"owner_resolution": "current_user",
"owner_type": "user",
"visibility": "public",
}


def test_anonymous_scope_omits_scope_request(tmp_path: Path) -> None:
config_dir = tmp_path / ".roar"
config_dir.mkdir(parents=True)
(config_dir / "config.toml").write_text('[scope]\nmode = "anonymous"\n', encoding="utf-8")

with (
patch("roar.publish_auth.load_auth_state", return_value=_auth_state()),
patch("roar.publish_auth._has_ssh_auth_credentials", return_value=False),
):
context = load_publish_auth_context(start_dir=tmp_path, allow_public_without_binding=False)

assert context.scope_request is None


def test_project_bound_private_publish_keeps_project_scope_request(tmp_path: Path) -> None:
config_dir = tmp_path / ".roar"
config_dir.mkdir(parents=True)
Expand Down
Loading