mirror of
https://github.com/ansible/awx.git
synced 2026-03-01 00:38:45 -03:30
Merge pull request #8754 from ryanpetrello/strict-csp
Introduce a strict Content-Security-Policy Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -248,6 +248,7 @@ TEMPLATES = [
|
|||||||
'django.template.context_processors.static',
|
'django.template.context_processors.static',
|
||||||
'django.template.context_processors.tz',
|
'django.template.context_processors.tz',
|
||||||
'django.contrib.messages.context_processors.messages',
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
'awx.ui.context_processors.csp',
|
||||||
'social_django.context_processors.backends',
|
'social_django.context_processors.backends',
|
||||||
'social_django.context_processors.login_redirect',
|
'social_django.context_processors.login_redirect',
|
||||||
],
|
],
|
||||||
|
|||||||
8
awx/ui/context_processors.py
Normal file
8
awx/ui/context_processors.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import base64
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def csp(request):
|
||||||
|
return {
|
||||||
|
'csp_nonce': base64.encodebytes(os.urandom(32)).decode().rstrip(),
|
||||||
|
}
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "PORT=3001 HTTPS=true DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts start",
|
"start": "PORT=3001 HTTPS=true DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts start",
|
||||||
"build": "react-scripts build",
|
"build": "INLINE_RUNTIME_CHUNK=false react-scripts build",
|
||||||
"test": "TZ='UTC' react-scripts test --coverage --watchAll=false",
|
"test": "TZ='UTC' react-scripts test --coverage --watchAll=false",
|
||||||
"test-watch": "TZ='UTC' react-scripts test",
|
"test-watch": "TZ='UTC' react-scripts test",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
<% if (process.env.NODE_ENV === 'production') { %>
|
||||||
|
<script nonce="{{ csp_nonce }}" type="text/javascript">
|
||||||
|
window.NONCE_ID = '{{ csp_nonce }}';
|
||||||
|
</script>
|
||||||
|
<meta
|
||||||
|
http-equiv="Content-Security-Policy"
|
||||||
|
content="default-src 'self'; connect-src 'self' ws: wss:; style-src 'self' 'unsafe-inline'; script-src 'self' 'nonce-{{ csp_nonce }}' *.pendo.io; img-src 'self' *.pendo.io data:;"
|
||||||
|
/>
|
||||||
|
<% } %>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
@@ -12,6 +21,10 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="app" style="height: 100%"></div>
|
<% if (process.env.NODE_ENV === 'production') { %>
|
||||||
|
<style nonce="{{ csp_nonce }}">.app{height: 100%;}</style><div id="app" class="app"></div>
|
||||||
|
<% } else { %>
|
||||||
|
<div id="app" style="height: 100%"></div>
|
||||||
|
<% } %>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
import './setupCSP';
|
||||||
import '@patternfly/react-core/dist/styles/base.css';
|
import '@patternfly/react-core/dist/styles/base.css';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import { BrandName } from './variables';
|
import { BrandName } from './variables';
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import Ansi from 'ansi-to-html';
|
|
||||||
import hasAnsi from 'has-ansi';
|
|
||||||
import { AllHtmlEntities } from 'html-entities';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
JobEventLine,
|
JobEventLine,
|
||||||
@@ -9,84 +6,18 @@ import {
|
|||||||
JobEventLineText,
|
JobEventLineText,
|
||||||
} from './shared';
|
} from './shared';
|
||||||
|
|
||||||
const EVENT_START_TASK = 'playbook_on_task_start';
|
|
||||||
const EVENT_START_PLAY = 'playbook_on_play_start';
|
|
||||||
const EVENT_STATS_PLAY = 'playbook_on_stats';
|
|
||||||
const TIME_EVENTS = [EVENT_START_TASK, EVENT_START_PLAY, EVENT_STATS_PLAY];
|
|
||||||
|
|
||||||
const ansi = new Ansi({
|
|
||||||
stream: true,
|
|
||||||
colors: {
|
|
||||||
0: '#000',
|
|
||||||
1: '#A30000',
|
|
||||||
2: '#486B00',
|
|
||||||
3: '#795600',
|
|
||||||
4: '#00A',
|
|
||||||
5: '#A0A',
|
|
||||||
6: '#004368',
|
|
||||||
7: '#AAA',
|
|
||||||
8: '#555',
|
|
||||||
9: '#F55',
|
|
||||||
10: '#5F5',
|
|
||||||
11: '#FF5',
|
|
||||||
12: '#55F',
|
|
||||||
13: '#F5F',
|
|
||||||
14: '#5FF',
|
|
||||||
15: '#FFF',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const entities = new AllHtmlEntities();
|
|
||||||
|
|
||||||
function getTimestamp({ created }) {
|
|
||||||
const date = new Date(created);
|
|
||||||
|
|
||||||
const dateHours = date.getHours();
|
|
||||||
const dateMinutes = date.getMinutes();
|
|
||||||
const dateSeconds = date.getSeconds();
|
|
||||||
|
|
||||||
const stampHours = dateHours < 10 ? `0${dateHours}` : dateHours;
|
|
||||||
const stampMinutes = dateMinutes < 10 ? `0${dateMinutes}` : dateMinutes;
|
|
||||||
const stampSeconds = dateSeconds < 10 ? `0${dateSeconds}` : dateSeconds;
|
|
||||||
|
|
||||||
return `${stampHours}:${stampMinutes}:${stampSeconds}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLineTextHtml({ created, event, start_line, stdout }) {
|
|
||||||
const sanitized = entities.encode(stdout);
|
|
||||||
return sanitized.split('\r\n').map((lineText, index) => {
|
|
||||||
let html;
|
|
||||||
if (hasAnsi(lineText)) {
|
|
||||||
html = ansi.toHtml(lineText);
|
|
||||||
} else {
|
|
||||||
html = lineText;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index === 1 && TIME_EVENTS.includes(event)) {
|
|
||||||
const time = getTimestamp({ created });
|
|
||||||
html += `<span class="time">${time}</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
lineNumber: start_line + index,
|
|
||||||
html,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function JobEvent({
|
function JobEvent({
|
||||||
counter,
|
counter,
|
||||||
created,
|
|
||||||
event,
|
|
||||||
isClickable,
|
|
||||||
onJobEventClick,
|
|
||||||
stdout,
|
stdout,
|
||||||
start_line,
|
|
||||||
style,
|
style,
|
||||||
type,
|
type,
|
||||||
|
lineTextHtml,
|
||||||
|
isClickable,
|
||||||
|
onJobEventClick,
|
||||||
}) {
|
}) {
|
||||||
return !stdout ? null : (
|
return !stdout ? null : (
|
||||||
<div style={style} type={type}>
|
<div style={style} type={type}>
|
||||||
{getLineTextHtml({ created, event, start_line, stdout }).map(
|
{lineTextHtml.map(
|
||||||
({ lineNumber, html }) =>
|
({ lineNumber, html }) =>
|
||||||
lineNumber >= 0 && (
|
lineNumber >= 0 && (
|
||||||
<JobEventLine
|
<JobEventLine
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
|
||||||
import JobEvent from './JobEvent';
|
import JobEvent from './JobEvent';
|
||||||
|
|
||||||
const mockOnPlayStartEvent = {
|
const mockOnPlayStartEvent = {
|
||||||
@@ -24,23 +23,64 @@ const selectors = {
|
|||||||
lineText: 'JobEventLineText',
|
lineText: 'JobEventLineText',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const singleDigitTimestampEvent = {
|
||||||
|
...mockOnPlayStartEvent,
|
||||||
|
created: '2019-07-11T08:01:02.906001Z',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockSingleDigitTimestampEventLineTextHtml = [
|
||||||
|
{ lineNumber: 0, html: '' },
|
||||||
|
{
|
||||||
|
lineNumber: 1,
|
||||||
|
html:
|
||||||
|
'PLAY [add hosts to inventory] **************************************************<span class="time">08:01:02</span>',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockAnsiLineTextHtml = [
|
||||||
|
{
|
||||||
|
lineNumber: 4,
|
||||||
|
html: '<span class="output--1977390340">ok: [localhost]</span>',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockOnPlayStartLineTextHtml = [
|
||||||
|
{ lineNumber: 0, html: '' },
|
||||||
|
{
|
||||||
|
lineNumber: 1,
|
||||||
|
html:
|
||||||
|
'PLAY [add hosts to inventory] **************************************************<span class="time">18:11:22</span>',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
describe('<JobEvent />', () => {
|
describe('<JobEvent />', () => {
|
||||||
test('initially renders successfully', () => {
|
test('initially renders successfully', () => {
|
||||||
mountWithContexts(<JobEvent {...mockOnPlayStartEvent} />);
|
mountWithContexts(
|
||||||
|
<JobEvent
|
||||||
|
lineTextHtml={mockOnPlayStartLineTextHtml}
|
||||||
|
{...mockOnPlayStartEvent}
|
||||||
|
/>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('playbook event timestamps are rendered', () => {
|
test('playbook event timestamps are rendered', () => {
|
||||||
let wrapper = mountWithContexts(<JobEvent {...mockOnPlayStartEvent} />);
|
let wrapper = mountWithContexts(
|
||||||
|
<JobEvent
|
||||||
|
lineTextHtml={mockOnPlayStartLineTextHtml}
|
||||||
|
{...mockOnPlayStartEvent}
|
||||||
|
/>
|
||||||
|
);
|
||||||
let lineText = wrapper.find(selectors.lineText);
|
let lineText = wrapper.find(selectors.lineText);
|
||||||
expect(
|
expect(
|
||||||
lineText.filterWhere(e => e.text().includes('18:11:22'))
|
lineText.filterWhere(e => e.text().includes('18:11:22'))
|
||||||
).toHaveLength(1);
|
).toHaveLength(1);
|
||||||
|
|
||||||
const singleDigitTimestampEvent = {
|
wrapper = mountWithContexts(
|
||||||
...mockOnPlayStartEvent,
|
<JobEvent
|
||||||
created: '2019-07-11T08:01:02.906001Z',
|
lineTextHtml={mockSingleDigitTimestampEventLineTextHtml}
|
||||||
};
|
{...singleDigitTimestampEvent}
|
||||||
wrapper = mountWithContexts(<JobEvent {...singleDigitTimestampEvent} />);
|
/>
|
||||||
|
);
|
||||||
lineText = wrapper.find(selectors.lineText);
|
lineText = wrapper.find(selectors.lineText);
|
||||||
expect(
|
expect(
|
||||||
lineText.filterWhere(e => e.text().includes('08:01:02'))
|
lineText.filterWhere(e => e.text().includes('08:01:02'))
|
||||||
@@ -48,12 +88,14 @@ describe('<JobEvent />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('ansi stdout colors are rendered as html', () => {
|
test('ansi stdout colors are rendered as html', () => {
|
||||||
const wrapper = mountWithContexts(<JobEvent {...mockRunnerOnOkEvent} />);
|
const wrapper = mountWithContexts(
|
||||||
|
<JobEvent lineTextHtml={mockAnsiLineTextHtml} {...mockRunnerOnOkEvent} />
|
||||||
|
);
|
||||||
const lineText = wrapper.find(selectors.lineText);
|
const lineText = wrapper.find(selectors.lineText);
|
||||||
expect(
|
expect(
|
||||||
lineText
|
lineText
|
||||||
.html()
|
.html()
|
||||||
.includes('<span style="color:#486B00">ok: [localhost]</span>')
|
.includes('<span class="output--1977390340">ok: [localhost]</span>')
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
@@ -10,6 +10,9 @@ import {
|
|||||||
InfiniteLoader,
|
InfiniteLoader,
|
||||||
List,
|
List,
|
||||||
} from 'react-virtualized';
|
} from 'react-virtualized';
|
||||||
|
import Ansi from 'ansi-to-html';
|
||||||
|
import hasAnsi from 'has-ansi';
|
||||||
|
import { AllHtmlEntities } from 'html-entities';
|
||||||
|
|
||||||
import AlertModal from '../../../components/AlertModal';
|
import AlertModal from '../../../components/AlertModal';
|
||||||
import { CardBody } from '../../../components/Card';
|
import { CardBody } from '../../../components/Card';
|
||||||
@@ -32,6 +35,106 @@ import {
|
|||||||
AdHocCommandsAPI,
|
AdHocCommandsAPI,
|
||||||
} from '../../../api';
|
} from '../../../api';
|
||||||
|
|
||||||
|
const EVENT_START_TASK = 'playbook_on_task_start';
|
||||||
|
const EVENT_START_PLAY = 'playbook_on_play_start';
|
||||||
|
const EVENT_STATS_PLAY = 'playbook_on_stats';
|
||||||
|
const TIME_EVENTS = [EVENT_START_TASK, EVENT_START_PLAY, EVENT_STATS_PLAY];
|
||||||
|
|
||||||
|
const ansi = new Ansi({
|
||||||
|
stream: true,
|
||||||
|
colors: {
|
||||||
|
0: '#000',
|
||||||
|
1: '#A30000',
|
||||||
|
2: '#486B00',
|
||||||
|
3: '#795600',
|
||||||
|
4: '#00A',
|
||||||
|
5: '#A0A',
|
||||||
|
6: '#004368',
|
||||||
|
7: '#AAA',
|
||||||
|
8: '#555',
|
||||||
|
9: '#F55',
|
||||||
|
10: '#5F5',
|
||||||
|
11: '#FF5',
|
||||||
|
12: '#55F',
|
||||||
|
13: '#F5F',
|
||||||
|
14: '#5FF',
|
||||||
|
15: '#FFF',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const entities = new AllHtmlEntities();
|
||||||
|
|
||||||
|
function getTimestamp({ created }) {
|
||||||
|
const date = new Date(created);
|
||||||
|
|
||||||
|
const dateHours = date.getHours();
|
||||||
|
const dateMinutes = date.getMinutes();
|
||||||
|
const dateSeconds = date.getSeconds();
|
||||||
|
|
||||||
|
const stampHours = dateHours < 10 ? `0${dateHours}` : dateHours;
|
||||||
|
const stampMinutes = dateMinutes < 10 ? `0${dateMinutes}` : dateMinutes;
|
||||||
|
const stampSeconds = dateSeconds < 10 ? `0${dateSeconds}` : dateSeconds;
|
||||||
|
|
||||||
|
return `${stampHours}:${stampMinutes}:${stampSeconds}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const styleAttrPattern = new RegExp('style="[^"]*"', 'g');
|
||||||
|
|
||||||
|
function createStyleAttrHash(styleAttr) {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < styleAttr.length; i++) {
|
||||||
|
hash = (hash << 5) - hash; // eslint-disable-line no-bitwise
|
||||||
|
hash += styleAttr.charCodeAt(i);
|
||||||
|
hash &= hash; // eslint-disable-line no-bitwise
|
||||||
|
}
|
||||||
|
return `${hash}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceStyleAttrs(html) {
|
||||||
|
const allStyleAttrs = [...new Set(html.match(styleAttrPattern))];
|
||||||
|
const cssMap = {};
|
||||||
|
let result = html;
|
||||||
|
for (let i = 0; i < allStyleAttrs.length; i++) {
|
||||||
|
const styleAttr = allStyleAttrs[i];
|
||||||
|
const cssClassName = `output-${createStyleAttrHash(styleAttr)}`;
|
||||||
|
|
||||||
|
cssMap[cssClassName] = styleAttr.replace('style="', '').slice(0, -1);
|
||||||
|
result = result.split(styleAttr).join(`class="${cssClassName}"`);
|
||||||
|
}
|
||||||
|
return { cssMap, result };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLineTextHtml({ created, event, start_line, stdout }) {
|
||||||
|
const sanitized = entities.encode(stdout);
|
||||||
|
let lineCssMap = {};
|
||||||
|
const lineTextHtml = [];
|
||||||
|
|
||||||
|
sanitized.split('\r\n').forEach((lineText, index) => {
|
||||||
|
let html;
|
||||||
|
if (hasAnsi(lineText)) {
|
||||||
|
const { cssMap, result } = replaceStyleAttrs(ansi.toHtml(lineText));
|
||||||
|
html = result;
|
||||||
|
lineCssMap = { ...lineCssMap, ...cssMap };
|
||||||
|
} else {
|
||||||
|
html = lineText;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === 1 && TIME_EVENTS.includes(event)) {
|
||||||
|
const time = getTimestamp({ created });
|
||||||
|
html += `<span class="time">${time}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
lineTextHtml.push({
|
||||||
|
lineNumber: start_line + index,
|
||||||
|
html,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
lineCssMap,
|
||||||
|
lineTextHtml,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const HeaderTitle = styled.div`
|
const HeaderTitle = styled.div`
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -54,6 +157,8 @@ const OutputWrapper = styled.div`
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
height: calc(100vh - 350px);
|
height: calc(100vh - 350px);
|
||||||
outline: 1px solid #d7d7d7;
|
outline: 1px solid #d7d7d7;
|
||||||
|
${({ cssMap }) =>
|
||||||
|
Object.keys(cssMap).map(className => `.${className}{${cssMap[className]}}`)}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const OutputFooter = styled.div`
|
const OutputFooter = styled.div`
|
||||||
@@ -122,6 +227,7 @@ class JobOutput extends Component {
|
|||||||
remoteRowCount: 0,
|
remoteRowCount: 0,
|
||||||
isHostModalOpen: false,
|
isHostModalOpen: false,
|
||||||
hostEvent: {},
|
hostEvent: {},
|
||||||
|
cssMap: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
this.cache = new CellMeasurerCache({
|
this.cache = new CellMeasurerCache({
|
||||||
@@ -164,7 +270,7 @@ class JobOutput extends Component {
|
|||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
// recompute row heights for any job events that have transitioned
|
// recompute row heights for any job events that have transitioned
|
||||||
// from loading to loaded
|
// from loading to loaded
|
||||||
const { currentlyLoading } = this.state;
|
const { currentlyLoading, cssMap } = this.state;
|
||||||
let shouldRecomputeRowHeights = false;
|
let shouldRecomputeRowHeights = false;
|
||||||
prevState.currentlyLoading
|
prevState.currentlyLoading
|
||||||
.filter(n => !currentlyLoading.includes(n))
|
.filter(n => !currentlyLoading.includes(n))
|
||||||
@@ -172,6 +278,9 @@ class JobOutput extends Component {
|
|||||||
shouldRecomputeRowHeights = true;
|
shouldRecomputeRowHeights = true;
|
||||||
this.cache.clear(n);
|
this.cache.clear(n);
|
||||||
});
|
});
|
||||||
|
if (Object.keys(cssMap).length !== Object.keys(prevState.cssMap).length) {
|
||||||
|
shouldRecomputeRowHeights = true;
|
||||||
|
}
|
||||||
if (shouldRecomputeRowHeights) {
|
if (shouldRecomputeRowHeights) {
|
||||||
if (this.listRef.recomputeRowHeights) {
|
if (this.listRef.recomputeRowHeights) {
|
||||||
this.listRef.recomputeRowHeights();
|
this.listRef.recomputeRowHeights();
|
||||||
@@ -300,6 +409,13 @@ class JobOutput extends Component {
|
|||||||
return isHost;
|
return isHost;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let actualLineTextHtml = [];
|
||||||
|
if (results[index]) {
|
||||||
|
const { lineTextHtml, lineCssMap } = getLineTextHtml(results[index]);
|
||||||
|
this.setState(({ cssMap }) => ({ cssMap: { ...cssMap, ...lineCssMap } }));
|
||||||
|
actualLineTextHtml = lineTextHtml;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CellMeasurer
|
<CellMeasurer
|
||||||
key={key}
|
key={key}
|
||||||
@@ -314,6 +430,7 @@ class JobOutput extends Component {
|
|||||||
onJobEventClick={() => this.handleHostEventClick(results[index])}
|
onJobEventClick={() => this.handleHostEventClick(results[index])}
|
||||||
className="row"
|
className="row"
|
||||||
style={style}
|
style={style}
|
||||||
|
lineTextHtml={actualLineTextHtml}
|
||||||
{...results[index]}
|
{...results[index]}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -389,7 +506,9 @@ class JobOutput extends Component {
|
|||||||
handleResize({ width }) {
|
handleResize({ width }) {
|
||||||
if (width !== this._previousWidth) {
|
if (width !== this._previousWidth) {
|
||||||
this.cache.clearAll();
|
this.cache.clearAll();
|
||||||
this.listRef.recomputeRowHeights();
|
if (this.listRef?.recomputeRowHeights) {
|
||||||
|
this.listRef.recomputeRowHeights();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this._previousWidth = width;
|
this._previousWidth = width;
|
||||||
}
|
}
|
||||||
@@ -404,6 +523,7 @@ class JobOutput extends Component {
|
|||||||
hostEvent,
|
hostEvent,
|
||||||
isHostModalOpen,
|
isHostModalOpen,
|
||||||
remoteRowCount,
|
remoteRowCount,
|
||||||
|
cssMap,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
if (hasContentLoading) {
|
if (hasContentLoading) {
|
||||||
@@ -415,60 +535,62 @@ class JobOutput extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<Fragment>
|
||||||
{isHostModalOpen && (
|
<CardBody>
|
||||||
<HostEventModal
|
{isHostModalOpen && (
|
||||||
onClose={this.handleHostModalClose}
|
<HostEventModal
|
||||||
isOpen={isHostModalOpen}
|
onClose={this.handleHostModalClose}
|
||||||
hostEvent={hostEvent}
|
isOpen={isHostModalOpen}
|
||||||
|
hostEvent={hostEvent}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<OutputHeader>
|
||||||
|
<HeaderTitle>
|
||||||
|
<StatusIcon status={job.status} />
|
||||||
|
<h1>{job.name}</h1>
|
||||||
|
</HeaderTitle>
|
||||||
|
<OutputToolbar job={job} onDelete={this.handleDeleteJob} />
|
||||||
|
</OutputHeader>
|
||||||
|
<HostStatusBar counts={job.host_status_counts} />
|
||||||
|
<PageControls
|
||||||
|
onScrollFirst={this.handleScrollFirst}
|
||||||
|
onScrollLast={this.handleScrollLast}
|
||||||
|
onScrollNext={this.handleScrollNext}
|
||||||
|
onScrollPrevious={this.handleScrollPrevious}
|
||||||
/>
|
/>
|
||||||
)}
|
<OutputWrapper cssMap={cssMap}>
|
||||||
<OutputHeader>
|
<InfiniteLoader
|
||||||
<HeaderTitle>
|
isRowLoaded={this.isRowLoaded}
|
||||||
<StatusIcon status={job.status} />
|
loadMoreRows={this.loadMoreRows}
|
||||||
<h1>{job.name}</h1>
|
rowCount={remoteRowCount}
|
||||||
</HeaderTitle>
|
>
|
||||||
<OutputToolbar job={job} onDelete={this.handleDeleteJob} />
|
{({ onRowsRendered, registerChild }) => (
|
||||||
</OutputHeader>
|
<AutoSizer nonce={window.NONCE_ID} onResize={this.handleResize}>
|
||||||
<HostStatusBar counts={job.host_status_counts} />
|
{({ width, height }) => {
|
||||||
<PageControls
|
return (
|
||||||
onScrollFirst={this.handleScrollFirst}
|
<List
|
||||||
onScrollLast={this.handleScrollLast}
|
ref={ref => {
|
||||||
onScrollNext={this.handleScrollNext}
|
this.listRef = ref;
|
||||||
onScrollPrevious={this.handleScrollPrevious}
|
registerChild(ref);
|
||||||
/>
|
}}
|
||||||
<OutputWrapper>
|
deferredMeasurementCache={this.cache}
|
||||||
<InfiniteLoader
|
height={height || 1}
|
||||||
isRowLoaded={this.isRowLoaded}
|
onRowsRendered={onRowsRendered}
|
||||||
loadMoreRows={this.loadMoreRows}
|
rowCount={remoteRowCount}
|
||||||
rowCount={remoteRowCount}
|
rowHeight={this.cache.rowHeight}
|
||||||
>
|
rowRenderer={this.rowRenderer}
|
||||||
{({ onRowsRendered, registerChild }) => (
|
scrollToAlignment="start"
|
||||||
<AutoSizer onResize={this.handleResize}>
|
width={width || 1}
|
||||||
{({ width, height }) => {
|
overscanRowCount={20}
|
||||||
return (
|
/>
|
||||||
<List
|
);
|
||||||
ref={ref => {
|
}}
|
||||||
this.listRef = ref;
|
</AutoSizer>
|
||||||
registerChild(ref);
|
)}
|
||||||
}}
|
</InfiniteLoader>
|
||||||
deferredMeasurementCache={this.cache}
|
<OutputFooter />
|
||||||
height={height || 1}
|
</OutputWrapper>
|
||||||
onRowsRendered={onRowsRendered}
|
</CardBody>
|
||||||
rowCount={remoteRowCount}
|
|
||||||
rowHeight={this.cache.rowHeight}
|
|
||||||
rowRenderer={this.rowRenderer}
|
|
||||||
scrollToAlignment="start"
|
|
||||||
width={width || 1}
|
|
||||||
overscanRowCount={20}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</AutoSizer>
|
|
||||||
)}
|
|
||||||
</InfiniteLoader>
|
|
||||||
<OutputFooter />
|
|
||||||
</OutputWrapper>
|
|
||||||
{deletionError && (
|
{deletionError && (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
isOpen={deletionError}
|
isOpen={deletionError}
|
||||||
@@ -480,7 +602,7 @@ class JobOutput extends Component {
|
|||||||
<ErrorDetail error={deletionError} />
|
<ErrorDetail error={deletionError} />
|
||||||
</AlertModal>
|
</AlertModal>
|
||||||
)}
|
)}
|
||||||
</CardBody>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
30
awx/ui_next/src/setupCSP.js
Normal file
30
awx/ui_next/src/setupCSP.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
// Set a special variable to add `nonce` attributes to all styles/script tags
|
||||||
|
// See https://github.com/webpack/webpack/pull/3210
|
||||||
|
__webpack_nonce__ = window.NONCE_ID;
|
||||||
|
|
||||||
|
// Send report when a CSP violation occurs
|
||||||
|
// See: https://w3c.github.io/webappsec-csp/2/#violation-reports
|
||||||
|
// See: https://developer.mozilla.org/en-US/docs/Web/API/SecurityPolicyViolationEvent
|
||||||
|
document.addEventListener('securitypolicyviolation', e => {
|
||||||
|
const violation = {
|
||||||
|
'csp-report': {
|
||||||
|
'blocked-uri': e.blockedURI,
|
||||||
|
'document-uri': e.documentURI,
|
||||||
|
'effective-directive': e.effectiveDirective,
|
||||||
|
'original-policy': e.originalPolicy,
|
||||||
|
referrer: e.referrer,
|
||||||
|
'status-code': e.statusCode,
|
||||||
|
'violated-directive': e.violatedDirective,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (e.sourceFile) violation['csp-report']['source-file'] = e.sourceFile;
|
||||||
|
if (e.lineNumber) violation['csp-report']['line-number'] = e.lineNumber;
|
||||||
|
if (e.columnNumber) violation['csp-report']['column-number'] = e.columnNumber;
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', '/csp-violation/', true);
|
||||||
|
xhr.setRequestHeader('content-type', 'application/csp-report');
|
||||||
|
xhr.send(JSON.stringify(violation));
|
||||||
|
});
|
||||||
@@ -19,3 +19,8 @@ global.console = {
|
|||||||
...console,
|
...console,
|
||||||
debug: jest.fn(),
|
debug: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// This global variable is part of our Content Security Policy framework
|
||||||
|
// and so this mock ensures that we don't encounter a reference error
|
||||||
|
// when running the tests
|
||||||
|
global.__webpack_nonce__ = null;
|
||||||
|
|||||||
@@ -69,8 +69,6 @@ data:
|
|||||||
|
|
||||||
# HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
|
# HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
|
||||||
add_header Strict-Transport-Security max-age=15768000;
|
add_header Strict-Transport-Security max-age=15768000;
|
||||||
add_header Content-Security-Policy "default-src 'self'; connect-src 'self' ws: wss:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' *.pendo.io; img-src 'self' *.pendo.io data:; report-uri /csp-violation/";
|
|
||||||
add_header X-Content-Security-Policy "default-src 'self'; connect-src 'self' ws: wss:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' *.pendo.io; img-src 'self' *.pendo.io data:; report-uri /csp-violation/";
|
|
||||||
|
|
||||||
# Protect against click-jacking https://www.owasp.org/index.php/Testing_for_Clickjacking_(OTG-CLIENT-009)
|
# Protect against click-jacking https://www.owasp.org/index.php/Testing_for_Clickjacking_(OTG-CLIENT-009)
|
||||||
add_header X-Frame-Options "DENY";
|
add_header X-Frame-Options "DENY";
|
||||||
|
|||||||
@@ -67,8 +67,6 @@ http {
|
|||||||
|
|
||||||
# HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
|
# HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
|
||||||
add_header Strict-Transport-Security max-age=15768000;
|
add_header Strict-Transport-Security max-age=15768000;
|
||||||
add_header Content-Security-Policy "default-src 'self'; connect-src 'self' ws: wss:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' *.pendo.io; img-src 'self' *.pendo.io data:; report-uri /csp-violation/";
|
|
||||||
add_header X-Content-Security-Policy "default-src 'self'; connect-src 'self' ws: wss:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' *.pendo.io; img-src 'self' *.pendo.io data:; report-uri /csp-violation/";
|
|
||||||
|
|
||||||
# Protect against click-jacking https://www.owasp.org/index.php/Testing_for_Clickjacking_(OTG-CLIENT-009)
|
# Protect against click-jacking https://www.owasp.org/index.php/Testing_for_Clickjacking_(OTG-CLIENT-009)
|
||||||
add_header X-Frame-Options "DENY";
|
add_header X-Frame-Options "DENY";
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ server {
|
|||||||
|
|
||||||
# HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
|
# HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
|
||||||
add_header Strict-Transport-Security max-age=15768000;
|
add_header Strict-Transport-Security max-age=15768000;
|
||||||
add_header Content-Security-Policy "default-src 'self'; connect-src 'self' ws: wss:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' *.pendo.io; img-src 'self' *.pendo.io data:; report-uri /csp-violation/";
|
|
||||||
add_header X-Content-Security-Policy "default-src 'self'; connect-src 'self' ws: wss:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' *.pendo.io; img-src 'self' *.pendo.io data:; report-uri /csp-violation/";
|
|
||||||
|
|
||||||
location /static/ {
|
location /static/ {
|
||||||
root /awx_devel;
|
root /awx_devel;
|
||||||
@@ -84,8 +82,6 @@ server {
|
|||||||
|
|
||||||
# HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
|
# HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
|
||||||
add_header Strict-Transport-Security max-age=15768000;
|
add_header Strict-Transport-Security max-age=15768000;
|
||||||
add_header Content-Security-Policy "default-src 'self'; connect-src 'self' ws: wss:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' *.pendo.io; img-src 'self' *.pendo.io data:; report-uri /csp-violation/";
|
|
||||||
add_header X-Content-Security-Policy "default-src 'self'; connect-src 'self' ws: wss:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' *.pendo.io; img-src 'self' *.pendo.io data:; report-uri /csp-violation/";
|
|
||||||
|
|
||||||
location /static/ {
|
location /static/ {
|
||||||
root /awx_devel;
|
root /awx_devel;
|
||||||
|
|||||||
Reference in New Issue
Block a user