GitLab and Harbor registries no longer report a misleading auth error when browsing
This commit is contained in:
3 files changed
+78
-4
No files matched your search
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Pure catalog-failure classification (#873). No docker/db imports, so it's
|
||||
* unit-testable without pulling better-sqlite3 into the test process.
|
||||
*
|
||||
* GitLab, Harbor, and other registries refuse /v2/_catalog to non-admins by returning
|
||||
* 401 with `error="insufficient_scope"` in WWW-Authenticate - the token is VALID, the
|
||||
* operation just isn't permitted. That is NOT an auth failure. A real bad/expired token
|
||||
* instead returns `error="invalid_token"`. Verified empirically on gitlab.com and a
|
||||
* self-hosted GitLab (admin gets catalog 200; non-admin and gitlab.com get
|
||||
* insufficient_scope; a bad token gets invalid_token).
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param status HTTP status of the catalog response
|
||||
* @param wwwAuthenticate the response's WWW-Authenticate header (may be null)
|
||||
* @param authState 'authed' (we obtained and sent a bearer token), 'rejected' (registry
|
||||
* HAS stored credentials but the token exchange failed - bad/expired creds), or 'anon'
|
||||
* (no credentials configured)
|
||||
* @returns 'not_supported' (registry won't list - point user to search) or 'auth_failed'
|
||||
*/
|
||||
export function classifyCatalogFailure(
|
||||
status: number,
|
||||
wwwAuthenticate: string | null,
|
||||
authState: 'authed' | 'rejected' | 'anon'
|
||||
): 'not_supported' | 'auth_failed' {
|
||||
const err = (wwwAuthenticate || '').match(/error="([^"]+)"/i)?.[1]?.toLowerCase();
|
||||
// An explicit error code is authoritative regardless of how we authenticated.
|
||||
if (err === 'insufficient_scope') return 'not_supported';
|
||||
if (err === 'invalid_token') return 'auth_failed';
|
||||
// Credentials were configured but the token exchange failed -> real auth problem.
|
||||
if (authState === 'rejected') return 'auth_failed';
|
||||
// We authenticated fine but still can't list -> the token lacks catalog permission
|
||||
// (GitLab/Harbor restrict it to admins). Anonymous 401 is the normal login challenge.
|
||||
if ((status === 401 || status === 403) && authState === 'authed') return 'not_supported';
|
||||
return 'auth_failed';
|
||||
}
|
||||
|
||||
/** User-facing message for a registry that won't allow catalog listing (#873). */
|
||||
export const CATALOG_NOT_SUPPORTED_MSG =
|
||||
'This registry does not allow listing all images (GitLab and Harbor restrict it to admins). Search for an image by its full path instead.';
|
||||
@@ -1,7 +1,7 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getRegistry } from '$lib/server/db';
|
||||
import { getRegistryAuth, isHarborRegistry, harborListRepositories, parseRegistryUrl } from '$lib/server/docker';
|
||||
import { getRegistryAuth, isHarborRegistry, harborListRepositories, parseRegistryUrl, classifyCatalogFailure, CATALOG_NOT_SUPPORTED_MSG } from '$lib/server/docker';
|
||||
|
||||
const PAGE_SIZE = 100;
|
||||
|
||||
@@ -53,7 +53,19 @@ export const GET: RequestHandler = async ({ url }) => {
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
return json({ error: 'Catalog listing not available. This registry may not support the _catalog endpoint (common with GitLab and Harbor). Try searching for images by name instead.' }, { status: response.status });
|
||||
// GitLab/Harbor refuse catalog to non-admins with a VALID token (#873):
|
||||
// distinguish "listing not permitted" from a real auth failure.
|
||||
const hasCreds = !!(registry.username && registry.password);
|
||||
const authState = hasCreds ? (authHeader ? 'authed' : 'rejected') : 'anon';
|
||||
const kind = classifyCatalogFailure(
|
||||
response.status,
|
||||
response.headers.get('WWW-Authenticate'),
|
||||
authState
|
||||
);
|
||||
if (kind === 'not_supported') {
|
||||
return json({ error: CATALOG_NOT_SUPPORTED_MSG, notSupported: true }, { status: response.status });
|
||||
}
|
||||
return json({ error: 'Authentication failed. Check the registry credentials.' }, { status: response.status });
|
||||
}
|
||||
if (response.status === 404) {
|
||||
return json({ error: 'Registry does not support V2 catalog API' }, { status: 404 });
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getRegistry } from '$lib/server/db';
|
||||
import { getRegistryAuth, isHarborRegistry, harborSearchRepositories, parseRegistryUrl } from '$lib/server/docker';
|
||||
import { getRegistryAuth, isHarborRegistry, harborSearchRepositories, parseRegistryUrl, classifyCatalogFailure, CATALOG_NOT_SUPPORTED_MSG } from '$lib/server/docker';
|
||||
|
||||
/** Thrown when the registry refuses catalog listing to a valid token (GitLab/Harbor, #873). */
|
||||
class CatalogNotSupportedError extends Error {}
|
||||
|
||||
interface SearchResult {
|
||||
name: string;
|
||||
@@ -165,7 +168,20 @@ async function searchCatalog(registry: any, term: string, limit: number): Promis
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
throw new Error('Authentication failed. This registry may not support catalog listing (common with GitLab and Harbor deploy tokens).');
|
||||
// Same GitLab/Harbor catalog case as Browse (#873): a valid token that
|
||||
// isn't allowed to list is NOT an auth failure. Throw a marked error so
|
||||
// searchPrivateRegistry can surface the right message.
|
||||
const hasCreds = !!(registry.username && registry.password);
|
||||
const authState = hasCreds ? (authHeader ? 'authed' : 'rejected') : 'anon';
|
||||
const kind = classifyCatalogFailure(
|
||||
response.status,
|
||||
response.headers.get('WWW-Authenticate'),
|
||||
authState
|
||||
);
|
||||
if (kind === 'not_supported') {
|
||||
throw new CatalogNotSupportedError(CATALOG_NOT_SUPPORTED_MSG);
|
||||
}
|
||||
throw new Error('Authentication failed. Check the registry credentials.');
|
||||
}
|
||||
throw new Error(`Registry returned error: ${response.status}`);
|
||||
}
|
||||
@@ -234,6 +250,12 @@ export const GET: RequestHandler = async ({ url }) => {
|
||||
|
||||
return json(results);
|
||||
} catch (error: any) {
|
||||
// Registry won't allow catalog listing (GitLab/Harbor, #873) - not an error the
|
||||
// user can fix. Return an empty result with a hint, not a 500 auth failure.
|
||||
if (error instanceof CatalogNotSupportedError) {
|
||||
return json({ results: [], notSupported: true, message: CATALOG_NOT_SUPPORTED_MSG });
|
||||
}
|
||||
|
||||
console.error('Failed to search images:', error);
|
||||
|
||||
if (error.code === 'ECONNREFUSED') {
|
||||
|
||||
Reference in new issue
Block a user