yast/yast-installation

View on GitHub
src/lib/installation/clients/security_proposal.rb

Summary

Maintainability
A
25 mins
Test Coverage
# Copyright (c) [2017] SUSE LLC
#
# All Rights Reserved.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of version 2 of the GNU General Public License as published
# by the Free Software Foundation.
#
# This program 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 General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, contact SUSE LLC.
#
# To contact SUSE LLC about this file by physical or electronic mail, you may
# find current contact information at www.suse.com.

require "yast"
require "erb"
require "y2firewall/firewalld/api"
require "installation/security_settings"
require "installation/dialogs/security"
require "installation/proposal_client"

Yast.import "PackagesProposal"

module Installation
  module Clients
    # Firewall and SSH installation proposal client
    class SecurityProposal < ::Installation::ProposalClient
      include Yast::I18n
      include Yast::Logger

      # [Installation::SecuritySettings] Stores the proposal settings
      attr_accessor :settings

      SERVICES_LINKS = [
        LINK_ENABLE_FIREWALL = "security--enable_firewall".freeze,
        LINK_DISABLE_FIREWALL = "security--disable_firewall".freeze,
        LINK_OPEN_SSH_PORT = "security--open_ssh".freeze,
        LINK_BLOCK_SSH_PORT = "security--close_ssh".freeze,
        LINK_ENABLE_SSHD = "security--enable_sshd".freeze,
        LINK_DISABLE_SSHD = "security--disable_sshd".freeze,
        LINK_OPEN_VNC = "security--open_vnc".freeze,
        LINK_CLOSE_VNC = "security--close_vnc".freeze,
        LINK_CPU_MITIGATIONS = "security--cpu_mitigations".freeze
      ].freeze

      LINK_DIALOG = "security".freeze

      # Constructor
      def initialize
        super
        Yast.import "UI"
        Yast.import "HTML"
        textdomain "installation"

        @settings ||= ::Installation::SecuritySettings.instance
      end

      def description
        {
          # Proposal title
          "rich_text_title" => _("Security"),
          # Menu entry label
          "menu_title"      => _("&Security"),
          "id"              => LINK_DIALOG
        }
      end

      def make_proposal(_attrs)
        {
          "preformatted_proposal" => preformatted_proposal,
          "warning_level"         => :warning,
          "links"                 => SERVICES_LINKS,
          "warning"               => warning
        }
      end

      def preformatted_proposal
        Yast::HTML.List(proposals)
      end

      def warning
        return nil unless @settings.access_problem?

        # TRANSLATORS: proposal warning text preventing the user to block
        # the root login by error.
        _("The 'root' user uses only SSH key-based authentication. <br>" \
          "With the current settings the user might not be allowed to login.")
      end

      def ask_user(param)
        chosen_link = param["chosen_id"]
        result = :next
        log.info "User clicked #{chosen_link}"

        if SERVICES_LINKS.include?(chosen_link)
          call_proposal_action_for(chosen_link)
        elsif chosen_link == LINK_DIALOG
          result = ::Installation::Dialogs::Security.new(@settings).run
        else
          raise "INTERNAL ERROR: unknown action '#{chosen_link}' for proposal client"
        end

        { "workflow_sequence" => result }
      end

      def write
        { "success" => true }
      end

    private

      # Obtain and call the corresponding method for the clicked link.
      def call_proposal_action_for(link)
        action = link.gsub("security--", "")
        if action == "cpu_mitigations"
          bootloader_dialog
        else
          @settings.public_send("#{action}!")
        end
      end

      # Array with the available proposal descriptions.
      #
      # @return [Array<String>] services and ports descriptions
      def proposals
        # Filter proposals with content
        [cpu_mitigations_proposal, firewall_proposal, sshd_proposal,
         ssh_port_proposal, vnc_fw_proposal, lsm_proposal,
         polkit_default_priv_proposal].compact
      end

      # Returns the cpu mitigation part of the bootloader proposal description
      # Returns nil if this part should be skipped
      # @return [String] proposal html text
      def cpu_mitigations_proposal
        require "bootloader/bootloader_factory"
        bl = ::Bootloader::BootloaderFactory.current
        return nil unless bl.respond_to?(:cpu_mitigations)

        mitigations = bl.cpu_mitigations

        res = _("CPU Mitigations: ") + "<a href=\"#{LINK_CPU_MITIGATIONS}\">" +
          ERB::Util.html_escape(mitigations.to_human_string) + "</a>"
        log.info "mitigations output #{res.inspect}"
        res
      end

      def bootloader_dialog
        require "bootloader/config_dialog"
        Yast.import "Bootloader"

        begin
          # do it in own dialog window
          Yast::Wizard.CreateDialog
          dialog = ::Bootloader::ConfigDialog.new(initial_tab: :kernel)
          settings = Yast::Bootloader.Export
          result = dialog.run
          if result == :next
            Yast::Bootloader.proposed_cfg_changed = true
          else
            Yast::Bootloader.Import(settings)
          end
        ensure
          Yast::Wizard.CloseDialog
        end
      end

      # Returns the VNC-port part of the firewall proposal description
      # Returns nil if this part should be skipped
      # @return [String] proposal html text
      def vnc_fw_proposal
        # It only makes sense to show the blocked ports if firewall is
        # enabled (bnc#886554)
        return nil unless @settings.enable_firewall
        # Show VNC port only if installing over VNC
        return nil unless Linuxrc.vnc

        if @settings.open_vnc
          _("VNC ports will be open (<a href=\"%s\">block</a>)") % LINK_CLOSE_VNC
        else
          _("VNC ports will be blocked (<a href=\"%s\">open</a>)") % LINK_OPEN_VNC
        end
      end

      # Returns the SSH-port part of the firewall proposal description
      # Returns nil if this part should be skipped
      # @return [String] proposal html text
      def ssh_port_proposal
        return nil unless @settings.enable_firewall

        if @settings.open_ssh
          _("SSH port will be open (<a href=\"%s\">block</a>)") % LINK_BLOCK_SSH_PORT
        else
          _("SSH port will be blocked (<a href=\"%s\">open</a>)") % LINK_OPEN_SSH_PORT
        end
      end

      # Returns the Firewalld service part of the firewall proposal description
      # @return [String] proposal html text
      def firewall_proposal
        if @settings.enable_firewall
          _(
            "Firewall will be enabled (<a href=\"%s\">disable</a>)"
          ) % LINK_DISABLE_FIREWALL
        else
          _(
            "Firewall will be disabled (<a href=\"%s\">enable</a>)"
          ) % LINK_ENABLE_FIREWALL
        end
      end

      # Returns the SSH service part of the firewall proposal description
      # @return [String] proposal html text
      def sshd_proposal
        # Check if only public key auth is configured, and if yes,
        # enable SSHD and open the SSH port; but only now, after we are sure
        # that the user was prompted for the root password (bsc#1211764).
        @settings.propose
        if @settings.enable_sshd
          _(
            "SSH service will be enabled (<a href=\"%s\">disable</a>)"
          ) % LINK_DISABLE_SSHD
        else
          _(
            "SSH service will be disabled (<a href=\"%s\">enable</a>)"
          ) % LINK_ENABLE_SSHD
        end
      end

      def polkit_default_priv_proposal
        value = @settings.polkit_default_privileges.to_s
        human_value = @settings.human_polkit_privileges[value]

        format(_("PolicyKit Default Privileges: %s"), human_value)
      end

      # Returns the text describing the Linux Security Module proposal or nil in case that there is
      # no module selected explicitly.
      #
      # @return [String, nil] returns the description of the selected LSM or nil in case no module
      #   is selected explicitly
      def lsm_proposal
        return nil unless @settings.lsm_config.configurable?

        # add required patterns
        log.info("Setting LSM resolvables to : #{@settings.lsm_config.needed_patterns}")
        Yast::PackagesProposal.SetResolvables("LSM", :pattern, @settings.lsm_config.needed_patterns)
        selected = @settings.lsm_config.selected
        case selected&.id
        when :selinux
          # TRANSLATORS: Proposal's text describing that the active Linux Security Major Module
          # after the installation will be SELinux running in the selected mode which could be
          # 'enforcing', 'permissive' or 'disabled'
          format(_(
                   "Major Linux Security Module: %{module} in '%{mode}' mode"
                 ), module: selected.label, mode: selected.mode.to_human_string)
        when :apparmor, :none
          # TRANSLATORS: Proposal's text describing that the active Linux Security Major Module
          # after the installation will be AppArmor
          format(_("Major Linux Security Module: %{module}"), module: selected.label)
        end
      end
    end
  end
end