Skip to content

release: Promote external-source installs + PAT + discovery to beta - #30

Merged
rubenvdlinde merged 11 commits into
betafrom
development
May 2, 2026
Merged

release: Promote external-source installs + PAT + discovery to beta#30
rubenvdlinde merged 11 commits into
betafrom
development

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Summary

Promotes the consolidated development work to beta. Combines three already-merged feature PRs:

OpenSpec hygiene already done on each feature PR: 4 canonical specs at openspec/specs/{version-management,external-sources,pat-management,app-discovery}/spec.md (all status: implemented); 0 active changes; 3 archived under openspec/changes/archive/2026-05-02-*/.

Test plan

  • vendor/bin/phpunit -c tests/phpunit-unit-only.xml --no-coverage — 91 tests, 213 assertions
  • Browser-verified throughout dev (allowlist enforcement, GitHub release listing, PAT CRUD with real-vs-bad token, discover endpoint with installedOnly + source filter)

Introduces a parallel install flow for non-App-Store sources (initial driver:
public GitHub releases). The signed App Store path is unchanged; the new path
trades the Nextcloud code-signing chain for: a trusted-source allowlist
(default ConductionNL/*), strict appId/version match against the extracted
appinfo/info.xml, and optional SHA-256 verification when the release publishes
a sibling .sha256 asset.

- New SourceInterface + SourceRegistry + AppStoreSource/GithubReleaseSource
  drivers; per-app source binding persisted as JSON in app config
- TrustedSourceList enforces the allowlist with fnmatch-style globs
- ExternalReleaseInstallerService runs the same maintenance-mode/backup-on-
  failure flow as the signed installer; both share post-extract migrations
  via new InstallFinalizer (so signed and external paths cannot drift on
  upgrade semantics)
- ApiController gains GET /api/sources, GET/POST /api/source/{appId}/binding
  and accepts an optional source override on version-list and install; injects
  IGroupManager/IUserSession/ServerVersion via constructor (was \OC::server)
- 39 PHPUnit unit tests covering source binding, allowlist matching, GitHub
  source HTTP behaviour, source-id parsing; new bootstrap-unit-only.php so
  unit tests run without the full Nextcloud server bootstrap

OpenSpec proposal: openspec/changes/add-external-source-installs/

Browser-verified end-to-end against running Nextcloud:
- /api/admin-check → ok
- /api/sources → returns 2 drivers + ConductionNL/* default allowlist
- POST /api/source/randomapp/bind {owner: randomuser} → 403 with allowlist message
- POST /api/source/openregister/bind {owner: ConductionNL} → 200, binding written
- GET /api/app/openregister/versions → returns real GitHub releases
- GET /api/app/larpingapp/versions (unbound) → falls through to App Store
- ?source=appstore override returns App Store versions without changing binding
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
Adds a single GET /api/discover?q={query}&sources={csv}&installedOnly={bool}
endpoint that aggregates results from registered discovery providers and
returns a uniform result shape the UI can render as cards with one
"Install from {source}" button per source candidate per app.

- DiscoveryProviderInterface + DiscoveryHit + DiscoveryResult value objects
- AppStoreDiscovery: substring filter across name/summary/description/
  categories on the cached App Store catalog (1-hour TTL via app config);
  exact-match scoring ranks exact > prefix > substring
- GithubPrivateDiscovery: enabled when current admin has any PAT visible.
  For each PAT, scopes a GitHub repo search to the PAT's target_pattern
  (ConductionNL/* → org:ConductionNL user:ConductionNL; foo/bar → repo:foo/bar)
  and surfaces matching repos. Repos outside the trusted-source allowlist
  still surface but with installable=false + a clear actionable message.
  Wildcard-only patterns (target_pattern=*) are skipped to avoid leaking
  global searches.
- GithubSearchDiscovery: opt-in via app config flag
  `app_versions.discovery.github_search_enabled`. Disabled by default.
  Public unauthenticated search with topic:nextcloud-app filter.
  Honors trusted-source allowlist same way GithubPrivateDiscovery does.
- DiscoveryAggregator: merges hits by appId, builds sourceCandidates list
  per app, prefers App Store summary/icon when present, annotates with
  installedVersion from IAppManager, sorts installed apps first.
  Provider failures surface in `errors` envelope without breaking the
  whole search.
- ApiController gains GET /api/discover with admin gate + query length
  validation (2-100 chars).
- 27 new PHPUnit tests covering: aggregator dedup, multi-provider merge,
  installedOnly filter, source filter, sort order, error propagation,
  App Store summary preference; AppStore fetch+filter+score+caching;
  GitHub Private scope clause builder, allowlist annotation, wildcard skip,
  PAT iteration; GitHub Search toggle, allowlist annotation, rate-limit
  graceful degradation, malformed JSON.

OpenSpec proposal: openspec/changes/add-app-discovery-search/

Browser-verified end-to-end against running Nextcloud:
- GET /api/discover?q=register → 3 providers reported (appstore enabled,
  github-private disabled because no PATs, github-search disabled because
  flag off); 2 hits including openregister (already installed at
  0.2.13-unstable.80) and registertocontact
- GET /api/discover?q=a → 400 "Query must be at least 2 characters"
- GET /api/discover?q=open&installedOnly=true → 2 results (opencatalogi,
  openregister), both with installedVersion populated
- After `occ config:app:set app_versions discovery.github_search_enabled
  --value=true`, q=opencatalogi merges App Store + GitHub Search into one
  row with sourceCandidates=[(appstore, installable=true),
  (github-search, installable=false)] (the GitHub repo found wasn't in
  ConductionNL/* allowlist, so installable=false with the right reason)
- 91 unit tests pass (39 from PR #25 + 25 from PR #26 + 27 new)

UI work (Vue search bar, source chips, result cards) deliberately out of
scope here — the existing frontend bundle is not built in dev and the UI
overhaul is its own design pass. The new endpoint is consumable by any
client (planned Vue rewrite, admin tooling, openapi.json).

Tracking issue #24 covers public Software Catalogus as a future provider.
- Promote `external-sources` to canonical spec at openspec/specs/external-sources/spec.md (status: implemented)
- Merge ADDED/MODIFIED requirements into openspec/specs/version-management/spec.md (status: idea → implemented): adds Source binding, Explicit source override, and rewrites Fetch Available Versions to query the bound source
- Move openspec/changes/add-external-source-installs/ → openspec/changes/archive/2026-05-02-add-external-source-installs/ per OpenSpec convention
- 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/
- Promote `app-discovery` to canonical spec at openspec/specs/app-discovery/spec.md (status: implemented)
- Move openspec/changes/add-app-discovery-search/ → openspec/changes/archive/2026-05-02-add-app-discovery-search/
…installs

feat: Add external-source install path with trusted-source allowlist
…ement

feat: Add encrypted PAT storage for GitHub private-repo installs
…arch

feat: Add multi-source app-discovery search API
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant