Skip to content

Commit

Permalink
Merge branch 'SLE-15-GA' into add-public-keys-management-master
Browse files Browse the repository at this point in the history
* SLE-15-GA:
  Fix the test suite
  Update src/include/users/dialogs.rb
  Update from code review
  Update src/include/users/dialogs.rb
  Bump version and update changes file
  Add help text to the authorized keys dialog
  Fix detection of changes in the authorized keys list
  Use the UsersSimple module to save the public key during installation
  Adapt Users.pm and UsersPasswd.pm to the simplified SSHAuthorizedKeys API
  Adapt SSHAuthorizedKeys to the new SSHAuthorizedKeyring API
  SSHAuthorizedKeyring handles only 1 home directory
  Update the old testsuite
  Add missing textdomain
  Add support to manage authorized keys through the UI
  Replace ssh-keygen usage with just some Ruby code
  Allow to set a public key for the root user (#173)
  release 4.0.7
  read ssh keys from root user only if the user exists (bsc#1112119, bsc#1107456)
  • Loading branch information
imobachgs committed Nov 9, 2018
2 parents b19638e + 848fdeb commit d6bb5d6
Show file tree
Hide file tree
Showing 28 changed files with 600 additions and 210 deletions.
7 changes: 7 additions & 0 deletions package/yast2-users.changes
@@ -1,3 +1,10 @@
-------------------------------------------------------------------
Fri Nov 9 08:01:15 UTC 2018 - igonzalezsosa@suse.com

- Add public keys handling support in an installed system
(related to fate#324690).
- 4.0.9

-------------------------------------------------------------------
Fri Nov 2 08:46:35 UTC 2018 - igonzalezsosa@suse.com

Expand Down
2 changes: 1 addition & 1 deletion package/yast2-users.spec
Expand Up @@ -17,7 +17,7 @@


Name: yast2-users
Version: 4.0.8
Version: 4.0.9
Release: 0

BuildRoot: %{_tmppath}/%{name}-%{version}-build
Expand Down
130 changes: 130 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(EditAuthorizedKeysDialogHelp())
current = ret
end
if ret == :plugins
UI.ReplaceWidget(:tabContents, get_plugins_term.call)
Wizard.SetHelpText(PluginDialogHelp())
Expand Down Expand Up @@ -2469,5 +2486,118 @@ 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)
# TRANSLATORS: this error happens when the selected public key is a duplicated
# (already present in the list of public keys)
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
# TRANSLATORS: title of the dialog to select a public key to be used when logging
# via SSH
path = Yast::UI.AskForExistingFile("", "*.pub", _("Select a public key"))
return if path.nil?
Y2Users::SSHPublicKey.new(File.read(path))
rescue Y2Users::SSHPublicKey::InvalidKey
# TRANSLATORS: this error happens when the file selected by the user is not a valid public
# key
Yast2::Popup.show(
_("The selected file does not contain a valid public key"), headline: :error
)
rescue Errno::ENOENT
# TRANSLATORS: this error happens when the user selected a file that has just been removed
# (the file selector may contain outdated information)
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
key_present = !user.fetch("authorized_keys", []).empty?
UI.ChangeWidget(Id(:remove_authorized_key), :Enabled, key_present)
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(
# TRANSLATORS: this fingerprint is a hash that can be used to identify a public key
# (and it is usually long string containing letters, numbers and other symbols)
_("Fingerprint"),
# TRANSLATORS: as fingerprint is hard to remember or identify for a user, a public
# key can include a comment to make things easier
_("Comment")
),
items
),
HBox(
# TRANSLATORS: a push button label
PushButton(Id(:add_authorized_key), _("&Add...")),
PushButton(Id(:remove_authorized_key), Yast::Label.RemoveButton),
HStretch()
)
)
end
end
end
10 changes: 10 additions & 0 deletions src/include/users/helps.rb
Expand Up @@ -602,5 +602,15 @@ def AuthentizationDialogHelp

help_text
end

# @return [String] Help text for the authorized keys dialog
def EditAuthorizedKeysDialogHelp
_(
"<p>\n" \
"List of public keys that the user can use to authenticate when logging in via SSH.\n" \
"You can select any public key from your computer through the <b>Add...</b> button.\n" \
"</p>\n"
)
end
end
end
95 changes: 46 additions & 49 deletions src/lib/users/ssh_authorized_keyring.rb
Expand Up @@ -23,14 +23,14 @@ module Yast
module Users
# Read, write and store SSH authorized keys.
#
# This class manages authorized keys for each home directory in the
# system.
# This class manages authorized keys for a home directory in the system.
class SSHAuthorizedKeyring
include Logger

# @return [Hash<String,Array<SSHAuthorizedKey>>] Authorized keys indexed by home directory
# @return [Array<String>] List of authorized keys
attr_reader :keys
private :keys
# @return [String] Home directory path
attr_reader :home

# Base class to use in file/directory problems
class PathError < StandardError
Expand Down Expand Up @@ -78,32 +78,26 @@ class NotRegularSSHDirectory < PathError
def default_message; "SSH directory is not a regular directory" end
end

# The authorized_keys is not a regular file (potentially insecure).
class NotRegularAuthorizedKeysFile < PathError
# @return default_message [String] Default error message
def default_message; "authorized_keys is not a regular file" end
end

# Constructor
def initialize
@keys = {}
def initialize(home)
@keys = []
@home = home
end

# Add/register a keys
#
# This method does not make any change to the system. For that,
# see #write_keys.
#
# @param home [String] Home directory where the key will be stored
# @return [Array<SSHAuthorizedKey>] Registered authorized keys
def add_keys(home, new_keys)
keys[home] = new_keys
end

# Returns the keys for a given home directory
#
# @return [Array<SSHAuthorizedKey>] List of authorized keys
def [](home)
keys[home] || []
# @return [Array<String>] Registered authorized keys
def add_keys(new_keys)
keys.concat(new_keys)
end

# Determines if the keyring is empty
Expand All @@ -116,14 +110,12 @@ def empty?
# Read keys from a given home directory and add them to the keyring
#
# @param path [String] User's home directory
# @return [Boolean] +true+ if some key was found
def read_keys(home)
path = authorized_keys_path(home)
return false unless FileUtils::Exists(path)
authorized_keys = SSHAuthorizedKeysFile.new(path).keys
keys[home] = authorized_keys unless authorized_keys.empty?
log.info "Read #{authorized_keys.size} keys from #{path}"
!authorized_keys.empty?
# @return [Array<String>] List of authorized keys
def read_keys
path = authorized_keys_path
@keys = FileUtils::Exists(path) ? SSHAuthorizedKeysFile.new(path).keys : []
log.info "Read #{@keys.size} keys from #{path}"
@keys
end

# Write user keys to the given file
Expand All @@ -132,17 +124,17 @@ def read_keys(home)
# created inheriting owner/group and setting permissions to SSH_DIR_PERM.
#
# @param path [String] User's home directory
# @return [Boolean] +true+ if keys were written; +false+ otherwise
def write_keys(home)
return false if keys[home].nil?
def write_keys
remove_authorized_keys_file
return if keys.empty?
if !FileUtils::Exists(home)
log.error("Home directory '#{home}' does not exist!")
raise HomeDoesNotExist.new(home)
end
user = FileUtils::GetOwnerUserID(home)
group = FileUtils::GetOwnerGroupID(home)
create_ssh_dir(home, user, group)
write_file(home, user, group)
create_ssh_dir(user, group)
write_file(user, group)
end

private
Expand All @@ -162,8 +154,8 @@ def write_keys(home)
# @return [String] Path to the user's SSH directory
#
# @see SSH_DIR
def ssh_dir_path(home)
File.join(home, SSH_DIR)
def ssh_dir_path
@ssh_dir_path ||= File.join(home, SSH_DIR)
end

# Determine the path to the user's authorized keys file
Expand All @@ -175,8 +167,8 @@ def ssh_dir_path(home)
# @see AUTHORIZED_KEYS_FILE
#
# @see #ssh_dir_path
def authorized_keys_path(home)
File.join(ssh_dir_path(home), AUTHORIZED_KEYS_FILE)
def authorized_keys_path
@authorized_keys_path ||= File.join(ssh_dir_path, AUTHORIZED_KEYS_FILE)
end

# Find or creates the SSH directory
Expand All @@ -192,16 +184,16 @@ def authorized_keys_path(home)
#
# @raise NotRegularSSHDirectory
# @raise CouldNotCreateSSHDirectory
def create_ssh_dir(home, user, group)
ssh_dir = ssh_dir_path(home)
if FileUtils::Exists(ssh_dir)
raise NotRegularSSHDirectory.new(ssh_dir) unless FileUtils::IsDirectory(ssh_dir)
return ssh_dir
def create_ssh_dir(user, group)
if FileUtils::Exists(ssh_dir_path)
raise NotRegularSSHDirectory.new(ssh_dir_path) unless FileUtils::IsDirectory(ssh_dir_path)
return ssh_dir_path
end
ret = SCR.Execute(Path.new(".target.mkdir"), ssh_dir)
ret = SCR.Execute(Path.new(".target.mkdir"), ssh_dir_path)
log.info("Creating SSH directory: #{ret}")
raise CouldNotCreateSSHDirectory.new(ssh_dir) unless ret
FileUtils::Chown("#{user}:#{group}", ssh_dir, false) && FileUtils::Chmod(SSH_DIR_PERMS, ssh_dir, false)
raise CouldNotCreateSSHDirectory.new(ssh_dir_path) unless ret
FileUtils::Chown("#{user}:#{group}", ssh_dir_path, false) &&
FileUtils::Chmod(SSH_DIR_PERMS, ssh_dir_path, false)
end

# Write authorized keys file
Expand All @@ -210,14 +202,19 @@ def create_ssh_dir(home, user, group)
# @param user [Fixnum] Users's UID
# @param group [Fixnum] Group's GID
# @param perms [String] Permissions (in form "0700")
def write_file(home, owner, group)
path = authorized_keys_path(home)
file = SSHAuthorizedKeysFile.new(path)
file.keys = keys[home]
log.info "Writing #{keys[home].size} keys in #{path}"
file.save && FileUtils::Chown("#{owner}:#{group}", path, false)
def write_file(owner, group)
file = SSHAuthorizedKeysFile.new(authorized_keys_path)
file.keys = keys
log.info "Writing #{keys.size} keys in #{authorized_keys_path}"
file.save && FileUtils::Chown("#{owner}:#{group}", authorized_keys_path, false)
rescue SSHAuthorizedKeysFile::NotRegularFile
raise NotRegularAuthorizedKeysFile.new(path)
raise NotRegularAuthorizedKeysFile.new(authorized_keys_path)
end

# Remove the authorized keys file
def remove_authorized_keys_file
return unless FileUtils::Exists(authorized_keys_path)
SCR.Execute(Path.new(".target.remove"), authorized_keys_path)
end
end
end
Expand Down

0 comments on commit d6bb5d6

Please sign in to comment.