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 @@ -13,19 +13,21 @@
import handler.exceptions.BadRequestException;
import handler.model.DescribeSessionTemplatesRequestData;
import handler.model.DescribeSessionTemplatesResponse;
import handler.model.DescribeUsersRequestData;
import handler.model.Error;
import handler.model.FilterToken;
import handler.model.FilterTokenStrict;
import handler.model.SessionTemplate;
import handler.model.User;
import handler.services.SessionTemplateService;
import handler.services.UserService;
import handler.utils.Filter;
import handler.utils.NextToken;
import handler.utils.Sort;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jetty.util.StringUtil;
import org.mariadb.jdbc.util.StringUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
Expand All @@ -34,9 +36,14 @@
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.stream.Collectors;

import static handler.errors.CommonErrorsEnum.BAD_REQUEST_ERROR;
import static handler.errors.DescribeSessionTemplatesErrors.DESCRIBE_SESSION_TEMPLATES_DEFAULT_MESSAGE;
Expand All @@ -46,6 +53,7 @@
@RequiredArgsConstructor
public class DescribeSessionTemplatesController implements DescribeSessionTemplatesApi {
private final SessionTemplateService sessionTemplateService;
private final UserService userService;
private final Filter<DescribeSessionTemplatesRequestData, SessionTemplate> sessionTemplateFilter;
private final Sort<DescribeSessionTemplatesRequestData, SessionTemplate> sessionTemplateSort;
private final AbstractAuthorizationEngine authorizationEngine;
Expand All @@ -67,6 +75,8 @@ public ResponseEntity<DescribeSessionTemplatesResponse> describeSessionTemplates
try {
log.info("Received describeSessionTemplates request: {}", request);

LoginUsernameResolution resolution = resolveLoginUsernameFilters(request);

String username = SecurityContextHolder.getContext().getAuthentication().getName();

List<SessionTemplate> sessionTemplates = new ArrayList<>();
Expand All @@ -89,6 +99,8 @@ public ResponseEntity<DescribeSessionTemplatesResponse> describeSessionTemplates
}
}

filteredSessionTemplates = preFilterExclusions(filteredSessionTemplates, resolution);

filteredSessionTemplates = sessionTemplateFilter.getFiltered(request, filteredSessionTemplates);

if (request.getUserId() != null && authorizationEngine.isAuthorized(PrincipalType.User, username, SystemAction.describeSessionTemplatesForOthers)) {
Expand Down Expand Up @@ -137,4 +149,125 @@ private List<SessionTemplate> getAuthorizedSessionTemplates(List<SessionTemplate
}
return authorizedSessionTemplates;
}

private record LoginUsernameResolution(
Set<String> excludedCreatedByUserIds,
Set<String> excludedLastModifiedByUserIds
) {}

private List<SessionTemplate> preFilterExclusions(List<SessionTemplate> templates, LoginUsernameResolution resolution) {
return templates.stream()
.filter(t -> !resolution.excludedCreatedByUserIds.contains(t.getCreatedBy()))
.filter(t -> !resolution.excludedLastModifiedByUserIds.contains(t.getLastModifiedBy()))
.toList();
}

private LoginUsernameResolution resolveLoginUsernameFilters(DescribeSessionTemplatesRequestData request) {
Set<String> excludedCreatedBy = new HashSet<>();
Set<String> excludedLastModifiedBy = new HashSet<>();

if (!CollectionUtils.isEmpty(request.getCreatedByLoginUsername())) {
ResolvedFilters resolved = resolveLoginUsernameFiltersToUserIdFilters(request.getCreatedByLoginUsername());
request.setCreatedBy(resolved.filters);
excludedCreatedBy.addAll(resolved.excludedUserIds);
}

if (!CollectionUtils.isEmpty(request.getLastModifiedByLoginUsername())) {
ResolvedFilters resolved = resolveLoginUsernameFiltersToUserIdFilters(request.getLastModifiedByLoginUsername());
request.setLastModifiedBy(resolved.filters);
excludedLastModifiedBy.addAll(resolved.excludedUserIds);
}

if (!CollectionUtils.isEmpty(request.getUsersSharedWithLoginUsername())) {
request.setUsersSharedWith(resolveLoginUsernameFiltersForUsersSharedWith(request.getUsersSharedWithLoginUsername()));
}

return new LoginUsernameResolution(excludedCreatedBy, excludedLastModifiedBy);
}

private List<FilterTokenStrict> resolveLoginUsernameFiltersForUsersSharedWith(List<FilterToken> loginUsernameFilters) {
DescribeUsersRequestData userRequest = new DescribeUsersRequestData();
userRequest.setLoginUsernames(loginUsernameFilters.stream()
.map(f -> new FilterToken().operator(FilterToken.OperatorEnum.EQUAL).value(f.getValue()))
.toList());

List<User> users = userService.describeUsers(userRequest).getUsers();
Map<String, String> loginToUserId = users.stream()
.collect(Collectors.toMap(
u -> u.getLoginUsername() != null ? u.getLoginUsername() : u.getUserId(),
User::getUserId,
(a, b) -> a));

List<FilterTokenStrict> result = new ArrayList<>();
for (FilterToken filter : loginUsernameFilters) {
String userId = loginToUserId.get(filter.getValue());
FilterTokenStrict.OperatorEnum resultOp = filter.getOperator() == FilterToken.OperatorEnum.NOT_EQUAL
? FilterTokenStrict.OperatorEnum.NOT_EQUAL
: FilterTokenStrict.OperatorEnum.EQUAL;
result.add(new FilterTokenStrict().operator(resultOp).value(userId != null ? userId : filter.getValue()));
}
return result;
}

private record ResolvedFilters(List<FilterToken> filters, Set<String> excludedUserIds) {}

private ResolvedFilters resolveLoginUsernameFiltersToUserIdFilters(List<FilterToken> loginUsernameFilters) {
List<FilterToken> filters = new ArrayList<>();
Set<String> excludedUserIds = new HashSet<>();

// Group filters by lookup operator type to batch DB calls
Map<FilterToken.OperatorEnum, List<FilterToken>> filtersByLookupOp = new HashMap<>();
filtersByLookupOp.put(FilterToken.OperatorEnum.EQUAL, new ArrayList<>());
filtersByLookupOp.put(FilterToken.OperatorEnum.CONTAINS, new ArrayList<>());

for (FilterToken filter : loginUsernameFilters) {
FilterToken.OperatorEnum op = filter.getOperator();
FilterToken.OperatorEnum lookupOp = (op == FilterToken.OperatorEnum.CONTAINS || op == FilterToken.OperatorEnum.NOT_CONTAINS)
? FilterToken.OperatorEnum.CONTAINS : FilterToken.OperatorEnum.EQUAL;
filtersByLookupOp.get(lookupOp).add(filter);
}


// Batch lookup for each operator type
for (var entry : filtersByLookupOp.entrySet()) {
if (entry.getValue().isEmpty()) continue;

FilterToken.OperatorEnum lookupOp = entry.getKey();
List<FilterToken> filtersForOp = entry.getValue();

DescribeUsersRequestData userRequest = new DescribeUsersRequestData();
userRequest.setLoginUsernames(filtersForOp.stream()
.map(f -> new FilterToken().operator(lookupOp).value(f.getValue()))
.toList());

List<User> users = userService.describeUsers(userRequest).getUsers();
Copy link
Contributor

@mitshiv1905 mitshiv1905 Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A describeUsers() call per filter is too expensive. Can we group the calls by EQUAL, CONTAINS, NOT_CONTAINS? That way we will only need 3 calls at most.


for (FilterToken filter : filtersForOp) {
FilterToken.OperatorEnum op = filter.getOperator();
String filterVal = filter.getValue();

List<User> matchingUsers = users.stream().filter(u -> {
String displayName = u.getLoginUsername() != null ? u.getLoginUsername() : u.getUserId();
if (displayName == null) return false;
return lookupOp == FilterToken.OperatorEnum.EQUAL
? displayName.equals(filterVal)
: displayName.contains(filterVal);
}).toList();

if (matchingUsers.isEmpty()) {
if (op == FilterToken.OperatorEnum.EQUAL || op == FilterToken.OperatorEnum.CONTAINS) {
filters.add(filter);
}
} else if (op == FilterToken.OperatorEnum.NOT_CONTAINS) {
matchingUsers.forEach(u -> excludedUserIds.add(u.getUserId()));
} else {
FilterToken.OperatorEnum resultOp = (op == FilterToken.OperatorEnum.CONTAINS)
? FilterToken.OperatorEnum.EQUAL : op;
matchingUsers.forEach(u ->
filters.add(new FilterToken().operator(resultOp).value(u.getUserId())));
}
}
}
return new ResolvedFilters(filters, excludedUserIds);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,11 @@ public ResponseEntity<DescribeUserInfoResponse> describeUserInfo() {
try {
String username = SecurityContextHolder.getContext().getAuthentication().getName();

boolean usingExternalAuth = false;
if ((StringUtils.isNotEmpty(loginUsernameKey) || StringUtils.isNotEmpty(displayNameKey)
|| StringUtils.isNotEmpty(roleKey) || StringUtils.isNotEmpty(defaultGroupsKey))
&& authServerClientService != null) {
usingExternalAuth = true;
updateUserFromAuthServer(username);
}

Expand All @@ -70,7 +72,8 @@ public ResponseEntity<DescribeUserInfoResponse> describeUserInfo() {
.id(username)
.loginUsername(loginUsername)
.displayName(displayName)
.role(role);
.role(role)
.usingExternalAuth(usingExternalAuth);
return new ResponseEntity<>(describeCurrentUserResponse, HttpStatus.OK);
} catch (UsernameNotFoundException e) {
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ public class Filter<T, U> {
entry("disableRetryOnFailure", new String[]{"disableRetryOnFailure"})
)),
entry(DescribeUsersRequestData.class, Map.ofEntries(
entry("loginUsernames", new String[]{"loginUsername", "userId"}),
entry("userIds", new String[]{"userId"}),
entry("displayNames", new String[]{"displayName"}),
entry("roles", new String[]{"role"}),
Expand Down Expand Up @@ -320,9 +321,6 @@ private boolean filter(Object value, Object filter) {
private boolean isFiltered(U obj, String[] property, Object filter) {
try {
Object object = PropertyAccessorFactory.forBeanPropertyAccess(obj).getPropertyValue(property[0]);
if(object == null) {
return false;
}
if(object instanceof List objects) {
for(Object objectProperty: objects) {
if(objectProperty != null) {
Expand All @@ -342,7 +340,13 @@ else if(filter(objectProperty, filter)) {
return false;
}
else {
return filter(object, filter);
for (String prop : property) {
Object value = PropertyAccessorFactory.forBeanPropertyAccess(obj).getPropertyValue(prop);
if (value != null) {
return filter(value, filter);
}
}
return false;
}
}
catch(NullValueInNestedPathException e) {
Expand Down
23 changes: 21 additions & 2 deletions dcv-access-console-handler/src/main/java/handler/utils/Sort.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,26 @@

@Component
public class Sort<T, U> {
private static final String USER_ID_KEY = "UserId";
private static final String LOGIN_USERNAME_PROPERTY = "loginUsername";
private static final String USER_ID_PROPERTY = "userId";

@AllArgsConstructor
private class ValueComparator implements Comparator<U> {
private SortToken sortToken;
@Override
public int compare(U o1, U o2) {
Object propertyValue1 = PropertyAccessorFactory.forBeanPropertyAccess(o1).getPropertyValue(sortToken.getKey());
Object propertyValue2 = PropertyAccessorFactory.forBeanPropertyAccess(o2).getPropertyValue(sortToken.getKey());
Object propertyValue1;
Object propertyValue2;

if (USER_ID_KEY.equals(sortToken.getKey())) {
propertyValue1 = getLoginUsernameOrUserId(o1);
propertyValue2 = getLoginUsernameOrUserId(o2);
} else {
propertyValue1 = PropertyAccessorFactory.forBeanPropertyAccess(o1).getPropertyValue(sortToken.getKey());
propertyValue2 = PropertyAccessorFactory.forBeanPropertyAccess(o2).getPropertyValue(sortToken.getKey());
}

if(propertyValue1 == null && propertyValue2 == null) {
return 0;
}
Expand All @@ -46,6 +59,12 @@ public int compare(U o1, U o2) {
}
throw new UnsupportedOperationException("Failed to sort: Sorting not defined for " + propertyValue1.getClass());
}

private Object getLoginUsernameOrUserId(U obj) {
Object loginUsername = PropertyAccessorFactory.forBeanPropertyAccess(obj).getPropertyValue(LOGIN_USERNAME_PROPERTY);
if (loginUsername != null) return loginUsername;
return PropertyAccessorFactory.forBeanPropertyAccess(obj).getPropertyValue(USER_ID_PROPERTY);
}
}

public List<U> getSorted(T request, List<U> list) {
Expand Down
Loading