Warn when Cypress detects policies that may interfere with auto… (#4405)

* add chrome policy check that is logged to stdout/displayed in GUI

* improve warning message

* add desktop-gui warning when launching browser

* update tests

* clean up adding warnings to browsers in the gui

* support multiple warnings in gui

* update on-link

* don't show warning in open mode

* add test for warning getting attached, add hyperlink to warning tooltip

* render tooltip warnings with markdown

* pass NO_CHECK_DEPS=1 to skip dep check

* @cypress/react-tooltip@0.5.0 - adds clickability to tooltips

* add link to issue

* dont need to update reporter and runner

* use synchronous console.log, not async errors.log for displaying yellow warnings

* use trigger, do not build up events manually

/cc @flotwig


Co-authored-by: Jennifer Shehane <jennifer@cypress.io>
Co-authored-by: Brian Mann <brian.mann86@gmail.com>
This commit is contained in:
Zach Bloomquist
2019-07-03 09:35:58 -04:00
committed by Brian Mann
parent 59d9b55bbf
commit 178e9bf0ab
19 changed files with 394 additions and 48 deletions

View File

@@ -151,7 +151,7 @@ describe "Project Nav", ->
it "sends the required parameters to launch a browser", ->
browserArg = @ipc.launchBrowser.getCall(0).args[0].browser
expect(browserArg).to.have.keys([
"family", "name", "path", "version", "majorVersion", "displayName", "info", "isChosen", "custom"
"family", "name", "path", "version", "majorVersion", "displayName", "info", "isChosen", "custom", "warning"
])
expect(browserArg.path).to.include('/')
expect(browserArg.family).to.equal('chrome')
@@ -232,6 +232,30 @@ describe "Project Nav", ->
cy.get(".browsers-list")
.find(".dropdown-toggle").should("not.be.visible")
describe "browser has a warning attached", ->
beforeEach ->
@browsers = [{
"name": "chromium",
"displayName": "Chromium",
"family": "chrome",
"version": "49.0.2609.0",
"path": "/Users/bmann/Downloads/chrome-mac/Chromium.app/Contents/MacOS/Chromium",
"majorVersion": "49",
"warning": "Cypress detected policy settings on your computer that may cause issues with using this browser. For more information, see https://on.cypress.io/bad-browser-policy"
}]
@config.browsers = @browsers
@openProject.resolve(@config)
it "shows warning icon with linkified tooltip", ->
cy.get(".browsers .fa-exclamation-triangle").trigger("mouseover")
cy.get(".cy-tooltip")
.should("contain", "Cypress detected policy settings on your computer that may cause issues with using this browser. For more information, see")
.get(".cy-tooltip a")
.click()
.then () ->
expect(@ipc.externalOpen).to.be.calledWith("https://on.cypress.io/bad-browser-policy")
describe "custom browser available", ->
beforeEach ->
@config.browsers.push({
@@ -274,7 +298,6 @@ describe "Project Nav", ->
it "shows info icon with tooltip", ->
cy.get(".browsers .fa-info-circle")
.then ($el) ->
$el[0].dispatchEvent(new Event("mouseover", {bubbles: true}))
.trigger("mouseover")
cy.get(".cy-tooltip")
.should("contain", @info)

View File

@@ -80,3 +80,41 @@ describe "WarningMessage", ->
.click()
.then ->
expect(@ipc.externalOpen).not.to.be.called
context "with multiple warnings", ->
beforeEach ->
@warningObj2 = {type: "GOOD_BUT_NOT_TOO_GOOD", name: "Fairly good warning", message: "Other message"}
it "shows multiple warnings", ->
cy.shouldBeOnProjectSpecs().then =>
@ipc.onProjectWarning.yield(null, @warningObj)
@ipc.onProjectWarning.yield(null, @warningObj2)
cy.get(".alert-warning")
.should("have.length", 2)
.should("be.visible")
.first()
.should("contain", "Some warning")
cy.get(".alert-warning")
.its('1')
.should("contain", "Other message")
it "can dismiss the warnings", ->
cy.shouldBeOnProjectSpecs().then =>
@ipc.onProjectWarning.yield(null, @warningObj)
@ipc.onProjectWarning.yield(null, @warningObj2)
cy.get(".alert-warning")
.should("contain", "Some warning")
.should("contain", "Other message")
.get(".alert-warning button")
.first()
.click()
cy.get(".alert-warning")
.should("not.contain", "Some warning")
.should("contain", "Other message")
.get(".alert-warning button")
.click()
cy.get(".alert-warning")
.should("not.contain", "Some warning")
.should("not.contain", "Other message")

View File

@@ -26,7 +26,7 @@
"devDependencies": {
"@babel/plugin-proposal-object-rest-spread": "7.4.4",
"@cypress/icons": "0.7.0",
"@cypress/json-schemas": "5.32.1",
"@cypress/json-schemas": "5.31.3",
"@cypress/react-tooltip": "0.5.0",
"bin-up": "1.2.0",
"bluebird": "3.5.3",

View File

@@ -135,6 +135,12 @@
margin-right: 4px;
}
.nav .browser-warning {
color: $red-primary;
margin-left: 6px;
margin-right: 4px;
}
.browser-info-tooltip {
background: #ececec;
border-color: #c7c7c7;

View File

@@ -9,6 +9,7 @@ export default class Browser {
@observable majorVersion
@observable info
@observable custom
@observable warning
@observable isChosen = false
constructor (browser) {
@@ -20,6 +21,7 @@ export default class Browser {
this.majorVersion = browser.majorVersion
this.info = browser.info
this.custom = browser.custom
this.warning = browser.warning
}
@computed get icon () {

View File

@@ -0,0 +1,37 @@
import React from 'react'
import Markdown from 'markdown-it'
import ipc from '../lib/ipc'
const md = new Markdown({
html: true,
linkify: true,
})
export default class MarkdownRenderer extends React.PureComponent {
componentDidMount () {
this.node.addEventListener('click', this._clickHandler)
}
componentWillUnmount () {
this.node.removeEventListener('click', this._clickHandler)
}
_clickHandler (e) {
if (e.target.href) {
e.preventDefault()
return ipc.externalOpen(e.target.href)
}
}
render () {
return (
<span ref={(node) => this.node = node}
dangerouslySetInnerHTML={{
__html: md.render(this.props.markdown),
}}>
</span>
)
}
}

View File

@@ -2,6 +2,7 @@ import React, { Component } from 'react'
import { observer } from 'mobx-react'
import Tooltip from '@cypress/react-tooltip'
import Dropdown from '../dropdown/dropdown'
import MarkdownRenderer from '../lib/markdown-renderer'
import projectsApi from '../projects/projects-api'
@@ -73,11 +74,28 @@ export default class Browsers extends Component {
{prefixText}{' '}
{browser.displayName}{' '}
{browser.majorVersion}
{this._warn(browser)}
{this._info(browser)}
</span>
)
}
_warn (browser) {
if (!browser.warning) return null
return (
<span className='browser-warning'>
<Tooltip
title={<MarkdownRenderer markdown={browser.warning}/>}
placement='bottom'
className='browser-info-tooltip cy-tooltip'
>
<i className='fa fa-exclamation-triangle' />
</Tooltip>
</span>
)
}
_info (browser) {
if (!browser.info) return null

View File

@@ -53,7 +53,7 @@ export default class Project {
@observable browserState = 'closed'
@observable resolvedConfig
@observable error
@observable warning
@observable warnings = []
@observable apiError
@observable parentTestsFolderDisplay
@observable integrationExampleName
@@ -209,18 +209,23 @@ export default class Project {
this.error = null
}
@action setWarning (warning) {
@action addWarning (warning) {
if (!this.dismissedWarnings[this._serializeWarning(warning)]) {
this.warning = warning
this.warnings.push(warning)
}
}
@action clearWarning () {
if (this.warning) {
this.dismissedWarnings[this._serializeWarning(this.warning)] = true
@action clearWarning (warning) {
if (!warning) {
// calling with no warning clears all warnings
return this.warnings.map((warning) => {
return this.clearWarning(warning)
})
}
this.warning = null
this.dismissedWarnings[this._serializeWarning(warning)] = true
this.warnings = _.without(this.warnings, warning)
}
_serializeWarning (warning) {

View File

@@ -39,15 +39,11 @@ class Project extends Component {
if (this.props.project.error) return <ErrorMessage error={this.props.project.error} onTryAgain={this._reopenProject}/>
const { warning } = this.props.project
return (
<div>
<ProjectNav project={this.props.project}/>
<div className='project-content'>
{warning &&
<WarningMessage warning={warning} onClearWarning={this._removeWarning}/>
}
{this._renderWarnings()}
{this._currentView()}
</div>
<OnBoarding project={this.props.project}/>
@@ -72,8 +68,16 @@ class Project extends Component {
}
}
_removeWarning = () => {
this.props.project.clearWarning()
_renderWarnings = () => {
const { warnings } = this.props.project
return warnings.map((warning, i) =>
(<WarningMessage key={i} warning={warning} onClearWarning={() => this._removeWarning(warning)}/>)
)
}
_removeWarning = (warning) => {
this.props.project.clearWarning(warning)
}
_reopenProject = () => {

View File

@@ -1,32 +1,9 @@
import React, { Component } from 'react'
import { observer } from 'mobx-react'
import Markdown from 'markdown-it'
import ipc from '../lib/ipc'
const md = new Markdown({
html: true,
linkify: true,
})
import MarkdownRenderer from '../lib/markdown-renderer'
@observer
class WarningMessage extends Component {
componentDidMount () {
this.warningMessageNode.addEventListener('click', this._clickHandler)
}
componentWillUnmount () {
this.warningMessageNode.removeEventListener('click', this._clickHandler)
}
_clickHandler (e) {
if (e.target.href) {
e.preventDefault()
return ipc.externalOpen(e.target.href)
}
}
render () {
const warningText = this.props.warning.message.split('\n').join('<br />')
@@ -36,9 +13,9 @@ class WarningMessage extends Component {
<i className='fa fa-warning'></i>{' '}
<strong>Warning</strong>
</p>
<div ref={(node) => this.warningMessageNode = node} dangerouslySetInnerHTML={{
__html: md.render(warningText),
}}></div>
<div>
<MarkdownRenderer markdown={warningText}/>
</div>
<button className='btn btn-link close' onClick={this.props.onClearWarning}>
<i className='fa fa-remove' />
</button>

View File

@@ -168,7 +168,7 @@ const openProject = (project) => {
})
ipc.onProjectWarning((__, warning) => {
project.setWarning(warning)
project.addWarning(warning)
})
return ipc.openProject(project.path)

View File

@@ -35,6 +35,8 @@ export type FoundBrowser = Browser & {
custom?: boolean
/** optional info that will be shown in the GUI */
info?: string
/** optional warning that will be shown in the GUI */
warning?: string
}
// all common type definition for this module

View File

@@ -788,6 +788,20 @@ getMsgByType = (type, arg1 = {}, arg2) ->
Provide a path to an existing fixture file.
"""
when "BAD_POLICY_WARNING"
"""
Cypress detected policy settings on your computer that may cause issues.
The following policies were detected that may prevent Cypress from automating Chrome:
> #{arg1.join('\n > ')}
For more information, see https://on.cypress.io/bad-browser-policy
"""
when "BAD_POLICY_WARNING_TOOLTIP"
"""
Cypress detected policy settings on your computer that may cause issues with using this browser. For more information, see https://on.cypress.io/bad-browser-policy
"""
get = (type, arg1, arg2) ->
msg = getMsgByType(type, arg1, arg2)

View File

@@ -14,6 +14,7 @@ Updater = require("../updater")
Project = require("../project")
openProject = require("../open_project")
ensureUrl = require("../util/ensure-url")
chromePolicyCheck = require("../util/chrome_policy_check")
browsers = require("../browsers")
konfig = require("../konfig")
@@ -204,6 +205,11 @@ handleEvent = (options, bus, event, id, type, arg) ->
.then (browsers = []) ->
options.config = _.assign(options.config, { browsers })
.then ->
chromePolicyCheck.run (err) ->
options.config.browsers.forEach (browser) ->
if browser.family == 'chrome'
browser.warning = errors.getMsgByType('BAD_POLICY_WARNING_TOOLTIP')
openProject.create(arg, options, {
onFocusTests: onFocusTests
onSpecChanged: onSpecChanged

View File

@@ -25,6 +25,7 @@ terminal = require("../util/terminal")
specsUtil = require("../util/specs")
humanTime = require("../util/human_time")
electronApp = require("../util/electron_app")
chromePolicyCheck = require("../util/chrome_policy_check")
color = (val, c) ->
chalk[c](val)
@@ -341,6 +342,9 @@ writeOutput = (outputPath, results) ->
fs.outputJsonAsync(outputPath, results)
onWarning = (err) ->
console.log(chalk.yellow(err.message))
openProjectCreate = (projectRoot, socketId, options) ->
## now open the project to boot the server
## putting our web client app in headless mode
@@ -351,8 +355,7 @@ openProjectCreate = (projectRoot, socketId, options) ->
morgan: false
report: true
isTextTerminal: options.isTextTerminal
onWarning: (err) ->
console.log(err.message)
onWarning
onError: (err) ->
console.log("")
if err.details
@@ -953,6 +956,9 @@ module.exports = {
if not specs.length
errors.throw('NO_SPECS_FOUND', config.integrationFolder, specPattern)
if browser.family == 'chrome'
chromePolicyCheck.run(onWarning)
runAllSpecs = ({ beforeSpecRun, afterSpecRun, runUrl }, parallelOverride = parallel) =>
@runSpecs({
beforeSpecRun

View File

@@ -0,0 +1,111 @@
const _ = require('lodash')
const debug = require('debug')('cypress:server:chrome_policy_check')
const errors = require('../errors')
const os = require('os')
// https://www.chromium.org/administrators/policy-list-3#Proxy
// https://www.chromium.org/administrators/policy-list-3#ProxySettings
const BAD_PROXY_POLICY_NAMES = [
'ProxySettings',
'ProxyMode',
'ProxyServerMode',
'ProxyServer',
'ProxyPacUrl',
'ProxyBypassList',
]
// https://www.chromium.org/administrators/policy-list-3#Extensions
const BAD_EXTENSION_POLICY_NAMES = [
'ExtensionInstallBlacklist',
'ExtensionInstallWhitelist',
'ExtensionInstallForcelist',
'ExtensionInstallSources',
'ExtensionAllowedTypes',
'ExtensionAllowInsecureUpdates',
'ExtensionSettings',
'UninstallBlacklistedExtensions',
]
const POLICY_KEYS = [
'Software\\Policies\\Google\\Chrome',
'Software\\Policies\\Google\\Chromium',
]
const POLICY_HKEYS = [
'HKEY_LOCAL_MACHINE',
'HKEY_CURRENT_USER',
]
function warnIfPolicyMatches (policyNames, allPolicies, warningName, cb) {
const matchedPolicyPaths = _.chain(policyNames)
.map((policyName) => {
return _.chain(allPolicies)
.find({ name: policyName })
.get('fullPath')
.value()
})
.filter()
.value()
if (!matchedPolicyPaths.length) {
return
}
cb(errors.get(warningName, matchedPolicyPaths))
}
function getRunner ({ enumerateValues }) {
function getAllPolicies () {
return _.flattenDeep(
POLICY_KEYS.map((key) => {
return POLICY_HKEYS.map((hkey) => {
return enumerateValues(hkey, key)
.map((value) => {
value.fullPath = `${hkey}\\${key}\\${value.name}`
return value
})
})
})
)
}
return function run (cb) {
try {
debug('running chrome policy check')
const policies = getAllPolicies()
const badPolicyNames = _.concat(BAD_PROXY_POLICY_NAMES, BAD_EXTENSION_POLICY_NAMES)
debug('received policies %o', { policies, badPolicyNames })
warnIfPolicyMatches(badPolicyNames, policies, 'BAD_POLICY_WARNING', cb)
} catch (err) {
debug('error running policy check %o', { err })
}
}
}
module.exports = {
run: _.noop,
getRunner,
}
/**
* Only check on Windows. While it is possible for macOS/Linux to have preferences set that
* override Cypress's settings, it's never been reported as an issue and would require more
* native extensions to support checking.
* https://github.com/cypress-io/cypress/issues/4391
*/
if (os.platform() === 'win32') {
try {
const registryJs = require('@cypress/registry-js')
module.exports = {
run: getRunner(registryJs),
getRunner,
}
} catch (err) {
debug('error initializing chrome policy check %o', { err })
}
}

View File

@@ -0,0 +1,64 @@
require("../spec_helper")
_ = require("lodash")
{ stripIndent } = require("common-tags")
chromePolicyCheck = require("#{root}lib/util/chrome_policy_check")
describe "lib/util/chrome_policy_check", ->
context ".getRunner returns a function", ->
it "calls callback with an error if policies are found", ->
run = chromePolicyCheck.getRunner({
enumerateValues: (hkey, key) ->
## mock a registry with a couple of policies
_.get({
'HKEY_LOCAL_MACHINE': {
'Software\\Policies\\Google\\Chrome': [
{ name: 'ProxyServer' }
]
},
'HKEY_CURRENT_USER': {
'Software\\Policies\\Google\\Chromium': [
{ name: 'ExtensionSettings' }
]
},
}, "#{hkey}.#{key}", [])
})
cb = sinon.stub()
run(cb)
expect(cb).to.be.calledOnce
expect(cb.getCall(0).args[0].message).to.eq(stripIndent """
Cypress detected policy settings on your computer that may cause issues.
The following policies were detected that may prevent Cypress from automating Chrome:
> HKEY_LOCAL_MACHINE\\Software\\Policies\\Google\\Chrome\\ProxyServer
> HKEY_CURRENT_USER\\Software\\Policies\\Google\\Chromium\\ExtensionSettings
For more information, see https://on.cypress.io/bad-browser-policy
"""
)
it "does not call callback if no policies are found", ->
run = chromePolicyCheck.getRunner({
enumerateValues: _.constant([])
})
cb = sinon.stub()
run(cb)
expect(cb).to.not.be.called
it "fails silently if enumerateValues throws", ->
run = chromePolicyCheck.getRunner({
enumerateValues: -> throw new Error('blah')
})
cb = sinon.stub()
run(cb)
expect(cb).to.not.be.called

View File

@@ -5,6 +5,7 @@ EE = require("events")
extension = require("@packages/extension")
electron = require("electron")
Promise = require("bluebird")
chromePolicyCheck = require("#{root}../lib/util/chrome_policy_check")
cache = require("#{root}../lib/cache")
logger = require("#{root}../lib/logger")
Project = require("#{root}../lib/project")
@@ -493,6 +494,34 @@ describe "lib/gui/events", ->
}
)
it "attaches warning to Chrome browsers when Chrome policy check fails", ->
sinon.stub(openProject, "create").resolves()
@options.browser = "/foo"
browsers.getAllBrowsersWith.withArgs("/foo").resolves([{family: 'chrome'}, {family: 'some other'}])
sinon.stub(chromePolicyCheck, "run").callsArgWith(0, new Error)
@handleEvent("open:project", "/_test-output/path/to/project").then =>
expect(browsers.getAllBrowsersWith).to.be.calledWith(@options.browser)
expect(openProject.create).to.be.calledWithMatch(
"/_test-output/path/to/project",
{
browser: "/foo",
config: {
browsers: [
{
family: "chrome"
warning: "Cypress detected policy settings on your computer that may cause issues with using this browser. For more information, see https://on.cypress.io/bad-browser-policy"
},
{
family: "some other"
}
]
}
}
)
describe "close:project", ->
beforeEach ->
sinon.stub(Project.prototype, "close").withArgs({sync: true}).resolves()

View File

@@ -6,6 +6,10 @@ const fs = require('fs')
const path = require('path')
const stripAnsi = require('strip-ansi')
if (process.env.NO_CHECK_DEPS) {
process.exit(0)
}
const args = require('minimist')(process.argv.slice(2))
const cwd = args.cwd || process.cwd()