Skip to content

Commit

Permalink
Add User pages (#348)
Browse files Browse the repository at this point in the history
* Users in a system to include editing system admin flag and remove from system
* Global user viewing to include adding users, editing users, removing users, and adding users to a system.
* New tests
  • Loading branch information
chrisrohr authored Sep 11, 2023
1 parent e01dcac commit 3f8d84b
Show file tree
Hide file tree
Showing 14 changed files with 988 additions and 22 deletions.
2 changes: 1 addition & 1 deletion service/src/main/java/org/kiwiproject/champagne/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public void run(AppConfig configuration, Environment environment) {
environment.jersey().register(new DeploymentEnvironmentResource(deploymentEnvironmentDao, auditRecordDao, errorDao, manualTaskService));
environment.jersey().register(new HostConfigurationResource(hostDao, componentDao, tagDao, auditRecordDao, errorDao));
environment.jersey().register(new TaskResource(releaseDao, releaseStatusDao, taskDao, taskStatusDao, deploymentEnvironmentDao, auditRecordDao, errorDao));
environment.jersey().register(new UserResource(userDao, auditRecordDao, errorDao));
environment.jersey().register(new UserResource(userDao, deployableSystemDao, auditRecordDao, errorDao));
environment.jersey().register(new ApplicationErrorResource(errorDao));
environment.jersey().register(new DeployableSystemResource(deployableSystemDao, userDao, auditRecordDao, errorDao));
environment.jersey().register(new TagResource(tagDao, auditRecordDao, errorDao));
Expand Down
23 changes: 22 additions & 1 deletion service/src/main/java/org/kiwiproject/champagne/dao/UserDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import org.kiwiproject.champagne.model.User;
import org.kiwiproject.champagne.dao.mappers.UserInSystemMapper;
import org.kiwiproject.champagne.dao.mappers.UserMapper;
import org.kiwiproject.champagne.model.User;
import org.kiwiproject.champagne.model.UserInSystem;

@RegisterRowMapper(UserMapper.class)
public interface UserDao {
Expand All @@ -33,4 +35,23 @@ public interface UserDao {

@SqlUpdate("delete from users where id = :id")
int deleteUser(@Bind("id") long id);

@SqlQuery("select u.*, usd.system_admin as system_admin from users u left join users_deployable_systems usd on u.id = usd.user_id where usd.deployable_system_id = :systemId offset :offset limit :limit")
@RegisterRowMapper(UserInSystemMapper.class)
List<UserInSystem> findPagedUsersInSystem(@Bind("systemId") long systemId, @Bind("offset") int offset, @Bind("limit") int limit);

@SqlQuery("select count(*) from users u left join users_deployable_systems usd on u.id = usd.user_id where usd.deployable_system_id = :systemId")
long countUsersInSystem(@Bind("systemId") long systemId);

@SqlUpdate("delete from users_deployable_systems where user_id = :userId and deployable_system_id = :systemId")
int removeUserFromSystem(@Bind("userId") long userId, @Bind("systemId") long systemId);

@SqlUpdate("update users_deployable_systems set system_admin = true where user_id = :userId and deployable_system_id = :systemId")
int makeUserAdminInSystem(@Bind("userId") long userId, @Bind("systemId") long systemId);

@SqlUpdate("update users_deployable_systems set system_admin = false where user_id = :userId and deployable_system_id = :systemId")
int makeUserNonAdminInSystem(@Bind("userId") long userId, @Bind("systemId") long systemId);

@SqlUpdate("insert into users_deployable_systems (user_id, deployable_system_id) values (:userId, :systemId)")
int addUserToSystem(@Bind("userId") long userId, @Bind("systemId") long systemId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.kiwiproject.champagne.dao.mappers;

import static org.kiwiproject.jdbc.KiwiJdbc.instantFromTimestamp;

import java.sql.ResultSet;
import java.sql.SQLException;

import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;
import org.kiwiproject.champagne.model.User;
import org.kiwiproject.champagne.model.UserInSystem;

public class UserInSystemMapper implements RowMapper<UserInSystem> {

@Override
public UserInSystem map(ResultSet r, StatementContext ctx) throws SQLException {

var user = User.builder()
.id(r.getLong("id"))
.createdAt(instantFromTimestamp(r, "created_at"))
.updatedAt(instantFromTimestamp(r, "updated_at"))
.firstName(r.getString("first_name"))
.lastName(r.getString("last_name"))
.displayName(r.getString("display_name"))
.systemIdentifier(r.getString("system_identifier"))
.admin(r.getBoolean("admin"))
.build();

return UserInSystem.builder()
.user(user)
.systemAdmin(r.getBoolean("system_admin"))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
package org.kiwiproject.champagne.model;

import java.time.Instant;
import java.util.List;

import jakarta.validation.constraints.NotBlank;
import lombok.Builder;
import lombok.Value;
import lombok.With;

/**
* Core model for user information.
*
* @implNote This model is soft-deletable because we will be linking to users to track auditable events (created/updated) and if the user is hard deleted
* then we will lose the pedigree of the changes.
*/
@Value
@Builder
Expand All @@ -33,4 +32,7 @@ public class User {
String systemIdentifier;

boolean admin;

@With
List<DeployableSystem> systems;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.kiwiproject.champagne.model;

import lombok.Builder;
import lombok.Value;
import lombok.experimental.Delegate;

/**
* Core model for user information in relation to a specific deployable system. This model delegates a true User object.
*/
@Value
@Builder
public class UserInSystem {

@Delegate
User user;

boolean systemAdmin;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
import static org.kiwiproject.base.KiwiPreconditions.requireNotNull;
import static org.kiwiproject.champagne.util.DeployableSystems.checkUserAdminOfSystem;
import static org.kiwiproject.champagne.util.DeployableSystems.getSystemIdOrThrowBadRequest;
import static org.kiwiproject.jaxrs.KiwiStandardResponses.standardGetResponse;
import static org.kiwiproject.search.KiwiSearching.zeroBasedOffset;

Expand All @@ -25,9 +27,11 @@
import jakarta.ws.rs.core.Response;
import org.dhatim.dropwizard.jwt.cookie.authentication.DefaultJwtCookiePrincipal;
import org.kiwiproject.champagne.dao.AuditRecordDao;
import org.kiwiproject.champagne.dao.DeployableSystemDao;
import org.kiwiproject.champagne.dao.UserDao;
import org.kiwiproject.champagne.model.AuditRecord.Action;
import org.kiwiproject.champagne.model.User;
import org.kiwiproject.champagne.model.UserInSystem;
import org.kiwiproject.dropwizard.error.dao.ApplicationErrorDao;
import org.kiwiproject.spring.data.KiwiPage;

Expand All @@ -38,14 +42,18 @@
public class UserResource extends AuditableResource {

private final UserDao userDao;
private final DeployableSystemDao deployableSystemDao;

public UserResource(UserDao userDao, AuditRecordDao auditRecordDao, ApplicationErrorDao errorDao) {
public UserResource(UserDao userDao, DeployableSystemDao deployableSystemDao, AuditRecordDao auditRecordDao, ApplicationErrorDao errorDao) {
super(auditRecordDao, errorDao);

this.userDao = userDao;
this.deployableSystemDao = deployableSystemDao;
}

@GET
@Path("/all")
@RolesAllowed("admin")
@Timed
@ExceptionMetered
public Response listUsers(@QueryParam("pageNumber") @DefaultValue("1") int pageNumber,
Expand All @@ -54,12 +62,29 @@ public Response listUsers(@QueryParam("pageNumber") @DefaultValue("1") int pageN
var offset = zeroBasedOffset(pageNumber, pageSize);

var users = userDao.findPagedUsers(offset, pageSize);
users = users.stream().map(user -> user.withSystems(deployableSystemDao.findDeployableSystemsForUser(user.getId()))).toList();
var total = userDao.countUsers();

var page = KiwiPage.of(pageNumber, pageSize, total, users);
return Response.ok(page).build();
}

@GET
@Timed
@ExceptionMetered
public Response listUsersInSystem(@QueryParam("pageNumber") @DefaultValue("1") int pageNumber,
@QueryParam("pageSize") @DefaultValue("25") int pageSize) {

var systemId = getSystemIdOrThrowBadRequest();
var offset = zeroBasedOffset(pageNumber, pageSize);

var users = userDao.findPagedUsersInSystem(systemId, offset, pageSize);
var total = userDao.countUsersInSystem(systemId);

var page = KiwiPage.of(pageNumber, pageSize, total, users);
return Response.ok(page).build();
}

@POST
@RolesAllowed("admin")
@Timed
Expand Down Expand Up @@ -112,4 +137,60 @@ public Response updateUser(@NotNull @Valid User userToUpdate) {

return Response.accepted().build();
}

@DELETE
@Path("/system/{id}")
@Timed
@ExceptionMetered
public Response removeUserFromSystem(@PathParam("id") long id) {
checkUserAdminOfSystem();

var systemId = getSystemIdOrThrowBadRequest();
var removeCount = userDao.removeUserFromSystem(id, systemId);
if (removeCount > 0) {
auditAction(id, UserInSystem.class, Action.DELETED);
}

return Response.noContent().build();
}

@PUT
@Path("/system/{id}")
@Timed
@ExceptionMetered
public Response updateSystemAdminStatus(@PathParam("id") long id, SystemAdminRequest request) {
checkUserAdminOfSystem();

var systemId = getSystemIdOrThrowBadRequest();
var updateCount = 0;

if (request.admin) {
updateCount = userDao.makeUserAdminInSystem(id, systemId);
} else {
updateCount = userDao.makeUserNonAdminInSystem(id, systemId);
}

if (updateCount > 0) {
auditAction(id, UserInSystem.class, Action.UPDATED);
}

return Response.accepted().build();
}

public record SystemAdminRequest(boolean admin) {}

@POST
@Path("/system/{systemId}/{id}")
@RolesAllowed("admin")
@Timed
@ExceptionMetered
public Response addUserToSystem(@PathParam("systemId") long systemId, @PathParam("id") long id) {
var insertCount = userDao.addUserToSystem(id, systemId);

if (insertCount > 0) {
auditAction(id, User.class, Action.UPDATED);
}

return Response.accepted().build();
}
}
107 changes: 104 additions & 3 deletions service/src/test/java/org/kiwiproject/champagne/dao/UserDaoTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;
import static org.kiwiproject.champagne.util.TestObjects.insertDeployableSystem;
import static org.kiwiproject.champagne.util.TestObjects.insertUserRecord;
import static org.kiwiproject.champagne.util.TestObjects.insertUserToDeployableSystemLink;
import static org.kiwiproject.collect.KiwiLists.first;
import static org.kiwiproject.test.util.DateTimeTestHelper.assertTimeDifferenceWithinTolerance;

Expand All @@ -12,14 +14,14 @@
import org.assertj.core.api.SoftAssertions;
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension;
import org.jdbi.v3.core.Handle;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.kiwiproject.champagne.model.User;
import org.kiwiproject.champagne.dao.mappers.UserMapper;
import org.kiwiproject.champagne.model.User;
import org.kiwiproject.test.junit.jupiter.Jdbi3DaoExtension;
import org.kiwiproject.test.junit.jupiter.PostgresLiquibaseTestExtension;

Expand Down Expand Up @@ -181,7 +183,7 @@ void shouldReturnZeroWhenNoUsersFound() {
class DeleteUser {

@Test
void shouldDeleteUserSuccessfully(SoftAssertions softly) {
void shouldDeleteUserSuccessfully() {
var userId = insertUserRecord(handle, "jdoe");

dao.deleteUser(userId);
Expand All @@ -192,4 +194,103 @@ void shouldDeleteUserSuccessfully(SoftAssertions softly) {

}

@Nested
class FindPagedUsersInSystem {

@Test
void shouldReturnListOfUsers() {
var systemId = insertDeployableSystem(handle, "kiwi");
var userId = insertUserRecord(handle, "fooBar", "Foo", "Bar");
insertUserToDeployableSystemLink(handle, userId, systemId, false);

var users = dao.findPagedUsersInSystem(systemId, 0, 10);
assertThat(users)
.extracting("systemIdentifier", "firstName", "lastName")
.contains(tuple("fooBar", "Foo", "Bar"));
}

@Test
void shouldReturnEmptyListWhenNoUsersFound() {
insertUserRecord(handle, "fooBar", "Foo", "Bar");

var users = dao.findPagedUsersInSystem(1L, 10, 10);
assertThat(users).isEmpty();
}
}

@Nested
class CountUsersInSystem {

@Test
void shouldReturnCountOfUsers() {
var systemId = insertDeployableSystem(handle, "kiwi");
var userId = insertUserRecord(handle, "fooBar", "Foo", "Bar");
insertUserToDeployableSystemLink(handle, userId, systemId, false);

var count = dao.countUsers();
assertThat(count).isOne();
}

@Test
void shouldReturnZeroWhenNoUsersFound() {
var count = dao.countUsers();
assertThat(count).isZero();
}
}

@Nested
class RemoveUserFromSystem {

@Test
void shouldRemoveUserFromSystem() {
var systemId = insertDeployableSystem(handle, "kiwi");
var userId = insertUserRecord(handle, "fooBar", "Foo", "Bar");
insertUserToDeployableSystemLink(handle, userId, systemId, false);

var updateCount = dao.removeUserFromSystem(userId, systemId);
assertThat(updateCount).isOne();
}
}

@Nested
class MakeUserAdminInSystem {

@Test
void shouldMakeTheGivenUserAnAdminInTheGivenSystem() {
var systemId = insertDeployableSystem(handle, "kiwi");
var userId = insertUserRecord(handle, "fooBar", "Foo", "Bar");
insertUserToDeployableSystemLink(handle, userId, systemId, false);

var updateCount = dao.makeUserAdminInSystem(userId, systemId);
assertThat(updateCount).isOne();
}
}

@Nested
class MakeUserNonAdminInSystem {

@Test
void shouldMakeTheGivenUserANonAdminInTheGivenSystem() {
var systemId = insertDeployableSystem(handle, "kiwi");
var userId = insertUserRecord(handle, "fooBar", "Foo", "Bar");
insertUserToDeployableSystemLink(handle, userId, systemId, true);

var updateCount = dao.makeUserNonAdminInSystem(userId, systemId);
assertThat(updateCount).isOne();
}
}

@Nested
class AddUserToSystem {

@Test
void shouldAddTheGivenUserToTheGivenSystem() {
var systemId = insertDeployableSystem(handle, "kiwi");
var userId = insertUserRecord(handle, "fooBar", "Foo", "Bar");

var updateCount = dao.addUserToSystem(userId, systemId);
assertThat(updateCount).isOne();
}
}

}
Loading

0 comments on commit 3f8d84b

Please sign in to comment.