This is an automated email from the ASF dual-hosted git repository.

DaanHoogland pushed a commit to branch 
ghi12039-optional-password-for-saml-account
in repository https://gitbox.apache.org/repos/asf/cloudstack.git

commit 46ca0dd45d7b0ebeea982665bb2aef7500808946
Author: Daan Hoogland <[email protected]>
AuthorDate: Sat Aug 22 23:05:37 2026 +0200

    make password an optional parameter/field in createAccount
---
 .../command/admin/account/CreateAccountCmd.java    | 12 ++++++++----
 .../admin/account/CreateAccountCmdTest.java        | 19 ++++++++-----------
 ui/public/locales/en.json                          |  1 +
 ui/src/views/iam/AddAccount.vue                    | 22 ++++++++++++++++++++--
 4 files changed, 37 insertions(+), 17 deletions(-)

diff --git 
a/api/src/main/java/org/apache/cloudstack/api/command/admin/account/CreateAccountCmd.java
 
b/api/src/main/java/org/apache/cloudstack/api/command/admin/account/CreateAccountCmd.java
index cc154ed964b..f9d218dca2e 100644
--- 
a/api/src/main/java/org/apache/cloudstack/api/command/admin/account/CreateAccountCmd.java
+++ 
b/api/src/main/java/org/apache/cloudstack/api/command/admin/account/CreateAccountCmd.java
@@ -37,6 +37,7 @@ import org.apache.cloudstack.context.CallContext;
 
 import com.cloud.user.Account;
 import com.cloud.user.UserAccount;
+import com.cloud.utils.PasswordGenerator;
 
 
 @APICommand(name = "createAccount", description = "Creates an account", 
responseObject = AccountResponse.class, entityType = {Account.class},
@@ -75,8 +76,8 @@ public class CreateAccountCmd extends BaseCmd {
 
     @Parameter(name = ApiConstants.PASSWORD,
                type = CommandType.STRING,
-               required = true,
-               description = "Clear text password (Default hashed to 
SHA256SALT). If you wish to use any other hashing algorithm, you would need to 
write a custom authentication adapter See Docs section.")
+               description = "Clear text password (Default hashed to 
SHA256SALT). If you wish to use any other hashing algorithm, you would need to 
write a custom authentication adapter See Docs section. "
+                       + "If omitted, a random password is generated, e.g. for 
an account that will only ever authenticate externally via SAML/LDAP.")
     private String password;
 
     @Parameter(name = ApiConstants.TIMEZONE,
@@ -191,10 +192,13 @@ public class CreateAccountCmd extends BaseCmd {
 
     /**
      * TODO: this should be done through a validator. for now replicating the 
validation logic in create account and user
+     *
+     * <p>A blank password generates a random one instead of failing, since an 
account that will
+     * only ever authenticate externally (SAML/LDAP) has no need for the admin 
to set one.
      */
     private void validateParams() {
-        if(StringUtils.isEmpty(getPassword())) {
-            throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Empty 
passwords are not allowed");
+        if (StringUtils.isEmpty(getPassword())) {
+            password = PasswordGenerator.generateRandomPassword(12);
         }
         if (getAccountType() == null && (getRoleId() == null || getRoleId() < 
1L)) {
             throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Neither 
account type and role ID are not provided");
diff --git 
a/api/src/test/java/org/apache/cloudstack/api/command/admin/account/CreateAccountCmdTest.java
 
b/api/src/test/java/org/apache/cloudstack/api/command/admin/account/CreateAccountCmdTest.java
index 365646de7a3..b8aa857d87d 100644
--- 
a/api/src/test/java/org/apache/cloudstack/api/command/admin/account/CreateAccountCmdTest.java
+++ 
b/api/src/test/java/org/apache/cloudstack/api/command/admin/account/CreateAccountCmdTest.java
@@ -19,7 +19,6 @@
 package org.apache.cloudstack.api.command.admin.account;
 
 import org.apache.cloudstack.acl.RoleService;
-import org.apache.cloudstack.api.ApiErrorCode;
 import org.apache.cloudstack.api.ServerApiException;
 import org.apache.cloudstack.context.CallContext;
 import org.apache.logging.log4j.Logger;
@@ -81,28 +80,26 @@ public class CreateAccountCmdTest {
     }
 
     @Test
-    public void testExecuteWithNullPassword() {
+    public void testExecuteWithNullPasswordGeneratesOne() {
         ReflectionTestUtils.setField(createAccountCmd, "password", null);
         try {
             createAccountCmd.execute();
-            Assert.fail("should throw exception for a null password");
         } catch (ServerApiException e) {
-            Assert.assertEquals(ApiErrorCode.PARAM_ERROR, e.getErrorCode());
-            Assert.assertEquals("Empty passwords are not allowed", 
e.getMessage());
+            Assert.assertTrue("Received exception as the mock accountService 
createUserAccount returns null user", true);
         }
-        Mockito.verify(accountService, 
Mockito.never()).createUserAccount(createAccountCmd);
+        Assert.assertNotNull("a password should be generated for accounts that 
authenticate externally", createAccountCmd.getPassword());
+        Mockito.verify(accountService, 
Mockito.times(1)).createUserAccount(createAccountCmd);
     }
 
     @Test
-    public void testExecuteWithEmptyPassword() {
+    public void testExecuteWithEmptyPasswordGeneratesOne() {
         ReflectionTestUtils.setField(createAccountCmd, "password", "");
         try {
             createAccountCmd.execute();
-            Assert.fail("should throw exception for a empty password");
         } catch (ServerApiException e) {
-            Assert.assertEquals(ApiErrorCode.PARAM_ERROR, e.getErrorCode());
-            Assert.assertEquals("Empty passwords are not allowed", 
e.getMessage());
+            Assert.assertTrue("Received exception as the mock accountService 
createUserAccount returns null user", true);
         }
-        Mockito.verify(accountService, 
Mockito.never()).createUserAccount(createAccountCmd);
+        Assert.assertNotNull("a password should be generated for accounts that 
authenticate externally", createAccountCmd.getPassword());
+        Mockito.verify(accountService, 
Mockito.times(1)).createUserAccount(createAccountCmd);
     }
 }
diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index f57460efa48..3be5b7d280b 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -3982,6 +3982,7 @@
 "message.restart.vpc": "Please confirm that you want to restart the VPC.",
 "message.restart.vpc.remark": "Please confirm that you want to restart the VPC 
<p><i>Remark: making a non-redundant VPC redundant will force a clean up. The 
Networks will not be available for a couple of minutes</i>.</p>",
 "message.running.custom.action": "Running action",
+"message.saml.account.no.password": "This account will authenticate via SAML 
SSO, so no password is needed — one will be generated automatically.",
 "message.scale.processing": "Scale in progress",
 "message.scaledown.policies": "Please add at least a ScaleDown policy. The 
AutoScale Group will be scaled down when all conditions in a ScaleDown policy 
are matched. ScaleDown policies will be checked after ScaleUp policies.",
 "message.scaledown.policy.continue": "Please add at least condition to 
ScaleDown policy to continue",
diff --git a/ui/src/views/iam/AddAccount.vue b/ui/src/views/iam/AddAccount.vue
index a211fd2d8c3..f95836362d4 100644
--- a/ui/src/views/iam/AddAccount.vue
+++ b/ui/src/views/iam/AddAccount.vue
@@ -53,7 +53,7 @@
             v-model:value="form.username"
             :placeholder="apiParams.username.description" />
         </a-form-item>
-        <a-row :gutter="12">
+        <a-row :gutter="12" v-if="!form.samlenable">
           <a-col :md="24" :lg="12">
             <a-form-item ref="password" name="password">
               <template #label>
@@ -75,6 +75,12 @@
             </a-form-item>
           </a-col>
         </a-row>
+        <a-alert
+          v-else
+          type="info"
+          show-icon
+          :message="$t('message.saml.account.no.password')"
+          style="margin-bottom: 12px;" />
         <a-form-item ref="email" name="email">
           <template #label>
             <tooltip-label :title="$t('label.email')" 
:tooltip="apiParams.email.description"/>
@@ -250,6 +256,14 @@ export default {
         }
       },
       immediate: false
+    },
+    'form.samlenable' (samlEnabled) {
+      // a SAML-authenticated account never logs in with a native password
+      this.rules.password = samlEnabled ? [] : [{ required: true, message: 
this.$t('message.error.required.input') }]
+      this.rules.confirmpassword = samlEnabled ? [] : [
+        { required: true, message: this.$t('message.error.required.input') },
+        { validator: this.validateConfirmPassword }
+      ]
     }
   },
   methods: {
@@ -408,12 +422,16 @@ export default {
         const params = {
           roleid: values.roleid,
           username: values.username,
-          password: values.password,
           email: values.email,
           firstname: values.firstname,
           lastname: values.lastname,
           domainid: values.domainid
         }
+        if (!values.samlenable) {
+          // SAML-authenticated accounts never log in with a native password; 
let the API
+          // generate one rather than asking the admin to set one that will 
never be used
+          params.password = values.password
+        }
         if (this.isValidValueForKey(values, 'account') && 
values.account.length > 0) {
           params.account = values.account
         }

Reply via email to