Copilot commented on code in PR #12294:
URL: https://github.com/apache/cloudstack/pull/12294#discussion_r2667289180


##########
api/src/test/java/org/apache/cloudstack/api/command/admin/user/CreateUserCmdTest.java:
##########
@@ -95,6 +95,6 @@ public void testExecuteWithEmptyPassword() {
             Assert.assertEquals(ApiErrorCode.PARAM_ERROR,e.getErrorCode());
             Assert.assertEquals("Empty passwords are not allowed", 
e.getMessage());
         }
-        Mockito.verify(accountService, Mockito.never()).createUser(null, null, 
null, null, null, null, null, null, null);
+        Mockito.verify(accountService, Mockito.never()).createUser(null, null, 
null, null, null, null, null, null, null, true);

Review Comment:
   The test expects 'false' for the password change required parameter on line 
85, but line 98 expects 'true'. This inconsistency appears incorrect - when 
testing empty password validation, the passwordChangeRequired parameter value 
should not matter, and should likely be 'false' for consistency with the other 
tests.
   ```suggestion
           Mockito.verify(accountService, Mockito.never()).createUser(null, 
null, null, null, null, null, null, null, null, false);
   ```



##########
server/src/main/java/com/cloud/user/AccountManagerImpl.java:
##########
@@ -1544,14 +1558,21 @@ public UserVO createUser(String userName, String 
password, String firstName, Str
         verifyCallerPrivilegeForUserOrAccountOperations(account);
         UserVO user;
         user = createUser(account.getId(), userName, password, firstName, 
lastName, email, timeZone, userUUID, source);
+        if (isPasswordChangeRequired) {
+            long callerAccountId = CallContext.current().getCallingAccountId();
+            if ((isRootAdmin(callerAccountId) || 
isDomainAdmin(callerAccountId))) {

Review Comment:
   According to the PR description, password change enforcement should only be 
applicable to normal user roles, excluding admin and domain admin. However, 
this code doesn't validate the target account's role type before allowing the 
password change flag to be set. Consider adding a check to prevent enforcing 
password changes on Admin or DomainAdmin accounts.
   ```suggestion
               if (account.getType() == Account.Type.ADMIN || account.getType() 
== Account.Type.DOMAIN_ADMIN) {
                   logger.warn("Enforcing password change is not permitted for 
account type [{}].", account.getType());
                   throw new InvalidParameterValueException("CloudStack does 
not support enforcing password change for Admin or DomainAdmin users.");
               }
               long callerAccountId = 
CallContext.current().getCallingAccountId();
               if (isRootAdmin(callerAccountId) || 
isDomainAdmin(callerAccountId)) {
   ```



##########
server/src/main/java/com/cloud/user/AccountManagerImpl.java:
##########
@@ -1585,10 +1606,36 @@ public UserAccount updateUser(UpdateUserCmd 
updateUserCmd) {
         if (mandate2FA != null && mandate2FA) {
             user.setUser2faEnabled(true);
         }
+        updatePasswordChangeRequired(caller, updateUserCmd, user);
         _userDao.update(user.getId(), user);
         return _userAccountDao.findById(user.getId());
     }
 
+    private void updatePasswordChangeRequired(User caller, UpdateUserCmd 
updateUserCmd, UserVO user) {
+        User.Source userSource = user.getSource();
+        if ((userSource == User.Source.SAML2 || userSource == 
User.Source.SAML2DISABLED || userSource == User.Source.LDAP)
+                && updateUserCmd.isPasswordChangeRequired()) {
+            logger.warn("Enforcing password change is not permitted for source 
[{}].", user.getSource());
+            throw new InvalidParameterValueException("CloudStack does not 
support enforcing password change for SAML or LDAP users.");
+        }
+
+        boolean isCallerSameAsUser = user.getId() == caller.getId();
+        boolean isPasswordResetRequired = 
updateUserCmd.isPasswordChangeRequired() && !isCallerSameAsUser;
+        // Admins only can enforce passwordChangeRequired for user
+        if (isRootAdmin(caller.getAccountId()) || 
isDomainAdmin(caller.getAccountId())) {
+            if (isPasswordResetRequired) {
+                _userDetailsDao.addDetail(user.getId(), 
PasswordChangeRequired, "true", false);
+            }
+        }

Review Comment:
   According to the PR description, password change enforcement should only be 
applicable to normal user roles, excluding admin and domain admin. However, 
this method doesn't validate the target user's role type before allowing the 
password change flag to be set. Consider adding a check to prevent enforcing 
password changes on Admin or DomainAdmin users, similar to the SAML/LDAP 
validation above.



##########
plugins/api/discovery/src/main/java/org/apache/cloudstack/discovery/ApiDiscoveryServiceImpl.java:
##########
@@ -66,6 +69,7 @@ public class ApiDiscoveryServiceImpl extends 
ComponentLifecycleBase implements A
     List<APIChecker> _apiAccessCheckers = null;
     List<PluggableService> _services = null;
     protected static Map<String, ApiDiscoveryResponse> 
s_apiNameDiscoveryResponseMap = null;
+    public static final List<String> APIS_ALLOWED_FOR_PASSWORD_CHANGE = 
Arrays.asList("login", "logout", "updateUser", "listApis");

Review Comment:
   The PR description states that the ListApis response should contain 5 APIs 
including "listUsers": ["login", "logout", "updateUser", "listUsers", 
"listApis"]. However, this constant only includes 4 APIs and is missing 
"listUsers". Either the constant needs to be updated to include "listUsers", or 
the PR description needs to be corrected.
   ```suggestion
       public static final List<String> APIS_ALLOWED_FOR_PASSWORD_CHANGE = 
Arrays.asList("login", "logout", "updateUser", "listUsers", "listApis");
   ```



##########
ui/src/views/iam/ChangeUserPassword.vue:
##########
@@ -49,6 +49,11 @@
             v-model:value="form.confirmpassword"
             :placeholder="$t('label.confirmpassword.description')"/>
         </a-form-item>
+        <a-form-item v-if="isAdminOrDomainAdmin() && isCallerNotSameAsUser()" 
name="passwordChangeRequired" ref="passwordChangeRequired">
+            <a-checkbox v-model:checked="form.passwordChangeRequired">
+              {{ $t('label.change.password.onlogin') }}
+            </a-checkbox>
+        </a-form-item>

Review Comment:
   The condition should also verify that the target user has a normal user role 
(resource.accounttype === 0) before displaying this checkbox. According to the 
PR description, password change enforcement is only applicable to normal users, 
excluding admin and domain admin.



##########
ui/src/config/section/user.js:
##########
@@ -82,6 +82,23 @@ export default {
       popup: true,
       component: shallowRef(defineAsyncComponent(() => 
import('@/views/iam/EditUser.vue')))
     },
+    {
+      api: 'updateUser',
+      icon: 'redo-outlined',
+      label: 'label.change.password.enforce',
+      dataView: true,
+      args: ['passwordchangerequired'],
+      mapping: {
+        passwordchangerequired: {
+          value: (record) => { return true }
+        }
+      },
+      popup: true,
+      show: (record, store) => {
+        return ['Admin', 'DomainAdmin'].includes(store.userInfo.roletype) &&
+          !record.isdefault && (store.userInfo.id !== record.id)

Review Comment:
   The show condition should also verify that the target user has a normal user 
role (accounttype === 0) before displaying this action. According to the PR 
description, password change enforcement is only applicable to normal users, 
excluding admin and domain admin. Consider adding a check like: 
record.accounttype === 0
   ```suggestion
             !record.isdefault &&
             (store.userInfo.id !== record.id) &&
             record.accounttype === 0
   ```



##########
ui/src/views/iam/ForceChangePassword.vue:
##########
@@ -0,0 +1,269 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+<template>
+  <div class="user-layout-wrapper">
+    <div class="container">
+      <div class="user-layout-content">
+        <a-card :bordered="false" class="force-password-card">
+          <template #title>
+            <div style="text-align: center; font-size: 18px; font-weight: 
bold;">
+              {{ $t('label.action.change.password') }}
+            </div>
+            <div v-if="!isSubmitted" style="text-align: center; font-size: 
14px; color: #666; margin-top: 5px;">
+              {{ $t('message.change.password.required') }}
+            </div>
+          </template>
+          <a-spin :spinning="loading">
+          <div v-if="isSubmitted" class="success-state">
+            <check-outlined class="success-icon" />
+            <div class="success-text">
+              {{ $t('message.success.change.password') }}
+            </div>
+            <div class="success-subtext">
+               {{ $t('message.please.login.new.password') }}
+            </div>
+            <a-button
+              type="primary"
+              size="large"
+              block
+              @click="redirectToLogin()"
+              style="margin-top: 20px;"
+            >
+              {{ $t('label.login') }}
+            </a-button>
+          </div>
+
+          <a-form
+            v-else
+            :ref="formRef"
+            :model="form"
+            :rules="rules"
+            layout="vertical"
+            @finish="handleSubmit"
+            v-ctrl-enter="handleSubmit"
+          >
+            <a-form-item name="currentpassword" 
:label="$t('label.currentpassword')">
+              <a-input-password
+                v-model:value="form.currentpassword"
+                :placeholder="$t('label.currentpassword')"
+                size="large"
+                v-focus="true"
+              />
+            </a-form-item>
+
+            <a-form-item name="password" :label="$t('label.new.password')">
+              <a-input-password
+                v-model:value="form.password"
+                :placeholder="$t('label.new.password')"
+                size="large"
+              />
+            </a-form-item>
+
+            <a-form-item name="confirmpassword" 
:label="$t('label.confirmpassword')">
+              <a-input-password
+                v-model:value="form.confirmpassword"
+                :placeholder="$t('label.confirmpassword')"
+                size="large"
+              />
+            </a-form-item>
+
+            <a-form-item>
+              <a-button
+                type="primary"
+                size="large"
+                block
+                :loading="loading"
+                @click="handleSubmit"
+              >
+                {{ $t('label.ok') }}
+              </a-button>
+            </a-form-item>
+
+            <div class="actions">
+              <a @click="logoutAndRedirectToLogin()">{{ $t('label.logout') 
}}</a>
+            </div>
+          </a-form>
+          </a-spin>
+        </a-card>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { ref, reactive, toRaw } from 'vue'
+import { postAPI } from '@/api'
+import Cookies from 'js-cookie'
+import { PASSWORD_CHANGE_REQUIRED } from '@/store/mutation-types'
+
+export default {
+  name: 'ForceChangePassword',
+  data () {
+    return {
+      loading: false,
+      isSubmitted: false
+    }
+  },
+  created () {
+    this.formRef = ref()
+    this.form = reactive({})
+    this.isPasswordChangeRequired()
+  },
+  computed: {
+    rules () {
+      return {
+        currentpassword: [{ required: true, message: 
this.$t('message.error.current.password') }],
+        password: [{ required: true, message: 
this.$t('message.error.new.password') }],
+        confirmpassword: [
+          { required: true, message: this.$t('message.error.confirm.password') 
},
+          { validator: this.validateTwoPassword, trigger: 'change' }
+        ]
+      }
+    }
+  },
+  methods: {
+    async validateTwoPassword (rule, value) {
+      if (!value || value.length === 0) {
+        return Promise.resolve()
+      } else if (rule.field === 'confirmpassword') {
+        const form = this.form
+        const messageConfirm = this.$t('message.validate.equalto')
+        const passwordVal = form.password
+        if (passwordVal && passwordVal !== value) {
+          return Promise.reject(messageConfirm)
+        } else {
+          return Promise.resolve()
+        }
+      } else {
+        return Promise.resolve()
+      }
+    },
+    handleSubmit (e) {
+      e.preventDefault()
+      if (this.loading) return
+      this.formRef.value.validate().then(() => {
+        this.loading = true
+        const values = toRaw(this.form)
+        const userId = Cookies.get('userid')
+
+        const params = {
+          id: userId,
+          password: values.password,
+          currentpassword: values.currentpassword
+        }
+        postAPI('updateUser', params).then(() => {
+          this.$localStorage.remove(PASSWORD_CHANGE_REQUIRED)
+          this.handleLogout()
+          this.isSubmitted = true
+        }).catch(error => {
+          console.error(error)
+          this.$message.error(this.$t('message.error.change.password'))
+        }).finally(() => {
+          this.loading = false
+        })
+      }).catch(error => {
+        console.log('Validation failed:', error)

Review Comment:
   Remove these console logging statements before merging to production. Debug 
logging should not be left in production code.
   ```suggestion
         }).catch(() => {
           // Validation failed; errors are displayed via form validation rules.
   ```



##########
ui/src/views/iam/ForceChangePassword.vue:
##########
@@ -0,0 +1,269 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+<template>
+  <div class="user-layout-wrapper">
+    <div class="container">
+      <div class="user-layout-content">
+        <a-card :bordered="false" class="force-password-card">
+          <template #title>
+            <div style="text-align: center; font-size: 18px; font-weight: 
bold;">
+              {{ $t('label.action.change.password') }}
+            </div>
+            <div v-if="!isSubmitted" style="text-align: center; font-size: 
14px; color: #666; margin-top: 5px;">
+              {{ $t('message.change.password.required') }}
+            </div>
+          </template>
+          <a-spin :spinning="loading">
+          <div v-if="isSubmitted" class="success-state">
+            <check-outlined class="success-icon" />
+            <div class="success-text">
+              {{ $t('message.success.change.password') }}
+            </div>
+            <div class="success-subtext">
+               {{ $t('message.please.login.new.password') }}
+            </div>
+            <a-button
+              type="primary"
+              size="large"
+              block
+              @click="redirectToLogin()"
+              style="margin-top: 20px;"
+            >
+              {{ $t('label.login') }}
+            </a-button>
+          </div>
+
+          <a-form
+            v-else
+            :ref="formRef"
+            :model="form"
+            :rules="rules"
+            layout="vertical"
+            @finish="handleSubmit"
+            v-ctrl-enter="handleSubmit"
+          >
+            <a-form-item name="currentpassword" 
:label="$t('label.currentpassword')">
+              <a-input-password
+                v-model:value="form.currentpassword"
+                :placeholder="$t('label.currentpassword')"
+                size="large"
+                v-focus="true"
+              />
+            </a-form-item>
+
+            <a-form-item name="password" :label="$t('label.new.password')">
+              <a-input-password
+                v-model:value="form.password"
+                :placeholder="$t('label.new.password')"
+                size="large"
+              />
+            </a-form-item>
+
+            <a-form-item name="confirmpassword" 
:label="$t('label.confirmpassword')">
+              <a-input-password
+                v-model:value="form.confirmpassword"
+                :placeholder="$t('label.confirmpassword')"
+                size="large"
+              />
+            </a-form-item>
+
+            <a-form-item>
+              <a-button
+                type="primary"
+                size="large"
+                block
+                :loading="loading"
+                @click="handleSubmit"
+              >
+                {{ $t('label.ok') }}
+              </a-button>
+            </a-form-item>
+
+            <div class="actions">
+              <a @click="logoutAndRedirectToLogin()">{{ $t('label.logout') 
}}</a>
+            </div>
+          </a-form>
+          </a-spin>
+        </a-card>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { ref, reactive, toRaw } from 'vue'
+import { postAPI } from '@/api'
+import Cookies from 'js-cookie'
+import { PASSWORD_CHANGE_REQUIRED } from '@/store/mutation-types'
+
+export default {
+  name: 'ForceChangePassword',
+  data () {
+    return {
+      loading: false,
+      isSubmitted: false
+    }
+  },
+  created () {
+    this.formRef = ref()
+    this.form = reactive({})
+    this.isPasswordChangeRequired()
+  },
+  computed: {
+    rules () {
+      return {
+        currentpassword: [{ required: true, message: 
this.$t('message.error.current.password') }],
+        password: [{ required: true, message: 
this.$t('message.error.new.password') }],
+        confirmpassword: [
+          { required: true, message: this.$t('message.error.confirm.password') 
},
+          { validator: this.validateTwoPassword, trigger: 'change' }
+        ]
+      }
+    }
+  },
+  methods: {
+    async validateTwoPassword (rule, value) {
+      if (!value || value.length === 0) {
+        return Promise.resolve()
+      } else if (rule.field === 'confirmpassword') {
+        const form = this.form
+        const messageConfirm = this.$t('message.validate.equalto')
+        const passwordVal = form.password
+        if (passwordVal && passwordVal !== value) {
+          return Promise.reject(messageConfirm)
+        } else {
+          return Promise.resolve()
+        }
+      } else {
+        return Promise.resolve()
+      }
+    },
+    handleSubmit (e) {
+      e.preventDefault()
+      if (this.loading) return
+      this.formRef.value.validate().then(() => {
+        this.loading = true
+        const values = toRaw(this.form)
+        const userId = Cookies.get('userid')
+
+        const params = {
+          id: userId,
+          password: values.password,
+          currentpassword: values.currentpassword
+        }
+        postAPI('updateUser', params).then(() => {
+          this.$localStorage.remove(PASSWORD_CHANGE_REQUIRED)
+          this.handleLogout()

Review Comment:
   The handleLogout call should be awaited before setting isSubmitted to true. 
Without await, isSubmitted may be set before the logout completes, potentially 
causing the success state to display while the user is still logged in.
   ```suggestion
           postAPI('updateUser', params).then(async () => {
             this.$localStorage.remove(PASSWORD_CHANGE_REQUIRED)
             await this.handleLogout()
   ```



##########
ui/src/views/iam/ForceChangePassword.vue:
##########
@@ -0,0 +1,269 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+<template>
+  <div class="user-layout-wrapper">
+    <div class="container">
+      <div class="user-layout-content">
+        <a-card :bordered="false" class="force-password-card">
+          <template #title>
+            <div style="text-align: center; font-size: 18px; font-weight: 
bold;">
+              {{ $t('label.action.change.password') }}
+            </div>
+            <div v-if="!isSubmitted" style="text-align: center; font-size: 
14px; color: #666; margin-top: 5px;">
+              {{ $t('message.change.password.required') }}
+            </div>
+          </template>
+          <a-spin :spinning="loading">
+          <div v-if="isSubmitted" class="success-state">
+            <check-outlined class="success-icon" />

Review Comment:
   The component 'check-outlined' is used but not imported. You need to import 
'CheckOutlined' from '@ant-design/icons-vue' in the script section for this 
component to work properly.



##########
ui/src/views/iam/AddUser.vue:
##########
@@ -384,6 +389,10 @@ export default {
       if (this.isValidValueForKey(rawParams, 'timezone') && 
rawParams.timezone.length > 0) {
         params.timezone = rawParams.timezone
       }
+      console.log('rawParams.passwordChangeRequired', 
rawParams.passwordChangeRequired)

Review Comment:
   Remove this console.log statement before merging to production. Debug 
logging should not be left in production code.
   ```suggestion
   
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to