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
1 change: 1 addition & 0 deletions cwms-data-api/src/main/java/cwms/cda/api/Controllers.java
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ public final class Controllers {
public static final String PREFIX = "prefix";
public static final String PROJECT_LIKE = "project-like";

public static final String USERNAME_LIKE = "username-like";
public static final String APPLICATION_ID = "application-id";
public static final String REVOKE_EXISTING = "revoke-existing";
public static final String REVOKE_TIMEOUT = "revoke-timeout";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,27 @@
import static cwms.cda.api.Controllers.*;
import static cwms.cda.data.dao.JooqDao.getDslContext;

import java.util.List;

import javax.servlet.http.HttpServletResponse;
import org.jooq.DSLContext;

import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;

import cwms.cda.ApiServlet;
import cwms.cda.api.ClobController;
import cwms.cda.api.Controllers;
import cwms.cda.api.errors.CdaError;
import cwms.cda.data.dao.UserDao;
import cwms.cda.data.dto.Clobs;
import cwms.cda.data.dto.CwmsDTOPaginated;
import cwms.cda.data.dto.auth.ApiKey;
import cwms.cda.data.dto.auth.users.User;
import cwms.cda.data.dto.auth.users.Users;
import cwms.cda.formatters.ContentType;
import cwms.cda.formatters.Formats;
import cwms.cda.security.Role;
import io.javalin.apibuilder.CrudHandler;
import io.javalin.core.security.RouteRole;
import io.javalin.core.util.Header;
import io.javalin.http.Context;
import io.javalin.http.HttpCode;
import io.javalin.plugin.openapi.annotations.OpenApi;
import io.javalin.plugin.openapi.annotations.OpenApiContent;
import io.javalin.plugin.openapi.annotations.OpenApiParam;
import io.javalin.plugin.openapi.annotations.OpenApiRequestBody;
import io.javalin.plugin.openapi.annotations.OpenApiResponse;
import io.javalin.plugin.openapi.annotations.OpenApiSecurity;

Expand Down Expand Up @@ -66,9 +57,12 @@ public void delete(Context ctx, String username) {

@OpenApi(
queryParams = {
@OpenApiParam(allowEmptyValue = true, name = OFFICE, type = String.class,
@OpenApiParam(allowEmptyValue = true, name = OFFICE,
description = "Show only users with active privileges in a given office."
+ Controllers.OFFICE_DESCRIPTION ),
@OpenApiParam(name = USERNAME_LIKE,
description = "Posix <a href=\"regexp.html\">regular expression</a> "
+ " matching against the username"),
@OpenApiParam(name = PAGE,
description = "This end point can return a lot of data, this "
+ "identifies where in the request you are. This is an opaque"
Expand Down Expand Up @@ -105,6 +99,7 @@ public void getAll(Context ctx) {
try (final Timer.Context ignored = markAndTime(GET_ALL)) {
DSLContext dsl = getDslContext(ctx);
String office = ctx.queryParam(OFFICE);
String usernameRegex = ctx.queryParam(USERNAME_LIKE);

String formatHeader = ctx.header(Header.ACCEPT);
ContentType contentType = Formats.parseHeader(formatHeader, Users.class);
Expand All @@ -125,7 +120,7 @@ public void getAll(Context ctx) {
Boolean.class, false, metrics,
name(UsersController.class.getName(), GET_ALL));
UserDao dao = new UserDao(dsl);
Users users = dao.getAll(cursor, pageSize, office, includeRoles);
Users users = dao.getAll(cursor, pageSize, office, includeRoles, usernameRegex);

String result = Formats.format(contentType, users);

Expand Down
45 changes: 26 additions & 19 deletions cwms-data-api/src/main/java/cwms/cda/data/dao/UserDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.util.Map;
import java.util.Optional;

import cwms.cda.data.dto.auth.users.UsersPageCursor;
import org.jooq.CommonTableExpression;
import org.jooq.Condition;
import org.jooq.DSLContext;
Expand Down Expand Up @@ -133,7 +134,7 @@ public List<String> getRoles() {
});
}

public Users getAll(String cursor, int pageSize, String office, boolean includeRoles) {
public Users getAll(String cursor, int pageSize, String office, boolean includeRoles, String usernameRegex) {
final AV_SEC_USERS vUserGroups = AV_SEC_USERS.AV_SEC_USERS.as("ug");
final Table<?> vUsers = AT_SEC_CWMS_USERS.as("ut");
final Field<String> userId = field(name(vUsers.getName(),"USERID"), String.class);
Expand All @@ -148,6 +149,7 @@ public Users getAll(String cursor, int pageSize, String office, boolean includeR
String cursorUserId = null;
int pageSizeTmp = pageSize;
String limitOffice = null;
String pageUsernameRegex = usernameRegex;

Condition whereClause = office == null ? DSL.noCondition()
// If we are including only those users with permissions to a specific office
Expand All @@ -158,6 +160,11 @@ public Users getAll(String cursor, int pageSize, String office, boolean includeR
.and(vUserGroups.IS_MEMBER.eq("T")).asField().gt(1)
;

// Apply username regex filter (case-insensitive) if provided
if (usernameRegex != null && !usernameRegex.isEmpty()) {
whereClause = whereClause.and(JooqDao.caseInsensitiveLikeRegexNullTrue(userId, usernameRegex));
}

if (cursor == null || cursor.isEmpty()) {
SelectConditionStep<Record1<Integer>> count = dsl.select(count(asterisk()))
.from(vUsers)
Expand All @@ -168,27 +175,27 @@ public Users getAll(String cursor, int pageSize, String office, boolean includeR
total = rec.value1();
}
} else {
final String[] parts = CwmsDTOPaginated.decodeCursor(cursor, "||");
Optional<UsersPageCursor> pageCursorOpt = CwmsDTOPaginated.decodeCursor(cursor, UsersPageCursor.class);

logger.atFine().log("decoded cursor: " + String.join("||", parts));
for (String p : parts) {
logger.atFinest().log(p);
}

if (parts.length > 1) {
cursorUserId = parts[0];
total = Integer.parseInt(parts[2]);
pageSizeTmp = Integer.parseInt(parts[1]);
limitOffice = parts[3].equals("null") ? null : parts[3];
if(pageCursorOpt.isPresent()) {
UsersPageCursor pageCursor = pageCursorOpt.get();
cursorUserId = pageCursor.getCursorUserId();
total = pageCursor.getTotal();
pageSizeTmp = pageCursor.getPageSize();
limitOffice = pageCursor.getLimitOffice();
pageUsernameRegex = pageCursor.getUsernameRegex();

// Rebuild the where clause to match the initial conditions
whereClause = limitOffice == null ? DSL.noCondition()
// If we are including only those users with permissions to a specific office
// we limit to those users that also have an entry in the at_sec_cwms_users_group table.
: dsl.select(count(asterisk()))
.from(vUserGroups)
.where(upper(vUserGroups.DB_OFFICE_ID).eq(upper(limitOffice)))
.and(vUserGroups.IS_MEMBER.eq("T")).asField().gt(1);
// If we are including only those users with permissions to a specific office
// we limit to those users that also have an entry in the at_sec_cwms_users_group table.
: dsl.select(count(asterisk()))
.from(vUserGroups)
.where(upper(vUserGroups.DB_OFFICE_ID).eq(upper(limitOffice)))
.and(vUserGroups.IS_MEMBER.eq("T")).asField().gt(1);
if (pageUsernameRegex != null && !pageUsernameRegex.isEmpty()) {
whereClause = whereClause.and(JooqDao.caseInsensitiveLikeRegexNullTrue(userId, pageUsernameRegex));
}
}
}

Expand Down Expand Up @@ -230,7 +237,7 @@ public Users getAll(String cursor, int pageSize, String office, boolean includeR

logger.atFine().log("%s", lazy(() -> query.getSQL(ParamType.INLINED)));

final Users.Builder builder = new Users.Builder(cursor, pageSizeTmp, total, limitOffice);
final Users.Builder builder = new Users.Builder(cursor, pageSizeTmp, total, limitOffice, pageUsernameRegex);

final Map<String, User.Builder> tmpUsers = new LinkedHashMap<>();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package cwms.cda.data.dto;

import com.fasterxml.jackson.annotation.JsonProperty;

import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Base64;
import java.util.Base64.Decoder;
import java.util.Base64.Encoder;
import com.google.common.flogger.FluentLogger;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -132,6 +135,21 @@ public static String[] decodeCursor(String cursor, String delimiter) {
return new String[0];
}

public static <T extends PageCursor> Optional<T> decodeCursor(String cursor, Class<T> clazz) {
return decodeCursor(cursor, CwmsDTOPaginated.delimiter, clazz);
}

public static <T extends PageCursor> Optional<T> decodeCursor(String cursor, String delimiter, Class<T> clazz) {
try {
T typed = clazz.getDeclaredConstructor().newInstance();
typed.decodeCursor(cursor, delimiter);
return Optional.of(typed);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
logger.atInfo().withCause(e).log("Failed to instantiate cursor class %s", clazz.getName());
}
return Optional.empty();
}

public static String encodeCursor(String page, int pageSize) {
return encodeCursor(CwmsDTOPaginated.delimiter, page, pageSize);
}
Expand All @@ -150,6 +168,14 @@ public static String encodeCursor(String delimiter, Object ... parts)
encoder.encodeToString(Arrays.stream(parts).map(String::valueOf).collect(Collectors.joining(delimiter)).getBytes());
}

public static String encodeCursor(PageCursor pageCursor) {
return encodeCursor(CwmsDTOPaginated.delimiter, pageCursor);
}

public static String encodeCursor(String delimiter, PageCursor cursor) {
return cursor.encode(encoder, delimiter);
}

public static class CursorCheck implements Function1<String,Boolean> {
private static Pattern base64 = Pattern.compile("^[-A-Za-z0-9+/]*={0,3}$");
@Override
Expand Down
38 changes: 38 additions & 0 deletions cwms-data-api/src/main/java/cwms/cda/data/dto/PageCursor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package cwms.cda.data.dto;

import java.util.Base64.Encoder;

public interface PageCursor {
/**
* Decodes the provided cursor string using the specified delimiter and sets the appropriate fields in the implementing class.
* @param cursor the encoded cursor string to decode
* @param delimiter the delimiter used to separate fields in the encoded cursor string. By default, this is ||
*/
void decodeCursor(String cursor, String delimiter);

/**
* Encodes the fields of the implementing class into a cursor string using the specified encoder and delimiter.
* @param encoder the Base64 Encoder to use for encoding the cursor string
* @param delimiter the delimiter used to separate fields in the encoded cursor string. By default, this is ||
* @return the encoded cursor string representing the current state of the implementing class's fields
*/
String encode(Encoder encoder, String delimiter);

/**
* Encodes a field that may be null into a string representation. If the field is null, it returns the string "null". Otherwise, it returns the string representation of the field.
* @param field the field to encode, which may be null
* @return a string representation of the field, where null is represented as "null"
*/
static String encodeNullableField(Object field) {
return field == null ? "null" : field.toString();
}

/**
* Decodes a string representation of a field that may be null. If the input string is "null", it returns null. Otherwise, it returns the input string as is.
* @param field the string representation of the field to decode, where "null" represents a null value
* @return the decoded field, which is null if the input string is "null", or the input string itself if it is not "null". The caller is responsible for converting the string to the appropriate type if necessary.
*/
static String decodeNullableField(String field) {
return "null".equals(field) ? null : field;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,9 @@
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;

import cwms.cda.data.dto.CwmsDTO;
import cwms.cda.data.dto.CwmsDTOPaginated;
import cwms.cda.formatters.annotations.FormattableWith;
import cwms.cda.formatters.json.JsonV1;
import cwms.cda.formatters.json.JsonV2;
import io.swagger.v3.oas.annotations.media.Schema;
import cwms.cda.formatters.Formats;

Expand Down Expand Up @@ -55,12 +53,14 @@ public static class Builder {
private final Users workingUsers;
private final Optional<String> nextPage;
private final String limitOffice;
private final String userNameRegex;


public Builder(String cursor, int pageSize, int total, String limitOffice) {
public Builder(String cursor, int pageSize, int total, String limitOffice, String userNameRegex) {
workingUsers = new Users(cursor, pageSize, total);
this.nextPage = Optional.empty();
this.limitOffice = limitOffice;
this.userNameRegex = userNameRegex;
}

/**
Expand All @@ -78,6 +78,7 @@ public Builder(@JsonProperty("page") String cursor,
workingUsers = new Users(cursor, pageSize, total);
this.nextPage = Optional.of(nextPage != null ? nextPage : "end");
this.limitOffice = null; // Not used when processing existing JSON, value is encoded in next-page for the query.
this.userNameRegex = null; // Not used when processing existing JSON, value is encoded in next-page for the query.
}

public Users build() {
Expand All @@ -87,7 +88,12 @@ public Users build() {
}
else if (this.workingUsers.users.size() == this.workingUsers.pageSize && !this.workingUsers.users.isEmpty()) {
User lastUser = this.workingUsers.users.get(this.workingUsers.users.size() - 1);
this.workingUsers.nextPage = encodeCursor(CwmsDTOPaginated.delimiter, lastUser.getUserName(), this.workingUsers.pageSize, this.workingUsers.total, this.limitOffice);
UsersPageCursor pageCursor = new UsersPageCursor.Builder(lastUser.getUserName(), this.workingUsers.pageSize, this.workingUsers.total)
.withLimitOffice(this.limitOffice)
.withUsernameRegex(this.userNameRegex)
.build();
this.workingUsers.nextPage = CwmsDTOPaginated.encodeCursor(pageCursor);

} else {
this.workingUsers.nextPage = null;
}
Expand Down
Loading
Loading