From 95a42ff2c21de5ca9a226d262e767337ff15aef6 Mon Sep 17 00:00:00 2001 From: Daan Hoogland Date: Sat, 22 Aug 2026 20:55:56 +0200 Subject: [PATCH 1/3] allow domain admins to add ldap users --- .../META-INF/db/schema-42210to42300.sql | 17 +++++++++++++++++ .../api/command/LdapCreateAccountCmd.java | 3 ++- .../api/command/LdapImportUsersCmd.java | 3 ++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index 417d69a162eb..4e27cc87a5e7 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -651,3 +651,20 @@ WHERE `name`='user.vm.readonly.details' AND `value` IS NOT NULL; -- usage records introduced in 4.22.1 (cumulative and per-VM) can coexist. See #13399. CALL `cloud_usage`.`IDEMPOTENT_DROP_INDEX`('id', 'cloud_usage.usage_volume'); CALL `cloud_usage`.`IDEMPOTENT_ADD_UNIQUE_INDEX`('cloud_usage.usage_volume', 'id', '(volume_id ASC, created ASC, vm_id ASC)'); + +-- @APICommand(authorized=...) alone doesn't grant access under dynamic RBAC (the default) - +-- Backfill them for any role that can already create accounts natively. +INSERT INTO cloud.role_permissions (uuid, role_id, rule, permission, sort_order) +SELECT uuid(), role_id, 'ldapCreateAccount', permission, sort_order +FROM cloud.role_permissions rp +WHERE rule = 'createAccount' AND NOT EXISTS(SELECT 1 FROM cloud.role_permissions rp_ WHERE rp.role_id = rp_.role_id AND rp_.rule = 'ldapCreateAccount'); + +INSERT INTO cloud.role_permissions (uuid, role_id, rule, permission, sort_order) +SELECT uuid(), role_id, 'linkAccountToLdap', permission, sort_order +FROM cloud.role_permissions rp +WHERE rule = 'createAccount' AND NOT EXISTS(SELECT 1 FROM cloud.role_permissions rp_ WHERE rp.role_id = rp_.role_id AND rp_.rule = 'linkAccountToLdap'); + +INSERT INTO cloud.role_permissions (uuid, role_id, rule, permission, sort_order) +SELECT uuid(), role_id, 'importLdapUsers', permission, sort_order +FROM cloud.role_permissions rp +WHERE rule = 'createAccount' AND NOT EXISTS(SELECT 1 FROM cloud.role_permissions rp_ WHERE rp.role_id = rp_.role_id AND rp_.rule = 'importLdapUsers'); diff --git a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/command/LdapCreateAccountCmd.java b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/command/LdapCreateAccountCmd.java index 3aa3e0502268..8835c9fe4b0b 100644 --- a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/command/LdapCreateAccountCmd.java +++ b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/command/LdapCreateAccountCmd.java @@ -44,7 +44,8 @@ import java.security.SecureRandom; import java.util.Map; -@APICommand(name = "ldapCreateAccount", description = "Creates an account from an LDAP user", responseObject = AccountResponse.class, since = "4.2.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +@APICommand(name = "ldapCreateAccount", description = "Creates an account from an LDAP user", responseObject = AccountResponse.class, since = "4.2.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.DomainAdmin}) public class LdapCreateAccountCmd extends BaseCmd { private static final String s_name = "createaccountresponse"; diff --git a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/command/LdapImportUsersCmd.java b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/command/LdapImportUsersCmd.java index d6df27ccfcb9..f497e49d6a01 100644 --- a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/command/LdapImportUsersCmd.java +++ b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/command/LdapImportUsersCmd.java @@ -56,7 +56,8 @@ import com.cloud.user.UserAccount; import com.cloud.utils.StringUtils; -@APICommand(name = "importLdapUsers", description = "Import LDAP users", responseObject = LdapUserResponse.class, since = "4.3.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +@APICommand(name = "importLdapUsers", description = "Import LDAP users", responseObject = LdapUserResponse.class, since = "4.3.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.DomainAdmin}) public class LdapImportUsersCmd extends BaseListCmd { From 65baf667082f164aaf90bbf71a88fe2bb1af0df9 Mon Sep 17 00:00:00 2001 From: Daan Hoogland Date: Mon, 2 Nov 2026 16:15:47 +0100 Subject: [PATCH 2/3] stricter check on LDAP configuration accessability --- ui/src/store/modules/user.js | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/ui/src/store/modules/user.js b/ui/src/store/modules/user.js index 4103ff920d63..d3835afea94a 100644 --- a/ui/src/store/modules/user.js +++ b/ui/src/store/modules/user.js @@ -451,6 +451,9 @@ const user = { commit('SET_NAME', result.firstname + ' ' + result.lastname) commit('SET_AVATAR', result.icon?.base64image || '') store.dispatch('SetCsLatestVersion', result.rolename) + store.dispatch('UpdateConfiguration').catch(error => { + reject(error) + }) }).catch(error => { reject(error) }) @@ -464,13 +467,6 @@ const user = { }).catch(ignored => { }) - getAPI('listLdapConfigurations').then(response => { - const ldapEnable = (response.ldapconfigurationresponse.count > 0) - commit('SET_LDAP', ldapEnable) - }).catch(error => { - reject(error) - }) - getAPI('cloudianIsEnabled').then(response => { const cloudian = response.cloudianisenabledresponse.cloudianisenabled || {} commit('SET_CLOUDIAN', cloudian) @@ -601,13 +597,15 @@ const user = { }) }, UpdateConfiguration ({ commit }) { - return new Promise((resolve, reject) => { - getAPI('listLdapConfigurations').then(response => { - const ldapEnable = (response.ldapconfigurationresponse.count > 0) - commit('SET_LDAP', ldapEnable) - }).catch(error => { - reject(error) - }) + // Root admins see LDAP as enabled if any configuration exists; everyone else + // only when a global configuration or one linked to their own domain exists + const userInfo = store.getters.userInfo + const queries = userInfo.roletype === 'Admin' + ? [{ listall: true }] + : [{}, { domainid: userInfo.domainid }] + return Promise.all(queries.map(params => getAPI('listLdapConfigurations', params))).then(responses => { + const ldapEnable = responses.some(response => response.ldapconfigurationresponse.count > 0) + commit('SET_LDAP', ldapEnable) }) }, SetDomainStore ({ commit }, domainStore) { From a096613ed26cc3966b7f8bce5740914c42fe392f Mon Sep 17 00:00:00 2001 From: Daan Hoogland Date: Mon, 2 Nov 2026 16:31:09 +0100 Subject: [PATCH 3/3] update upgrade path --- .../META-INF/db/schema-42210to42300.sql | 17 -------------- .../META-INF/db/schema-42300to2400.sql | 22 +++++++++++++++++++ 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index 4e27cc87a5e7..417d69a162eb 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -651,20 +651,3 @@ WHERE `name`='user.vm.readonly.details' AND `value` IS NOT NULL; -- usage records introduced in 4.22.1 (cumulative and per-VM) can coexist. See #13399. CALL `cloud_usage`.`IDEMPOTENT_DROP_INDEX`('id', 'cloud_usage.usage_volume'); CALL `cloud_usage`.`IDEMPOTENT_ADD_UNIQUE_INDEX`('cloud_usage.usage_volume', 'id', '(volume_id ASC, created ASC, vm_id ASC)'); - --- @APICommand(authorized=...) alone doesn't grant access under dynamic RBAC (the default) - --- Backfill them for any role that can already create accounts natively. -INSERT INTO cloud.role_permissions (uuid, role_id, rule, permission, sort_order) -SELECT uuid(), role_id, 'ldapCreateAccount', permission, sort_order -FROM cloud.role_permissions rp -WHERE rule = 'createAccount' AND NOT EXISTS(SELECT 1 FROM cloud.role_permissions rp_ WHERE rp.role_id = rp_.role_id AND rp_.rule = 'ldapCreateAccount'); - -INSERT INTO cloud.role_permissions (uuid, role_id, rule, permission, sort_order) -SELECT uuid(), role_id, 'linkAccountToLdap', permission, sort_order -FROM cloud.role_permissions rp -WHERE rule = 'createAccount' AND NOT EXISTS(SELECT 1 FROM cloud.role_permissions rp_ WHERE rp.role_id = rp_.role_id AND rp_.rule = 'linkAccountToLdap'); - -INSERT INTO cloud.role_permissions (uuid, role_id, rule, permission, sort_order) -SELECT uuid(), role_id, 'importLdapUsers', permission, sort_order -FROM cloud.role_permissions rp -WHERE rule = 'createAccount' AND NOT EXISTS(SELECT 1 FROM cloud.role_permissions rp_ WHERE rp.role_id = rp_.role_id AND rp_.rule = 'importLdapUsers'); diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql index 3365f1f855ba..14e9f04bf3dc 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql @@ -239,3 +239,25 @@ CREATE TABLE IF NOT EXISTS `cloud`.`instance_boot_group_details` ( PRIMARY KEY (`id`), CONSTRAINT `fk_instance_boot_group_details__group_id` FOREIGN KEY (`boot_group_id`) REFERENCES `instance_boot_group` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- @APICommand(authorized=...) alone doesn't grant access under dynamic RBAC (the default). +-- Backfill the LDAP account APIs for any role that can already create accounts natively. +INSERT INTO cloud.role_permissions (uuid, role_id, rule, permission, sort_order) +SELECT uuid(), role_id, 'listLdapUsers', permission, sort_order +FROM cloud.role_permissions rp +WHERE rule = 'createAccount' AND NOT EXISTS(SELECT 1 FROM cloud.role_permissions rp_ WHERE rp.role_id = rp_.role_id AND rp_.rule = 'listLdapUsers'); + +INSERT INTO cloud.role_permissions (uuid, role_id, rule, permission, sort_order) +SELECT uuid(), role_id, 'ldapCreateAccount', permission, sort_order +FROM cloud.role_permissions rp +WHERE rule = 'createAccount' AND NOT EXISTS(SELECT 1 FROM cloud.role_permissions rp_ WHERE rp.role_id = rp_.role_id AND rp_.rule = 'ldapCreateAccount'); + +INSERT INTO cloud.role_permissions (uuid, role_id, rule, permission, sort_order) +SELECT uuid(), role_id, 'linkAccountToLdap', permission, sort_order +FROM cloud.role_permissions rp +WHERE rule = 'createAccount' AND NOT EXISTS(SELECT 1 FROM cloud.role_permissions rp_ WHERE rp.role_id = rp_.role_id AND rp_.rule = 'linkAccountToLdap'); + +INSERT INTO cloud.role_permissions (uuid, role_id, rule, permission, sort_order) +SELECT uuid(), role_id, 'importLdapUsers', permission, sort_order +FROM cloud.role_permissions rp +WHERE rule = 'createAccount' AND NOT EXISTS(SELECT 1 FROM cloud.role_permissions rp_ WHERE rp.role_id = rp_.role_id AND rp_.rule = 'importLdapUsers');