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

dahn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/main by this push:
     new 73c3339bf15 List users by their authentication source (#10115)
73c3339bf15 is described below

commit 73c3339bf157dbafe25eec5eee148231605d34ef
Author: Bernardo De Marco Gonçalves <[email protected]>
AuthorDate: Thu Dec 19 10:12:55 2024 -0300

    List users by their authentication source (#10115)
---
 .../api/command/admin/user/ListUsersCmd.java       |  24 ++++
 .../cloudstack/api/response/UserResponse.java      |   2 +-
 .../java/com/cloud/api/query/QueryManagerImpl.java |  12 +-
 .../com/cloud/api/query/QueryManagerImplTest.java  |   3 +
 ui/public/locales/en.json                          |   4 +
 ui/src/components/view/DetailsTab.vue              |  12 ++
 ui/src/components/view/SearchView.vue              |  30 ++++-
 ui/src/config/section/user.js                      |  23 +++-
 ui/src/views/iam/AddUser.vue                       | 124 +++++++++++----------
 9 files changed, 167 insertions(+), 67 deletions(-)

diff --git 
a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUsersCmd.java
 
b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUsersCmd.java
index 27a78c738c9..2f29b1ec1e4 100644
--- 
a/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUsersCmd.java
+++ 
b/api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUsersCmd.java
@@ -16,9 +16,11 @@
 // under the License.
 package org.apache.cloudstack.api.command.admin.user;
 
+import com.cloud.exception.InvalidParameterValueException;
 import com.cloud.server.ResourceIcon;
 import com.cloud.server.ResourceTag;
 import com.cloud.user.Account;
+import com.cloud.user.User;
 import org.apache.cloudstack.acl.RoleType;
 import org.apache.cloudstack.api.command.user.UserCmd;
 import org.apache.cloudstack.api.response.ResourceIconResponse;
@@ -30,6 +32,7 @@ import org.apache.cloudstack.api.Parameter;
 import org.apache.cloudstack.api.ResponseObject.ResponseView;
 import org.apache.cloudstack.api.response.ListResponse;
 import org.apache.cloudstack.api.response.UserResponse;
+import org.apache.commons.lang3.EnumUtils;
 
 import java.util.List;
 
@@ -63,6 +66,10 @@ public class ListUsersCmd extends 
BaseListAccountResourcesCmd implements UserCmd
             description = "flag to display the resource icon for users")
     private Boolean showIcon;
 
+    @Parameter(name = ApiConstants.USER_SOURCE, type = CommandType.STRING, 
since = "4.21.0.0",
+            description = "List users by their authentication source. Valid 
values are: native, ldap, saml2 and saml2disabled.")
+    private String userSource;
+
     /////////////////////////////////////////////////////
     /////////////////// Accessors ///////////////////////
     /////////////////////////////////////////////////////
@@ -91,6 +98,23 @@ public class ListUsersCmd extends 
BaseListAccountResourcesCmd implements UserCmd
         return showIcon != null ? showIcon : false;
     }
 
+    public User.Source getUserSource() {
+        if (userSource == null) {
+            return null;
+        }
+
+        User.Source source = EnumUtils.getEnumIgnoreCase(User.Source.class, 
userSource);
+        if (source == null || List.of(User.Source.OAUTH2, 
User.Source.UNKNOWN).contains(source)) {
+            throw new InvalidParameterValueException(String.format("Invalid 
user source: %s. Valid values are: native, ldap, saml2 and saml2disabled.", 
userSource));
+        }
+
+        if (source == User.Source.NATIVE) {
+            return User.Source.UNKNOWN;
+        }
+
+        return source;
+    }
+
     /////////////////////////////////////////////////////
     /////////////// API Implementation///////////////////
     /////////////////////////////////////////////////////
diff --git 
a/api/src/main/java/org/apache/cloudstack/api/response/UserResponse.java 
b/api/src/main/java/org/apache/cloudstack/api/response/UserResponse.java
index df97a915700..5e4e6e1f3c8 100644
--- a/api/src/main/java/org/apache/cloudstack/api/response/UserResponse.java
+++ b/api/src/main/java/org/apache/cloudstack/api/response/UserResponse.java
@@ -67,7 +67,7 @@ public class UserResponse extends BaseResponse implements 
SetResourceIconRespons
     @Param(description = "the account type of the user")
     private Integer accountType;
 
-    @SerializedName("usersource")
+    @SerializedName(ApiConstants.USER_SOURCE)
     @Param(description = "the source type of the user in lowercase, such as 
native, ldap, saml2")
     private String userSource;
 
diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java 
b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
index 976d3817a0a..69979bd89c1 100644
--- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
+++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
@@ -695,7 +695,7 @@ public class QueryManagerImpl extends 
MutualExclusiveIdsManagerBase implements Q
         String keyword = null;
 
         Pair<List<UserAccountJoinVO>, Integer> result =  
getUserListInternal(caller, permittedAccounts, listAll, id,
-                username, type, accountName, state, keyword, null, domainId, 
recursive, null);
+                username, type, accountName, state, keyword, null, domainId, 
recursive, null, null);
         ListResponse<UserResponse> response = new ListResponse<UserResponse>();
         List<UserResponse> userResponses = 
ViewResponseHelper.createUserResponse(ResponseView.Restricted, 
CallContext.current().getCallingAccount().getDomainId(),
                 result.first().toArray(new 
UserAccountJoinVO[result.first().size()]));
@@ -723,6 +723,7 @@ public class QueryManagerImpl extends 
MutualExclusiveIdsManagerBase implements Q
         Object state = cmd.getState();
         String keyword = cmd.getKeyword();
         String apiKeyAccess = cmd.getApiKeyAccess();
+        User.Source userSource = cmd.getUserSource();
 
         Long domainId = cmd.getDomainId();
         boolean recursive = cmd.isRecursive();
@@ -731,11 +732,11 @@ public class QueryManagerImpl extends 
MutualExclusiveIdsManagerBase implements Q
 
         Filter searchFilter = new Filter(UserAccountJoinVO.class, "id", true, 
startIndex, pageSizeVal);
 
-        return getUserListInternal(caller, permittedAccounts, listAll, id, 
username, type, accountName, state, keyword, apiKeyAccess, domainId, recursive, 
searchFilter);
+        return getUserListInternal(caller, permittedAccounts, listAll, id, 
username, type, accountName, state, keyword, apiKeyAccess, domainId, recursive, 
searchFilter, userSource);
     }
 
     private Pair<List<UserAccountJoinVO>, Integer> getUserListInternal(Account 
caller, List<Long> permittedAccounts, boolean listAll, Long id, Object 
username, Object type,
-            String accountName, Object state, String keyword, String 
apiKeyAccess, Long domainId, boolean recursive, Filter searchFilter) {
+            String accountName, Object state, String keyword, String 
apiKeyAccess, Long domainId, boolean recursive, Filter searchFilter, 
User.Source userSource) {
         Ternary<Long, Boolean, ListProjectResourcesCriteria> 
domainIdRecursiveListProject = new Ternary<Long, Boolean, 
ListProjectResourcesCriteria>(domainId, recursive, null);
         accountMgr.buildACLSearchParameters(caller, id, accountName, null, 
permittedAccounts, domainIdRecursiveListProject, listAll, false);
         domainId = domainIdRecursiveListProject.first();
@@ -761,6 +762,7 @@ public class QueryManagerImpl extends 
MutualExclusiveIdsManagerBase implements Q
         sb.and("domainId", sb.entity().getDomainId(), Op.EQ);
         sb.and("accountName", sb.entity().getAccountName(), Op.EQ);
         sb.and("state", sb.entity().getState(), Op.EQ);
+        sb.and("userSource", sb.entity().getSource(), Op.EQ);
         if (apiKeyAccess != null) {
             sb.and("apiKeyAccess", sb.entity().getApiKeyAccess(), Op.EQ);
         }
@@ -827,6 +829,10 @@ public class QueryManagerImpl extends 
MutualExclusiveIdsManagerBase implements Q
             }
         }
 
+        if (userSource != null) {
+            sc.setParameters("userSource", userSource.toString());
+        }
+
         return _userAccountJoinDao.searchAndCount(sc, searchFilter);
     }
 
diff --git a/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java 
b/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java
index 42ea1ad4556..5bfb0553040 100644
--- a/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java
+++ b/server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java
@@ -505,11 +505,13 @@ public class QueryManagerImplTest {
         Account.Type accountType = Account.Type.ADMIN;
         Long domainId = 1L;
         String apiKeyAccess = "Disabled";
+        User.Source userSource = User.Source.NATIVE;
         Mockito.when(cmd.getUsername()).thenReturn(username);
         Mockito.when(cmd.getAccountName()).thenReturn(accountName);
         Mockito.when(cmd.getAccountType()).thenReturn(accountType);
         Mockito.when(cmd.getDomainId()).thenReturn(domainId);
         Mockito.when(cmd.getApiKeyAccess()).thenReturn(apiKeyAccess);
+        Mockito.when(cmd.getUserSource()).thenReturn(userSource);
 
         UserAccountJoinVO user = new UserAccountJoinVO();
         DomainVO domain = Mockito.mock(DomainVO.class);
@@ -531,6 +533,7 @@ public class QueryManagerImplTest {
         Mockito.verify(sc).setParameters("type", accountType);
         Mockito.verify(sc).setParameters("domainId", domainId);
         Mockito.verify(sc).setParameters("apiKeyAccess", false);
+        Mockito.verify(sc).setParameters("userSource", userSource.toString());
         Mockito.verify(userAccountJoinDao, Mockito.times(1)).searchAndCount(
                 any(SearchCriteria.class), any(Filter.class));
     }
diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index e03aee00599..49f3246461f 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -1320,6 +1320,7 @@
 "label.lbprovider": "Load balancer provider",
 "label.lbruleid": "Load balancer ID",
 "label.lbtype": "Load balancer type",
+"label.ldap": "LDAP",
 "label.ldap.configuration": "LDAP configuration",
 "label.ldap.group.name": "LDAP group",
 "label.level": "Level",
@@ -1487,6 +1488,7 @@
 "label.name": "Name",
 "label.name.optional": "Name (Optional)",
 "label.nat": "BigSwitch BCF NAT enabled",
+"label.native": "Native",
 "label.ncc": "NCC",
 "label.netmask": "Netmask",
 "label.netscaler": "NetScaler",
@@ -1984,7 +1986,9 @@
 "label.s3.secret.key": "Secret key",
 "label.s3.socket.timeout": "Socket timeout",
 "label.s3.use.https": "Use HTTPS",
+"label.saml": "SAML",
 "label.saml.disable": "SAML disable",
+"label.saml.disabled": "SAML Disabled",
 "label.saml.enable": "SAML enable",
 "label.samlenable": "Authorize SAML SSO",
 "label.samlentity": "Identity provider",
diff --git a/ui/src/components/view/DetailsTab.vue 
b/ui/src/components/view/DetailsTab.vue
index f5f180c8f19..3622f87e67d 100644
--- a/ui/src/components/view/DetailsTab.vue
+++ b/ui/src/components/view/DetailsTab.vue
@@ -124,6 +124,9 @@
               </div>
             </div>
           </div>
+          <div v-else-if="item === 'usersource'">
+            {{ $t(getUserSourceLabel(dataResource[item])) }}
+          </div>
           <div v-else>{{ dataResource[item] }}</div>
         </div>
       </a-list-item>
@@ -406,6 +409,15 @@ export default {
       })
 
       return resources
+    },
+    getUserSourceLabel (source) {
+      if (source === 'saml2') {
+        source = 'saml'
+      } else if (source === 'saml2disabled') {
+        source = 'saml.disabled'
+      }
+
+      return `label.${source}`
     }
   }
 }
diff --git a/ui/src/components/view/SearchView.vue 
b/ui/src/components/view/SearchView.vue
index a72039393cc..a32a0d1ecb5 100644
--- a/ui/src/components/view/SearchView.vue
+++ b/ui/src/components/view/SearchView.vue
@@ -306,7 +306,8 @@ export default {
         }
         if (['zoneid', 'domainid', 'imagestoreid', 'storageid', 'state', 
'account', 'hypervisor', 'level',
           'clusterid', 'podid', 'groupid', 'entitytype', 'accounttype', 
'systemvmtype', 'scope', 'provider',
-          'type', 'scope', 'managementserverid', 'serviceofferingid', 
'diskofferingid', 'networkid', 'usagetype', 'restartrequired', 
'guestiptype'].includes(item)
+          'type', 'scope', 'managementserverid', 'serviceofferingid', 
'diskofferingid', 'networkid',
+          'usagetype', 'restartrequired', 'guestiptype', 
'usersource'].includes(item)
         ) {
           type = 'list'
         } else if (item === 'tags') {
@@ -435,6 +436,13 @@ export default {
         ]
         this.fields[apiKeyAccessIndex].loading = false
       }
+
+      if (arrayField.includes('usersource')) {
+        const userSourceIndex = this.fields.findIndex(item => item.name === 
'usersource')
+        this.fields[userSourceIndex].loading = true
+        this.fields[userSourceIndex].opts = 
this.fetchAvailableUserSourceTypes()
+        this.fields[userSourceIndex].loading = false
+      }
     },
     async fetchDynamicFieldData (arrayField, searchKeyword) {
       const promises = []
@@ -1294,6 +1302,26 @@ export default {
           })
       })
     },
+    fetchAvailableUserSourceTypes () {
+      return [
+        {
+          id: 'native',
+          name: 'label.native'
+        },
+        {
+          id: 'saml2',
+          name: 'label.saml'
+        },
+        {
+          id: 'saml2disabled',
+          name: 'label.saml.disabled'
+        },
+        {
+          id: 'ldap',
+          name: 'label.ldap'
+        }
+      ]
+    },
     onSearch (value) {
       this.paramsFilter = {}
       this.searchQuery = value
diff --git a/ui/src/config/section/user.js b/ui/src/config/section/user.js
index 60a55973f8c..a18994fd6ce 100644
--- a/ui/src/config/section/user.js
+++ b/ui/src/config/section/user.js
@@ -17,6 +17,7 @@
 
 import { shallowRef, defineAsyncComponent } from 'vue'
 import store from '@/store'
+import { i18n } from '@/locales'
 
 export default {
   name: 'accountuser',
@@ -26,13 +27,31 @@ export default {
   hidden: true,
   permission: ['listUsers'],
   searchFilters: () => {
-    var filters = []
+    const filters = ['usersource']
     if (store.getters.userInfo.roletype === 'Admin') {
       filters.push('apikeyaccess')
     }
     return filters
   },
-  columns: ['username', 'state', 'firstname', 'lastname', 'email', 'account', 
'domain'],
+  columns: [
+    'username', 'state', 'firstname', 'lastname',
+    'email', 'account', 'domain',
+    {
+      field: 'userSource',
+      customTitle: 'userSource',
+      userSource: (record) => {
+        let { usersource: source } = record
+
+        if (source === 'saml2') {
+          source = 'saml'
+        } else if (source === 'saml2disabled') {
+          source = 'saml.disabled'
+        }
+
+        return i18n.global.t(`label.${source}`)
+      }
+    }
+  ],
   details: ['username', 'id', 'firstname', 'lastname', 'email', 'usersource', 
'timezone', 'rolename', 'roletype', 'is2faenabled', 'account', 'domain', 
'created'],
   tabs: [
     {
diff --git a/ui/src/views/iam/AddUser.vue b/ui/src/views/iam/AddUser.vue
index 49bca327896..e922c404ca9 100644
--- a/ui/src/views/iam/AddUser.vue
+++ b/ui/src/views/iam/AddUser.vue
@@ -313,75 +313,79 @@ export default {
     isValidValueForKey (obj, key) {
       return key in obj && obj[key] != null
     },
-    handleSubmit (e) {
+    async handleSubmit (e) {
       e.preventDefault()
       if (this.loading) return
-      this.formRef.value.validate().then(() => {
-        const values = toRaw(this.form)
-        this.loading = true
-        const params = {
-          username: values.username,
-          password: values.password,
-          email: values.email,
-          firstname: values.firstname,
-          lastname: values.lastname,
-          accounttype: 0
-        }
-
-        if (this.account) {
-          params.account = this.account
-        } else if (this.accountList[values.account]) {
-          params.account = this.accountList[values.account].name
-        }
 
-        if (this.domainid) {
-          params.domainid = this.domainid
-        } else if (values.domainid) {
-          params.domainid = values.domainid
-        }
+      await this.formRef.value.validate()
+        .catch(error => 
this.formRef.value.scrollToField(error.errorFields[0].name))
 
-        if (this.isValidValueForKey(values, 'timezone') && 
values.timezone.length > 0) {
-          params.timezone = values.timezone
-        }
+      this.loading = true
+      const values = toRaw(this.form)
+      try {
+        const userCreationResponse = await this.createUser(values)
+        this.$notification.success({
+          message: this.$t('label.create.user'),
+          description: `${this.$t('message.success.create.user')} 
${values.username}`
+        })
 
-        api('createUser', {}, 'POST', params).then(response => {
-          this.$emit('refresh-data')
+        const user = userCreationResponse?.createuserresponse?.user
+        if (values.samlenable && user) {
+          await api('authorizeSamlSso', {
+            enable: values.samlenable,
+            entityid: values.samlentity,
+            userid: user.id
+          })
           this.$notification.success({
-            message: this.$t('label.create.user'),
-            description: `${this.$t('message.success.create.user')} 
${params.username}`
+            message: this.$t('label.samlenable'),
+            description: this.$t('message.success.enable.saml.auth')
           })
-          const user = response.createuserresponse.user
-          if (values.samlenable && user) {
-            api('authorizeSamlSso', {
-              enable: values.samlenable,
-              entityid: values.samlentity,
-              userid: user.id
-            }).then(response => {
-              this.$notification.success({
-                message: this.$t('label.samlenable'),
-                description: this.$t('message.success.enable.saml.auth')
-              })
-            }).catch(error => {
-              this.$notification.error({
-                message: this.$t('message.request.failed'),
-                description: (error.response && error.response.headers && 
error.response.headers['x-description']) || error.message,
-                duration: 0
-              })
-            })
-          }
+        }
+
+        this.closeAction()
+        this.$emit('refresh-data')
+      } catch (error) {
+        if (error?.config?.params?.command === 'authorizeSamlSso') {
           this.closeAction()
-        }).catch(error => {
-          this.$notification.error({
-            message: this.$t('message.request.failed'),
-            description: (error.response && error.response.headers && 
error.response.headers['x-description']) || error.message,
-            duration: 0
-          })
-        }).finally(() => {
-          this.loading = false
+          this.$emit('refresh-data')
+        }
+
+        this.$notification.error({
+          message: this.$t('message.request.failed'),
+          description: error?.response?.headers['x-description'] || 
error.message,
+          duration: 0
         })
-      }).catch(error => {
-        this.formRef.value.scrollToField(error.errorFields[0].name)
-      })
+      } finally {
+        this.loading = false
+      }
+    },
+    async createUser (rawParams) {
+      const params = {
+        username: rawParams.username,
+        password: rawParams.password,
+        email: rawParams.email,
+        firstname: rawParams.firstname,
+        lastname: rawParams.lastname,
+        accounttype: 0
+      }
+
+      if (this.account) {
+        params.account = this.account
+      } else if (this.accountList[rawParams.account]) {
+        params.account = this.accountList[rawParams.account].name
+      }
+
+      if (this.domainid) {
+        params.domainid = this.domainid
+      } else if (rawParams.domainid) {
+        params.domainid = rawParams.domainid
+      }
+
+      if (this.isValidValueForKey(rawParams, 'timezone') && 
rawParams.timezone.length > 0) {
+        params.timezone = rawParams.timezone
+      }
+
+      return api('createUser', {}, 'POST', params)
     },
     async validateConfirmPassword (rule, value) {
       if (!value || value.length === 0) {

Reply via email to