Skip to content

Commit

Permalink
fix: network error upon login for tsc admin profile (#1381)
Browse files Browse the repository at this point in the history
Co-authored-by: Derek Roberts <derek.roberts@gmail.com>
  • Loading branch information
Ricardo Campos and DerekRoberts authored Jul 11, 2024
1 parent 1248fad commit f396b42
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ public SaveSeedlotFormDtoClassA getFormProgressClassA(
return saveSeedlotFormService.getFormClassA(seedlotNumber);
}

/** Retreive only the progress_status column from the form progress table. */
/** Retrieve only the progress_status column from the form progress table. */
@GetMapping("/{seedlotNumber}/a-class-form-progress/status")
@Operation(
summary = "Retrieve the progress status of an a-class reg form.",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package ca.bc.gov.backendstartapi.security;

import ca.bc.gov.backendstartapi.config.SparLog;
import ca.bc.gov.backendstartapi.entity.embeddable.AuditInformation;
import ca.bc.gov.backendstartapi.exception.ClientIdForbiddenException;
import ca.bc.gov.backendstartapi.exception.UserNotFoundException;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -100,4 +102,28 @@ public String getLoggedUserIdirOrBceId() {
public AuditInformation createAuditCurrentUser() {
return new AuditInformation(getLoggedUserId());
}

/**
* Verify if the service initiator has the correct access.
*
* @param clientId to verify
* @throw an {@link ClientIdForbiddenException}
*/
public void verifySeedlotAccessPrivilege(String clientId) {
Optional<UserInfo> userInfo = getLoggedUserInfo();

if (userInfo.isEmpty()) {
throw new UserNotFoundException();
}

if (userInfo.get().roles().contains("SPAR_TSC_ADMIN")) {
SparLog.info("Request allowed, TSC Admin role found!");
return;
}

if (!userInfo.get().clientIds().contains(clientId)) {
SparLog.info("Request denied due to user not having client id: {}", clientId);
throw new ClientIdForbiddenException();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@
import ca.bc.gov.backendstartapi.dto.SaveSeedlotFormDtoClassA;
import ca.bc.gov.backendstartapi.entity.SaveSeedlotProgressEntityClassA;
import ca.bc.gov.backendstartapi.entity.seedlot.Seedlot;
import ca.bc.gov.backendstartapi.exception.ClientIdForbiddenException;
import ca.bc.gov.backendstartapi.exception.JsonParsingException;
import ca.bc.gov.backendstartapi.exception.RevisionCountMismatchException;
import ca.bc.gov.backendstartapi.exception.SeedlotFormProgressNotFoundException;
import ca.bc.gov.backendstartapi.exception.SeedlotNotFoundException;
import ca.bc.gov.backendstartapi.repository.SaveSeedlotProgressRepositoryClassA;
import ca.bc.gov.backendstartapi.repository.SeedlotRepository;
import ca.bc.gov.backendstartapi.security.LoggedUserService;
import ca.bc.gov.backendstartapi.security.UserInfo;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
Expand Down Expand Up @@ -44,7 +42,7 @@ public RevisionCountDto saveFormClassA(

String seedlotApplicantClientNumber = relatedSeedlot.getApplicantClientNumber();

verifySeedlotAccessPrivilege(seedlotApplicantClientNumber);
loggedUserService.verifySeedlotAccessPrivilege(seedlotApplicantClientNumber);

Optional<SaveSeedlotProgressEntityClassA> optionalEntityToSave =
saveSeedlotProgressRepositoryClassA.findById(seedlotNumber);
Expand Down Expand Up @@ -98,7 +96,7 @@ public RevisionCountDto saveFormClassA(
}

/**
* Retreives a {@link SaveSeedlotProgressEntityClassA} then convert it to {@link
* Retrieves a {@link SaveSeedlotProgressEntityClassA} then convert it to {@link
* SaveSeedlotFormDtoClassA} upon return.
*/
public SaveSeedlotFormDtoClassA getFormClassA(@NonNull String seedlotNumber) {
Expand All @@ -113,7 +111,7 @@ public SaveSeedlotFormDtoClassA getFormClassA(@NonNull String seedlotNumber) {
SparLog.info("A-class seedlot progress found for seedlot number {}", seedlotNumber);

String seedlotApplicantClientNumber = form.get().getSeedlot().getApplicantClientNumber();
verifySeedlotAccessPrivilege(seedlotApplicantClientNumber);
loggedUserService.verifySeedlotAccessPrivilege(seedlotApplicantClientNumber);
}

return form.map(
Expand All @@ -133,7 +131,7 @@ public JsonNode getFormStatusClassA(String seedlotNumber) {
seedlotRepository.findById(seedlotNumber).orElseThrow(SeedlotNotFoundException::new);

String seedlotApplicantClientNumber = relatedSeedlot.getApplicantClientNumber();
verifySeedlotAccessPrivilege(seedlotApplicantClientNumber);
loggedUserService.verifySeedlotAccessPrivilege(seedlotApplicantClientNumber);

ObjectMapper mapper = new ObjectMapper();

Expand All @@ -157,20 +155,4 @@ public JsonNode getFormStatusClassA(String seedlotNumber) {
throw new JsonParsingException();
}
}

/**
* Verify if the service initiator has the correct access.
*
* @param seedlot to verify
* @throw an {@link ClientIdForbiddenException}
*/
private void verifySeedlotAccessPrivilege(String seedlotApplicantClientNumber) {
Optional<UserInfo> userInfo = loggedUserService.getLoggedUserInfo();

if (userInfo.isEmpty() || !userInfo.get().clientIds().contains(seedlotApplicantClientNumber)) {
SparLog.info(
"Request denied due to user not having client id: {}", seedlotApplicantClientNumber);
throw new ClientIdForbiddenException();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
import ca.bc.gov.backendstartapi.entity.idclass.SeedlotParentTreeId;
import ca.bc.gov.backendstartapi.entity.seedlot.Seedlot;
import ca.bc.gov.backendstartapi.entity.seedlot.SeedlotOrchard;
import ca.bc.gov.backendstartapi.exception.ClientIdForbiddenException;
import ca.bc.gov.backendstartapi.exception.GeneticClassNotFoundException;
import ca.bc.gov.backendstartapi.exception.InvalidSeedlotRequestException;
import ca.bc.gov.backendstartapi.exception.NoSpuForOrchardException;
Expand Down Expand Up @@ -215,9 +214,7 @@ public Optional<Page<Seedlot>> getSeedlotByClientId(
String clientId, int pageNumber, int pageSize) {
Optional<UserInfo> userInfo = loggedUserService.getLoggedUserInfo();

if (userInfo.isPresent() && !userInfo.get().clientIds().contains(clientId)) {
throw new ClientIdForbiddenException();
}
loggedUserService.verifySeedlotAccessPrivilege(clientId);

SparLog.info(
"Retrieving paginated list of seedlots for the user: {} with client id: {}",
Expand Down Expand Up @@ -257,12 +254,8 @@ public SeedlotDto getSingleSeedlotInfo(@NonNull String seedlotNumber) {
SparLog.info("Seedlot number {} found", seedlotNumber);

String clientId = seedlotEntity.getApplicantClientNumber();
Optional<UserInfo> userInfo = loggedUserService.getLoggedUserInfo();

if (userInfo.isPresent() && !userInfo.get().clientIds().contains(clientId)) {
SparLog.info("User has no access to seedlot {}, request denied.", seedlotNumber);
throw new ClientIdForbiddenException();
}
loggedUserService.verifySeedlotAccessPrivilege(clientId);

SeedlotDto seedlotDto = new SeedlotDto();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package ca.bc.gov.backendstartapi.security;

import static org.mockito.Mockito.when;

import ca.bc.gov.backendstartapi.exception.ClientIdForbiddenException;
import ca.bc.gov.backendstartapi.exception.UserNotFoundException;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith(SpringExtension.class)
class LoggedUserServiceTest {

@Mock UserAuthenticationHelper userAuthenticationHelper;

private LoggedUserService loggedUserService;

@BeforeEach
void setup() {
loggedUserService = new LoggedUserService(userAuthenticationHelper);
}

private UserInfo mockUserInfo(Set<String> roles, List<String> clients) {
return new UserInfo(
"123456789@idir",
"Bilbo",
"Baggings",
"bilbo.baggings@gov.bc.ca",
"Baggings, Bilbo LWRS:EX",
"BAGGINGS",
null,
IdentityProvider.IDIR,
roles != null ? roles : Set.of(),
clients != null ? clients : List.of(),
"abcdef123456789");
}

@Test
@DisplayName("Verify seedlot access privilege happy path should succeed")
void verifySeedlotAccessPrivilege_happyPath_shouldSucceed() {
String clientId = "00012345";

when(userAuthenticationHelper.getUserInfo())
.thenReturn(
Optional.of(mockUserInfo(Set.of("FAKE_ROLE_TEST_00012345"), List.of("00012345"))));

Assertions.assertDoesNotThrow(
() -> {
loggedUserService.verifySeedlotAccessPrivilege(clientId);
});
}

@Test
@DisplayName("Verify seedlot access privilege no user should succeed")
void verifySeedlotAccessPrivilege_noUser_shouldSucceed() {
String clientId = "00012345";

when(userAuthenticationHelper.getUserInfo()).thenReturn(Optional.empty());

Assertions.assertThrows(
UserNotFoundException.class,
() -> {
loggedUserService.verifySeedlotAccessPrivilege(clientId);
});
}

@Test
@DisplayName("Verify seedlot access privilege tsc admin should succeed")
void verifySeedlotAccessPrivilege_tscAdmin_shouldSucceed() {
String clientId = "00012345";

when(userAuthenticationHelper.getUserInfo())
.thenReturn(Optional.of(mockUserInfo(Set.of("SPAR_TSC_ADMIN"), null)));

Assertions.assertDoesNotThrow(
() -> {
loggedUserService.verifySeedlotAccessPrivilege(clientId);
});
}

@Test
@DisplayName("Verify seedlot access privilege regular user should fail")
void verifySeedlotAccessPrivilege_regularUser_shouldSucceed() {
String clientId = "00012345";

when(userAuthenticationHelper.getUserInfo())
.thenReturn(
Optional.of(mockUserInfo(Set.of("FAKE_ROLE_TEST_00012345"), List.of("00012346"))));

Assertions.assertThrows(
ClientIdForbiddenException.class,
() -> {
loggedUserService.verifySeedlotAccessPrivilege(clientId);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
Expand Down Expand Up @@ -349,6 +350,10 @@ void getUserSeedlots_noPageSize_shouldSucceed() {
void getUserSeedlots_forbidden_shouldFail() {
when(loggedUserService.getLoggedUserInfo()).thenReturn(Optional.of(UserInfo.createDevUser()));

doThrow(ClientIdForbiddenException.class)
.when(loggedUserService)
.verifySeedlotAccessPrivilege(any());

Assertions.assertThrows(
ClientIdForbiddenException.class,
() -> {
Expand Down
21 changes: 19 additions & 2 deletions frontend/src/components/SeedlotCards/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ export const cards = [
highlighted: false,
isEmpty: false,
emptyTitle: '',
emptyDescription: ''
emptyDescription: '',
displayForAdmin: true,
displayForNonAdmin: true
},
// {
// id: '2',
Expand All @@ -35,6 +37,21 @@ export const cards = [
highlighted: false,
isEmpty: false,
emptyTitle: '',
emptyDescription: ''
emptyDescription: '',
displayForAdmin: true,
displayForNonAdmin: true
},
{
id: '4',
image: 'Farm_01',
header: 'Review seedlots',
description: 'Check all seedlots that are waiting for approval',
link: ROUTES.TSC_SEEDLOTS_TABLE,
highlighted: false,
isEmpty: false,
emptyTitle: '',
emptyDescription: '',
displayForAdmin: true,
displayForNonAdmin: false
}
];
60 changes: 38 additions & 22 deletions frontend/src/components/SeedlotCards/index.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,46 @@
import React from 'react';

import React, { useContext } from 'react';
import { Row, Column } from '@carbon/react';

import StandardCard from '../Card/StandardCard';
import { cards } from './constants';

import './styles.scss';
import AuthContext from '../../contexts/AuthContext';

const SeedlotCards = () => {
const { isTscAdmin } = useContext(AuthContext);

const SeedlotCards = () => (
<Row className="seedlot-activities-cards">
{
cards.map((card) => (
<Column sm={4} md={4} lg={8} xlg={8} max={4} key={card.id}>
<StandardCard
image={card.image}
header={card.header}
description={card.description}
url={card.link}
isEmpty={card.isEmpty}
emptyTitle={card.emptyTitle}
emptyDescription={card.emptyDescription}
/>
</Column>
))
const shouldDisplayCard = (card: any) => {
let display = card.displayForNonAdmin;
if (isTscAdmin && display === true) {
display = true;
}
</Row>
);
if (!isTscAdmin) {
return card.displayForNonAdmin;
}
if (!card.displayForAdmin) {
return false;
}
return card.displayForAdmin;
};

return (
<Row className="seedlot-activities-cards">
{
cards.filter((c) => shouldDisplayCard(c)).map((card) => (
<Column sm={4} md={4} lg={8} xlg={8} max={4} key={card.id}>
<StandardCard
image={card.image}
header={card.header}
description={card.description}
url={card.link}
isEmpty={card.isEmpty}
emptyTitle={card.emptyTitle}
emptyDescription={card.emptyDescription}
/>
</Column>
))
}
</Row>
);
};

export default SeedlotCards;
1 change: 1 addition & 0 deletions frontend/src/contexts/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro
if (famUser.clientRoles.length === 1) {
// If a user has only 1 client role then set it right away.
setSelectedClientRoles(famUser.clientRoles[0]);
setIsTscAdmin(famUser.clientRoles[0].roles.includes(TSC_ADMIN_ROLE));
} else {
restoreSelectedClient(famUser);
}
Expand Down

0 comments on commit f396b42

Please sign in to comment.