List users by their authentication source (#10115)

This commit is contained in:
Bernardo De Marco Gonçalves 2024-12-19 10:12:55 -03:00 committed by GitHub
parent 54bc150140
commit 73c3339bf1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 171 additions and 71 deletions

View File

@ -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///////////////////
/////////////////////////////////////////////////////

View File

@ -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;

View File

@ -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);
}

View File

@ -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));
}

View File

@ -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",

View File

@ -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}`
}
}
}

View File

@ -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

View File

@ -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: [
{

View File

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