Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PIMS-2029 ERP Visibility #2654

Merged
merged 18 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions express-api/src/constants/projectStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,9 @@ export enum ProjectStatus {
// CONTRACT_IN_PLACE_UNCONDITIONAL = 43, // Disabled
CLOSE_OUT = 44,
}

/**
* Projects and properties in ERP are shown to agencies outside of their owning agency.
* Adding new statuses to this list will reveal them to outside agencies.
*/
export const exposedProjectStatuses = [ProjectStatus.APPROVED_FOR_ERP];
26 changes: 25 additions & 1 deletion express-api/src/controllers/buildings/buildingsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { SSOUser } from '@bcgov/citz-imb-sso-express';
import { Building } from '@/typeorm/Entities/Building';
import { checkUserAgencyPermission, isAdmin, isAuditor } from '@/utilities/authorizationChecks';
import { Roles } from '@/constants/roles';
import { AppDataSource } from '@/appDataSource';
import { exposedProjectStatuses } from '@/constants/projectStatus';
import { ProjectProperty } from '@/typeorm/Entities/ProjectProperty';

/**
* @description Gets all buildings satisfying the filter parameters.
Expand Down Expand Up @@ -53,7 +56,28 @@ export const getBuilding = async (req: Request, res: Response) => {

if (!building) {
return res.status(404).send('Building matching this ID was not found.');
} else if (!(await checkUserAgencyPermission(kcUser, [building.AgencyId], permittedRoles))) {
}

// Get related projects
const projects = (
await AppDataSource.getRepository(ProjectProperty).find({
where: {
BuildingId: building.Id,
},
relations: {
Project: true,
},
})
).map((pp) => pp.Project);
// Are any related projects in ERP? If so, they should be visible to outside agencies.
const isVisibleToOtherAgencies = projects.some((project) =>
exposedProjectStatuses.includes(project.StatusId),
);

if (
!(await checkUserAgencyPermission(kcUser, [building.AgencyId], permittedRoles)) &&
!isVisibleToOtherAgencies
) {
return res.status(403).send('You are not authorized to view this building.');
}
return res.status(200).send(building);
Expand Down
27 changes: 26 additions & 1 deletion express-api/src/controllers/parcels/parcelsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import userServices from '@/services/users/usersServices';
import { Parcel } from '@/typeorm/Entities/Parcel';
import { Roles } from '@/constants/roles';
import { checkUserAgencyPermission, isAdmin, isAuditor } from '@/utilities/authorizationChecks';
import { AppDataSource } from '@/appDataSource';
import { ProjectProperty } from '@/typeorm/Entities/ProjectProperty';
import { exposedProjectStatuses } from '@/constants/projectStatus';

/**
* @description Gets information about a particular parcel by the Id provided in the URL parameter.
Expand All @@ -23,9 +26,31 @@ export const getParcel = async (req: Request, res: Response) => {
const permittedRoles = [Roles.ADMIN, Roles.AUDITOR];
const kcUser = req.user as unknown as SSOUser;
const parcel = await parcelServices.getParcelById(parcelId);

if (!parcel) {
return res.status(404).send('Parcel matching this internal ID not found.');
} else if (!(await checkUserAgencyPermission(kcUser, [parcel.AgencyId], permittedRoles))) {
}

// Get related projects
const projects = (
await AppDataSource.getRepository(ProjectProperty).find({
where: {
ParcelId: parcel.Id,
},
relations: {
Project: true,
},
})
).map((pp) => pp.Project);
// Are any related projects in ERP? If so, they should be visible to outside agencies.
const isVisibleToOtherAgencies = projects.some((project) =>
exposedProjectStatuses.includes(project.StatusId),
);

if (
!(await checkUserAgencyPermission(kcUser, [parcel.AgencyId], permittedRoles)) &&
!isVisibleToOtherAgencies
) {
return res.status(403).send('You are not authorized to view this parcel.');
}
return res.status(200).send(parcel);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { DeepPartial } from 'typeorm';
import { Project } from '@/typeorm/Entities/Project';
import { Roles } from '@/constants/roles';
import notificationServices from '@/services/notifications/notificationServices';
import { exposedProjectStatuses } from '@/constants/projectStatus';

/**
* @description Get disposal project by either the numeric id or projectNumber.
Expand All @@ -28,7 +29,13 @@ export const getDisposalProject = async (req: Request, res: Response) => {
return res.status(404).send('Project matching this internal ID not found.');
}

if (!(await checkUserAgencyPermission(user, [project.AgencyId], permittedRoles))) {
// Is the project in ERP? If so, it should be visible to outside agencies.
const isVisibleToOtherAgencies = exposedProjectStatuses.includes(project.StatusId);

if (
!(await checkUserAgencyPermission(user, [project.AgencyId], permittedRoles)) &&
!isVisibleToOtherAgencies
) {
return res.status(403).send('You are not authorized to view this project.');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Request, Response } from 'express';
import propertyServices from '@/services/properties/propertiesServices';
import {
ImportResultFilterSchema,
MapFilter,
MapFilterSchema,
PropertyUnionFilterSchema,
} from '@/controllers/properties/propertiesSchema';
Expand Down Expand Up @@ -68,7 +69,7 @@ export const getPropertiesForMap = async (req: Request, res: Response) => {

// Converts comma-separated lists to arrays, see schema
// Must remove empty arrays for TypeORM to work
const filterResult = {
const filterResult: MapFilter = {
...filter.data,
AgencyIds: filter.data.AgencyIds.length ? filter.data.AgencyIds : undefined,
ClassificationIds: filter.data.ClassificationIds.length
Expand All @@ -81,11 +82,12 @@ export const getPropertiesForMap = async (req: Request, res: Response) => {
RegionalDistrictIds: filter.data.RegionalDistrictIds.length
? filter.data.RegionalDistrictIds
: undefined,
// UserAgencies included to separate requested filter on agencies vs user's restriction on agencies
UserAgencies: undefined,
};

// Controlling for agency search visibility
const kcUser = req.user;
// admin and suditors can see any property
const permittedRoles = [Roles.ADMIN, Roles.AUDITOR];
// Admins and auditors see all, otherwise...
if (!(isAdmin(kcUser) || isAuditor(kcUser))) {
Expand All @@ -99,7 +101,7 @@ export const getPropertiesForMap = async (req: Request, res: Response) => {
if (!requestedAgencies || !userHasAgencies) {
// Then only show that user's agencies instead.
const usersAgencies = await userServices.getAgencies(kcUser.preferred_username);
filterResult.AgencyIds = usersAgencies;
filterResult.UserAgencies = usersAgencies;
}
}

Expand Down
2 changes: 2 additions & 0 deletions express-api/src/controllers/properties/propertiesSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const MapFilterSchema = z.object({
PropertyTypeIds: arrayFromString(numberSchema),
Name: z.string().optional(),
RegionalDistrictIds: arrayFromString(numberSchema),
UserAgencies: z.array(z.number().int()).optional(),
});

export type MapFilter = z.infer<typeof MapFilterSchema>;
Expand All @@ -47,6 +48,7 @@ export const PropertyUnionFilterSchema = z.object({
administrativeArea: z.string().optional(),
landArea: z.string().optional(),
updatedOn: z.string().optional(),
projectStatus: z.string().optional(),
quickFilter: z.string().optional(),
sortKey: z.string().optional(),
sortOrder: z.string().optional(),
Expand Down
19 changes: 14 additions & 5 deletions express-api/src/services/projects/projectsServices.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AppDataSource } from '@/appDataSource';
import { ProjectStatus } from '@/constants/projectStatus';
import { exposedProjectStatuses, ProjectStatus } from '@/constants/projectStatus';
import { ProjectType } from '@/constants/projectType';
import { Agency } from '@/typeorm/Entities/Agency';
import { Building } from '@/typeorm/Entities/Building';
Expand Down Expand Up @@ -925,11 +925,20 @@ const getProjects = async (filter: ProjectFilter) => {
}),
);

// Restricts based on user's agencies
// Only non-admins have this set in the controller
if (filter.agencyId?.length) {
query.andWhere('agency_id IN(:...list)', {
list: filter.agencyId,
});
query.andWhere(
new Brackets((qb) => {
// Restricts based on user's agencies
qb.orWhere('agency_id IN(:...list)', {
list: filter.agencyId,
});
// But also allow for ERP projects to be visible
qb.orWhere('status_id IN(:...exposedProjectStatuses)', {
exposedProjectStatuses: exposedProjectStatuses,
});
}),
);
}

// Add quickfilter part
Expand Down
117 changes: 86 additions & 31 deletions express-api/src/services/properties/propertiesServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import userServices from '../users/usersServices';
import { Brackets, FindOptionsWhere, ILike, In, QueryRunner } from 'typeorm';
import { SSOUser } from '@bcgov/citz-imb-sso-express';
import { PropertyType } from '@/constants/propertyType';
import { ProjectStatus } from '@/constants/projectStatus';
import { exposedProjectStatuses, ProjectStatus } from '@/constants/projectStatus';
import { ProjectProperty } from '@/typeorm/Entities/ProjectProperty';
import { ProjectStatus as ProjectStatusEntity } from '@/typeorm/Entities/ProjectStatus';
import { parentPort } from 'worker_threads';
Expand Down Expand Up @@ -182,35 +182,79 @@ const findLinkedProjectsForProperty = async (buildingId?: number, parcelId?: num
* @returns A promise that resolves to an array of properties matching the filter criteria.
*/
const getPropertiesForMap = async (filter?: MapFilter) => {
const properties = await AppDataSource.getRepository(MapProperties).find({
// Select only the properties needed to render map markers and sidebar
select: {
Id: true,
Location: {
x: true,
y: true,
},
PropertyTypeId: true,
ClassificationId: true,
Name: true,
PID: true,
PIN: true,
AdministrativeAreaId: true,
AgencyId: true,
Address1: true,
// Select only the properties needed to render map markers and sidebar
const selectObject = {
Id: true,
Location: {
x: true,
y: true,
},
PropertyTypeId: true,
ClassificationId: true,
Name: true,
PID: true,
PIN: true,
AdministrativeAreaId: true,
AgencyId: true,
Address1: true,
ProjectStatusId: true,
};

const filterBase: FindOptionsWhere<MapProperties> = {
ClassificationId: filter.ClassificationIds ? In(filter.ClassificationIds) : undefined,
AdministrativeAreaId: filter.AdministrativeAreaIds
? In(filter.AdministrativeAreaIds)
: undefined,
PID: filter.PID,
PIN: filter.PIN,
Address1: filter.Address ? ILike(`%${filter.Address}%`) : undefined,
Name: filter.Name ? ILike(`%${filter.Name}%`) : undefined,
PropertyTypeId: filter.PropertyTypeIds ? In(filter.PropertyTypeIds) : undefined,
RegionalDistrictId: filter.RegionalDistrictIds ? In(filter.RegionalDistrictIds) : undefined,
};

/**
* If the user's agencies were defined, then they didn't have permissions to see all the agencies.
* This path allows a user to filter by agencies they belong to.
* If no agency filter is requested, it filters by the user's agencies, but also
* includes properties with a project status that would expose them to users
* outside of the owning agency.
*/
if (filter.UserAgencies) {
// Did they request to filter on agencies? Only use the crossover of their agencies and the filter
const agencies = filter.AgencyIds
? filter.AgencyIds.filter((a) => filter.UserAgencies.includes(a))
: filter.UserAgencies;

const properties = await AppDataSource.getRepository(MapProperties).find({
select: selectObject,
where: filter.AgencyIds
? {
...filterBase,
AgencyId: In(agencies),
}
: [
{
...filterBase,
AgencyId: In(agencies),
},
{
...filterBase,
ProjectStatusId: In(exposedProjectStatuses),
},
],
});
return properties;
}
/**
* This path is for users that pass the admin/auditor role check.
* Search will function unchanged from the request.
*/
const properties = await AppDataSource.getRepository(MapProperties).find({
select: selectObject,
where: {
ClassificationId: filter.ClassificationIds ? In(filter.ClassificationIds) : undefined,
...filterBase,
AgencyId: filter.AgencyIds ? In(filter.AgencyIds) : undefined,
AdministrativeAreaId: filter.AdministrativeAreaIds
? In(filter.AdministrativeAreaIds)
: undefined,
PID: filter.PID,
PIN: filter.PIN,
Address1: filter.Address ? ILike(`%${filter.Address}%`) : undefined,
Name: filter.Name ? ILike(`%${filter.Name}%`) : undefined,
PropertyTypeId: filter.PropertyTypeIds ? In(filter.PropertyTypeIds) : undefined,
RegionalDistrictId: filter.RegionalDistrictIds ? In(filter.RegionalDistrictIds) : undefined,
},
});
return properties;
Expand Down Expand Up @@ -799,6 +843,8 @@ const collectFindOptions = (filter: PropertyUnionFilter) => {
);
if (filter.propertyType)
options.push(constructFindOptionFromQuerySingleSelect('PropertyType', filter.propertyType));
if (filter.projectStatus)
options.push(constructFindOptionFromQuerySingleSelect('ProjectStatus', filter.projectStatus));
return options;
};

Expand All @@ -817,11 +863,20 @@ const getPropertiesUnion = async (filter: PropertyUnionFilter) => {
}),
);

// Restricts based on user's agencies
// Only non-admins have this set in the controller
if (filter.agencyIds?.length) {
query.andWhere('agency_id IN(:...list)', {
list: filter.agencyIds,
});
query.andWhere(
new Brackets((qb) => {
// Restricts based on user's agencies
qb.orWhere('agency_id IN(:...list)', {
list: filter.agencyIds,
});
// But also allow for ERP projects to be visible
qb.orWhere('project_status_id IN(:...exposedProjectStatuses)', {
exposedProjectStatuses: exposedProjectStatuses,
});
}),
);
}

// Add quickfilter part
Expand Down
Loading
Loading