theonedev/onedev · error · UnauthorizedException
This api can only be accessed via cluster credential
Error message
This api can only be accessed via cluster credential
What it means
ClusterResource.downloadSiteFiles is an internal cluster-only API that streams files from the server's site directory matching a pattern set. It rejects any caller not authenticated as the system principal via the cluster credential, throwing UnauthorizedException "This api can only be accessed via cluster credential".
Solutions
- Use the cluster credential (server secret) authentication for this endpoint — it is meant for inter-server cluster communication only.
- Verify both member servers share the same cluster secret/token so SecurityUtils.isSystem() evaluates true.
- Do not call this endpoint from user code; use the OneDev UI or public APIs to access site files.
- Check reverse proxy / auth middleware is not stripping the cluster auth header before it reaches OneDev.
Example fix
// before curl -H "Authorization: Bearer <personal-access-token>" \ https://onedev.example.com/api/cluster/site-files?path=cache // after (cluster member using cluster credential setup, not a PAT) // ensure the request originates from an authenticated cluster member; // configure identical cluster secrets on both servers, then call via the // cluster client rather than raw HTTP with user tokens.
Defensive patterns
Strategy: try-catch
Validate before calling
if (!SecurityUtils.isSystem())
throw new IllegalStateException("Cluster endpoint requires cluster credential, not user auth"); Try / catch
try {
return target("cluster/site-files").get(byte[].class);
} catch (ForbiddenException | UnauthorizedException e) {
// not called with cluster credential; fix client authentication
} Prevention
- Never call internal /api/cluster/* endpoints with personal access tokens.
- Keep cluster secrets identical across all member servers.
- Document these endpoints as internal-only for your team.
When it happens
Trigger: Calling GET on the site-files endpoint with normal user credentials, a regular access token, or no credentials instead of the server's cluster secret; pointing a third-party tool at this internal endpoint.
Common situations: Manually curling the endpoint for debugging without cluster auth; a member server whose cluster token is missing/mismatched fails the system check; firewall-routed requests stripping the credential header; API exploration with a regular PAT.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- Account is disabled
- Authentication required
- Invalid or expired access token
- Not authenticated
- Two-factor authentication not enabled
AI-assisted analysis of theonedev/onedev@d44925c47c (2026-09-06).
Data as JSON: /api/errors/780069907addf398.
Report an issue: GitHub.
Appendix: source
Thrown at server-core/src/main/java/io/onedev/server/cluster/ClusterResource.java:102
private PackBlobService packBlobService;
@Inject
private BuildService buildService;
@Inject
private RunCacheService cacheService;
@Inject
private WorkExecutionService workExecutionService;
@Path("/site-files")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
@GET
public Response downloadSiteFiles(@QueryParam("path") String path,
@QueryParam("patterns") String patterns,
@QueryParam("readLock") String readLock) {
if (!SecurityUtils.isSystem())
throw new UnauthorizedException("This api can only be accessed via cluster credential");
StreamingOutput output = os -> {
Runnable sendFiles = () -> {
File directory = new File(Bootstrap.getSiteDir(), path);
PatternSet patternSet = PatternSet.parse(patterns);
patternSet.getExcludes().add(SHARE_TEST_DIR + "/**");
TarUtils.tar(directory, patternSet.getIncludes(), patternSet.getExcludes(), os, false);
};
if (readLock != null)
read(readLock, sendFiles);
else
sendFiles.run();
};
return ok(output).build();
}
@Path("/site-file")
@Produces(MediaType.APPLICATION_OCTET_STREAM)View on GitHub (pinned to d44925c47c)