diff --git a/server/wfprev-api/src/main/java/ca/bc/gov/nrs/wfprev/SpringSecurityAuditorAware.java b/server/wfprev-api/src/main/java/ca/bc/gov/nrs/wfprev/SpringSecurityAuditorAware.java new file mode 100644 index 00000000..b9c63ca6 --- /dev/null +++ b/server/wfprev-api/src/main/java/ca/bc/gov/nrs/wfprev/SpringSecurityAuditorAware.java @@ -0,0 +1,29 @@ +package ca.bc.gov.nrs.wfprev; + +import org.springframework.data.domain.AuditorAware; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Component +public class SpringSecurityAuditorAware implements AuditorAware { + + @Override + public Optional getCurrentAuditor() { + return Optional.ofNullable(SecurityContextHolder.getContext()) + .map(context -> context.getAuthentication()) + .filter(Authentication::isAuthenticated) + .map(authentication -> { + Object principal = authentication.getPrincipal(); + if (principal instanceof DefaultOAuth2AuthenticatedPrincipal) { + // Extract username or preferred identifier + DefaultOAuth2AuthenticatedPrincipal oauthPrincipal = (DefaultOAuth2AuthenticatedPrincipal) principal; + return (String) oauthPrincipal.getAttribute("preferred_username"); // Adjust key to match your provider + } + throw new IllegalStateException("Principal is not of type DefaultOAuth2AuthenticatedPrincipal"); + }); + } +} \ No newline at end of file diff --git a/server/wfprev-api/src/main/java/ca/bc/gov/nrs/wfprev/WfprevApiApplication.java b/server/wfprev-api/src/main/java/ca/bc/gov/nrs/wfprev/WfprevApiApplication.java index 3e6ec273..f071ace8 100644 --- a/server/wfprev-api/src/main/java/ca/bc/gov/nrs/wfprev/WfprevApiApplication.java +++ b/server/wfprev-api/src/main/java/ca/bc/gov/nrs/wfprev/WfprevApiApplication.java @@ -7,6 +7,7 @@ import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.core.Ordered; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.web.filter.ForwardedHeaderFilter; import com.fasterxml.jackson.annotation.JsonFormat; @@ -19,6 +20,7 @@ import jakarta.servlet.DispatcherType; @SpringBootApplication +@EnableJpaAuditing(auditorAwareRef = "springSecurityAuditorAware") public class WfprevApiApplication { /* * Run the application as a JAR diff --git a/server/wfprev-api/src/main/java/ca/bc/gov/nrs/wfprev/data/entities/ProjectEntity.java b/server/wfprev-api/src/main/java/ca/bc/gov/nrs/wfprev/data/entities/ProjectEntity.java index 868e10df..a376c4be 100644 --- a/server/wfprev-api/src/main/java/ca/bc/gov/nrs/wfprev/data/entities/ProjectEntity.java +++ b/server/wfprev-api/src/main/java/ca/bc/gov/nrs/wfprev/data/entities/ProjectEntity.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -24,13 +25,14 @@ import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.io.Serializable; import java.math.BigDecimal; import java.util.Date; import java.util.UUID; - @Entity +@EntityListeners(AuditingEntityListener.class) @Table(name = "project") @JsonIgnoreProperties(ignoreUnknown = false) @Data @@ -39,6 +41,7 @@ @AllArgsConstructor @NoArgsConstructor public class ProjectEntity implements Serializable { + @Id @UuidGenerator @GeneratedValue(strategy = GenerationType.UUID) @@ -49,7 +52,7 @@ public class ProjectEntity implements Serializable { @JoinColumn(name = "project_type_code") private ProjectTypeCodeEntity projectTypeCode; - @Column(name = "project_number", columnDefinition="Decimal(10)", insertable = false, updatable = true) + @Column(name = "project_number", columnDefinition = "Decimal(10)", insertable = false, updatable = true) private Integer projectNumber; @NotNull @@ -81,7 +84,7 @@ public class ProjectEntity implements Serializable { @Column(name = "bc_parks_region_org_unit_id", columnDefinition = "Decimal(10)") private Integer bcParksRegionOrgUnitId; - @Column(name = "bcParksSectionOrgUnitId", columnDefinition = "Decimal(10)") + @Column(name = "bc_parks_section_org_unit_id", columnDefinition = "Decimal(10)") private Integer bcParksSectionOrgUnitId; @NotNull @@ -144,7 +147,7 @@ public class ProjectEntity implements Serializable { private Integer revisionCount; @ManyToOne(fetch = FetchType.EAGER, optional = false) - @JoinColumn(name="project_status_code") + @JoinColumn(name = "project_status_code") private ProjectStatusCodeEntity projectStatusCode; @CreatedBy @@ -166,4 +169,4 @@ public class ProjectEntity implements Serializable { @NotNull @Column(name = "update_date") private Date updateDate; -} +} \ No newline at end of file diff --git a/server/wfprev-api/src/main/java/ca/bc/gov/nrs/wfprev/services/ProjectService.java b/server/wfprev-api/src/main/java/ca/bc/gov/nrs/wfprev/services/ProjectService.java index 340b312c..e92bffd7 100644 --- a/server/wfprev-api/src/main/java/ca/bc/gov/nrs/wfprev/services/ProjectService.java +++ b/server/wfprev-api/src/main/java/ca/bc/gov/nrs/wfprev/services/ProjectService.java @@ -78,18 +78,9 @@ public ProjectModel createOrUpdateProject(ProjectModel resource) throws ServiceE try { if (resource.getProjectGuid() == null) { resource.setCreateDate(new Date()); - //TODO - Fix to use proper user - resource.setCreateUser("SYSTEM"); resource.setProjectGuid(UUID.randomUUID().toString()); - resource.setCreateDate(new Date()); - resource.setCreateUser("SYSTEM"); resource.setRevisionCount(0); // Initialize revision count for new records } - // Set audit fields - resource.setUpdateDate(new Date()); - //TODO - Fix to use proper user - resource.setUpdateUser("SYSTEM"); - ProjectEntity entity = projectResourceAssembler.toEntity(resource); // Load ForestAreaCode with null checks diff --git a/server/wfprev-api/src/test/java/ca/bc/gov/nrs/wfprev/SpringSecurityAuditorAwareTest.java b/server/wfprev-api/src/test/java/ca/bc/gov/nrs/wfprev/SpringSecurityAuditorAwareTest.java new file mode 100644 index 00000000..1926ae1c --- /dev/null +++ b/server/wfprev-api/src/test/java/ca/bc/gov/nrs/wfprev/SpringSecurityAuditorAwareTest.java @@ -0,0 +1,114 @@ +package ca.bc.gov.nrs.wfprev; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class SpringSecurityAuditorAwareTest { + + private SpringSecurityAuditorAware auditorAware; + + private MockedStatic securityContextHolderMock; + + @BeforeEach + void setUp() { + auditorAware = new SpringSecurityAuditorAware(); + securityContextHolderMock = Mockito.mockStatic(SecurityContextHolder.class); + } + + @AfterEach + void tearDown() { + securityContextHolderMock.close(); + } + + @Test + void getCurrentAuditor_authenticatedUser_returnsUsername() { + // Given: A valid SecurityContext with an authenticated user + SecurityContext mockSecurityContext = mock(SecurityContext.class); + Authentication mockAuthentication = mock(Authentication.class); + DefaultOAuth2AuthenticatedPrincipal mockPrincipal = mock(DefaultOAuth2AuthenticatedPrincipal.class); + + when(SecurityContextHolder.getContext()).thenReturn(mockSecurityContext); + when(mockSecurityContext.getAuthentication()).thenReturn(mockAuthentication); + when(mockAuthentication.isAuthenticated()).thenReturn(true); + when(mockAuthentication.getPrincipal()).thenReturn(mockPrincipal); + when(mockPrincipal.getAttribute("preferred_username")).thenReturn("test_user"); + + // When: getCurrentAuditor is called + Optional result = auditorAware.getCurrentAuditor(); + + // Then: The correct username is returned + assertEquals(Optional.of("test_user"), result); + } + + @Test + void getCurrentAuditor_userNotAuthenticated_returnsEmptyOptional() { + // Given: A valid SecurityContext with an unauthenticated user + SecurityContext mockSecurityContext = mock(SecurityContext.class); + Authentication mockAuthentication = mock(Authentication.class); + + when(SecurityContextHolder.getContext()).thenReturn(mockSecurityContext); + when(mockSecurityContext.getAuthentication()).thenReturn(mockAuthentication); + when(mockAuthentication.isAuthenticated()).thenReturn(false); + + // When: getCurrentAuditor is called + Optional result = auditorAware.getCurrentAuditor(); + + // Then: An empty Optional is returned + assertEquals(Optional.empty(), result); + } + + @Test + void getCurrentAuditor_invalidPrincipalType_throwsIllegalStateException() { + // Given: A valid SecurityContext with an invalid principal type + SecurityContext mockSecurityContext = mock(SecurityContext.class); + Authentication mockAuthentication = mock(Authentication.class); + + when(SecurityContextHolder.getContext()).thenReturn(mockSecurityContext); + when(mockSecurityContext.getAuthentication()).thenReturn(mockAuthentication); + when(mockAuthentication.isAuthenticated()).thenReturn(true); + when(mockAuthentication.getPrincipal()).thenReturn("InvalidPrincipal"); + + // When & Then: getCurrentAuditor throws an IllegalStateException + IllegalStateException exception = assertThrows(IllegalStateException.class, auditorAware::getCurrentAuditor); + assertEquals("Principal is not of type DefaultOAuth2AuthenticatedPrincipal", exception.getMessage()); + } + + @Test + void getCurrentAuditor_nullSecurityContext_returnsEmptyOptional() { + // Given: A null SecurityContext + when(SecurityContextHolder.getContext()).thenReturn(null); + + // When: getCurrentAuditor is called + Optional result = auditorAware.getCurrentAuditor(); + + // Then: An empty Optional is returned + assertEquals(Optional.empty(), result); + } + + @Test + void getCurrentAuditor_nullAuthentication_returnsEmptyOptional() { + // Given: A valid SecurityContext with a null Authentication + SecurityContext mockSecurityContext = mock(SecurityContext.class); + + when(SecurityContextHolder.getContext()).thenReturn(mockSecurityContext); + when(mockSecurityContext.getAuthentication()).thenReturn(null); + + // When: getCurrentAuditor is called + Optional result = auditorAware.getCurrentAuditor(); + + // Then: An empty Optional is returned + assertEquals(Optional.empty(), result); + } +} \ No newline at end of file