feat: Add encrypted PAT storage for GitHub private-repo installs - #28
Merged
Conversation
Adds Personal Access Token (PAT) management so admins can install Nextcloud apps from private GitHub repositories. PATs are per-admin by default, can optionally be shared with other admins, are validated for least-privilege scope on upload, encrypted at rest via ICrypto, and never returned via the API in plaintext. - New table app_versions_pats (migration Version1000Date20260502120000) - Pat entity + PatMapper with findVisibleTo / findOwnedBy / deleteByOwner - PatManager.useToken() decrypts only inside a callback; plaintext is immediately discarded afterwards. Token never appears in API responses (only first 4 + last 4 chars as `tokenHint`) - PatValidator probes GET https://api.github.com/user: - Classic PAT (ghp_*): rejects any scope outside repo / public_repo by parsing X-OAuth-Scopes - Fine-grained PAT (github_pat_*): GitHub does not expose configured permissions; accepted with explicit `unverifiable_scope` warning - 401 → "Token is invalid or revoked"; 403 → rate limit message - Captures `github-authentication-token-expiration` header into expires_at - PatResolver picks the highest-priority non-expired PAT visible to the current uid that matches the binding's owner/repo (owner-owned > shared, then most-specific glob) - GithubReleaseSource and ExternalReleaseInstallerService now resolve a PAT for the bound owner/repo and attach `Authorization: Bearer <token>` to GitHub API + asset-download + sha256 fetches when a PAT is present. Public-only path (no PAT) unchanged. - PatDeeplinkBuilder generates prefilled GitHub URLs: - Classic: full prefill (scopes=repo, description with NC host) - Fine-grained: page link + structured instructions array - UserDeletedListener removes PATs owned by a deleted Nextcloud user - ApiController gains GET/POST/PATCH/DELETE /api/pats and GET /api/pats/deeplink - 25 new PHPUnit tests for PatValidator (scope detection, expiry parsing, rejection paths, case-insensitive headers), PatManager (encryption roundtrip, hint builder, useToken callback semantics), PatResolver (pattern matching, owner priority, expired PAT skip), and PatDeeplinkBuilder (classic prefill, fine-grained instructions) OpenSpec proposal: openspec/changes/add-github-pat-management/ Browser-verified end-to-end: - Migration runs and creates table (with shortened index names; default-false on bool column omitted to satisfy MigrationService validation) - GET /api/pats returns empty list initially - GET /api/pats/deeplink?kind=classic returns prefilled URL with scopes=repo and a description including the Nextcloud host - GET /api/pats/deeplink?kind=fine-grained returns the page link plus a read-only-permissions instructions array - POST /api/pats with empty fields → 400 "label, targetPattern and token are required" - POST /api/pats with invalid token (ghp_definitelyinvalid...) → 400 "Token is invalid or revoked" (verified the validator hits api.github.com, reads 401, and surfaces a useful message rather than the catch-all) - DELETE /api/pats/{unknown id} → 404 - Proposal 1 path unchanged: openregister GitHub-bound versions list still returns 100 releases, no regression
…github-pat-management
- Promote `pat-management` to canonical spec at openspec/specs/pat-management/spec.md (status: implemented) - Move openspec/changes/add-github-pat-management/ → openspec/changes/archive/2026-05-02-add-github-pat-management/
This was referenced May 2, 2026
Closed
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds Personal Access Token (PAT) management so admins can install Nextcloud apps from private GitHub repositories. Per-admin by default, optional share-with-admins, encrypted at rest, validated for least-privilege scope on upload, plaintext never returned via API.
Stacks on top of the now-merged #25 (external-source install path). Final piece in the original 3-PR series alongside #27.
What's in this PR
lib/Db/Pat.phpentity +PatMapperVersion1000Date20260502120000createsapp_versions_patstable (with shortened index names + bool default omitted to satisfy MigrationService validation)PatManager.useToken()—ICryptoencrypt-on-create + decrypt-only-inside-callback. Plaintext never appears in API responses (only first-4/last-4 char hint)PatValidatorprobesGET https://api.github.com/user:ghp_*): rejects any scope outsiderepo/public_repogithub_pat_*): accepted withunverifiable_scopewarninggithub-authentication-token-expirationheader intoexpires_atPatResolver— owner-priority + glob matching + expiry skipGithubReleaseSourceandExternalReleaseInstallerServiceattachAuthorization: Bearer <token>when a PAT matches the binding'sowner/repoPatDeeplinkBuilder— full prefill for classic; page link + structured instructions for fine-grainedUserDeletedListenersweeps PATs owned by deleted usersApiControllergainsGET/POST/PATCH/DELETE /api/patsandGET /api/pats/deeplinkopenspec/specs/pat-management/spec.md(status: implemented), change folder moved toopenspec/changes/archive/2026-05-02-add-github-pat-management/Browser-verified
GET /api/patsGET /api/pats/deeplink?kind=classicscopes=repoand host descriptionGET /api/pats/deeplink?kind=fine-grainedPOST /api/patsempty fieldsPOST /api/patsinvalid tokenDELETE /api/pats/9999Test plan
vendor/bin/phpunit -c tests/phpunit-unit-only.xml --no-coverage(64 tests)reposcope → accepted