From 1da4d4f45c22423c8e2099d112fe1473560b0c5f Mon Sep 17 00:00:00 2001 From: Maksim Malchuk Date: Sat, 20 Aug 2016 02:33:02 +0300 Subject: [PATCH] Add GrubPassword module to the fuelmenu This change adds new GrubPassword module to the fuelmenu which can configure password for the editing grub menu. The module creates the default /boot/grub2/user.cfg file with hashed password only when it entered interactively. For security reasons the plain password never stored and the file always overwritten with new one provided. DocImpact Closes-Bug: #1552164 Change-Id: I3bc330133dd3d71ea62a7169a84d9ad802a4a3ef Signed-off-by: Maksim Malchuk --- fuelmenu/modules/__init__.py | 2 + fuelmenu/modules/grubpw.py | 143 +++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 fuelmenu/modules/grubpw.py diff --git a/fuelmenu/modules/__init__.py b/fuelmenu/modules/__init__.py index 3384566..c9dd5a0 100644 --- a/fuelmenu/modules/__init__.py +++ b/fuelmenu/modules/__init__.py @@ -19,6 +19,7 @@ from fuelmenu.modules.cobblerconf import CobblerConfig from fuelmenu.modules.dnsandhostname import DnsAndHostname from fuelmenu.modules.feature_groups import FeatureGroups from fuelmenu.modules.fueluser import FuelUser +from fuelmenu.modules.grubpw import GrubPassword from fuelmenu.modules.interfaces import Interfaces from fuelmenu.modules.ntpsetup import NtpSetup from fuelmenu.modules.restore import Restore @@ -40,6 +41,7 @@ __all__ = [ BootstrapImage, NtpSetup, RootPassword, + GrubPassword, FeatureGroups, Shell, Restore, diff --git a/fuelmenu/modules/grubpw.py b/fuelmenu/modules/grubpw.py new file mode 100644 index 0000000..c8f6882 --- /dev/null +++ b/fuelmenu/modules/grubpw.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python +# Copyright 2016 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging +import re +import urwid + +from fuelmenu.common import modulehelper as helper +from fuelmenu.common import utils + +log = logging.getLogger('fuelmenu.grubpw') + + +class GrubPassword(urwid.WidgetWrap): + def __init__(self, parent): + self.name = "Grub Password" + self.visible = True + self.parent = parent + + # UI text + self.header_content = ["Set Grub password.", "", + "Default user: root", ""] + self.fields = ["PASSWORD", "CONFIRM_PASSWORD"] + + self.defaults = { + "PASSWORD": {"label": "Enter new password", + "tooltip": "Use ASCII characters only", + "value": ""}, + "CONFIRM_PASSWORD": {"label": "Confirm new password", + "tooltip": "Use ASCII characters only", + "value": ""}, + } + + self.screen = None + + def check(self, args): + self.parent.footer.set_text("Checking data...") + self.parent.refreshScreen() + + responses = dict() + for index, fieldname in enumerate(self.fields): + if fieldname != helper.BLANK_KEY: + responses[fieldname] = self.edits[index].get_edit_text() + + password = responses["PASSWORD"] + errors = [] + + # passwords must match + if password != responses["CONFIRM_PASSWORD"]: + errors.append("Passwords do not match.") + + # password needs to be in ASCII character set + try: + password.decode('ascii') + except UnicodeDecodeError: + errors.append("Password contains non-ASCII characters.") + + if errors: + self.parent.footer.set_text("Errors occurred.") + log.error("Errors: %s %s", len(errors), errors) + helper.ModuleHelper.display_failed_check_dialog(self, errors) + return False + + # check empty password + if not password: + self.parent.footer.set_text("Password is empty, " + "no changes will be made.") + log.warning("Empty password, skipping.") + else: + self.parent.footer.set_text("No errors found.") + return password + + def apply(self, args): + password = self.check(args) + if password is False: + log.error("Check failed. Not applying") + return False + + if password: + return self.save(password) + return True + + def save(self, password): + if self.parent.save_only: + # We shouldn't change root password in save_only mode + return True + + # there is no convinient way to create grub2 pbkdf password + # grub2-mkpasswd-pbkdf2 can read input password only from stdin + # FYI: grub2-setpassword is the bash script which can't be used + # here because it blocks buffered input from stdin + cmd = ["/usr/bin/grub2-mkpasswd-pbkdf2"] + stdin = "{0}\n{0}".format(password) + errcode, out, errout = utils.execute(cmd, stdin=stdin) + + # parse grub2-mkpasswd-pbkdf2 output + pbkdf2 = re.findall('grub.pbkdf2.*', out, re.M) + + if errcode == 0 and pbkdf2 != []: + grub2_password = "GRUB2_PASSWORD={0}".format(pbkdf2[0]) + log.info(grub2_password) + + log.info("Creating new /boot/grub2/user.cfg file") + # overwrite /boot/grub2/user.cfg file if exists + with open("/boot/grub2/user.cfg", "w") as usercfg: + usercfg.write(grub2_password) + usercfg.write("\n") + usercfg.close() + + self.parent.footer.set_text("Changes applied successfully.") + log.info("Grub password successfully set.") + # Reset fields + self.cancel(None) + else: + log.error("Command grub2-mkpasswd-pbkdf2 failed with an error:" + "\"{0}\"".format(errout)) + self.parent.footer.set_text("Unable to apply changes. Check logs " + "for more details.") + return False + + return True + + def cancel(self, button): + helper.ModuleHelper.cancel(self, button) + + def refresh(self): + pass + + def screenUI(self): + return helper.ModuleHelper.screenUI(self, self.header_content, + self.fields, self.defaults)