Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@

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},
requestHasSensitiveInfo = true, responseHasSensitiveInfo = true)
public class CreateAccountCmd extends BaseCmd {

private static final int GENERATED_PASSWORD_LENGTH = 12;
Comment thread
DaanHoogland marked this conversation as resolved.
Resolved

/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
Expand Down Expand Up @@ -75,8 +77,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 will need to write a custom authentication adapter See Docs section. "

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 minor nit

+ "If omitted, a random password is generated, e.g. for an account that will only ever authenticate externally via SAML/LDAP.")
Comment thread
DaanHoogland marked this conversation as resolved.
Resolved
private String password;

@Parameter(name = ApiConstants.TIMEZONE,
Expand Down Expand Up @@ -191,10 +193,16 @@ public void execute() {

/**
* TODO: this should be done through a validator. for now replicating the validation logic in create account and user
*
* <p>An omitted 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. A password
* that is passed but blank is still rejected, as that is more likely a client error.
*/
private void validateParams() {
if(StringUtils.isEmpty(getPassword())) {
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Empty passwords are not allowed");
if (getPassword() == null) {
password = PasswordGenerator.generateRandomPassword(GENERATED_PASSWORD_LENGTH);
} else if (StringUtils.isBlank(getPassword())) {
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Blank passwords are not allowed");
}
Comment thread
DaanHoogland marked this conversation as resolved.
Resolved
if (getAccountType() == null && (getRoleId() == null || getRoleId() < 1L)) {
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Neither account type and role ID are not provided");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@

import org.apache.cloudstack.acl.RoleService;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.ResponseGenerator;
import org.apache.cloudstack.api.ResponseObject.ResponseView;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.AccountResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
Expand All @@ -37,6 +40,7 @@
import com.cloud.user.Account;
import com.cloud.user.AccountService;
import com.cloud.user.User;
import com.cloud.user.UserAccount;

public class CreateAccountCmdTest {
protected Logger logger = LogManager.getLogger(getClass());
Expand All @@ -45,6 +49,8 @@
private AccountService accountService;
@Mock
private RoleService roleService;
@Mock
private ResponseGenerator responseGenerator;

@InjectMocks
private CreateAccountCmd createAccountCmd = new CreateAccountCmd();
Expand Down Expand Up @@ -81,28 +87,33 @@
}

@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());
}
Mockito.verify(accountService, Mockito.never()).createUserAccount(createAccountCmd);
UserAccount userAccount = Mockito.mock(UserAccount.class);
Mockito.when(accountService.createUserAccount(createAccountCmd)).thenReturn(userAccount);
Mockito.when(responseGenerator.createUserAccountResponse(ResponseView.Full, userAccount)).thenReturn(Mockito.mock(AccountResponse.class));

Check warning on line 94 in api/src/test/java/org/apache/cloudstack/api/command/admin/account/CreateAccountCmdTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this mock creation to a local variable.

See more on https://sonarcloud.io/project/issues?id=apache_cloudstack&issues=AaHA-RsDpXBuwatgAAb0&open=AaHA-RsDpXBuwatgAAb0&pullRequest=13955

createAccountCmd.execute();

Assert.assertEquals("a password should be generated for accounts that authenticate externally",
12, createAccountCmd.getPassword().length());
Mockito.verify(accountService, Mockito.times(1)).createUserAccount(createAccountCmd);
}
Comment thread
DaanHoogland marked this conversation as resolved.
Resolved

@Test
public void testExecuteWithEmptyPassword() {
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());
}
ServerApiException e = Assert.assertThrows(ServerApiException.class, () -> createAccountCmd.execute());
Assert.assertEquals(ApiErrorCode.PARAM_ERROR, e.getErrorCode());
Assert.assertEquals("Blank passwords are not allowed", e.getMessage());
Mockito.verify(accountService, Mockito.never()).createUserAccount(createAccountCmd);
}

@Test
public void testExecuteWithWhitespacePassword() {
ReflectionTestUtils.setField(createAccountCmd, "password", " ");
ServerApiException e = Assert.assertThrows(ServerApiException.class, () -> createAccountCmd.execute());
Assert.assertEquals(ApiErrorCode.PARAM_ERROR, e.getErrorCode());
Mockito.verify(accountService, Mockito.never()).createUserAccount(createAccountCmd);
}
}
1 change: 1 addition & 0 deletions ui/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -4077,6 +4077,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",
Expand Down
22 changes: 20 additions & 2 deletions ui/src/views/iam/AddAccount.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Comment thread
DaanHoogland marked this conversation as resolved.
Resolved
<a-col :md="24" :lg="12">
<a-form-item ref="password" name="password">
<template #label>
Expand All @@ -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"/>
Expand Down Expand Up @@ -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 }
]
Comment thread
DaanHoogland marked this conversation as resolved.
Resolved
Comment thread
DaanHoogland marked this conversation as resolved.
Resolved
}
Comment on lines +260 to 267
},
methods: {
Expand Down Expand Up @@ -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
}
Expand Down
Loading