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) {