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();
}