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

sudo: Rework it for the local machine #13482

Merged
merged 13 commits into from
May 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 11 additions & 9 deletions doc/guide/packages.xml
Expand Up @@ -329,7 +329,7 @@ mypackage/test.min.js.gz
<link linkend="cockpit-bridge.1"><filename>cockpit-bridge</filename></link> connects
to various system APIs that the front end UI requests it to. There are additional
bridges for specific tasks that the main <filename>cockpit-bridge</filename> cannot
handle. For example tasks that should be carried out with privilege escalation.</para>
handle, such as using the PCP C library API.</para>

<para>These additional bridges can be registered in a <code>"bridges"</code> section of a
package's <filename>manifest.json</filename> file. Building such a bridge is a complex tasks, and
Expand All @@ -340,14 +340,12 @@ mypackage/test.min.js.gz

<programlisting>
{
"bridges": [
{
"match": { "superuser": null },
"environ": [ "SUDO_ASKPASS=/usr/bin/my-password-tool" ],
"spawn: [ "/usr/bin/sudo", "-n", "cockpit-bridge", "--privileged" ],
"problem": "access-denied"
}
]
"bridges": [
{
"match": { "payload": "metrics1" },
"spawn": [ "/usr/libexec/cockpit-pcp" ]
}
]
}
</programlisting>

Expand All @@ -368,6 +366,10 @@ mypackage/test.min.js.gz
options need to match for a given channel to be handed over to this
bridge.</para></listitem>
</varlistentry>
<varlistentry>
<term>privileged</term>
<listitem><para>If set to <code>true</code>, this marks the bridge as a superuser bridge. Cockpit will start one of these explicitly when trying to escalate the privileges of a session. A privileged bridge can not have a <code>"match"</code> property.</para></listitem>
</varlistentry>
<varlistentry>
<term>problem</term>
<listitem><para>If a problem is specified, and this bridge fails to start up then
Expand Down
38 changes: 13 additions & 25 deletions doc/guide/privileges.xml
Expand Up @@ -8,32 +8,20 @@
that has exactly the same privileges as if they logged in via SSH or on
the console.</para>

<para>In some cases Cockpit will try to escalate the privileges of the user
using <ulink url="https://www.freedesktop.org/wiki/Software/polkit/">Policy Kit</ulink>
or <ulink url="https://www.sudo.ws/">sudo</ulink>. If the user is able to escalate
privileges from the command line, then Cockpit will use that same capability to
perform certain privileged tasks.</para>
<para>However, Cockpit will usually try to escalate the privileges
of the user using <ulink
url="https://www.freedesktop.org/wiki/Software/polkit/">Policy
Kit</ulink> or <ulink url="https://www.sudo.ws/">sudo</ulink>. If
the user is able to escalate privileges from the command line by
typing in their password again (or without typing in any password),
then Cockpit will be able to escalate the privileges of the session
to "root" immediately upon login.</para>

<para>Cockpit can use the user's login password internally to escalate privileges
in these situations. By selecting the
<emphasis>Reuse my password for privileged tasks</emphasis> option on the login screen
the login password will be cached internally and passed to <emphasis>Policy Kit</emphasis>
when requested in order to escalate privileges.</para>

<para>To test out whether Cockpit can escalate privileges, you can run these commands
from a the <link linkend="feature-terminal">terminal built into Cockpit</link>.</para>

<programlisting>
$ sudo cockpit-bridge
...
$ pkexec cockpit-bridge
...
</programlisting>

<para>If either of these commands succeed without prompting for a password,
Cockpit will be able to start a privileged copy of the
<filename>cockpit-bridge</filename> and use it to perform privileged tasks
when necessary.</para>
<para>The user can change the privileges of a session from within
that session, via the "Administrative access" indicator in the top
bar. From that indicator, the user can drop "root" privileges and
regain them. On the next login, Cockpit will give the session the
same privileges.</para>

<para>Usually a user needs to be in the <code>wheel</code> Unix user group for the
user to be able to escalate privileges in this way. However both Policy Kit and
Expand Down
21 changes: 6 additions & 15 deletions doc/protocol.md
Expand Up @@ -85,6 +85,7 @@ The following fields are defined:
* "csrf-token": The web service will send a csrf-token for external channels.
* "os-release": The bridge sends fields from /etc/os-release which identify the system.
* "packages": The bridge sends a list of package names on the system.
* "superuser": Instructs a bridge about whether and how to start a superuser bridge.

If a problem occurs that requires shutdown of a transport, then the "problem"
field can be set to indicate why the shutdown will be shortly occurring.
Expand Down Expand Up @@ -370,22 +371,12 @@ If no fields are specified then all channels are terminated.
Command: logout
---------------

The "logout" command is sent by the shell to cockpit-ws. It discards credentials
for the logged in user. Optionally it disconnects the user.

The following fields are defined:

* "disconnect": if set to true then disconnect the user

Example logout message:

{
"command": "logout",
"disconnect": true
}

The "logout" command is broadcast to all bridge instances.
The "logout" command is sent by the shell to cockpit-ws. It
disconnects the user.

In older versions, the "logout" command was broadcast to all bridges
and used to terminate privileged ones. Privileged bridges are now
terminated by closing their transport.

Command: hint
-------------
Expand Down
104 changes: 104 additions & 0 deletions pkg/lib/superuser.jsx
@@ -0,0 +1,104 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2020 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/

import cockpit from "cockpit";

/* import { superuser } from "superuser.jsx";
*
* The "superuser" object indicates whether or not the current page
* can open superuser channels.
*
* - superuser.allowed
*
* This is true when the page can open superuser channels, and false
* otherwise. Right after page load, this field might be "null" until
* the real value has been received, but you should treat this as
* false.
*
* - superuser.addEventListener("changed", () => ...)
*
* The event handler is called whenever superuser.allowed has changed.
* A page should update its appearance according to superuser.allowed,
* and it should also re-initialize itself by opening all "superuser"
* channels again that are currently open.
*
* - superuser.reload_on_change()
*
* Calling this function instructs the "superuser" object to reload
* the page whenever "superuser.allowed" changes. This is a (bad)
* alternative to re-initializing the page and intended to be used
* only to help with the transition.
*
* Even if you are using "superuser.reload_on_change" to avoid having
* to re-initialize your page dynamically, you should still use the
* "changed" event to update the page appearance since
* "superuser.allowed" might still change a couple of times right
* after page.
*/

function Superuser() {
const proxy = cockpit.dbus(null, { bus: "internal" }).proxy("cockpit.Superuser", "/superuser");
let reload_on_change = false;

const compute_allowed = () => {
if (!proxy.valid || proxy.Current == "init")
return null;
return proxy.Current != "none";
};

const self = {
allowed: compute_allowed(),
reload_page_on_change: reload_page_on_change
};

cockpit.event_target(self);

proxy.wait(() => {
if (!proxy.valid) {
// Fall back to cockpit.permissions
const permission = cockpit.permission({ admin: true });
const changed = () => {
self.allowed = permission.allowed;
self.dispatchEvent("changed");
};
permission.addEventListener("changed", changed);
changed();
}
});

proxy.addEventListener("changed", () => {
const allowed = compute_allowed();
if (self.allowed != allowed) {
if (self.allowed != null && reload_on_change) {
window.location.reload(true);
} else {
self.allowed = allowed;
self.dispatchEvent("changed");
}
}
});

function reload_page_on_change() {
reload_on_change = true;
}

return self;
}

export const superuser = Superuser();
2 changes: 1 addition & 1 deletion pkg/networkmanager/index.html
Expand Up @@ -446,7 +446,7 @@ <h2 class="panel-title" id="network-interface-name"></h2>
<span id="network-interface-mac"></span>
<div class="panel-actions">
<button class="pf-c-button pf-m-danger network-privileged" id="network-interface-delete" translate="yes">Delete</button>
<span id="network-interface-delete-switch">
<span id="network-interface-delete-switch" class="network-privileged">
</span>
</div>
</div>
Expand Down
20 changes: 9 additions & 11 deletions pkg/networkmanager/interfaces.js
Expand Up @@ -22,6 +22,7 @@ import React from "react";
import ReactDOM from "react-dom";
import { OnOffSwitch } from "cockpit-components-onoff.jsx";
import cockpit from 'cockpit';
import { superuser } from 'superuser.jsx';

import firewall from './firewall-client.js';
import * as utils from './utils';
Expand Down Expand Up @@ -1480,17 +1481,14 @@ function make_network_plot_post_hook(unit) {
};
}

var permission = cockpit.permission({ admin: true });
$(permission).on("changed", update_network_privileged);

function update_network_privileged() {
$(".network-privileged").update_privileged(
permission, cockpit.format(
_("The user <b>$0</b> is not permitted to modify network settings"),
permission.user ? permission.user.name : '')
);
$(".network-privileged").toggle(!!superuser.allowed);
$(".network-privileged-disabled").toggleClass("disabled", !superuser.allowed);
}

superuser.reload_page_on_change();
superuser.addEventListener("changed", update_network_privileged);

/* Resource usage monitoring
*/

Expand Down Expand Up @@ -2543,7 +2541,7 @@ PageNetworkInterface.prototype = {
$('#network-interface-mac').empty();
if (can_edit_mac) {
$('#network-interface-mac').append(
$('<a tabindex="0">')
$('<a tabindex="0" class="network-privileged-disabled">')
.text(mac)
.syn_click(self.model, function () {
self.set_mac();
Expand Down Expand Up @@ -2690,7 +2688,7 @@ PageNetworkInterface.prototype = {
$('<td>').text(_("General")),
$('<td class="networking-controls">').append(
$('<label for="autoreconnect">').append(
$('<input type="checkbox" id="autoreconnect">')
$('<input type="checkbox" id="autoreconnect" class="network-privileged">')
.prop('checked', settings.connection.autoconnect)
.change(function () {
settings.connection.autoconnect = $(this).prop('checked');
Expand All @@ -2716,7 +2714,7 @@ PageNetworkInterface.prototype = {
.text(title)
.css('vertical-align', rows.length > 1 ? "top" : "center"),
$('<td>').append(
$('<a tabindex="0" class="network-privileged">')
$('<a tabindex="0" class="network-privileged-disabled">')
.append(link_text)
.syn_click(self.model, function () { configure() })));
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/playground/test.js
Expand Up @@ -8,7 +8,7 @@ $(function() {
$(".cockpit-internal-reauthorize .pf-c-button").on("click", function() {
$(".cockpit-internal-reauthorize span").text("checking...");
var cmd = "pkcheck --action-id org.freedesktop.policykit.exec --process $$ -u 2>&1";
cockpit.spawn(["sh", "-c", cmd])
cockpit.spawn(["sh", "-c", cmd], { superuser: "try" })
.stream(function(data) {
console.debug(data);
})
Expand Down Expand Up @@ -36,7 +36,7 @@ $(function() {
$(".lock-channel .pf-c-button").on("click", function() {
$(".lock-channel span").text("locking...");
cockpit.spawn(["flock", "-o", "/tmp/playground-test-lock", "-c", "echo locked; sleep infinity"],
{ err: "message" })
{ superuser: "try", err: "message" })
Copy link
Member

Choose a reason for hiding this comment

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

flock doesn't need superuser -- is this just a debugging leftover?

.stream(function(data) {
$(".lock-channel span").text(data);
})
Expand Down
7 changes: 3 additions & 4 deletions pkg/shell/base_index.js
Expand Up @@ -219,10 +219,9 @@ function Router(index) {
source.window.postMessage(message, origin);
}
} else if (control.command == "hint") {
if (control.credential) {
if (index.privileges)
index.privileges.update(control);
}
/* This is where we handle hint messages directed at
* the shell. Right now, there aren't any.
*/
}

/* Forward message to relevant frame */
Expand Down
23 changes: 1 addition & 22 deletions pkg/shell/index.html
Expand Up @@ -29,14 +29,7 @@
<div class="spinner spinner-md spinner-white">
</div>
</li>
<li class="credential-lock">
<span>
<!-- Display nothing when unprivileged -->
</span>
<span data-toggle="tooltip" data-placement="bottom" translate="title" title="Login has escalated admin privileges" class="navbar-text">
<i class="fa fa-unlock-alt"></i>
<span translate>Privileged</span>
</span>
<li id="super-user-indicator">
mvollmer marked this conversation as resolved.
Show resolved Hide resolved
</li>
<li id="navbar-oops" hidden>
<a role="link" tabindex="0"><span class="oops-status" translate="yes">Ooops!</span></a>
Expand Down Expand Up @@ -170,20 +163,6 @@ <h4 class="modal-title" translate="yes">Authentication</h4>
</div>
<div class="modal-body">
<table class="listing-ct">
<thead class="credential-lock">
<tr>
<td translate colspan="2">Password not usable for privileged tasks or to connect to other machines</td>
<td></td>
</tr>
<tr>
<td translate colspan="2">Reuse my password for privileged tasks and to connect to other machines</td>
<td class="listing-ct-actions">
<button class="pf-c-button pf-m-secondary credential-clear">
<span class="pficon pficon-close"></span>
</button>
</td>
</tr>
</thead>
<thead>
<tr id="credential-keys">
<td colspan="2" translate>Use the following keys to authenticate against other systems</td>
Expand Down
2 changes: 0 additions & 2 deletions pkg/shell/index.js
Expand Up @@ -20,7 +20,6 @@
import { machines } from "machines";
import { new_machine_dialog_manager } from "machine-dialogs";
import * as credentials from "./credentials";
import * as privileges from "./privileges";
import * as indexes from "./indexes";

var machines_inst = machines.instance();
Expand Down Expand Up @@ -51,7 +50,6 @@ var options = {
user_sel: "#content-user-name",
killer_sel: "#active-pages",
default_title: "Cockpit",
privileges: privileges.instance(),
};

indexes.machines_index(options, machines_inst, loader, dialogs);