Skip to content

Commit 73c3339

Browse files
List users by their authentication source (#10115)
1 parent 54bc150 commit 73c3339

File tree

9 files changed

+167
-67
lines changed

9 files changed

+167
-67
lines changed

api/src/main/java/org/apache/cloudstack/api/command/admin/user/ListUsersCmd.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616
// under the License.
1717
package org.apache.cloudstack.api.command.admin.user;
1818

19+
import com.cloud.exception.InvalidParameterValueException;
1920
import com.cloud.server.ResourceIcon;
2021
import com.cloud.server.ResourceTag;
2122
import com.cloud.user.Account;
23+
import com.cloud.user.User;
2224
import org.apache.cloudstack.acl.RoleType;
2325
import org.apache.cloudstack.api.command.user.UserCmd;
2426
import org.apache.cloudstack.api.response.ResourceIconResponse;
@@ -30,6 +32,7 @@
3032
import org.apache.cloudstack.api.ResponseObject.ResponseView;
3133
import org.apache.cloudstack.api.response.ListResponse;
3234
import org.apache.cloudstack.api.response.UserResponse;
35+
import org.apache.commons.lang3.EnumUtils;
3336

3437
import java.util.List;
3538

@@ -63,6 +66,10 @@ public class ListUsersCmd extends BaseListAccountResourcesCmd implements UserCmd
6366
description = "flag to display the resource icon for users")
6467
private Boolean showIcon;
6568

69+
@Parameter(name = ApiConstants.USER_SOURCE, type = CommandType.STRING, since = "4.21.0.0",
70+
description = "List users by their authentication source. Valid values are: native, ldap, saml2 and saml2disabled.")
71+
private String userSource;
72+
6673
/////////////////////////////////////////////////////
6774
/////////////////// Accessors ///////////////////////
6875
/////////////////////////////////////////////////////
@@ -91,6 +98,23 @@ public Boolean getShowIcon() {
9198
return showIcon != null ? showIcon : false;
9299
}
93100

101+
public User.Source getUserSource() {
102+
if (userSource == null) {
103+
return null;
104+
}
105+
106+
User.Source source = EnumUtils.getEnumIgnoreCase(User.Source.class, userSource);
107+
if (source == null || List.of(User.Source.OAUTH2, User.Source.UNKNOWN).contains(source)) {
108+
throw new InvalidParameterValueException(String.format("Invalid user source: %s. Valid values are: native, ldap, saml2 and saml2disabled.", userSource));
109+
}
110+
111+
if (source == User.Source.NATIVE) {
112+
return User.Source.UNKNOWN;
113+
}
114+
115+
return source;
116+
}
117+
94118
/////////////////////////////////////////////////////
95119
/////////////// API Implementation///////////////////
96120
/////////////////////////////////////////////////////

api/src/main/java/org/apache/cloudstack/api/response/UserResponse.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public class UserResponse extends BaseResponse implements SetResourceIconRespons
6767
@Param(description = "the account type of the user")
6868
private Integer accountType;
6969

70-
@SerializedName("usersource")
70+
@SerializedName(ApiConstants.USER_SOURCE)
7171
@Param(description = "the source type of the user in lowercase, such as native, ldap, saml2")
7272
private String userSource;
7373

server/src/main/java/com/cloud/api/query/QueryManagerImpl.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,7 @@ public ListResponse<UserResponse> searchForUsers(Long domainId, boolean recursiv
695695
String keyword = null;
696696

697697
Pair<List<UserAccountJoinVO>, Integer> result = getUserListInternal(caller, permittedAccounts, listAll, id,
698-
username, type, accountName, state, keyword, null, domainId, recursive, null);
698+
username, type, accountName, state, keyword, null, domainId, recursive, null, null);
699699
ListResponse<UserResponse> response = new ListResponse<UserResponse>();
700700
List<UserResponse> userResponses = ViewResponseHelper.createUserResponse(ResponseView.Restricted, CallContext.current().getCallingAccount().getDomainId(),
701701
result.first().toArray(new UserAccountJoinVO[result.first().size()]));
@@ -723,6 +723,7 @@ private Pair<List<UserAccountJoinVO>, Integer> searchForUsersInternal(ListUsersC
723723
Object state = cmd.getState();
724724
String keyword = cmd.getKeyword();
725725
String apiKeyAccess = cmd.getApiKeyAccess();
726+
User.Source userSource = cmd.getUserSource();
726727

727728
Long domainId = cmd.getDomainId();
728729
boolean recursive = cmd.isRecursive();
@@ -731,11 +732,11 @@ private Pair<List<UserAccountJoinVO>, Integer> searchForUsersInternal(ListUsersC
731732

732733
Filter searchFilter = new Filter(UserAccountJoinVO.class, "id", true, startIndex, pageSizeVal);
733734

734-
return getUserListInternal(caller, permittedAccounts, listAll, id, username, type, accountName, state, keyword, apiKeyAccess, domainId, recursive, searchFilter);
735+
return getUserListInternal(caller, permittedAccounts, listAll, id, username, type, accountName, state, keyword, apiKeyAccess, domainId, recursive, searchFilter, userSource);
735736
}
736737

737738
private Pair<List<UserAccountJoinVO>, Integer> getUserListInternal(Account caller, List<Long> permittedAccounts, boolean listAll, Long id, Object username, Object type,
738-
String accountName, Object state, String keyword, String apiKeyAccess, Long domainId, boolean recursive, Filter searchFilter) {
739+
String accountName, Object state, String keyword, String apiKeyAccess, Long domainId, boolean recursive, Filter searchFilter, User.Source userSource) {
739740
Ternary<Long, Boolean, ListProjectResourcesCriteria> domainIdRecursiveListProject = new Ternary<Long, Boolean, ListProjectResourcesCriteria>(domainId, recursive, null);
740741
accountMgr.buildACLSearchParameters(caller, id, accountName, null, permittedAccounts, domainIdRecursiveListProject, listAll, false);
741742
domainId = domainIdRecursiveListProject.first();
@@ -761,6 +762,7 @@ private Pair<List<UserAccountJoinVO>, Integer> getUserListInternal(Account calle
761762
sb.and("domainId", sb.entity().getDomainId(), Op.EQ);
762763
sb.and("accountName", sb.entity().getAccountName(), Op.EQ);
763764
sb.and("state", sb.entity().getState(), Op.EQ);
765+
sb.and("userSource", sb.entity().getSource(), Op.EQ);
764766
if (apiKeyAccess != null) {
765767
sb.and("apiKeyAccess", sb.entity().getApiKeyAccess(), Op.EQ);
766768
}
@@ -827,6 +829,10 @@ private Pair<List<UserAccountJoinVO>, Integer> getUserListInternal(Account calle
827829
}
828830
}
829831

832+
if (userSource != null) {
833+
sc.setParameters("userSource", userSource.toString());
834+
}
835+
830836
return _userAccountJoinDao.searchAndCount(sc, searchFilter);
831837
}
832838

server/src/test/java/com/cloud/api/query/QueryManagerImplTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,11 +505,13 @@ public void testSearchForUsers() {
505505
Account.Type accountType = Account.Type.ADMIN;
506506
Long domainId = 1L;
507507
String apiKeyAccess = "Disabled";
508+
User.Source userSource = User.Source.NATIVE;
508509
Mockito.when(cmd.getUsername()).thenReturn(username);
509510
Mockito.when(cmd.getAccountName()).thenReturn(accountName);
510511
Mockito.when(cmd.getAccountType()).thenReturn(accountType);
511512
Mockito.when(cmd.getDomainId()).thenReturn(domainId);
512513
Mockito.when(cmd.getApiKeyAccess()).thenReturn(apiKeyAccess);
514+
Mockito.when(cmd.getUserSource()).thenReturn(userSource);
513515

514516
UserAccountJoinVO user = new UserAccountJoinVO();
515517
DomainVO domain = Mockito.mock(DomainVO.class);
@@ -531,6 +533,7 @@ public void testSearchForUsers() {
531533
Mockito.verify(sc).setParameters("type", accountType);
532534
Mockito.verify(sc).setParameters("domainId", domainId);
533535
Mockito.verify(sc).setParameters("apiKeyAccess", false);
536+
Mockito.verify(sc).setParameters("userSource", userSource.toString());
534537
Mockito.verify(userAccountJoinDao, Mockito.times(1)).searchAndCount(
535538
any(SearchCriteria.class), any(Filter.class));
536539
}

ui/public/locales/en.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1320,6 +1320,7 @@
13201320
"label.lbprovider": "Load balancer provider",
13211321
"label.lbruleid": "Load balancer ID",
13221322
"label.lbtype": "Load balancer type",
1323+
"label.ldap": "LDAP",
13231324
"label.ldap.configuration": "LDAP configuration",
13241325
"label.ldap.group.name": "LDAP group",
13251326
"label.level": "Level",
@@ -1487,6 +1488,7 @@
14871488
"label.name": "Name",
14881489
"label.name.optional": "Name (Optional)",
14891490
"label.nat": "BigSwitch BCF NAT enabled",
1491+
"label.native": "Native",
14901492
"label.ncc": "NCC",
14911493
"label.netmask": "Netmask",
14921494
"label.netscaler": "NetScaler",
@@ -1984,7 +1986,9 @@
19841986
"label.s3.secret.key": "Secret key",
19851987
"label.s3.socket.timeout": "Socket timeout",
19861988
"label.s3.use.https": "Use HTTPS",
1989+
"label.saml": "SAML",
19871990
"label.saml.disable": "SAML disable",
1991+
"label.saml.disabled": "SAML Disabled",
19881992
"label.saml.enable": "SAML enable",
19891993
"label.samlenable": "Authorize SAML SSO",
19901994
"label.samlentity": "Identity provider",

ui/src/components/view/DetailsTab.vue

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@
124124
</div>
125125
</div>
126126
</div>
127+
<div v-else-if="item === 'usersource'">
128+
{{ $t(getUserSourceLabel(dataResource[item])) }}
129+
</div>
127130
<div v-else>{{ dataResource[item] }}</div>
128131
</div>
129132
</a-list-item>
@@ -406,6 +409,15 @@ export default {
406409
})
407410
408411
return resources
412+
},
413+
getUserSourceLabel (source) {
414+
if (source === 'saml2') {
415+
source = 'saml'
416+
} else if (source === 'saml2disabled') {
417+
source = 'saml.disabled'
418+
}
419+
420+
return `label.${source}`
409421
}
410422
}
411423
}

ui/src/components/view/SearchView.vue

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,8 @@ export default {
306306
}
307307
if (['zoneid', 'domainid', 'imagestoreid', 'storageid', 'state', 'account', 'hypervisor', 'level',
308308
'clusterid', 'podid', 'groupid', 'entitytype', 'accounttype', 'systemvmtype', 'scope', 'provider',
309-
'type', 'scope', 'managementserverid', 'serviceofferingid', 'diskofferingid', 'networkid', 'usagetype', 'restartrequired', 'guestiptype'].includes(item)
309+
'type', 'scope', 'managementserverid', 'serviceofferingid', 'diskofferingid', 'networkid',
310+
'usagetype', 'restartrequired', 'guestiptype', 'usersource'].includes(item)
310311
) {
311312
type = 'list'
312313
} else if (item === 'tags') {
@@ -435,6 +436,13 @@ export default {
435436
]
436437
this.fields[apiKeyAccessIndex].loading = false
437438
}
439+
440+
if (arrayField.includes('usersource')) {
441+
const userSourceIndex = this.fields.findIndex(item => item.name === 'usersource')
442+
this.fields[userSourceIndex].loading = true
443+
this.fields[userSourceIndex].opts = this.fetchAvailableUserSourceTypes()
444+
this.fields[userSourceIndex].loading = false
445+
}
438446
},
439447
async fetchDynamicFieldData (arrayField, searchKeyword) {
440448
const promises = []
@@ -1294,6 +1302,26 @@ export default {
12941302
})
12951303
})
12961304
},
1305+
fetchAvailableUserSourceTypes () {
1306+
return [
1307+
{
1308+
id: 'native',
1309+
name: 'label.native'
1310+
},
1311+
{
1312+
id: 'saml2',
1313+
name: 'label.saml'
1314+
},
1315+
{
1316+
id: 'saml2disabled',
1317+
name: 'label.saml.disabled'
1318+
},
1319+
{
1320+
id: 'ldap',
1321+
name: 'label.ldap'
1322+
}
1323+
]
1324+
},
12971325
onSearch (value) {
12981326
this.paramsFilter = {}
12991327
this.searchQuery = value

ui/src/config/section/user.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import { shallowRef, defineAsyncComponent } from 'vue'
1919
import store from '@/store'
20+
import { i18n } from '@/locales'
2021

2122
export default {
2223
name: 'accountuser',
@@ -26,13 +27,31 @@ export default {
2627
hidden: true,
2728
permission: ['listUsers'],
2829
searchFilters: () => {
29-
var filters = []
30+
const filters = ['usersource']
3031
if (store.getters.userInfo.roletype === 'Admin') {
3132
filters.push('apikeyaccess')
3233
}
3334
return filters
3435
},
35-
columns: ['username', 'state', 'firstname', 'lastname', 'email', 'account', 'domain'],
36+
columns: [
37+
'username', 'state', 'firstname', 'lastname',
38+
'email', 'account', 'domain',
39+
{
40+
field: 'userSource',
41+
customTitle: 'userSource',
42+
userSource: (record) => {
43+
let { usersource: source } = record
44+
45+
if (source === 'saml2') {
46+
source = 'saml'
47+
} else if (source === 'saml2disabled') {
48+
source = 'saml.disabled'
49+
}
50+
51+
return i18n.global.t(`label.${source}`)
52+
}
53+
}
54+
],
3655
details: ['username', 'id', 'firstname', 'lastname', 'email', 'usersource', 'timezone', 'rolename', 'roletype', 'is2faenabled', 'account', 'domain', 'created'],
3756
tabs: [
3857
{

0 commit comments

Comments
 (0)