Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,6 +32,7 @@
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;

Expand Down Expand Up @@ -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 ///////////////////////
/////////////////////////////////////////////////////
Expand Down Expand Up @@ -91,6 +98,23 @@ public Boolean getShowIcon() {
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///////////////////
/////////////////////////////////////////////////////
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
12 changes: 9 additions & 3 deletions server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -695,7 +695,7 @@ public ListResponse<UserResponse> searchForUsers(Long domainId, boolean recursiv
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()]));
Expand Down Expand Up @@ -723,6 +723,7 @@ private Pair<List<UserAccountJoinVO>, Integer> searchForUsersInternal(ListUsersC
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();
Expand All @@ -731,11 +732,11 @@ private Pair<List<UserAccountJoinVO>, Integer> searchForUsersInternal(ListUsersC

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();
Expand All @@ -761,6 +762,7 @@ private Pair<List<UserAccountJoinVO>, Integer> getUserListInternal(Account calle
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);
}
Expand Down Expand Up @@ -827,6 +829,10 @@ private Pair<List<UserAccountJoinVO>, Integer> getUserListInternal(Account calle
}
}

if (userSource != null) {
sc.setParameters("userSource", userSource.toString());
}

return _userAccountJoinDao.searchAndCount(sc, searchFilter);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -505,11 +505,13 @@ public void testSearchForUsers() {
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);
Expand All @@ -531,6 +533,7 @@ public void testSearchForUsers() {
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));
}
Expand Down
4 changes: 4 additions & 0 deletions ui/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions ui/src/components/view/DetailsTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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}`
}
}
}
Expand Down
30 changes: 29 additions & 1 deletion ui/src/components/view/SearchView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down Expand Up @@ -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 = []
Expand Down Expand Up @@ -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
Expand Down
23 changes: 21 additions & 2 deletions ui/src/config/section/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import { shallowRef, defineAsyncComponent } from 'vue'
import store from '@/store'
import { i18n } from '@/locales'

export default {
name: 'accountuser',
Expand All @@ -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: [
{
Expand Down
Loading
Loading