mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 08:42:29 +01:00
List users by their authentication source (#10115)
This commit is contained in:
parent
54bc150140
commit
73c3339bf1
@ -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///////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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: [
|
||||
{
|
||||
|
||||
@ -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
|
||||
}
|
||||
await this.formRef.value.validate()
|
||||
.catch(error => this.formRef.value.scrollToField(error.errorFields[0].name))
|
||||
|
||||
if (this.domainid) {
|
||||
params.domainid = this.domainid
|
||||
} else if (values.domainid) {
|
||||
params.domainid = values.domainid
|
||||
}
|
||||
|
||||
if (this.isValidValueForKey(values, 'timezone') && values.timezone.length > 0) {
|
||||
params.timezone = values.timezone
|
||||
}
|
||||
|
||||
api('createUser', {}, 'POST', params).then(response => {
|
||||
this.$emit('refresh-data')
|
||||
this.$notification.success({
|
||||
message: this.$t('label.create.user'),
|
||||
description: `${this.$t('message.success.create.user')} ${params.username}`
|
||||
})
|
||||
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()
|
||||
}).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.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}`
|
||||
})
|
||||
}).catch(error => {
|
||||
this.formRef.value.scrollToField(error.errorFields[0].name)
|
||||
})
|
||||
|
||||
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.samlenable'),
|
||||
description: this.$t('message.success.enable.saml.auth')
|
||||
})
|
||||
}
|
||||
|
||||
this.closeAction()
|
||||
this.$emit('refresh-data')
|
||||
} catch (error) {
|
||||
if (error?.config?.params?.command === 'authorizeSamlSso') {
|
||||
this.closeAction()
|
||||
this.$emit('refresh-data')
|
||||
}
|
||||
|
||||
this.$notification.error({
|
||||
message: this.$t('message.request.failed'),
|
||||
description: error?.response?.headers['x-description'] || error.message,
|
||||
duration: 0
|
||||
})
|
||||
} 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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user