Skip to content

Commit

Permalink
Add support to manage authorized keys through the UI
Browse files Browse the repository at this point in the history
  • Loading branch information
imobachgs committed Nov 8, 2018
1 parent 3f13449 commit 0e24eec
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 12 deletions.
117 changes: 117 additions & 0 deletions src/include/users/dialogs.rb
Expand Up @@ -26,6 +26,10 @@
# Jiri Suchomel <jsuchome@suse.cz>
#
# $Id$

require "users/ssh_public_key"
require "yast2/popup"

module Yast
module UsersDialogsInclude
def initialize_users_dialogs(include_target)
Expand Down Expand Up @@ -903,6 +907,10 @@ def EditUserDialog(what)
)
end

if user["type"] == "local" || user["uid"] == "root"
tabs << Item(Id(:authorized_keys), _("SSH Public Keys"))
end

# Now initialize the list of plugins: we must know now if there is some available.
# UsersPlugins will filter out plugins we cannot use for given type
plugin_clients = UsersPlugins.Apply(
Expand Down Expand Up @@ -1593,6 +1601,10 @@ def EditUserDialog(what)
end
end

if current == :authorized_keys
handle_authorized_keys_input(ret, user)
end

# inside plugins dialog
if current == :plugins
plugin_client = Convert.to_string(
Expand Down Expand Up @@ -1778,6 +1790,11 @@ def EditUserDialog(what)
Wizard.SetHelpText(EditUserPasswordDialogHelp())
current = ret
end
if ret == :authorized_keys
display_authorized_keys_tab(user)
Wizard.SetHelpText("Some useful help!")
current = ret
end
if ret == :plugins
UI.ReplaceWidget(:tabContents, get_plugins_term.call)
Wizard.SetHelpText(PluginDialogHelp())
Expand Down Expand Up @@ -2469,5 +2486,105 @@ def GroupSave
Users.CommitGroup
:next
end

# Handles authorized keys list events
#
# @param action [Symbol] Action to handle (:add_authorized_key and :remove_authorized_key)
# @param user [Hash] User to update
def handle_authorized_keys_input(action, user)
case action
when :add_authorized_key
add_authorized_key(user)
when :remove_authorized_key
remove_authorized_key(user)
end
end

# Adds an authorized key to the list
#
# @note This method drives the UI and handle error conditions
#
# @param user [Hash] User to update
def add_authorized_key(user)
key = read_public_key
return if key.nil?
if user.fetch("authorized_keys", []).include?(key.to_s)
Yast2::Popup.show(
_("The selected public key is already present in the list"), headline: :error
)
return
end

user["authorized_keys"] ||= []
user["authorized_keys"] << key.to_s
display_authorized_keys_tab(user)
end

# Asks for the path and retrieves the public key
def read_public_key
path = Yast::UI.AskForExistingFile("", "*.pub", _("Select a public key"))
return if path.nil?
Y2Users::SSHPublicKey.new(File.read(path))
rescue Y2Users::SSHPublicKey::InvalidKey
Yast2::Popup.show(
_("The selected file does not contain a valid public key"), headline: :error
)
rescue Errno::ENOENT
Yast2::Popup.show(
_("Could not read the file containing the public key"), headline: :error
)
end

# Removes the selected key from the list
#
# @param user [Hash] User to update
def remove_authorized_key(user)
selected_row = UI.QueryWidget(Id(:authorized_keys_table), :CurrentItem)
user["authorized_keys"].delete_at(selected_row)

rows_qty = UI.QueryWidget(Id(:authorized_keys_table), :Items).size - 1
next_selected_row = selected_row == rows_qty ? selected_row - 1 : selected_row
display_authorized_keys_tab(user, next_selected_row)
end

# Displays the authorized keys tab
#
# @param user [Hash] User to update
# @param selected_row [Integer] Current selected row
def display_authorized_keys_tab(user, selected_row = nil)
UI.ReplaceWidget(:tabContents, get_authorized_keys_term(user))
UI.SetFocus(Id(:authorized_keys_table))
UI.ChangeWidget(Id(:authorized_keys_table), :CurrentItem, selected_row) if selected_row
no_keys = user.fetch("authorized_keys", []).empty?
UI.ChangeWidget(Id(:remove_authorized_key), :Enabled, !no_keys)
end


# Generates content for the authorized keys tab
#
# @param user [Hash] User to get the list of authorized keys
def get_authorized_keys_term(user)
items = user.fetch("authorized_keys", []).each_with_index.map do |content, idx|
key = Y2Users::SSHPublicKey.new(content)
Item(Id(idx), key.formatted_fingerprint, key.comment)
end

VBox(
Table(
Id(:authorized_keys_table),
Opt(:notify),
Header(
_("Fingerprint"),
_("Comment")
),
items
),
HBox(
PushButton(Id(:add_authorized_key), _("Add...")),
PushButton(Id(:remove_authorized_key), Yast::Label.RemoveButton),
HStretch()
)
)
end
end
end
29 changes: 17 additions & 12 deletions src/modules/Users.pm
Expand Up @@ -3051,6 +3051,10 @@ sub AddUser {
$user_in_work{"type"} = $type;
$user_in_work{"what"} = "add_user";

if (defined($data{"authorized_keys"})) {
$user_in_work{"authorized_keys"} = $data{"authorized_keys"};
}

UsersCache->SetUserType ($type);

if (!defined $user_in_work{"uidNumber"}) {
Expand Down Expand Up @@ -3361,6 +3365,10 @@ sub UserReallyModified {
if (($user{"plugin_modified"} || 0) == 1) {
return 1; #TODO save special plugin_modified global value?
}

# check the list of authorized keys
return 1 unless ($user{"authorized_keys"} ~~ $org_user{"authorized_keys"});

# grouplist can be ignored, it is a modification of groups
while ( my ($key, $value) = each %org_user) {
last if $ret;
Expand Down Expand Up @@ -4485,8 +4493,6 @@ sub Write {
$mode = $user{"home_mode"};
}
UsersRoutines->ChmodHome($home, $mode);
# Write authorized keys to user's home (FATE#319471)
SSHAuthorizedKeys->write_keys($home);
}
}
Syslog->Log ("User added by YaST: name=$username, uid=$uid, gid=$gid, home=$home");
Expand Down Expand Up @@ -4534,6 +4540,11 @@ sub Write {
}
}
}

# Write authorized keys to user's home (FATE#319471)
my $authorized_keys = $user{"authorized_keys"};
SSHAuthorizedKeys->import_keys($home, $authorized_keys);
SSHAuthorizedKeys->write_keys($home);
}
}
}
Expand Down Expand Up @@ -5894,6 +5905,8 @@ sub ImportUser {
"grouplist" => \%grouplist,
"homeDirectory" => $user->{"homeDirectory"} || $user->{"home"} || $home,
"type" => $type,
# Import authorized keys from profile (FATE#319471)
"authorized_keys" => $user->{"authorized_keys"},
"modified" => "imported"
);
}
Expand Down Expand Up @@ -5925,10 +5938,6 @@ sub ImportUser {
$ret{"shadowLastChange"} = LastChangeIsNow ();
}

# Import authorized keys from profile (FATE#319471)
if ($user->{"authorized_keys"} && $ret{"homeDirectory"}) {
SSHAuthorizedKeys->import_keys($ret{"homeDirectory"}, $user->{"authorized_keys"});
}
return \%ret;
}

Expand Down Expand Up @@ -6437,12 +6446,8 @@ sub ExportUser {
if (%user_shadow) {
$ret{"password_settings"} = \%user_shadow;
}
if ($user->{"homeDirectory"}) {
# Export authorized keys to profile (FATE#319471)
my $keys = SSHAuthorizedKeys->export_keys($user->{"homeDirectory"});
if (@$keys) {
$ret{"authorized_keys"} = $keys;
}
if ($user->{"homeDirectory"} && $user->{"authorized_keys"}) {
$ret{"authorized_keys"} = $user->{"authorized_keys"};
}
return \%ret;
}
Expand Down
1 change: 1 addition & 0 deletions src/modules/UsersPasswd.pm
Expand Up @@ -304,6 +304,7 @@ sub read_group {
sub read_authorized_keys {
foreach my $user (values %{$users{"local"}}) {
SSHAuthorizedKeys->read_keys($user->{"homeDirectory"});
$user->{"authorized_keys"} = SSHAuthorizedKeys->export_keys($user->{"homeDirectory"});
}

# Read authorized keys also from root's home (bsc#1066342)
Expand Down
136 changes: 136 additions & 0 deletions test/dialogs_test.rb
Expand Up @@ -46,4 +46,140 @@ def initialize
expect(exp_date).to eq("1970-01-31")
end
end

context "public keys handling" do
let(:user) do
{ "username" => "root", "authorized_keys" => authorized_keys }
end

describe "#handle_authorized_keys_input" do
let(:key1) { instance_double(Y2Users::SSHPublicKey, to_s: "ssh-rsa 1...") }
let(:key2) { instance_double(Y2Users::SSHPublicKey, to_s: "ssh-rsa 2...") }

before do
allow(subject).to receive(:read_public_key).and_return(key1)
end

context "when the user adds a public key" do
let(:authorized_keys) { [] }

it "adds the public key to the user" do
subject.handle_authorized_keys_input(:add_authorized_key, user)
expect(user["authorized_keys"]).to_not be_empty
end

context "and the public key was already selected" do
let(:authorized_keys) { [key1.to_s] }

it "displays an error" do
expect(Yast2::Popup).to receive(:show)
.with("The selected public key is already present in the list", headline: :error)
subject.handle_authorized_keys_input(:add_authorized_key, user)
end
end
end

context "when the user removes a public key" do
let(:authorized_keys) { [key1.to_s, key2.to_s] }

before do
allow(Yast::UI).to receive(:QueryWidget).with(Id(:authorized_keys_table), :CurrentItem)
.and_return(1)
allow(Yast::UI).to receive(:QueryWidget).with(Id(:authorized_keys_table), :Items)
.and_return([Item(Id(0), "fingerprint#1", "comment#1")])
end

it "removes the public key from the user" do
subject.handle_authorized_keys_input(:remove_authorized_key, user)
expect(user["authorized_keys"]).to eq([key1.to_s])
end
end
end

describe "#read_public_key" do
let(:path) { FIXTURES_PATH.join("id_rsa.pub").to_s }

before do
allow(Yast::UI).to receive(:AskForExistingFile).and_return(path)
end

context "when the user selects a file" do
it "returns the public key" do
key = subject.read_public_key
expect(key.comment).to eq("dummy1@example.net")
end
end

context "when the user selects an invalid file" do
let(:path) { FIXTURES_PATH.join("users.yml").to_s }

it "displays an error" do
expect(Yast2::Popup).to receive(:show)
.with("The selected file does not contain a valid public key", headline: :error)
subject.read_public_key
end
end

context "when the user file that does not exist" do
let(:path) { FIXTURES_PATH.join("non-existent").to_s }

it "displays an error" do
expect(Yast2::Popup).to receive(:show)
.with("Could not read the file containing the public key", headline: :error)
subject.read_public_key
end
end

context "when the user cancels the dialog" do
let(:path) { nil }

it "returns nil" do
expect(subject.read_public_key).to eq(nil)
end
end
end

describe "#display_authorized_keys_tab" do
let(:user) do
{ "username" => "root", "authorized_keys" => authorized_keys }
end

before do
allow(Yast::UI).to receive(:ChangeWidget)
.with(Id(:remove_authorized_key), :Enabled, anything)
end

context "when a public keys is found" do
let(:authorized_keys) { ["ssh-rsa ..."] }

it "enables the 'remove' button" do
expect(Yast::UI).to receive(:ChangeWidget)
.with(Id(:remove_authorized_key), :Enabled, true)
subject.display_authorized_keys_tab(user)
end
end

context "when no public keys are found" do
let(:authorized_keys) { [] }

it "disables the 'remove' button" do
expect(Yast::UI).to receive(:ChangeWidget)
.with(Id(:remove_authorized_key), :Enabled, false)
subject.display_authorized_keys_tab(user)
end
end

context "when a row is selected" do
let(:authorized_keys) { ["ssh-rsa ..."] }

it "selects the corresponding row in the table" do
expect(Yast::UI).to receive(:ChangeWidget)
.with(Id(:authorized_keys_table), :CurrentItem, 0)
subject.display_authorized_keys_tab(user, 0)
end
end
end

describe "#get_authorized_keys_term"
end
end

0 comments on commit 0e24eec

Please sign in to comment.