Skip to content
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
15 changes: 9 additions & 6 deletions submit-api/src/submit_api/resources/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,9 @@ class PackageUpdateRequests(Resource):
@cross_origin(origins=allowedorigins())
def post(package_id):
"""Create an update request."""
# Check package-aware access control (mp_create, w_create, or eao_create)
PackageAccessControl.check_package_access(package_id, PackageOperation.CREATE)
# Check basic package access (user can view the package)
# Item-level permissions (including GIS) are validated in PackageService.create_update_request
authorization.has_access_to_package(package_id)

create_update_request_data = CreateUpdateRequestSchema().load(API.payload)
package_with_created_update_request = PackageService.create_update_request(
Expand Down Expand Up @@ -219,8 +220,9 @@ class PackageUpdateRequest(Resource):
@cross_origin(origins=allowedorigins())
def patch(package_id, update_request_id):
"""Accept an update request."""
# Check package-aware access control (mp_create, w_create, or eao_create)
PackageAccessControl.check_package_access(package_id, PackageOperation.CREATE)
# Check basic package access (user can view the package)
# Item-level permissions (including GIS) are validated in PackageService.accept_update_request
authorization.has_access_to_package(package_id)

accept_update_request = PackageService.accept_update_request(package_id, update_request_id)
return StaffPackageSchema().dump(accept_update_request), HTTPStatus.OK
Expand All @@ -236,8 +238,9 @@ def patch(package_id, update_request_id):
@cross_origin(origins=allowedorigins())
def delete(package_id, update_request_id):
"""Withdraw an update request."""
# Check package-aware access control (mp_create, w_create, or eao_create)
PackageAccessControl.check_package_access(package_id, PackageOperation.CREATE)
# Check basic package access (user can view the package)
# Item-level permissions (including GIS) are validated in PackageService.withdraw_update_request
authorization.has_access_to_package(package_id)

withdraw_update_request = PackageService.withdraw_update_request(package_id, update_request_id)
return StaffPackageSchema().dump(withdraw_update_request), HTTPStatus.OK
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ import { BCDesignTokens } from "epic.theme";
import { useDocumentRow } from "@/hooks/useDocumentRow";
import { usePackageRoles } from "@/hooks/usePackageRoles";
import { useState } from "react";
import { useAccount } from "@/store/accountStore";
import { lazy, Suspense } from "react";
import { GIS_ITEM_TYPE_NAME } from "@/utils/constants";
import { useGetGeoUploads } from "@/hooks/api/useGeo";
import { useHasRole } from "@/hooks/common";
import { EPIC_SUBMIT_ROLE } from "@/models/Role";

const MapPreviewModal = lazy(() =>
import("@/components/App/Map/MapPreviewModal").then((m) => ({
Expand All @@ -53,16 +54,15 @@ export default function DocumentRow({
documentSubmission;
const packageType = propsPackageType || submissionPackage?.type;
const name = submitted_document?.name || "";
const { roles } = useAccount();

// Check if this is a GIS document (item type name "Geospatial Information")
const isGISDocument = submissionItem.type.name === GIS_ITEM_TYPE_NAME;

// Get the correct package-specific roles (include document folder for GIS handling)
const packageRoles = usePackageRoles(submissionPackage, packageType, submitted_document?.folder);

// Check if user has GIS permissions
const hasGISPermissions = roles?.includes('gis_extended_edit') || roles?.includes('full_access');
// Check if user has GIS permissions (includes full_access check)
const hasGISPermissions = useHasRole(EPIC_SUBMIT_ROLE.gis_extended_edit);

// State for GIS preview modal
const [showPreviewModal, setShowPreviewModal] = useState(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ import SubmissionItemReviewConfirmation from "@/components/App/Submission/Submis
import DocumentRow from "@/components/App/Submission/DocumentRow";
import { useMemo } from "react";
import { getSubmissionItemLabel } from "@/utils";
import { useAccount } from "@/store/accountStore";
import { usePackageRoles } from "@/hooks/usePackageRoles";
import { GIS_ITEM_TYPE_NAME } from "@/utils/constants";
import { useHasRole } from "@/hooks/common";
import { EPIC_SUBMIT_ROLE } from "@/models/Role";

export default function StaffSubmissionItemTableRow({
item,
Expand All @@ -50,7 +51,6 @@ export default function StaffSubmissionItemTableRow({
const { submissions, id } = item;

const { submitted_on } = submissionPackage;
const { roles } = useAccount();

const name = useMemo(() => {
return getSubmissionItemLabel(item.type.name);
Expand Down Expand Up @@ -88,11 +88,11 @@ export default function StaffSubmissionItemTableRow({
// Get the appropriate roles for this package type
const packageRoles = usePackageRoles(submissionPackage, packageType);

// Check if user has the appropriate create role for this package type
const hasPackageCreateRole = roles?.includes(packageRoles.create);
// Check if user has the appropriate create role for this package type (includes full_access check)
const hasPackageCreateRole = useHasRole(packageRoles.create);

// Check if user has GIS permissions
const hasGISPermissions = roles?.includes('gis_extended_edit') || roles?.includes('full_access');
// Check if user has GIS permissions (includes full_access check)
const hasGISPermissions = useHasRole(EPIC_SUBMIT_ROLE.gis_extended_edit);

// Determine if Request Update should be shown
// For GIS items: show if user has GIS permissions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import { UPDATE_REQUEST_STATUS } from "@/models/UpdateRequest";
import ActionSplitButton from "@/components/Shared/ActionSplitButton/ActionSplitButton";
import { UpdateRequestAccordion } from "./UpdateRequestAccordion";
import { useUpdatePackageUpdateRequestNote, useCreatePackageUpdateRequesNote } from "@/hooks/api/usePackages";
import { useAccount } from "@/store/accountStore";
import { useHasRole } from "@/hooks/common";
import { EPIC_SUBMIT_ROLE } from "@/models/Role";

interface SentRequestCollapsibleProps {
request: SentRequest;
Expand Down Expand Up @@ -51,10 +52,9 @@ export const SentRequestCollapsible: React.FC<SentRequestCollapsibleProps> = ({
const [isEditingNote, setIsEditingNote] = useState(false);
const [noteText, setNoteText] = useState(request.note || "");
const maxNoteLength = 500;
const { roles } = useAccount();

// Check if user has GIS permissions
const hasGISPermissions = roles?.includes('gis_extended_edit') || roles?.includes('full_access');
// Check if user has GIS permissions (includes full_access check)
const hasGISPermissions = useHasRole(EPIC_SUBMIT_ROLE.gis_extended_edit);

// Determine if actions should be disabled for GIS requests
const isActionDisabled = isGISRequest && !hasGISPermissions;
Expand Down
62 changes: 61 additions & 1 deletion submit-web/src/hooks/common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useMediaQuery, useTheme } from "@mui/material";
import { useEffect } from "react";
import { useEffect, useMemo } from "react";
import { useAccount } from "@/store/accountStore";
import { EPIC_SUBMIT_ROLE, EpicSubmitRole } from "@/models/Role";

export const useIsMobile = () => {
const theme = useTheme();
Expand All @@ -13,3 +15,61 @@ export const useMounted = (callback: () => void) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(callback, []);
};

/**
* Hook to check if the current user has a specific role or full_access.
* This encapsulates the common pattern of checking for a role OR full_access.
*
* @param requiredRole - The specific role to check for (e.g., 'mp_create', 'w_edit')
* @returns boolean - true if user has the required role OR full_access
*
* @example
* const hasCreatePermission = useHasRole(EPIC_SUBMIT_ROLE.mp_create);
* const hasGISPermission = useHasRole(EPIC_SUBMIT_ROLE.gis_extended_edit);
*/
export const useHasRole = (requiredRole: EpicSubmitRole | string): boolean => {
const { roles } = useAccount();

return useMemo(() => {
if (!roles) return false;
return roles.includes(requiredRole) || roles.includes(EPIC_SUBMIT_ROLE.full_access);
}, [roles, requiredRole]);
};

/**
* Hook to check if the current user has ANY of the specified roles or full_access.
*
* @param requiredRoles - Array of roles to check for
* @returns boolean - true if user has any of the required roles OR full_access
*
* @example
* const hasAnyEditRole = useHasAnyRole([EPIC_SUBMIT_ROLE.mp_edit, EPIC_SUBMIT_ROLE.w_edit]);
*/
export const useHasAnyRole = (requiredRoles: (EpicSubmitRole | string)[]): boolean => {
const { roles } = useAccount();

return useMemo(() => {
if (!roles) return false;
if (roles.includes(EPIC_SUBMIT_ROLE.full_access)) return true;
return requiredRoles.some(role => roles.includes(role));
}, [roles, requiredRoles]);
};

/**
* Hook to check if the current user has ALL of the specified roles or full_access.
*
* @param requiredRoles - Array of roles to check for
* @returns boolean - true if user has all of the required roles OR full_access
*
* @example
* const hasAllRoles = useHasAllRoles([EPIC_SUBMIT_ROLE.mp_view, EPIC_SUBMIT_ROLE.mp_edit]);
*/
export const useHasAllRoles = (requiredRoles: (EpicSubmitRole | string)[]): boolean => {
const { roles } = useAccount();

return useMemo(() => {
if (!roles) return false;
if (roles.includes(EPIC_SUBMIT_ROLE.full_access)) return true;
return requiredRoles.every(role => roles.includes(role));
}, [roles, requiredRoles]);
};
12 changes: 9 additions & 3 deletions submit-web/src/hooks/useStaffSubmissionPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { useNavigate } from "@tanstack/react-router";
import { useAccount } from "@/store/accountStore";
import { hasPermission } from "@/components/Shared/PermissionGate/utils";
import { EPIC_SUBMIT_ROLE } from "@/models/Role";
import { useHasRole } from "@/hooks/common";

interface UseStaffSubmissionPageOptions {
submissionPackageId: number;
Expand Down Expand Up @@ -174,11 +175,16 @@ export function useStaffSubmissionPage({
].includes(approval_type as SubmissionPackageApprovalType);

// Only users with w_extended_edit permission can approve packages
// GIS-only users should not be able to approve packages
const canApprove = hasPermission({
// GIS-only users (without full_access) should not be able to approve packages
const hasExtendedEdit = hasPermission({
permissions: account.roles || [],
scopes: [EPIC_SUBMIT_ROLE.w_extended_edit],
}) && !account.roles?.includes(EPIC_SUBMIT_ROLE.gis_extended_edit);
});
const hasGISRole = useHasRole(EPIC_SUBMIT_ROLE.gis_extended_edit);
const hasFullAccess = useHasRole(EPIC_SUBMIT_ROLE.full_access);

// Exclude GIS-only users (those with GIS role but not full_access)
const canApprove = hasExtendedEdit && !(hasGISRole && !hasFullAccess);
const showApproveButtons =
isPackageAcknowledged &&
approval_type == SubmissionPackageApprovalType.C &&
Expand Down
Loading