Skip to content

Commit

Permalink
Merge pull request #553 from cloud-gov/bh-csp-headers
Browse files Browse the repository at this point in the history
Use middleware to add nonce and CSP headers
  • Loading branch information
hursey013 committed Nov 1, 2024
2 parents 380d4ff + 2681bb8 commit 61de4e1
Show file tree
Hide file tree
Showing 23 changed files with 346 additions and 231 deletions.
19 changes: 18 additions & 1 deletion __tests__/middleware.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it, beforeAll, afterEach } from '@jest/globals';
import { NextRequest, NextResponse } from 'next/server';
import jwt from 'jsonwebtoken';
import { middleware } from '@/middleware.ts';
import middleware from '@/middleware.ts';
// Need to disable eslint for this import because
// you need to import the module you're going to mock with Jest
// eslint-disable-next-line no-unused-vars
Expand Down Expand Up @@ -57,6 +57,10 @@ describe('/login', () => {
expect(location).toMatch('state=baz');
expect(location).toMatch('response_type=code');
});

it('has CSP headers present', () => {
expect(response.headers.get('content-security-policy')).not.toBeNull();
});
});

describe('auth/login/callback', () => {
Expand Down Expand Up @@ -291,3 +295,16 @@ describe('/orgs/* when logged in', () => {
});
});
});

describe('withCSP', () => {
it('should modify request headers', async () => {
// setup
const request = new NextRequest(new URL('/', process.env.ROOT_URL));

const response = await middleware(request);

// Assert that the headers were added as expected
expect(response.headers.get('content-security-policy')).not.toBeNull();
expect(response.headers.get('x-nonce')).not.toBeNull();
});
});
26 changes: 0 additions & 26 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,10 @@
const path = require('path');

const cspHeader = `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' blob: data:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`;

module.exports = {
generateBuildId: async () => {
// placeholder build id for development
return '0.0.1';
},
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: cspHeader.replace(/\n/g, ''),
},
],
},
];
},
sassOptions: {
includePaths: [
path.join(__dirname, 'node_modules', '@uswds', 'uswds', 'packages'),
Expand Down
4 changes: 4 additions & 0 deletions src/app/orgs/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
'use server';

import { headers } from 'next/headers';
import { getOrgsPage } from '@/controllers/controllers';
import { OrganizationsList } from '@/components/OrganizationsList/OrganizationsList';
import { PageHeader } from '@/components/PageHeader';
import { LastViewedOrgLink } from '@/components/LastViewedOrgLink';
import { Timestamp } from '@/components/Timestamp';

export default async function OrgsPage() {
const headersList = await headers();
const nonce = headersList.get('x-nonce') || undefined;
const { payload } = await getOrgsPage();

return (
Expand All @@ -28,6 +31,7 @@ export default async function OrgsPage() {
memoryCurrentUsage={payload.memoryCurrentUsage}
spaceCounts={payload.spaceCounts}
roles={payload.roles}
nonce={nonce}
/>
</div>
);
Expand Down
13 changes: 10 additions & 3 deletions src/assets/stylesheets/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,14 @@
'palette-color-system-green-cool-vivid',
'palette-color-system-red-cool-vivid',
'palette-color-system-red-vivid'
// no trailing comma,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
// no trailing comma,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
),

$border-color-palettes: (
'palette-color-system-green-cool',
'palette-color-system-red-vivid',
'palette-color-system-gray-cool'
// no trailing comma,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
// no trailing comma,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
),

$top-palettes: (
Expand Down Expand Up @@ -164,6 +164,9 @@
text-overflow: clip;
text-overflow: ellipsis;
}
.text-underline-offset {
text-underline-offset: 0.7em;
}

/* Custom selector styles */

Expand Down Expand Up @@ -429,7 +432,11 @@ $error-color-dark: 'red-40v';
// ProgressBar

.progress__bg--infinite {
background: linear-gradient(90deg, color('blue-cool-20v') 78.42%, color('blue-cool-30v') 100%);
background: linear-gradient(
90deg,
color('blue-cool-20v') 78.42%,
color('blue-cool-30v') 100%
);
}

.progress__infinity-logo {
Expand Down
13 changes: 13 additions & 0 deletions src/components/Image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import NextImage, { getImageProps } from 'next/image';
import { ComponentProps } from 'react';

export default function Image(props: ComponentProps<typeof NextImage>) {
const { props: nextProps } = getImageProps({
...props,
});

// eslint-disable-next-line no-unused-vars
const { style: _omit, ...delegated } = nextProps;

return <img {...delegated} />;
}
4 changes: 3 additions & 1 deletion src/components/MemoryBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ import { formatMb } from '@/helpers/numbers';
export function MemoryBar({
memoryUsed,
memoryAllocated,
nonce,
}: {
memoryUsed?: number | null | undefined;
memoryAllocated?: number | null | undefined;
nonce: string | undefined;
}) {
const memoryUsedNum = memoryUsed || 0;
const mbRemaining = (memoryAllocated || 0) - memoryUsedNum;
return (
<div className="margin-top-3" data-testid="memory-bar">
<p className="font-sans-3xs text-uppercase text-bold">Memory:</p>

<ProgressBar total={memoryAllocated} fill={memoryUsedNum} />
<ProgressBar total={memoryAllocated} fill={memoryUsedNum} nonce={nonce} />

<div className="margin-top-1 display-flex flex-justify font-sans-3xs">
<div className="margin-right-1">
Expand Down
2 changes: 1 addition & 1 deletion src/components/NavGlobal/NavGlobal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Image from 'next/image';
import Image from '@/components/Image';

import CloudGovLogo from '@/components/svgs/CloudGovLogo';
import cloudPagesIcon from '@/../public/img/logos/cloud-pages-icon.svg';
Expand Down
2 changes: 1 addition & 1 deletion src/components/OrgPicker/OrgPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
'use client';
import React from 'react';
import { useState, useRef, useEffect } from 'react';
import Image from 'next/image';
import Image from '@/components/Image';
import { usePathname } from 'next/navigation';
import collapseIcon from '@/../public/img/uswds/usa-icons/expand_more.svg';
import { OrgPickerList } from './OrgPickerList';
Expand Down
3 changes: 3 additions & 0 deletions src/components/OrganizationsList/OrganizationsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export function OrganizationsList({
memoryCurrentUsage,
spaceCounts,
roles,
nonce,
}: {
orgs: Array<OrgObj>;
userCounts: { [orgGuid: string]: number };
Expand All @@ -20,6 +21,7 @@ export function OrganizationsList({
memoryCurrentUsage: { [orgGuid: string]: number };
spaceCounts: { [orgGuid: string]: number };
roles: { [orgGuid: string]: Array<string> };
nonce: string | undefined;
}) {
if (!orgs.length) {
return <>no orgs found</>;
Expand All @@ -44,6 +46,7 @@ export function OrganizationsList({
memoryCurrentUsage={memoryCurrentUsage[org.guid]}
spaceCount={spaceCounts[org.guid]}
roles={roles[org.guid]}
nonce={nonce}
/>
);
})}
Expand Down
5 changes: 4 additions & 1 deletion src/components/OrganizationsList/OrganizationsListCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import { formatInt } from '@/helpers/numbers';
import { MemoryBar } from '@/components/MemoryBar';
import { formatOrgRoleName } from '@/helpers/text';

export function OrganizationsListCard({
export async function OrganizationsListCard({
org,
userCount,
appCount,
memoryAllocated,
memoryCurrentUsage,
spaceCount,
roles,
nonce,
}: {
org: OrgObj;
userCount: number;
Expand All @@ -23,6 +24,7 @@ export function OrganizationsListCard({
memoryCurrentUsage: number;
spaceCount: number;
roles: Array<string>;
nonce: string | undefined;
}) {
const getOrgRolesText = (orgGuid: string): React.ReactNode => {
if (!roles || !roles.length) {
Expand Down Expand Up @@ -81,6 +83,7 @@ export function OrganizationsListCard({
<MemoryBar
memoryUsed={memoryCurrentUsage}
memoryAllocated={memoryAllocated}
nonce={nonce}
/>
</Card>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/OverlayDrawer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import React, { useRef, useEffect } from 'react';
import Image from 'next/image';
import Image from '@/components/Image';
import closeIcon from '@/../public/img/uswds/usa-icons/close.svg';

export function OverlayDrawer({
Expand Down
5 changes: 1 addition & 4 deletions src/components/Overlays/OverlayHeaderUsername.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@ export function OverlayHeaderUsername({
}) {
return (
<>
<h2
className="margin-top-0 margin-bottom-7 text-uppercase text-light underline-base-light text-underline font-sans-xs"
style={{ textUnderlineOffset: '0.7em' }}
>
<h2 className="margin-top-0 margin-bottom-7 text-uppercase text-light underline-base-light text-underline text-underline-offset font-sans-xs">
{header}
</h2>
{serviceAccount && (
Expand Down
3 changes: 3 additions & 0 deletions src/components/ProgressBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ export function ProgressBar({
threshold1 = 75, // percentage where color should change first, between 0 and 100
threshold2 = 90, // percentage where color should change next, between 0 and 100
changeColors = true,
nonce,
}: {
total: number | null | undefined;
fill: number;
threshold1?: number;
threshold2?: number;
changeColors?: boolean;
nonce: string | undefined;
}) {
const heightClass = 'height-1';
const percentage = total ? Math.floor((fill / total) * 100) : 100;
Expand All @@ -33,6 +35,7 @@ export function ProgressBar({
className={`${heightClass} radius-pill ${color}`}
style={{ width: `${percentage}%` }}
data-testid="progress"
nonce={nonce}
></div>
{!total && (
<span className="progress__infinity-logo">
Expand Down
2 changes: 1 addition & 1 deletion src/components/uswds/Banner.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import Image from 'next/image';
import Image from '@/components/Image';
import { useState } from 'react';

function BannerContent() {
Expand Down
2 changes: 1 addition & 1 deletion src/components/uswds/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Image from 'next/image';
import Image from '@/components/Image';

import cloudGovIcon from '@/../public/img/logos/cloud-gov-logo-full-grey.svg';

Expand Down
2 changes: 1 addition & 1 deletion src/components/uswds/Identifier.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Image from 'next/image';
import Image from '@/components/Image';

export function Identifier() {
const links = [
Expand Down
2 changes: 1 addition & 1 deletion src/components/uswds/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useRef, useEffect } from 'react';
import Image from 'next/image';
import Image from '@/components/Image';
import closeIcon from '@/../public/img/uswds/usa-icons/close.svg';

export const modalHeadingId = (item: { guid: string }) =>
Expand Down
Loading

0 comments on commit 61de4e1

Please sign in to comment.