Skip to content

Commit

Permalink
Add feature flag for to restrict QuickSight link to sysadmins (#135)
Browse files Browse the repository at this point in the history
  • Loading branch information
tdilauro authored Sep 18, 2024
1 parent f853860 commit 2ddcc4f
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 19 deletions.
8 changes: 8 additions & 0 deletions src/businessRules/roleBasedAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down
3 changes: 3 additions & 0 deletions src/components/LibraryStats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from "react";
import { LibraryStatistics } from "../interfaces";
import {
useMayRequestInventoryReports,
useMaySeeQuickSightLink,
useMayViewCollectionBarChart,
} from "../businessRules/roleBasedAccess";
import StatsTotalCirculationsGroup from "./StatsTotalCirculationsGroup";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -74,6 +76,7 @@ const LibraryStats = ({ stats, library }: LibraryStatsProps) => {
<StatsUsageReportsGroup
library={library}
inventoryReportsEnabled={inventoryReportRequestEnabled}
quicksightLinkEnabled={quicksightLinkEnabled}
usageDataTarget="_blank" // open in new tab or window
usageDataHref={quicksightPageUrl}
/>
Expand Down
2 changes: 1 addition & 1 deletion src/components/StatsCollectionsGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const StatsCollectionsGroup = ({
}: Props) => {
const content =
collections.length === 0 ? (
<span className="no-collections">No associated collections.</span>
<span className="no-content">No associated collections.</span>
) : showBarChart ? (
<StatsCollectionsBarChart collections={collections} />
) : (
Expand Down
35 changes: 22 additions & 13 deletions src/components/StatsUsageReportsGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type Props = {
heading?: string;
description?: string;
inventoryReportsEnabled: boolean;
quicksightLinkEnabled: boolean;
library?: string;
usageDataHref?: string;
usageDataLabel?: string;
Expand All @@ -24,6 +25,7 @@ const StatsUsageReportsGroup = ({
usageDataLabel = "View Usage",
usageDataTarget = "_self",
inventoryReportsEnabled,
quicksightLinkEnabled,
library = undefined,
}: Props) => {
const [showReportForm, setShowReportForm] = useState(false);
Expand All @@ -41,6 +43,11 @@ const StatsUsageReportsGroup = ({
<li>
<StatsGroup heading={heading} description={description}>
<>
{!inventoryReportsEnabled && !quicksightLinkEnabled && (
<span className="no-content">
Usage reporting is not available.
</span>
)}
{inventoryReportsEnabled && library && (
<>
<Button
Expand All @@ -63,19 +70,21 @@ const StatsUsageReportsGroup = ({
</>
</StatsGroup>
</li>
<li>
<div className="stat-link">
<a
href={usageDataHref}
target={usageDataTarget}
rel="noopener noreferrer"
>
{usageDataLabel}
</a>
&nbsp;&nbsp;
<i className="fa fa-external-link" />
</div>
</li>
{quicksightLinkEnabled && (
<li>
<div className="stat-link">
<a
href={usageDataHref}
target={usageDataTarget}
rel="noopener noreferrer"
>
{usageDataLabel}
</a>
&nbsp;&nbsp;
<i className="fa fa-external-link" />
</div>
</li>
)}
</ul>
</>
);
Expand Down
1 change: 1 addition & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface TestingFlags {
export interface FeatureFlags {
enableAutoList?: boolean;
reportsOnlyForSysadmins?: boolean;
quicksightOnlyForSysadmins?: boolean;
}

export interface Navigate {
Expand Down
11 changes: 6 additions & 5 deletions src/stylesheets/stats.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions src/utils/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ import { FeatureFlags } from "../interfaces";
export const defaultFeatureFlags: FeatureFlags = {
enableAutoList: true,
reportsOnlyForSysadmins: true,
quicksightOnlyForSysadmins: true,
};
93 changes: 93 additions & 0 deletions tests/jest/businessRules/roleBasedAccess.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ContextProviderProps } from "../../../src/components/ContextProvider";
import { ConfigurationSettings, FeatureFlags } from "../../../src/interfaces";
import {
useMayRequestInventoryReports,
useMaySeeQuickSightLink,
useMayViewCollectionBarChart,
} from "../../../src/businessRules/roleBasedAccess";

Expand Down Expand Up @@ -116,6 +117,98 @@ describe("Business rules for role-based access", () => {
});
});

describe("controls access to the quicksight link", () => {
const testAccess = (
expectedResult: boolean,
config: Partial<ConfigurationSettings>
) => {
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,
Expand Down
47 changes: 47 additions & 0 deletions tests/jest/components/Stats.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ContextProviderProps> = {
featureFlags: { quicksightOnlyForSysadmins: onlySysadmins },
roles,
quicksightPagePath: fakeQuickSightHref,
};
const {
container,
getByRole,
queryByRole,
queryByText,
} = renderWithProviders(<Stats library={sampleLibraryKey} />, {
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", () => {
Expand Down

0 comments on commit 2ddcc4f

Please sign in to comment.