Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

shell: Don't automatically connect to remote machines on navigation #20826

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/guide/Makefile-guide.am
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ GUIDE_INCLUDES = \
doc/guide/cockpit-session.xml \
doc/guide/cockpit-spawn.xml \
doc/guide/cockpit-util.xml \
doc/guide/multi-host.xml \
doc/guide/authentication.xml \
doc/guide/embedding.xml \
doc/guide/feature-firewall.xml \
Expand Down
1 change: 1 addition & 0 deletions doc/guide/cockpit-guide.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<xi:include href="https.xml"/>
<xi:include href="listen.xml"/>
<xi:include href="startup.xml"/>
<xi:include href="multi-host.xml"/>
<xi:include href="authentication.xml"/>
<xi:include href="sso.xml"/>
<xi:include href="cert-authentication.xml"/>
Expand Down
54 changes: 54 additions & 0 deletions doc/guide/multi-host.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?xml version="1.0"?>
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
"http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
<chapter id="multi-host">
<title>
Managing multiple hosts at the same time
</title>

<para>
Cockpit allows you to access multiple hosts in a single session,
by establishing SSH connections to other hosts. This is quite
similar to logging into these other hosts using the "ssh" command
on the command line, with one very important difference:
</para>
<para>
Code from the local host and all the remote hosts run at the same
time, in the same browser context. They are not sufficiently
isolated from each other in the browser. All code effectively has
the same privileges as the primary session on the local host.
</para>
<para>
Thus, <emphasis>you should only only connect to remote hosts that
you trust</emphasis>. You must be sure that none of the hosts that
you connect to will cause Cockpit to load malicious JavaScript
code into your browser.
</para>
<para>
Going forward, Cockpit will try to provide sufficient isolation to
make it safe to manage multiple hosts in a single Cockpit
session. But until we get there, Cockpit will at least warn you
before connecting to more than one host. It is also possible to
disable multiple hosts entirely, and some operating systems do
this already by default.
</para>
<para>
You can prevent loading of JavaScript, HTML, etc from more than
one host by adding this to <filename>cockpit.conf</filename>:
</para>
<programlisting>
[WebService]
AllowMultiHost=false
</programlisting>
<para>
When you allow multiple hosts in a single Cockpit session by
setting <code>AllowMultiHost</code> to true, then the user will be
warned once per session, before connecting to the second host. If
that is still too much, you can switch it off completely by adding
the following to <filename>cockpit.conf</filename>:
</para>
<programlisting>
[Session]
WarnBeforeConnecting=false
</programlisting>
</chapter>
12 changes: 12 additions & 0 deletions doc/man/cockpit.conf.xml
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,18 @@ IdleTimeout=15
<para>When not specified, there is no idle timeout by default.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>WarnBeforeConnecting</option></term>
<listitem>
<para>Whether to warn before connecting to remote hosts from the Shell. Defaults to true</para>
<informalexample>
<programlisting language="ini">
[Session]
WarnBeforeConnecting=false
</programlisting>
</informalexample>
</listitem>
</varlistentry>
</variablelist>
</refsect1>

Expand Down
10 changes: 10 additions & 0 deletions pkg/shell/base_index.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,16 @@ function Frames(index, setupIdleResetTimers) {

/* Need to create a new frame */
if (!frame) {
/* Never create new frames for machines that are not
connected yet. That would open a channel to them (for
loading the URL), which woould trigger the bridge to
attempt a log in. We want all logins to happen in a
single place (in hosts.jsx) so that we can get the
options right, and show a warning dialog.
*/
if (host != "localhost" && machine.state !== "connected")
return null;

new_frame = true;
frame = document.createElement("iframe");
frame.setAttribute("class", "container-frame");
Expand Down
118 changes: 96 additions & 22 deletions pkg/shell/hosts.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip";

import 'polyfills';
import { CockpitNav, CockpitNavItem } from "./nav.jsx";
import { HostModal } from "./hosts_dialog.jsx";
import { HostModal, try2Connect, codes } from "./hosts_dialog.jsx";
import { useLoggedInUser } from "hooks";

const _ = cockpit.gettext;
Expand Down Expand Up @@ -73,8 +73,8 @@ export class CockpitHosts extends React.Component {
editing: false,
current_user: "",
current_key: props.machine.key,
show_modal: false,
edit_machine: null,
modal_properties: null,
modal_callback: null,
};

this.toggleMenu = this.toggleMenu.bind(this);
Expand All @@ -89,6 +89,16 @@ export class CockpitHosts extends React.Component {
cockpit.user().then(user => {
this.setState({ current_user: user.name || "" });
}).catch(exc => console.log(exc));

window.trigger_connection_flow = machine => {
if (!this.state.modal_properties)
this.connectHost(machine);
};
this.props.index.navigate(null, true);
}

componentWillUnmount() {
window.trigger_connection_flow = null;
Comment on lines +100 to +101
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 2 added lines are not executed by any test.

}

static getDerivedStateFromProps(nextProps, prevState) {
Expand Down Expand Up @@ -116,12 +126,81 @@ export class CockpitHosts extends React.Component {
});
}

onAddNewHost() {
this.setState({ show_modal: true });
showModal(properties) {
return new Promise((resolve, reject) => {
this.setState({
modal_properties: properties,
modal_callback: result => { resolve(result); return Promise.resolve() },
});
});
}

async onAddNewHost() {
await this.showModal({ });
}

onHostEdit(event, machine) {
this.setState({ show_modal: true, edit_machine: machine });
async onHostEdit(event, machine) {
const connection_string = await this.showModal({ address: machine.address });
if (connection_string) {
const parts = this.props.machines.split_connection_string(connection_string);
const addr = this.props.hostAddr({ host: parts.address }, true);
if (machine == this.props.machine && parts.address != machine.address) {
this.props.loader.connect(parts.address);
this.props.jump(addr);
}
}
}

async connectHost(machine) {
if (machine.address == "localhost" || machine.state == "connected" || machine.state == "connecting")
return machine.connection_string;

let connection_string = null;

if (machine.problem && codes[machine.problem]) {
// trouble shooting
connection_string = await this.showModal({
address: machine.address,
template: codes[machine.problem],
});
} else if (!window.sessionStorage.getItem("connection-warning-shown")) {
// connect by launching into the "Connection warning" dialog.
connection_string = await this.showModal({
address: machine.address,
template: "connect"
});
} else {
// Try to connect without any dialog
try {
await try2Connect(this.props.machines, machine.connection_string);
connection_string = machine.connection_string;
} catch (err) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test.

// continue with troubleshooting in the dialog
connection_string = await this.showModal({
address: machine.address,
template: codes[err.problem] || "change-port",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test.

error_options: err,
});
}
}

if (connection_string) {
// make the rest of the shell aware that the machine is now connected
const parts = this.props.machines.split_connection_string(connection_string);
this.props.loader.connect(parts.address);
this.props.index.navigate();
}

return connection_string;
}

async onHostSwitch(machine) {
const connection_string = await this.connectHost(machine);
if (connection_string) {
const parts = this.props.machines.split_connection_string(connection_string);
const addr = this.props.hostAddr({ host: parts.address }, true);
this.props.jump(addr);
}
}

onEditHosts() {
Expand Down Expand Up @@ -180,7 +259,7 @@ export class CockpitHosts extends React.Component {
header={(m.user ? m.user : this.state.current_user) + " @"}
status={m.state === "failed" ? { type: "error", title: _("Connection error") } : null}
className={m.state}
jump={this.props.jump}
jump={() => this.onHostSwitch(m)}
actions={<>
<Tooltip content={_("Edit")} position="right">
<Button isDisabled={m.address === "localhost"} className="nav-action" hidden={!editing} onClick={e => this.onHostEdit(e, m)} key={m.label + "edit"} variant="secondary"><EditIcon /></Button>
Expand Down Expand Up @@ -240,20 +319,13 @@ export class CockpitHosts extends React.Component {
</HostsSelector>
}
</div>
{this.state.show_modal &&
<HostModal machines_ins={this.props.machines}
onClose={() => this.setState({ show_modal: false, edit_machine: null })}
address={this.state.edit_machine ? this.state.edit_machine.address : null}
caller_callback={this.state.edit_machine
? (new_connection_string) => {
const parts = this.props.machines.split_connection_string(new_connection_string);
if (this.state.edit_machine == this.props.machine && parts.address != this.state.edit_machine.address) {
const addr = this.props.hostAddr({ host: parts.address }, true);
this.props.jump(addr);
}
return Promise.resolve();
}
: null } />
{this.state.modal_properties &&
<HostModal machines_ins={this.props.machines}
onClose={() => this.setState({ modal_properties: null })}
{...this.state.modal_properties}
caller_callback={this.state.modal_callback}
caller_cancelled={() => this.state.modal_callback(null)}
/>
}
</>
);
Expand All @@ -263,6 +335,8 @@ export class CockpitHosts extends React.Component {
CockpitHosts.propTypes = {
machine: PropTypes.object.isRequired,
machines: PropTypes.object.isRequired,
index: PropTypes.object.isRequired,
loader: PropTypes.object.isRequired,
selector: PropTypes.string.isRequired,
hostAddr: PropTypes.func.isRequired,
jump: PropTypes.func.isRequired,
Expand Down
Loading