Skip to content

feat(backend): report repository visibility in the root listing - #14

Merged
TheMeinerLP merged 1 commit into
mainfrom
feat/repository-visibility-in-listing
Aug 9, 2026
Merged

feat(backend): report repository visibility in the root listing#14
TheMeinerLP merged 1 commit into
mainfrom
feat/repository-visibility-in-listing

Conversation

@TheMeinerLP

Copy link
Copy Markdown

Description

The dashboard overview lists repositories as rows carrying nothing but a name, so it cannot show whether a repository is public, hidden or private. The value exists on Repository at runtime but reached no endpoint.

Entries of the root listing (GET /api/maven/details, the variant without a {repository} path parameter) are now a new RepositoryDirectoryInfo and carry a visibility field.

Before:

{
  "name": "/",
  "files": [
    { "name": "releases", "type": "DIRECTORY" },
    { "name": "snapshots", "type": "DIRECTORY" }
  ],
  "type": "DIRECTORY"
}

After:

{
  "name": "/",
  "files": [
    { "name": "releases", "visibility": "PUBLIC", "type": "DIRECTORY" },
    { "name": "snapshots", "visibility": "PUBLIC", "type": "DIRECTORY" }
  ],
  "type": "DIRECTORY"
}

Design notes

  • SimpleDirectoryInfo is used for every directory listing, not just the repository root, and it is a public type in com.reposilite.storage.api that plugins construct. It is left untouched, so no folder inside a repository grows a meaningless visibility.
  • The new type lives in com.reposilite.storage.api because direct subclasses of the sealed FileDetails have to be declared in the same package and module. That is the reason a repository concept sits next to the storage ones, and the class carries a comment saying so.
  • The payload is additive. name and type are untouched, nothing was renamed, removed or retyped, and a client that ignores the new field keeps working. MavenFacade.findRepositories keeps its DirectoryInfo return type, so plugins compile unchanged.
  • The root route had no OpenAPI entry at all. It got one, documenting the new field, and the snapshot of the generated schema in reposilite-site/data/guides/developers/endpoints.md was extended to match.

Security

Emitting the visibility leaks nothing, and this was checked against RepositorySecurityProvider rather than assumed:

  • canAccessRepository returns true for PUBLIC, and for HIDDEN and PRIVATE only when accessTokenFacade.canSee(token, "/<repository>") holds. An absent token fails both.
  • RepositoryService.getRootDirectory applies that filter before mapping to the response, so a repository the caller cannot access never becomes an entry in the first place. It is the only path that builds this listing.
  • Therefore an anonymous caller receives public repositories only, and learning that those are public tells it nothing it did not already know. A token learns the visibility of exactly the repositories it may already see and browse.

A regression test asserts the negative case as well: a token scoped to /releases sees neither the hidden nor the private repository.

Type of change

  • feat - new feature

How was this tested?

  • ./gradlew build passes, including :reposilite-backend:test and :reposilite-backend:integration.
  • MavenFacadeTest.should describe the visibility of every listed repository: an anonymous caller gets public repositories only, each with PUBLIC; a manager token additionally gets the hidden and private ones with their own value.
  • RepositoryVisibilityIntegrationTest (new, boots a real instance and reads the actual JSON):
    • anonymous and manager listings carry the expected visibility per repository, with a hidden repository added through the shared configuration,
    • the directory shape a previous client reads (name, type, files) is unchanged,
    • a token scoped to an unrelated repository sees neither the hidden nor the private one.

Checklist

  • Commit messages follow the Conventional Commits convention documented in CLAUDE.md
  • Tests cover the change
  • ./gradlew build passes locally
  • Documentation and configuration samples are updated if the change is user-facing
  • Existing copyright headers are preserved; new files carry the appropriate header
  • No credentials, tokens or personal data are included in the diff

Out of scope, found on the way

StorageProvider.usage() was confirmed broken while reading this code, and it deserves its own issue rather than a fix here. FileSystemStorageProvider.usage() is getFileSize(Location.empty()), and getFileSize returns -1 for a directory, so it always reports -1. FixedQuota.canHold then computes maxSize - (-1) and PercentageQuota.canHold computes max - (-1), meaning neither quota ever accounts for what is already stored: they only reject a single upload larger than the whole limit. S3StorageProvider.usage() returns ok(-1) too, but it overrides canHold with Long.MAX_VALUE, so S3 has no quota at all.

The dashboard overview lists repositories with nothing but a name, so it
cannot tell a public repository from a hidden or private one. The value
is known at runtime but never left the backend.

Entries of the root listing are now RepositoryDirectoryInfo and carry a
visibility field. Every other directory keeps using SimpleDirectoryInfo,
which has no visibility to report. The field is additive: name and type
are unchanged, so clients written against the previous payload keep
working.

Nothing is leaked. Repositories the caller cannot access are dropped by
RepositorySecurityProvider.canAccessRepository before the response is
assembled, so an anonymous caller only ever learns that public
repositories are public.

Signed-off-by: TheMeinerLP <github@themeinerlp.dev>
@TheMeinerLP
TheMeinerLP force-pushed the feat/repository-visibility-in-listing branch from ec12904 to 32bda6f Compare August 8, 2026 21:56
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

Test results

 69 files   69 suites   8m 48s ⏱️
329 tests 328 ✅ 1 💤 0 ❌
331 runs  330 ✅ 1 💤 0 ❌

Results for commit 32bda6f.

@TheMeinerLP
TheMeinerLP merged commit 3d9d067 into main Aug 9, 2026
8 checks passed
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