Gitea's PUT/DELETE /user/starred/{owner}/{repo} require write:repository
token scope (verified against Gitea's route registration source), but
this app's OAuthClientConfig only ever requested read-only scopes,
enforced by a hard require() -- every star/unstar attempt was guaranteed
to fail server-side (403), surfaced as the reported red 'failed' star.
Adds write:repository as one narrow, explicit exception to the read-only
invariant (every other write scope, including the pre-existing unused
write:user, is still rejected). Requires one manual re-login on already
logged-in devices before it takes effect, since a refresh token carries
forward the scope it was originally granted under.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
104 lines
6.2 KiB
Markdown
104 lines
6.2 KiB
Markdown
# TASK-033: Request write:repository OAuth scope so starring actually works
|
|
|
|
Status: implemented; all required checks pass; live-installed, requires a one-time re-login on the device to take effect
|
|
Owner: Claude
|
|
Reviewer: Codex
|
|
|
|
## Outcome
|
|
|
|
Every attempt to star/unstar a repository was guaranteed to fail server-side, because Gitea's
|
|
`PUT`/`DELETE /user/starred/{owner}/{repo}` endpoints require `write:repository` OAuth scope
|
|
(verified against Gitea's own route registration source) while this app's `OAuthClientConfig` only
|
|
ever requested read-only scopes, enforced by a hard `require()` invariant. Live-reported as: the
|
|
new favorite star turns red (this app's failed-action color) but never actually favorites
|
|
anything. The human product owner was given three options — add the write scope, make favorites a
|
|
local-only bookmark, or remove the feature — and explicitly chose to add the scope. This task adds
|
|
`write:repository` as one narrow, explicit exception to that read-only invariant.
|
|
|
|
## Scope
|
|
|
|
- Included: `core/auth/protocol/OAuthClientConfig.kt` (new `WRITE_REPOSITORY` scope value, the
|
|
read-only `require()` narrowed to allow specifically that one scope, `UBGiteaOAuthDefaults.config`
|
|
now requests it), matching test additions in `OAuthClientConfigTest.kt`.
|
|
- Excluded: No change to `toggleFavorite`, `FavoriteStar`, or any other favorite/star UI or
|
|
ViewModel logic from TASK-018/TASK-030 — those were already correctly built against the
|
|
`RepositoryFavorites` interface; the only thing wrong was the token they were being handed never
|
|
had permission to actually do it. No change to any other OAuth scope (`WRITE_USER` remains
|
|
rejected, confirmed by a new test asserting it's still rejected even alongside the now-allowed
|
|
`WRITE_REPOSITORY`).
|
|
|
|
## Design notes
|
|
|
|
- **A narrow exception, not a blanket relaxation**: the original `require(scopes.all { it.isReadOnly })`
|
|
invariant existed to keep this app's OAuth footprint deliberately minimal — this task doesn't
|
|
remove that principle, it carves out exactly the one scope this app's own already-built favorite
|
|
feature actually needs, and only that one. `require(scopes.all { it.isReadOnly || it == OAuthScope.WRITE_REPOSITORY })`
|
|
still rejects every other write scope (including the pre-existing, still-unused `WRITE_USER`
|
|
value) — verified by a new test that pairs `WRITE_REPOSITORY` with `WRITE_USER` together and
|
|
confirms that combination is still rejected, so a future accidental addition of some other write
|
|
scope doesn't silently slip through just because one exception now exists.
|
|
- **Verified against Gitea's actual source, not assumed**: Gitea's route registration
|
|
(`routers/api/v1/api.go`) puts the whole `/starred/{owner}/{repo}` group under
|
|
`tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)`, with the PUT/DELETE methods
|
|
specifically requiring Write-level access within that category — `read:repository` alone is
|
|
insufficient no matter what, this isn't a bug that could have been fixed any other way from the
|
|
client side.
|
|
- **A one-time re-login is required and expected, not a regression of TASK-029**: TASK-029's
|
|
proactive refresh-token flow keeps a session alive indefinitely *for the scope it was originally
|
|
granted* — a refresh token from before this change carries the old (read-only) scope forward
|
|
automatically, and Gitea's refresh flow doesn't expand it. The device's current session therefore
|
|
needs one explicit new login (which will show the OAuth consent screen once more, listing the new
|
|
`write:repository` permission) before starring will actually start working; every login after
|
|
that continues to benefit from TASK-029's no-re-consent behavior exactly as before. This is
|
|
reported directly to the human product owner rather than silently worked around (e.g. by
|
|
force-clearing the stored session), since forcing a logout is a disruptive, irreversible-feeling
|
|
action that should be their call, not an automatic side effect of this change.
|
|
|
|
## Acceptance criteria
|
|
|
|
- [x] `write:repository` is requested alongside the app's existing read-only scopes.
|
|
- [x] Any other write scope (e.g. `write:user`) is still rejected by `OAuthClientConfig`'s
|
|
invariant, even when paired with the now-allowed `write:repository`.
|
|
- [x] Build, unit tests, and lint pass.
|
|
- [ ] Live confirmation, after a fresh login on the device, that starring/unstarring a repository
|
|
now actually succeeds (star turns filled/primary-colored, not red; the repository shows up
|
|
in/disappears from the FAVORITES list) — requires the human product owner to log out and back
|
|
in first, not yet done in this session.
|
|
|
|
## Required checks
|
|
|
|
```text
|
|
./gradlew compileDebugKotlin compileDebugUnitTestKotlin
|
|
./gradlew testDebugUnitTest
|
|
./gradlew assembleDebug lintDebug
|
|
```
|
|
|
|
## Evidence
|
|
|
|
- `OAuthClientConfig.kt`: new `OAuthScope.WRITE_REPOSITORY("write:repository", false)`; the
|
|
read-only `require()` narrowed with a source-cited comment; `UBGiteaOAuthDefaults.config`'s
|
|
scope set now includes it.
|
|
- `OAuthClientConfigTest.kt`: two new tests — `write:repository` alone (with a read scope) is now
|
|
accepted; `write:user` is still rejected even when `write:repository` is also present in the same
|
|
scope set.
|
|
- `./gradlew compileDebugKotlin compileDebugUnitTestKotlin`: BUILD SUCCESSFUL.
|
|
- `./gradlew testDebugUnitTest`: BUILD SUCCESSFUL, all unit tests passing (including the two new
|
|
ones above; the pre-existing `rejectsWriteScopeInTheProductionOAuthContract` test, which uses
|
|
`WRITE_USER` specifically, still passes unchanged).
|
|
- `./gradlew assembleDebug lintDebug`: BUILD SUCCESSFUL, 0 lint errors.
|
|
- Installed on the human product owner's device over wireless `adb` (`adb install -r`, streamed
|
|
install succeeded); the device's current session still carries the old, read-only-only scope
|
|
until the human product owner logs out and back in.
|
|
|
|
## Known limitations
|
|
|
|
- Requires a manual one-time re-login on the device before starring actually works — see Design
|
|
notes. Not yet done in this session.
|
|
- No change was made to retroactively reflect a repository's *already*-starred-on-the-server state
|
|
when the ALL list first loads (the pre-existing, unrelated `favoriteAction` limitation noted in
|
|
TASK-030 — still applies, unaffected by this task).
|
|
|
|
## Review findings
|
|
|
|
None yet — pending Codex/human review.
|