Add Security Warning Bar for untrusted notebooks (#970)

* Add Security Warning Bar for untrusted notebooks

* Update

* Another update

* Add a snapshot test

* UX updates

* More updates

* Add tests

* Update test snapshot

* Update string

* Update security message
This commit is contained in:
Tanuj Mittal
2021-08-10 11:24:26 -07:00
committed by GitHub
parent 51f3f9a718
commit f8ac36962b
16 changed files with 306 additions and 13 deletions

View File

@@ -14,6 +14,7 @@ import * as cdbActions from "../NotebookComponent/actions";
import loadTransform from "../NotebookComponent/loadTransform";
import { CdbAppState, SnapshotFragment, SnapshotRequest } from "../NotebookComponent/types";
import { NotebookUtil } from "../NotebookUtil";
import SecurityWarningBar from "../SecurityWarningBar/SecurityWarningBar";
import { AzureTheme } from "./AzureTheme";
import "./base.css";
import CellCreator from "./decorators/CellCreator";
@@ -107,6 +108,7 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
return (
<>
<div className="NotebookRendererContainer">
<SecurityWarningBar contentRef={this.props.contentRef} />
<div className="NotebookRenderer" ref={this.notebookRendererRef}>
<DndProvider backend={HTML5Backend}>
<KeyboardShortcuts contentRef={this.props.contentRef}>

View File

@@ -19,6 +19,12 @@
}
}
.disabledRunCellButton {
.runCellButton .ms-Button-flexContainer .ms-Button-icon {
color: @BaseMediumHigh;
}
}
.greyStopButton {
.runCellButton .ms-Button-flexContainer .ms-Button-icon {
color: @BaseMediumHigh;

View File

@@ -5,6 +5,7 @@ import { Dispatch } from "redux";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as cdbActions from "../NotebookComponent/actions";
import { CdbAppState } from "../NotebookComponent/types";
import { NotebookUtil } from "../NotebookUtil";
export interface PassedPromptProps {
id: string;
@@ -12,6 +13,7 @@ export interface PassedPromptProps {
status?: string;
executionCount?: number;
isHovered?: boolean;
isRunDisabled?: boolean;
runCell?: () => void;
stopCell?: () => void;
}
@@ -20,6 +22,7 @@ interface ComponentProps {
id: string;
contentRef: ContentRef;
isHovered?: boolean;
isNotebookUntrusted?: boolean;
children: (props: PassedPromptProps) => React.ReactNode;
}
@@ -47,6 +50,7 @@ export class PromptPure extends React.Component<Props> {
runCell: this.props.executeCell,
stopCell: this.props.stopExecution,
isHovered: this.props.isHovered,
isRunDisabled: this.props.isNotebookUntrusted,
})}
</div>
);
@@ -75,6 +79,7 @@ const makeMapStateToProps = (_state: CdbAppState, ownProps: ComponentProps): ((s
status,
executionCount,
isHovered,
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
};
};
return mapStateToProps;

View File

@@ -0,0 +1,27 @@
import { shallow } from "enzyme";
import { PassedPromptProps } from "./Prompt";
import { promptContent } from "./PromptContent";
describe("PromptContent", () => {
it("renders for busy status", () => {
const props: PassedPromptProps = {
id: "id",
contentRef: "contentRef",
status: "busy",
};
const wrapper = shallow(promptContent(props));
expect(wrapper).toMatchSnapshot();
});
it("renders when hovered", () => {
const props: PassedPromptProps = {
id: "id",
contentRef: "contentRef",
isHovered: true,
};
const wrapper = shallow(promptContent(props));
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,5 +1,6 @@
import { IconButton, Spinner, SpinnerSize } from "@fluentui/react";
import * as React from "react";
import { NotebookUtil } from "../NotebookUtil";
import { PassedPromptProps } from "./Prompt";
import "./Prompt.less";
@@ -23,15 +24,18 @@ export const promptContent = (props: PassedPromptProps): JSX.Element => {
</div>
);
} else if (props.isHovered) {
const playButtonText = "Run cell";
const playButtonText = props.isRunDisabled ? NotebookUtil.UntrustedNotebookRunHint : "Run cell";
return (
<IconButton
className="runCellButton"
iconProps={{ iconName: "MSNVideosSolid" }}
title={playButtonText}
ariaLabel={playButtonText}
onClick={props.runCell}
/>
<div className={props.isRunDisabled ? "disabledRunCellButton" : ""}>
<IconButton
className="runCellButton"
iconProps={{ iconName: "MSNVideosSolid" }}
title={playButtonText}
ariaLabel={playButtonText}
disabled={props.isRunDisabled}
onClick={props.runCell}
/>
</div>
);
} else {
return <div style={{ paddingTop: 7 }}>{promptText(props)}</div>;

View File

@@ -36,6 +36,7 @@ interface StateProps {
cellIdAbove: CellId;
cellIdBelow: CellId;
hasCodeOutput: boolean;
isNotebookUntrusted: boolean;
}
class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & StateProps> {
@@ -43,12 +44,16 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
render(): JSX.Element {
let items: IContextualMenuItem[] = [];
const isNotebookUntrusted = this.props.isNotebookUntrusted;
const runTooltip = isNotebookUntrusted ? NotebookUtil.UntrustedNotebookRunHint : undefined;
if (this.props.cellType === "code") {
items = items.concat([
{
key: "Run",
text: "Run",
title: runTooltip,
disabled: isNotebookUntrusted,
onClick: () => {
this.props.executeCell();
this.props.traceNotebookTelemetry(Action.NotebooksExecuteCellFromMenu, ActionModifiers.Mark);
@@ -223,6 +228,7 @@ const makeMapStateToProps = (state: AppState, ownProps: ComponentProps): ((state
cellIdAbove,
cellIdBelow,
hasCodeOutput: cellType === "code" && NotebookUtil.hasCodeCellOutput(cell as ImmutableCodeCell),
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, ownProps.contentRef),
};
};
return mapStateToProps;

View File

@@ -0,0 +1,60 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PromptContent renders for busy status 1`] = `
<div
className="greyStopButton"
style={
Object {
"left": 0,
"maxHeight": "100%",
"position": "sticky",
"top": 0,
"width": "100%",
"zIndex": 300,
}
}
>
<CustomizedIconButton
ariaLabel="Stop cell execution"
className="runCellButton"
iconProps={
Object {
"iconName": "CircleStopSolid",
}
}
style={
Object {
"position": "absolute",
}
}
title="Stop cell execution"
/>
<StyledSpinnerBase
size={3}
style={
Object {
"paddingTop": 5,
"position": "absolute",
"width": "100%",
}
}
/>
</div>
`;
exports[`PromptContent renders when hovered 1`] = `
<div
className=""
>
<CustomizedIconButton
ariaLabel="Run cell"
className="runCellButton"
iconProps={
Object {
"iconName": "MSNVideosSolid",
}
}
title="Run cell"
/>
</div>
`;

View File

@@ -4,6 +4,7 @@ import Immutable from "immutable";
import React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { NotebookUtil } from "../../../NotebookUtil";
interface ComponentProps {
contentRef: ContentRef;
@@ -14,6 +15,7 @@ interface StateProps {
cellMap: Immutable.Map<string, any>;
cellOrder: Immutable.List<string>;
focusedCell?: string | null;
isNotebookUntrusted: boolean;
}
interface DispatchProps {
@@ -59,8 +61,13 @@ export class KeyboardShortcuts extends React.Component<Props> {
cellOrder,
focusedCell,
cellMap,
isNotebookUntrusted,
} = this.props;
if (isNotebookUntrusted) {
return;
}
let ctrlKeyPressed = e.ctrlKey;
// Allow cmd + enter (macOS) to operate like ctrl + enter
if (process.platform === "darwin") {
@@ -125,6 +132,7 @@ export const makeMapStateToProps = (_state: AppState, ownProps: ComponentProps)
cellOrder,
cellMap,
focusedCell,
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
};
};
return mapStateToProps;