From 3db22f3d3893eb028e80d0cba2ca761a7b53dde5 Mon Sep 17 00:00:00 2001 From: Ben McClelland Date: Sun, 20 Sep 2026 13:07:05 -0700 Subject: [PATCH] fix: hide webui user management when server in single-user mode Single-root deployments reject /list-users with XAdminMethodNotSupported. Treat that response as a capability signal so the dashboard suppresses the expected error and removes unsupported user-management controls. --- webui/web/dashboard.html | 9 +++++++++ webui/web/js/api.js | 36 +++++++++++++++++++++++++++++++++++- webui/web/js/app.js | 12 ++++++++---- 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/webui/web/dashboard.html b/webui/web/dashboard.html index fd13bf6e..52efb32a 100644 --- a/webui/web/dashboard.html +++ b/webui/web/dashboard.html @@ -419,10 +419,19 @@ under the License. } async function loadGatewayUsers() { + if (!api.canManageGatewayUsers()) return true; + let users; try { users = await api.listUsers(); } catch (error) { + if (error.code === 'XAdminMethodNotSupported') { + api.setCanManageGatewayUsers(false); + document.querySelectorAll('[data-admin-users-only]').forEach(item => { + item.style.display = 'none'; + }); + return true; + } console.error('Error loading users:', error); showToast('Error loading users: ' + error.message, 'error'); return false; diff --git a/webui/web/js/api.js b/webui/web/js/api.js index 6c690ac6..ce8a6075 100644 --- a/webui/web/js/api.js +++ b/webui/web/js/api.js @@ -124,6 +124,7 @@ class VersityAPI { this._hasIAM = false; // Credentials validate against the IAM service this._hasS3 = false; // Credentials validate against the S3 data plane this._canListBuckets = false; // hasS3 is true and s3:ListAllMyBuckets is allowed + this._canManageGatewayUsers = true; // False when the gateway runs in single-user mode } /** @@ -295,6 +296,21 @@ class VersityAPI { sessionStorage.setItem('vgw_can_list_buckets', this._canListBuckets ? 'true' : 'false'); } + /** + * Check whether this gateway supports its account-management Admin API. + */ + canManageGatewayUsers() { + return this._canManageGatewayUsers; + } + + /** + * Record whether the gateway supports its account-management Admin API. + */ + setCanManageGatewayUsers(canManageGatewayUsers) { + this._canManageGatewayUsers = !!canManageGatewayUsers; + sessionStorage.setItem('vgw_can_manage_gateway_users', this._canManageGatewayUsers ? 'true' : 'false'); + } + /** * Load credentials from sessionStorage */ @@ -310,6 +326,7 @@ class VersityAPI { const hasIAM = sessionStorage.getItem('vgw_has_iam') === 'true'; const hasS3 = sessionStorage.getItem('vgw_has_s3') === 'true'; const canListBuckets = sessionStorage.getItem('vgw_can_list_buckets') === 'true'; + const canManageGatewayUsers = sessionStorage.getItem('vgw_can_manage_gateway_users') !== 'false'; // Support legacy single endpoint storage const legacyEndpoint = sessionStorage.getItem('vgw_endpoint'); @@ -325,6 +342,7 @@ class VersityAPI { this._hasIAM = hasIAM; this._hasS3 = hasS3; this._canListBuckets = canListBuckets; + this._canManageGatewayUsers = canManageGatewayUsers; return true; } return false; @@ -343,6 +361,7 @@ class VersityAPI { this._hasIAM = false; this._hasS3 = false; this._canListBuckets = false; + this._canManageGatewayUsers = true; this._userType = 'user'; this._accessibleGateways = []; sessionStorage.removeItem('vgw_admin_endpoint'); @@ -357,6 +376,7 @@ class VersityAPI { sessionStorage.removeItem('vgw_has_iam'); sessionStorage.removeItem('vgw_has_s3'); sessionStorage.removeItem('vgw_can_list_buckets'); + sessionStorage.removeItem('vgw_can_manage_gateway_users'); sessionStorage.removeItem('vgw_iam_probe_error'); sessionStorage.removeItem('vgw_user_type'); sessionStorage.removeItem('vgw_accessible_gateways'); @@ -857,7 +877,9 @@ class VersityAPI { const responseText = await response.text(); if (!response.ok) { - throw new Error(parseXmlErrorMessage(responseText, `HTTP ${response.status}: ${response.statusText}`)); + const error = new Error(parseXmlErrorMessage(responseText, `HTTP ${response.status}: ${response.statusText}`)); + error.code = parseXmlErrorCode(responseText); + throw error; } return responseText; @@ -2851,6 +2873,18 @@ function parseXmlErrorMessage(responseText, fallback) { return fallback; } +/** + * Extract an S3-compatible error code from an XML response body. + */ +function parseXmlErrorCode(responseText) { + try { + const xmlDoc = new DOMParser().parseFromString(responseText, 'text/xml'); + return xmlDoc.querySelector('Code')?.textContent || null; + } catch (e) { + return null; + } +} + /** * Normalize a parsed member-list field to an array */ diff --git a/webui/web/js/app.js b/webui/web/js/app.js index a57c13c1..04c42d91 100644 --- a/webui/web/js/app.js +++ b/webui/web/js/app.js @@ -101,6 +101,10 @@ function requireGatewayUsers() { window.location.href = 'iam-users.html'; return false; } + if (!api.canManageGatewayUsers()) { + window.location.href = defaultLandingPage(); + return false; + } return requireAdmin(); } @@ -420,9 +424,9 @@ function updateUserInfo() { * standalone-IAM deployment * data-s3-only the S3 data plane answered for these credentials * data-iam-only the standalone IAM service answered for them - * data-admin-users-only the inverse of data-iam-only: the gateway's own - * account store is still the user directory, rather - * than a configured standalone IAM service + * data-admin-users-only the gateway's own account store is available for + * management, rather than standalone IAM or + * single-user mode */ function initSidebarWithRole() { initSidebar(); @@ -435,7 +439,7 @@ function initSidebarWithRole() { if (!api.hasManagement()) hide('[data-management-only]'); if (!api.hasS3()) hide('[data-s3-only]'); if (!api.hasIAM()) hide('[data-iam-only]'); - if (api.hasIAM()) hide('[data-admin-users-only]'); + if (api.hasIAM() || !api.canManageGatewayUsers()) hide('[data-admin-users-only]'); reportIAMProbeFailure(); }