From 2ddcc4fcaf85a0f3d9cc0fa52d49984a407d20b5 Mon Sep 17 00:00:00 2001 From: Tim DiLauro Date: Wed, 18 Sep 2024 08:38:47 -0400 Subject: [PATCH] Add feature flag for to restrict QuickSight link to sysadmins (#135) --- src/businessRules/roleBasedAccess.ts | 8 ++ src/components/LibraryStats.tsx | 3 + src/components/StatsCollectionsGroup.tsx | 2 +- src/components/StatsUsageReportsGroup.tsx | 35 ++++--- src/interfaces.ts | 1 + src/stylesheets/stats.scss | 11 ++- src/utils/featureFlags.ts | 1 + .../businessRules/roleBasedAccess.test.ts | 93 +++++++++++++++++++ tests/jest/components/Stats.test.tsx | 47 ++++++++++ 9 files changed, 182 insertions(+), 19 deletions(-) diff --git a/src/businessRules/roleBasedAccess.ts b/src/businessRules/roleBasedAccess.ts index 1098fd29f..4bd327d74 100644 --- a/src/businessRules/roleBasedAccess.ts +++ b/src/businessRules/roleBasedAccess.ts @@ -8,6 +8,14 @@ type HasLibraryKeyProps = { [key: string]: unknown; }; +// If the `quicksightOnlyForSysadmins` feature flag is set, only system +// admins should see the QuickSight link. +export const useMaySeeQuickSightLink = (_: HasLibraryKeyProps): boolean => { + const admin = useAppAdmin(); + const onlyForSysAdmins = useAppFeatureFlags().quicksightOnlyForSysadmins; + return !onlyForSysAdmins || admin.isSystemAdmin(); +}; + // If the `reportsOnlyForSysadmins` feature flag is set, only system admins // may request inventory reports. export const useMayRequestInventoryReports = ( diff --git a/src/components/LibraryStats.tsx b/src/components/LibraryStats.tsx index 04823a8b9..5cdea404c 100644 --- a/src/components/LibraryStats.tsx +++ b/src/components/LibraryStats.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import { LibraryStatistics } from "../interfaces"; import { useMayRequestInventoryReports, + useMaySeeQuickSightLink, useMayViewCollectionBarChart, } from "../businessRules/roleBasedAccess"; import StatsTotalCirculationsGroup from "./StatsTotalCirculationsGroup"; @@ -45,6 +46,7 @@ const LibraryStats = ({ stats, library }: LibraryStatsProps) => { const inventoryReportRequestEnabled = useMayRequestInventoryReports({ library, }); + const quicksightLinkEnabled = useMaySeeQuickSightLink({ library }); const quicksightPageUrl = useAppContext().quicksightPagePath; let statsLayoutClass: string, dashboardTitle: string, implementation: string; @@ -74,6 +76,7 @@ const LibraryStats = ({ stats, library }: LibraryStatsProps) => { diff --git a/src/components/StatsCollectionsGroup.tsx b/src/components/StatsCollectionsGroup.tsx index 797790336..5e682b530 100644 --- a/src/components/StatsCollectionsGroup.tsx +++ b/src/components/StatsCollectionsGroup.tsx @@ -20,7 +20,7 @@ const StatsCollectionsGroup = ({ }: Props) => { const content = collections.length === 0 ? ( - No associated collections. + No associated collections. ) : showBarChart ? ( ) : ( diff --git a/src/components/StatsUsageReportsGroup.tsx b/src/components/StatsUsageReportsGroup.tsx index 0313adda9..5b7773114 100644 --- a/src/components/StatsUsageReportsGroup.tsx +++ b/src/components/StatsUsageReportsGroup.tsx @@ -8,6 +8,7 @@ type Props = { heading?: string; description?: string; inventoryReportsEnabled: boolean; + quicksightLinkEnabled: boolean; library?: string; usageDataHref?: string; usageDataLabel?: string; @@ -24,6 +25,7 @@ const StatsUsageReportsGroup = ({ usageDataLabel = "View Usage", usageDataTarget = "_self", inventoryReportsEnabled, + quicksightLinkEnabled, library = undefined, }: Props) => { const [showReportForm, setShowReportForm] = useState(false); @@ -41,6 +43,11 @@ const StatsUsageReportsGroup = ({
  • <> + {!inventoryReportsEnabled && !quicksightLinkEnabled && ( + + Usage reporting is not available. + + )} {inventoryReportsEnabled && library && ( <>
  • -
  • -
    - - {usageDataLabel} - -    - -
    -
  • + {quicksightLinkEnabled && ( +
  • + +
  • + )} ); diff --git a/src/interfaces.ts b/src/interfaces.ts index b12a6942b..3783e1389 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -48,6 +48,7 @@ export interface TestingFlags { export interface FeatureFlags { enableAutoList?: boolean; reportsOnlyForSysadmins?: boolean; + quicksightOnlyForSysadmins?: boolean; } export interface Navigate { diff --git a/src/stylesheets/stats.scss b/src/stylesheets/stats.scss index c5bf99f79..ab65c8cd0 100644 --- a/src/stylesheets/stats.scss +++ b/src/stylesheets/stats.scss @@ -61,12 +61,13 @@ margin: 0; overflow-wrap: normal; } + } - .no-collections { - margin: 10px; - font-style: italic; - color: $medium-dark-gray; - } + .no-content { + margin: 10px; + font-style: italic; + font-weight: bolder; + color: $medium-dark-gray; } .stat-group-description { diff --git a/src/utils/featureFlags.ts b/src/utils/featureFlags.ts index 6224f50d9..73f8fba90 100644 --- a/src/utils/featureFlags.ts +++ b/src/utils/featureFlags.ts @@ -6,4 +6,5 @@ import { FeatureFlags } from "../interfaces"; export const defaultFeatureFlags: FeatureFlags = { enableAutoList: true, reportsOnlyForSysadmins: true, + quicksightOnlyForSysadmins: true, }; diff --git a/tests/jest/businessRules/roleBasedAccess.test.ts b/tests/jest/businessRules/roleBasedAccess.test.ts index f2f889db8..352250ec2 100644 --- a/tests/jest/businessRules/roleBasedAccess.test.ts +++ b/tests/jest/businessRules/roleBasedAccess.test.ts @@ -4,6 +4,7 @@ import { ContextProviderProps } from "../../../src/components/ContextProvider"; import { ConfigurationSettings, FeatureFlags } from "../../../src/interfaces"; import { useMayRequestInventoryReports, + useMaySeeQuickSightLink, useMayViewCollectionBarChart, } from "../../../src/businessRules/roleBasedAccess"; @@ -116,6 +117,98 @@ describe("Business rules for role-based access", () => { }); }); + describe("controls access to the quicksight link", () => { + const testAccess = ( + expectedResult: boolean, + config: Partial + ) => { + const wrapper = setupWrapper(config); + const { result } = renderHook( + () => useMaySeeQuickSightLink({ library: libraryMatch }), + { wrapper } + ); + expect(result.current).toBe(expectedResult); + }; + + it("restricts access to only sysadmins, if the restriction feature flag is true", () => { + const featureFlags: FeatureFlags = { quicksightOnlyForSysadmins: true }; + + testAccess(true, { roles: [{ role: "system" }], featureFlags }); + + testAccess(false, { roles: [{ role: "manager-all" }], featureFlags }); + testAccess(false, { roles: [{ role: "librarian-all" }], featureFlags }); + + testAccess(false, { + roles: [{ role: "manager", library: libraryMatch }], + featureFlags, + }); + testAccess(false, { + roles: [{ role: "manager", library: libraryMismatch }], + featureFlags, + }); + testAccess(false, { + roles: [{ role: "librarian", library: libraryMatch }], + featureFlags, + }); + testAccess(false, { + roles: [{ role: "librarian", library: libraryMismatch }], + featureFlags, + }); + }); + + it("allows all users, if the restriction feature flag is is false", () => { + const featureFlags: FeatureFlags = { quicksightOnlyForSysadmins: false }; + + testAccess(true, { roles: [{ role: "system" }], featureFlags }); + + testAccess(true, { roles: [{ role: "manager-all" }], featureFlags }); + testAccess(true, { roles: [{ role: "librarian-all" }], featureFlags }); + + testAccess(true, { + roles: [{ role: "manager", library: libraryMatch }], + featureFlags, + }); + testAccess(true, { + roles: [{ role: "manager", library: libraryMismatch }], + featureFlags, + }); + testAccess(true, { + roles: [{ role: "librarian", library: libraryMatch }], + featureFlags, + }); + testAccess(true, { + roles: [{ role: "librarian", library: libraryMismatch }], + featureFlags, + }); + }); + + it("allows all users, if the restriction feature flag is not set", () => { + const featureFlags: FeatureFlags = {}; + + testAccess(true, { roles: [{ role: "system" }], featureFlags }); + + testAccess(true, { roles: [{ role: "manager-all" }], featureFlags }); + testAccess(true, { roles: [{ role: "librarian-all" }], featureFlags }); + + testAccess(true, { + roles: [{ role: "manager", library: libraryMatch }], + featureFlags, + }); + testAccess(true, { + roles: [{ role: "manager", library: libraryMismatch }], + featureFlags, + }); + testAccess(true, { + roles: [{ role: "librarian", library: libraryMatch }], + featureFlags, + }); + testAccess(true, { + roles: [{ role: "librarian", library: libraryMismatch }], + featureFlags, + }); + }); + }); + describe("controls access to the collection statistics barchart", () => { const testAccess = ( expectedResult: boolean, diff --git a/tests/jest/components/Stats.test.tsx b/tests/jest/components/Stats.test.tsx index 135da76a8..087a65356 100644 --- a/tests/jest/components/Stats.test.tsx +++ b/tests/jest/components/Stats.test.tsx @@ -434,6 +434,53 @@ describe("Dashboard Statistics", () => { expect(renderFor(false, managerAll)).not.toBeNull(); expect(renderFor(false, librarianAll)).not.toBeNull(); }); + + it("shows quicksight link only for sysadmins, if sysadmin-only flag set", () => { + const fakeQuickSightHref = "https://example.com/fakeQS"; + + // We'll use this function to test multiple scenarios. + const renderFor = ( + onlySysadmins: boolean, + roles: { role: string; library?: string }[] + ) => { + const contextProviderProps: Partial = { + featureFlags: { quicksightOnlyForSysadmins: onlySysadmins }, + roles, + quicksightPagePath: fakeQuickSightHref, + }; + const { + container, + getByRole, + queryByRole, + queryByText, + } = renderWithProviders(, { + contextProviderProps, + }); + + // We should always render a Usage reports group when a library is specified. + getByRole("heading", { + level: 3, + name: statGroupToHeading.usageReports, + }); + const usageReportLink = queryByRole("link", { name: /View Usage/i }); + if (usageReportLink) { + expect(usageReportLink).toHaveAttribute("href", fakeQuickSightHref); + } + + // Clean up the container after each render. + document.body.removeChild(container); + return usageReportLink; + }; + + // If the feature flag is set, the link should be visible only to sysadmins. + expect(renderFor(true, systemAdmin)).not.toBeNull(); + expect(renderFor(true, managerAll)).toBeNull(); + expect(renderFor(true, librarianAll)).toBeNull(); + // If the feature flag is false, the button should be visible to all users. + expect(renderFor(false, systemAdmin)).not.toBeNull(); + expect(renderFor(false, managerAll)).not.toBeNull(); + expect(renderFor(false, librarianAll)).not.toBeNull(); + }); }); describe("charting - custom tooltip", () => {