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 }
