diff --git a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java index 2f838bd155..e95f82877c 100644 --- a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java +++ b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java @@ -101,6 +101,7 @@ import cwms.cda.api.auth.ApiKeyController; import cwms.cda.api.auth.users.UserProfileController; import cwms.cda.api.auth.users.UsersController; +import cwms.cda.api.auth.userlists.UserListMembersController; import cwms.cda.api.auth.users.roles.AddRoleController; import cwms.cda.api.auth.users.roles.DeleteRolesController; import cwms.cda.api.auth.users.roles.GetRolesController; @@ -645,6 +646,7 @@ private void addUserManagementHandlers() { crud("/users/{user-name}", new UsersController(metrics), adminRoles); get("/roles", new GetRolesController(metrics), adminRoles); get("/user/profile", new UserProfileController(metrics), userRoles); + get("/user/list/{user-list-id}/members", new UserListMembersController(metrics), adminRoles); post("/user/{user-name}/roles/{office-id}", new AddRoleController(metrics), adminRoles); delete("/user/{user-name}/roles/{office-id}", new DeleteRolesController(metrics), adminRoles); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java b/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java index fd9d40553b..3a9d65c268 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java @@ -225,6 +225,7 @@ public final class Controllers { public static final String PROJECT_LIKE = "project-like"; public static final String USERNAME_LIKE = "username-like"; + public static final String USER_LIST_ID = "user-list-id"; public static final String APPLICATION_ID = "application-id"; public static final String REVOKE_EXISTING = "revoke-existing"; public static final String REVOKE_TIMEOUT = "revoke-timeout"; diff --git a/cwms-data-api/src/main/java/cwms/cda/api/auth/userlists/UserListMembersController.java b/cwms-data-api/src/main/java/cwms/cda/api/auth/userlists/UserListMembersController.java new file mode 100644 index 0000000000..655a51884f --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/api/auth/userlists/UserListMembersController.java @@ -0,0 +1,90 @@ +package cwms.cda.api.auth.userlists; + +import static cwms.cda.api.Controllers.GET_ONE; +import static cwms.cda.api.Controllers.OFFICE; +import static cwms.cda.api.Controllers.STATUS_200; +import static cwms.cda.api.Controllers.USER_LIST_ID; +import static cwms.cda.data.dao.JooqDao.getDslContext; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import cwms.cda.api.Controllers; +import cwms.cda.api.errors.RequiredQueryParameterException; +import cwms.cda.data.dao.UserListDao; +import cwms.cda.data.dto.Office; +import cwms.cda.data.dto.auth.userlists.UserListMembers; +import cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; +import io.javalin.core.util.Header; +import io.javalin.http.Context; +import io.javalin.http.Handler; +import io.javalin.plugin.openapi.annotations.HttpMethod; +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.OpenApiResponse; +import io.javalin.plugin.openapi.annotations.OpenApiSecurity; +import org.jooq.DSLContext; + +public final class UserListMembersController implements Handler { + public static final String TAG = "User Management"; + private final MetricRegistry metrics; + + public UserListMembersController(MetricRegistry metrics) { + this.metrics = metrics; + } + + private Timer.Context markAndTime(String subject) { + return Controllers.markAndTime(metrics, getClass().getName(), subject); + } + + @OpenApi( + pathParams = { + @OpenApiParam(name = USER_LIST_ID, required = true, + description = "The identifier of the user list to retrieve members for.") + }, + queryParams = { + @OpenApiParam(name = OFFICE, required = true, + description = "The office that owns the requested user list.") + }, + responses = { + @OpenApiResponse( + status = STATUS_200, + content = { + @OpenApiContent(from = UserListMembers.class, type = Formats.JSON) + } + ) + }, + security = { + @OpenApiSecurity(name = "gets overridden allows lock icon.") + }, + description = "Retrieve the members of a user list.", + method = HttpMethod.GET, + tags = {TAG} + ) + @Override + public void handle(Context ctx) { + try (final Timer.Context ignored = markAndTime(GET_ONE)) { + String office = ctx.queryParam(OFFICE); + if (office == null || office.isBlank()) { + throw new RequiredQueryParameterException(OFFICE); + } + + office = ctx.queryParamAsClass(OFFICE, String.class) + .check(Office::validOfficeNotNull, "Invalid office provided") + .get(); + + String userListId = ctx.pathParam(USER_LIST_ID); + DSLContext dsl = getDslContext(ctx); + UserListDao dao = new UserListDao(dsl); + UserListMembers members = dao.getMembers(office, userListId); + + String formatHeader = ctx.header(Header.ACCEPT); + ContentType contentType = Formats.parseHeader(formatHeader, UserListMembers.class); + String result = Formats.format(contentType, members); + + ctx.result(result); + ctx.contentType(contentType.toString()); + } + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/UserListDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/UserListDao.java new file mode 100644 index 0000000000..e40619a7f4 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/UserListDao.java @@ -0,0 +1,80 @@ +package cwms.cda.data.dao; + +import static org.jooq.impl.DSL.field; +import static org.jooq.impl.DSL.name; +import static org.jooq.impl.DSL.selectOne; +import static org.jooq.impl.DSL.table; +import static org.jooq.impl.DSL.upper; + +import cwms.cda.api.errors.NotFoundException; +import cwms.cda.data.dto.auth.userlists.UserListMember; +import cwms.cda.data.dto.auth.userlists.UserListMembers; +import java.util.List; +import java.util.Optional; +import org.jooq.Condition; +import org.jooq.DSLContext; +import org.jooq.Field; +import org.jooq.Table; + +public final class UserListDao extends Dao { + + private final Table avUserListMembers = table(name("cwms_20", "av_user_list_members")).as("ulm"); + private final Table atUserLists = table(name("cwms_20", "at_user_lists")).as("ul"); + private final Table cwmsOffice = table(name("cwms_20", "cwms_office")).as("co"); + + public UserListDao(DSLContext dsl) { + super(dsl); + } + + @Override + public Optional getByUniqueName(String uniqueName, String office) { + return Optional.empty(); + } + + public UserListMembers getMembers(String officeId, String userListId) { + if (!userListExists(officeId, userListId)) { + throw new NotFoundException("User list not found: " + officeId + "/" + userListId); + } + + Field viewOfficeId = field(name(avUserListMembers.getName(), "office_id"), String.class); + Field viewUserListId = field(name(avUserListMembers.getName(), "user_list_id"), String.class); + Field viewUserId = field(name(avUserListMembers.getName(), "user_id"), String.class); + Field viewFullName = field(name(avUserListMembers.getName(), "full_name"), String.class); + Field viewEmail = field(name(avUserListMembers.getName(), "email"), String.class); + + List members = dsl.select(viewOfficeId, viewUserListId, viewUserId, viewFullName, + viewEmail) + .from(avUserListMembers) + .where(ignoreCaseEq(viewOfficeId, officeId)) + .and(ignoreCaseEq(viewUserListId, userListId)) + .orderBy(viewFullName.asc().nullsLast(), viewUserId.asc()) + .fetch(record -> new UserListMember( + record.get(viewOfficeId), + record.get(viewUserListId), + record.get(viewUserId), + record.get(viewFullName), + record.get(viewEmail) + )); + + return new UserListMembers(members); + } + + private boolean userListExists(String officeId, String userListId) { + Field listOfficeCode = field(name(atUserLists.getName(), "db_office_code"), Number.class); + Field listUserListId = field(name(atUserLists.getName(), "user_list_id"), String.class); + Field officeCode = field(name(cwmsOffice.getName(), "office_code"), Number.class); + Field officeName = field(name(cwmsOffice.getName(), "office_id"), String.class); + + return dsl.fetchExists( + selectOne() + .from(atUserLists) + .join(cwmsOffice).on(listOfficeCode.eq(officeCode)) + .where(ignoreCaseEq(officeName, officeId)) + .and(ignoreCaseEq(listUserListId, userListId)) + ); + } + + private static Condition ignoreCaseEq(Field field, String value) { + return upper(field).eq(value.toUpperCase()); + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/auth/userlists/UserListMember.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/auth/userlists/UserListMember.java new file mode 100644 index 0000000000..f0e2da8675 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/auth/userlists/UserListMember.java @@ -0,0 +1,63 @@ +package cwms.cda.data.dto.auth.userlists; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import cwms.cda.data.dto.CwmsDTOBase; +import cwms.cda.formatters.Formats; +import cwms.cda.formatters.annotations.FormattableWith; +import cwms.cda.formatters.json.JsonV1; +import io.swagger.v3.oas.annotations.media.Schema; + +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) +@FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class, + aliases = {Formats.DEFAULT, Formats.JSON}) +public final class UserListMember extends CwmsDTOBase { + + @JsonProperty(required = true) + @Schema(description = "The owning CWMS office identifier for the user list.") + private final String officeId; + + @JsonProperty(required = true) + @Schema(description = "The identifier of the user list.") + private final String userListId; + + @JsonProperty(required = true) + @Schema(description = "The user identifier for the member.") + private final String userId; + + @Schema(description = "The user's display name.") + private final String fullName; + + @Schema(description = "The user's email address.") + private final String email; + + public UserListMember(String officeId, String userListId, String userId, String fullName, + String email) { + this.officeId = officeId; + this.userListId = userListId; + this.userId = userId; + this.fullName = fullName; + this.email = email; + } + + public String getOfficeId() { + return officeId; + } + + public String getUserListId() { + return userListId; + } + + public String getUserId() { + return userId; + } + + public String getFullName() { + return fullName; + } + + public String getEmail() { + return email; + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/auth/userlists/UserListMembers.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/auth/userlists/UserListMembers.java new file mode 100644 index 0000000000..a0525442d8 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/auth/userlists/UserListMembers.java @@ -0,0 +1,32 @@ +package cwms.cda.data.dto.auth.userlists; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import cwms.cda.data.dto.CwmsDTOBase; +import cwms.cda.formatters.Formats; +import cwms.cda.formatters.annotations.FormattableWith; +import cwms.cda.formatters.json.JsonV1; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Collections; +import java.util.List; + +@JsonRootName("user-list-members") +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) +@FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class, + aliases = {Formats.DEFAULT, Formats.JSON}) +public final class UserListMembers extends CwmsDTOBase { + + @JsonProperty(required = true) + @Schema(description = "Members in the requested user list.") + private final List members; + + public UserListMembers(List members) { + this.members = List.copyOf(members); + } + + public List getMembers() { + return Collections.unmodifiableList(members); + } +}