From 25c10639e93d9779d7fa61433131847253b1ab01 Mon Sep 17 00:00:00 2001 From: Merethe Hansen Date: Tue, 25 Jun 2024 08:56:59 -0700 Subject: [PATCH] Persist role updates and add audit columns to ApiUserRole and ApiUserFacility (#7828) * add audit columns and persist role updates * rework using constructors * comment formatting * assert exception on one method invocation * give ben demo user a valid role * use lombok getters and setters when possible * put back getter to add override annotation --- .../usds/simplereport/db/model/ApiUser.java | 65 +++--- .../db/model/ApiUserFacility.java | 28 +++ .../simplereport/db/model/ApiUserRole.java | 42 ++++ .../simplereport/service/ApiUserService.java | 4 + .../OrganizationInitializingService.java | 1 + .../application-create-sample-data.yaml | 2 +- .../db/changelog/db.changelog-master.yaml | 211 +++++++++++++++++- .../repository/DemoOktaRepositoryTest.java | 4 +- 8 files changed, 328 insertions(+), 29 deletions(-) create mode 100644 backend/src/main/java/gov/cdc/usds/simplereport/db/model/ApiUserFacility.java create mode 100644 backend/src/main/java/gov/cdc/usds/simplereport/db/model/ApiUserRole.java diff --git a/backend/src/main/java/gov/cdc/usds/simplereport/db/model/ApiUser.java b/backend/src/main/java/gov/cdc/usds/simplereport/db/model/ApiUser.java index 2595d0bb3b..a3130e18ba 100644 --- a/backend/src/main/java/gov/cdc/usds/simplereport/db/model/ApiUser.java +++ b/backend/src/main/java/gov/cdc/usds/simplereport/db/model/ApiUser.java @@ -1,14 +1,17 @@ package gov.cdc.usds.simplereport.db.model; +import static jakarta.persistence.CascadeType.ALL; + +import gov.cdc.usds.simplereport.config.authorization.OrganizationRole; import gov.cdc.usds.simplereport.db.model.auxiliary.PersonName; import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.JoinTable; -import jakarta.persistence.ManyToMany; +import jakarta.persistence.OneToMany; import java.util.Date; +import java.util.HashSet; import java.util.Set; +import java.util.stream.Collectors; import lombok.Getter; import lombok.Setter; import org.hibernate.annotations.DynamicUpdate; @@ -21,21 +24,21 @@ public class ApiUser extends EternalSystemManagedEntity implements PersonEntity @Column(nullable = false, updatable = true, unique = true) @NaturalId(mutable = true) + @Getter + @Setter private String loginEmail; - @Embedded private PersonName nameInfo; + @Setter @Embedded private PersonName nameInfo; @Column(nullable = true) + @Getter private Date lastSeen; - @ManyToMany - @JoinTable( - name = "api_user_facility", - joinColumns = @JoinColumn(name = "api_user_id"), - inverseJoinColumns = @JoinColumn(name = "facility_id")) - @Getter - @Setter - private Set facilities; + @OneToMany(cascade = ALL, mappedBy = "apiUser", orphanRemoval = true) + private Set facilityAssignments = new HashSet<>(); + + @OneToMany(cascade = ALL, mappedBy = "apiUser", orphanRemoval = true) + private Set roleAssignments = new HashSet<>(); protected ApiUser() { /* for hibernate */ } @@ -46,27 +49,37 @@ public ApiUser(String email, PersonName name) { lastSeen = null; } - public String getLoginEmail() { - return loginEmail; - } - - public void setLoginEmail(String newEmail) { - loginEmail = newEmail; - } - - public Date getLastSeen() { - return lastSeen; + @Override + public PersonName getNameInfo() { + return nameInfo; } public void updateLastSeen() { lastSeen = new Date(); } - public PersonName getNameInfo() { - return nameInfo; + public Set getFacilities() { + return this.facilityAssignments.stream() + .map(ApiUserFacility::getFacility) + .collect(Collectors.toSet()); } - public void setNameInfo(PersonName name) { - nameInfo = name; + public void setFacilities(Set facilities) { + this.facilityAssignments.clear(); + for (Facility facility : facilities) { + this.facilityAssignments.add(new ApiUserFacility(this, facility)); + } + } + + public void setRoles(Set newOrgRoles, Organization org) { + this.roleAssignments.clear(); + for (OrganizationRole orgRole : newOrgRoles) { + if (orgRole.equals(OrganizationRole.NO_ACCESS)) { + // the NO_ACCESS role is only relevant for the Okta implementation of authorization, and it + // doesn't need to be persisted in our tables + continue; + } + this.roleAssignments.add(new ApiUserRole(this, org, orgRole)); + } } } diff --git a/backend/src/main/java/gov/cdc/usds/simplereport/db/model/ApiUserFacility.java b/backend/src/main/java/gov/cdc/usds/simplereport/db/model/ApiUserFacility.java new file mode 100644 index 0000000000..eb65c2f33e --- /dev/null +++ b/backend/src/main/java/gov/cdc/usds/simplereport/db/model/ApiUserFacility.java @@ -0,0 +1,28 @@ +package gov.cdc.usds.simplereport.db.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Getter; + +@Entity(name = "api_user_facility") +public class ApiUserFacility extends AuditedEntity { + + @ManyToOne + @JoinColumn(name = "api_user_id", nullable = false) + private ApiUser apiUser; + + @ManyToOne + @JoinColumn(name = "facility_id", nullable = false) + @Getter + private Facility facility; + + protected ApiUserFacility() { + /* for hibernate */ + } + + public ApiUserFacility(ApiUser user, Facility facility) { + this.apiUser = user; + this.facility = facility; + } +} diff --git a/backend/src/main/java/gov/cdc/usds/simplereport/db/model/ApiUserRole.java b/backend/src/main/java/gov/cdc/usds/simplereport/db/model/ApiUserRole.java new file mode 100644 index 0000000000..7c02843b32 --- /dev/null +++ b/backend/src/main/java/gov/cdc/usds/simplereport/db/model/ApiUserRole.java @@ -0,0 +1,42 @@ +package gov.cdc.usds.simplereport.db.model; + +import gov.cdc.usds.simplereport.config.authorization.OrganizationRole; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +@Entity(name = "api_user_role") +public class ApiUserRole extends AuditedEntity { + + @ManyToOne + @JoinColumn(name = "api_user_id", nullable = false) + private ApiUser apiUser; + + @ManyToOne + @JoinColumn(name = "organization_id", nullable = false) + private Organization organization; + + @Column(nullable = false, columnDefinition = "organization_role") + @JdbcTypeCode(SqlTypes.NAMED_ENUM) + @Enumerated(EnumType.STRING) + private OrganizationRole role; + + protected ApiUserRole() { + /* for hibernate */ + } + + public ApiUserRole(ApiUser user, Organization org, OrganizationRole role) { + if (role.equals(OrganizationRole.NO_ACCESS)) { + throw new IllegalArgumentException( + "Invalid role NO_ACCESS when creating new user role assignment"); + } + this.apiUser = user; + this.organization = org; + this.role = role; + } +} diff --git a/backend/src/main/java/gov/cdc/usds/simplereport/service/ApiUserService.java b/backend/src/main/java/gov/cdc/usds/simplereport/service/ApiUserService.java index ff2e175490..17dae2a622 100644 --- a/backend/src/main/java/gov/cdc/usds/simplereport/service/ApiUserService.java +++ b/backend/src/main/java/gov/cdc/usds/simplereport/service/ApiUserService.java @@ -161,6 +161,7 @@ private UserInfo reprovisionUser( apiUser.setNameInfo(name); apiUser.setIsDeleted(false); apiUser.setFacilities(facilitiesFound); + apiUser.setRoles(roles, org); Optional orgRoles = roleClaims.map(c -> _orgService.getOrganizationRoles(c)); UserInfo user = new UserInfo(apiUser, orgRoles, false); @@ -188,6 +189,7 @@ private UserInfo createUserHelper( Set roles = getOrganizationRoles(role, accessAllFacilities); Set facilitiesFound = getFacilitiesToGiveAccess(org, roles, facilities); apiUser.setFacilities(facilitiesFound); + apiUser.setRoles(roles, org); Optional roleClaims = _oktaRepo.createUser(userIdentity, org, facilitiesFound, roles, active); @@ -245,6 +247,7 @@ public UserInfo updateUserPrivileges( UserInfo user = new UserInfo(apiUser, orgRoles, false); apiUser.setFacilities(facilitiesFound); + apiUser.setRoles(roles, org); createUserUpdatedAuditLog(apiUser.getInternalId(), getCurrentApiUser().getInternalId()); @@ -713,6 +716,7 @@ public void updateUserPrivilegesAndGroupAccess( Set facilitiesToGiveAccessTo = getFacilitiesToGiveAccess(newOrg, roles, new HashSet<>(facilities)); apiUser.setFacilities(facilitiesToGiveAccessTo); + apiUser.setRoles(roles, newOrg); _oktaRepo.updateUserPrivilegesAndGroupAccess( username, newOrg, facilitiesToGiveAccessTo, role.toOrganizationRole(), allFacilitiesAccess); diff --git a/backend/src/main/java/gov/cdc/usds/simplereport/service/OrganizationInitializingService.java b/backend/src/main/java/gov/cdc/usds/simplereport/service/OrganizationInitializingService.java index 3d309f9e7b..9e33f297d9 100644 --- a/backend/src/main/java/gov/cdc/usds/simplereport/service/OrganizationInitializingService.java +++ b/backend/src/main/java/gov/cdc/usds/simplereport/service/OrganizationInitializingService.java @@ -320,6 +320,7 @@ private void configureDemoUsers(List users, Map faci _orgRepo .findByExternalId(authorization.getOrganizationExternalId()) .orElseThrow(MisconfiguredUserException::new); + apiUser.setRoles(roles, org); log.info( "User={} will have roles={} in organization={}", identity.getUsername(), diff --git a/backend/src/main/resources/application-create-sample-data.yaml b/backend/src/main/resources/application-create-sample-data.yaml index 21b628b961..95abdaffb7 100644 --- a/backend/src/main/resources/application-create-sample-data.yaml +++ b/backend/src/main/resources/application-create-sample-data.yaml @@ -239,7 +239,7 @@ simple-report: organization-external-id: DIS_ORG facilities: - Testing Site - granted-roles: [] + granted-roles: ENTRY_ONLY - identity: username: jamar@example.com first-name: Jamar diff --git a/backend/src/main/resources/db/changelog/db.changelog-master.yaml b/backend/src/main/resources/db/changelog/db.changelog-master.yaml index fe5cb9a79a..b65319f24d 100644 --- a/backend/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/backend/src/main/resources/db/changelog/db.changelog-master.yaml @@ -5416,4 +5416,213 @@ databaseChangeLog: remarks: The internal database identifier for this entity. constraints: primaryKey: true - nullable: false \ No newline at end of file + nullable: false + - changeSet: + id: add-audit-cols-to-facility-table + author: tnd8@cdc.gov + changes: + - dropTable: + tableName: api_user_facility + - createTable: + tableName: api_user_facility + remarks: creates a new table to store the relationship between api_user and facility + columns: + - column: + name: internal_id + type: uuid + remarks: The internal database identifier for this entity. + constraints: + primaryKey: true + nullable: false + - column: + name: api_user_id + type: uuid + constraints: + nullable: false + foreignKeyName: fk__api_user_facility__api_user + references: api_user + - column: + name: facility_id + type: uuid + constraints: + nullable: false + foreignKeyName: fk__api_user_facility__facility + references: facility + - column: + name: created_at + type: DATETIME + remarks: The creation timestamp for this entity. + constraints: + nullable: false + - column: + name: updated_at + type: DATETIME + remarks: The timestamp for the most recent update of this entity. + constraints: + nullable: false + - column: + name: created_by + type: uuid + remarks: The user who created this entity. + constraints: + nullable: false + references: api_user + foreignKeyName: fk__anyentity__created_by + - column: + name: updated_by + type: uuid + remarks: The user who most recently updated this entity. + constraints: + nullable: false + references: api_user + foreignKeyName: fk__anyentity__updated_by + - sql: + sql: GRANT SELECT ON ${database.defaultSchemaName}.api_user_facility TO ${noPhiUsername}; + rollback: + - dropTable: + tableName: api_user_facility + - createTable: + tableName: api_user_facility + remarks: creates a new table to store the relationship between api_user and facility + columns: + - column: + name: api_user_id + type: uuid + constraints: + nullable: false + foreignKeyName: fk__api_user_facility__api_user + references: api_user + - column: + name: facility_id + type: uuid + constraints: + nullable: false + foreignKeyName: fk__api_user_facility__facility + references: facility + - sql: + sql: GRANT SELECT ON ${database.defaultSchemaName}.api_user_facility TO ${noPhiUsername}; + - changeSet: + id: add-audit-cols-and-enum-role-to-api_user_role-table + author: tnd8@cdc.gov + changes: + - dropTable: + tableName: api_user_role + - dropTable: + tableName: role + - createTable: + tableName: api_user_role + remarks: creates a new table to store the relationship between api_user and role + columns: + - column: + name: internal_id + type: uuid + remarks: The internal database identifier for this entity. + constraints: + primaryKey: true + nullable: false + - column: + name: api_user_id + type: uuid + constraints: + nullable: false + foreignKeyName: fk__api_user_facility__api_user + references: api_user + - column: + name: organization_id + type: uuid + constraints: + nullable: false + foreignKeyName: fk__api_user_role__organization + references: organization + - column: + name: role + type: ${database.defaultSchemaName}.ORGANIZATION_ROLE + remarks: The name of the role that is an enumerated value. + constraints: + nullable: false + - column: + name: created_at + type: DATETIME + remarks: The creation timestamp for this entity. + constraints: + nullable: false + - column: + name: updated_at + type: DATETIME + remarks: The timestamp for the most recent update of this entity. + constraints: + nullable: false + - column: + name: created_by + type: uuid + remarks: The user who created this entity. + constraints: + nullable: false + references: api_user + foreignKeyName: fk__anyentity__created_by + - column: + name: updated_by + type: uuid + remarks: The user who most recently updated this entity. + constraints: + nullable: false + references: api_user + foreignKeyName: fk__anyentity__updated_by + - sql: + sql: GRANT SELECT ON ${database.defaultSchemaName}.api_user_role TO ${noPhiUsername}; + rollback: + - dropTable: + tableName: api_user_role + - createTable: + tableName: role + remarks: named references to a OrganizationRole enum + columns: + - column: + name: internal_id + type: uuid + remarks: The internal database identifier for this entity. + constraints: + primaryKey: true + nullable: false + - column: + name: name + type: ${database.defaultSchemaName}.ORGANIZATION_ROLE + remarks: The name of the role that is an enumerated value. + constraints: + nullable: false + - sql: | + GRANT SELECT ON ${database.defaultSchemaName}.role TO ${noPhiUsername}; + - createTable: + tableName: api_user_role + remarks: creates a new table to store the relationship between api_user and role + columns: + - column: + name: internal_id + type: uuid + remarks: The internal database identifier for this entity. + constraints: + primaryKey: true + nullable: false + - column: + name: api_user_id + type: uuid + constraints: + nullable: false + foreignKeyName: fk__api_user_role__api_user + references: api_user + - column: + name: organization_id + type: uuid + constraints: + nullable: false + foreignKeyName: fk__api_user_role__organization + references: organization + - column: + name: role_id + type: uuid + constraints: + nullable: false + foreignKeyName: fk__api_user_role__role + references: role + - sql: | + GRANT SELECT ON ${database.defaultSchemaName}.api_user_role TO ${noPhiUsername}; diff --git a/backend/src/test/java/gov/cdc/usds/simplereport/idp/repository/DemoOktaRepositoryTest.java b/backend/src/test/java/gov/cdc/usds/simplereport/idp/repository/DemoOktaRepositoryTest.java index df8f4fc0d0..f4bfc020ee 100644 --- a/backend/src/test/java/gov/cdc/usds/simplereport/idp/repository/DemoOktaRepositoryTest.java +++ b/backend/src/test/java/gov/cdc/usds/simplereport/idp/repository/DemoOktaRepositoryTest.java @@ -400,7 +400,7 @@ void deactivateUser() { } @Test - void deleteOrgAndFacilities() { + void deleteOrg() { _repo.createUser(AMOS, ABC, Set.of(ABC_1), Set.of(OrganizationRole.USER), true); _repo.createUser( BRAD, @@ -414,6 +414,8 @@ void deleteOrgAndFacilities() { _repo.deleteOrganization(ABC); + Facility fakeFacility = getFacility(UUID.randomUUID(), ABC); + assertThrows(IllegalGraphqlArgumentException.class, () -> _repo.createFacility(fakeFacility)); assertThrows( IllegalGraphqlArgumentException.class, () -> _repo.getAllUsersForOrganization(ABC)); }