From a76bbe4f08c59c77255bdf28af518e62da68953f Mon Sep 17 00:00:00 2001 From: Friedrich Weber Date: Mon, 10 Jul 2017 16:01:42 +0200 Subject: [PATCH] Move most of pi-appliance to a Python module This is necessary in order to import the dialogs for integration testing. Working on #32 --- authappliance/menu.py | 1413 ++++++++++++++++++++++++++++++++++++ authappliance/pi-appliance | 1412 +---------------------------------- 2 files changed, 1415 insertions(+), 1410 deletions(-) create mode 100644 authappliance/menu.py diff --git a/authappliance/menu.py b/authappliance/menu.py new file mode 100644 index 0000000..df6a8fa --- /dev/null +++ b/authappliance/menu.py @@ -0,0 +1,1413 @@ +# -*- coding: utf-8 -*- +# copyright 2014 Cornelius Kölbel +# License: AGPLv3 +# contact: http://www.privacyidea.org +# +# This code is free software; you can redistribute it and/or +# License as published by the Free Software Foundation; either +# version 3 of the License, or any later version. +# +# This code 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 AFFERO GENERAL PUBLIC LICENSE for more details. +# +# You should have received a copy of the GNU Affero General Public +# License along with this program. If not, see . +""" +text dialog based setup tool to configure the privacyIDEA basics. +""" + +import locale +import argparse +import sys +import time +from dialog import Dialog +from authappliance.lib.appliance import (Backup, Audit, FreeRADIUSConfig, + ApacheConfig, + PrivacyIDEAConfig, + OSConfig, MySQLConfig, + DEFAULT_CONFIG, + CRON_USER, AUDIT_CMD, BACKUP_CMD, RemoteMySQLConfig, SERVICE_APACHE, + SERVICE_FREERADIUS) +from privacyidea.lib.auth import (create_db_admin, get_db_admins, + delete_db_admin) +from privacyidea.models import Admin +from privacyidea.app import create_app +from netaddr import IPAddress +from paramiko.client import SSHClient +from paramiko import SSHException, AutoAddPolicy, SFTPClient, Transport +from subprocess import Popen, PIPE +import random +import os +from tempfile import NamedTemporaryFile + +DESCRIPTION = __doc__ +VERSION = "2.0" + +#: This holds the services that should be restarted +#: at the end of the session. It should be modified +#: using mark_service_for_restart and +#: reset_services_for_restart. +services_for_restart = set() + + +def mark_service_for_restart(service): + """ + Add ``service`` to set of services that should be restarted. + """ + services_for_restart.add(service) + + +def reset_services_for_restart(): + """ + Clear the set of services that should be restarted. + """ + services_for_restart.clear() + + +class WebserverMenu(object): + def __init__(self, app, dialog): + self.app = app + self.os = OSConfig() + self.d = dialog + self.apache = ApacheConfig() + + def menu(self): + bt = "Webservice configuration" + while 1: + code, tags = self.d.menu("Configure Webserver", + choices=[("restart services", ""), + ("-" * 40, ""), + ("Generate selfsigned " + "certificate", ""), + ("-" * 40, ""), + ("Generate Certificate " + "Signing Request", ""), + ("Import certificate", ""), + ("-" * 40, ""), + ("Regenerate private key", "") + ], + cancel='Back', + backtitle=bt) + if code == self.d.DIALOG_OK: + if tags.startswith("restart"): + self.restart() + if tags.startswith("Regenerate private"): + self.generate_private_key() + if tags.startswith("Generate self"): + self.generate_self() + if tags.startswith("Generate Certificate"): + self.generate_csr() + if tags.startswith("Import"): + self.import_certificate() + else: + break + + def generate_self(self): + code = self.d.yesno("Do you want to recreate the self signed " + "certificate?") + if code == self.d.DIALOG_OK: + self.apache.create_self_signed() + mark_service_for_restart(SERVICE_APACHE) + + def generate_private_key(self): + code, tags = self.d.menu("Regenerate private key. " + "When regenerating the private key you also " + "need a new certificate! " + "Do not regenerate the private key while " + "you are waiting for a certificate signing " + "request to be signed. In this case " + "the certificate will not match the " + "private key anymore.", + choices=[("2048", "2048 bit"), + ("4096", "4096 bit"), + ("8192", "8192 bit")], + backtitle="Regenerate private key") + if code == self.d.DIALOG_OK: + self.apache.create_private_key(tags) + mark_service_for_restart(SERVICE_APACHE) + + def generate_csr(self): + csr = self.apache.generate_csr() + code = self.d.msgbox("The certificate signing request is written " + "to {0}. \nCopy this file and pass it to your " + "certificate authority for getting it signed. \n" + "Do not regenerate the private " + "key after you have sent the CSR to your " + "certificate authority.".format(csr), + width=70, height=10) + + def import_certificate(self): + cert, keyfile = self.apache.get_certificates() + homedir = os.getenv("HOME") + code = self.d.msgbox("You received a signed certificate from a " + "certificate authority. Copy this file to the " + "directory \n{0}.\n" + "In the following dialog you may choose, " + "which certificate you want to use for your " + "webserver.".format(homedir), + width=70, height=10) + + bt = "Import certificate (.pem, .crt, .der, .cer)" + files = self.apache.get_imports(homedir) + choices = [] + for bfile in sorted(files.keys()): + choices.append((bfile, "%s" % (files[bfile].get("time")))) + if len(choices) == 0: + self.d.msgbox("No certificate found!") + else: + code, tags = self.d.menu("Choose the certificate you want to " + "use for the webserver.", + choices=choices, + backtitle=bt, + width=78) + if code == self.d.DIALOG_OK: + self.apache.import_cert(homedir + "/" + tags, cert) + mark_service_for_restart(SERVICE_APACHE) + + def restart(self): + code = self.d.yesno("Do you want to restart the services for the " + "changes to take effect?") + if code == self.d.DIALOG_OK: + self.os.restart(SERVICE_APACHE) + + +class Peer(object): + files = ["/etc/privacyidea/enckey", "/etc/privacyidea/logging.cfg", + "/etc/privacyidea/private.pem", "/etc/privacyidea/public.pem"] + + def __init__(self, dialog, pConfig, dbConfig, remote_ip=None, + password=None, local_ip=None): + self.remote_ip = remote_ip + self.local_ip = local_ip + self.password = password + self.d = dialog + self.pConfig = pConfig + self.dbConfig = dbConfig + self.ssh = SSHClient() + self.os = OSConfig() + self.info = "" + self.file_local = "" + self.file_remote = "" + self.position_local = "" + self.position_remote = "" + + def get_peer_data(self): + bt = "Add another SQL Master" + code, ip = self.d.inputbox( + "Enter the IP Address of the other privacyIDEA server.", + backtitle=bt) + if code != self.d.DIALOG_OK: + return False + + try: + self.remote_ip = IPAddress(ip) + except: + self.d.msgbox("Invalid IP address") + return False + + code, password = self.d.passwordbox( + "Enter the root password of the other machine. Please NOTE: " + "On an Ubuntu machine Root SSH login with password is forbidden. " + "You need to change 'PermitRootLogin' in the " + "/etc/ssh/sshd_config. Otherwise authentication will fail.", + insecure=True, backtitle=bt) + if code != self.d.DIALOG_OK: + return False + + self.password = password + + code, local_ip = self.d.inputbox( + "Enter the local IP Address of this machine, to which the remote " + "server will connect.", backtitle=bt) + if code != self.d.DIALOG_OK: + return False + + try: + self.local_ip = IPAddress(local_ip) + except: + self.d.msgbox("Invalid IP address") + return False + + return True + + def add_info(self, new_info): + self.info += new_info + "\n" + self.d.infobox(self.info, height=20, width=60) + + def stop_redundancy(self): + self.info = "" + self.add_info("Stopping local webserver") + self.os.restart(service="apache2", action="stop") + + # To stop redundancy we remove + # several fields from the my.cnf file. + self.add_info("Removing config from my.cnf...") + self.dbConfig.set("mysqld", "bind-address", "127.0.0.1") + for setting in ["server-id", "auto_increment_increment", + "auto_increment_offset", "log_bin", "binlog_do_db"]: + self.dbConfig.delete("mysqld", setting) + + # delete the replication user + self.add_info("Dropping replicator user...") + _, err = self._execute_local_sql("drop user 'replicator'@'%';") + if err: + self.add_info("Error dropping replicator user!") + + # stop the slave + self.add_info("Stopping the slave...") + _, err = self._execute_local_sql("stop slave; reset slave;") + if err: + self.add_info("Error stopping slave: {0}".format(err)) + # restart mysql + self.add_info("Restarting MySQL server...") + self.dbConfig.restart() + self.add_info("Starting local webserver...") + self.os.restart(service="apache2", action="start") + self.add_info("done.") + self.d.scrollbox(self.info.decode('utf-8'), height=20, width=60) + + def _execute_local_sql(self, sql): + p = Popen(['mysql', '--defaults-extra-file=/etc/mysql/debian.cnf'], + stdin=PIPE, stdout=PIPE, stderr=PIPE) + output, err = p.communicate(sql) + if err: + self.add_info("====== ERROR =======") + self.add_info(err) + return output, err + + def _escape_for_shell(self, argument): + return "'{}'".format(argument.replace("'", r"'\''")) + + def _execute_remote_sql(self, sql): + assert '"' not in sql + stdin, stdout, stderr = self.ssh.exec_command( + 'echo {} | mysql --defaults-extra-file=/etc/mysql/debian.cnf'.format( + self._escape_for_shell(sql))) + err = stderr.read() + if err: + self.add_info("====== ERROR =======") + self.add_info(err) + output = stdout.read() + return output, err + + def setup_redundancy(self): + # + # Copy files in /etc/privacyidea + # + self.info = "" + self.add_info("copying files to remote server...") + transport = Transport((str(self.remote_ip), 22)) + transport.connect(username="root", password=self.password) + sftp = SFTPClient.from_transport(transport) + for file in self.files: + if os.path.exists(file): + sftp.put(file, file) + + # adjust remote pi.cfg + remote_config = PrivacyIDEAConfig(DEFAULT_CONFIG, opener=sftp.file) + local_config = PrivacyIDEAConfig(DEFAULT_CONFIG) + + new_remote_config = local_config.config.copy() + # copy the local pi.cfg to the remote config except the + # keys listed in ``save_remote_config_keys``. + save_remote_config_keys = ['SQLALCHEMY_DATABASE_URI'] + for key in save_remote_config_keys: + new_remote_config[key] = remote_config.config[key] + remote_config.config = new_remote_config + remote_config.save() + + # + # Setup my.cnf, locally and remotely + # + shared_my_cnf_values = {"auto_increment_increment": 2, + "log_bin": "/var/log/mysql/mysql-bin.log", + "binlog_do_db": "pi", + "bind-address": "0.0.0.0" + } + remote_my_cnf_values = {"server-id": 2, + "auto_increment_offset": 2} + local_my_cnf_values = {"server-id": 1, + "auto_increment_offset": 1} + + self.add_info("Setup my.cnf on local server...") + + for key, value in shared_my_cnf_values.items() + local_my_cnf_values.items(): + self.dbConfig.set('mysqld', key, value) + + self.add_info("Setup my.cnf on remote server...") + + remote_my_cnf = RemoteMySQLConfig(sftp) + for key, value in shared_my_cnf_values.items() + remote_my_cnf_values.items(): + remote_my_cnf.set('mysqld', key, value) + + sftp.close() + transport.close() + + # + # Restart services + # + self.add_info("Restarting local MySQL server...") + self.dbConfig.restart() + self.add_info("Restarting remote MySQL server...") + self.ssh.connect(str(self.remote_ip), username="root", + password=self.password) + stdin, stdout, stderr = self.ssh.exec_command("service mysql restart") + if stderr: + self.add_info(stderr.read()) + # + # Configuring mysql + # + # Before configuring mysql we stop the webserver + self.add_info("Stopping local webserver") + self.os.restart(service="apache2", action="stop") + self.add_info("Stopping remote webserver") + stdin, stdout, stderr = self.ssh.exec_command("service apache2 stop") + if stderr: + self.add_info(stderr.read()) + + self.add_info("Configuring MySQL on local server...") + # We start at 40, since 39 is "'" which might lead to confusion. + # Create a random password of random length + replicator_password = "".join([random.choice( + "01234567890abcdefghijklmnopqrstuvwxyzABZDEFGHIJKLMNOPQRSTUVWXYZ") + for x in + range(random.randrange(21, 31))]) + + # Add the replication users on both machines + self.add_info("Drop and add replication user on local machine...") + self._execute_local_sql("drop user if exists 'replicator'@'%';") + self._execute_local_sql("""create user 'replicator'@'%' identified by '{}'; + grant replication slave on *.* to 'replicator'@'%';""".format(replicator_password)) + + self.add_info("Drop and add replication user on remote machine...") + # Drop user + _, err = self._execute_remote_sql("drop user if exists 'replicator'@'%'") + self._execute_remote_sql("""create user 'replicator'@'%' identified by '{}'; + grant replication slave on *.* to 'replicator'@'%';""".format(replicator_password)) + # + # dump it and copy it to the other server + # + self.add_info("Dumping and copying the existing database...") + + dumpfile = NamedTemporaryFile(mode="w", delete=False) + p = Popen(["mysqldump", "--defaults-extra-file=/etc/mysql/debian.cnf", "--databases", "pi"], + # TODO: Need explicit password here? + stdout=dumpfile, stderr=PIPE) + output, err = p.communicate() + r = p.wait() + if r == 0: + self.add_info("Saved SQL dump to {0}".format(dumpfile.name)) + # copy the file.name to the remote machine and run the file. + self.add_info("Copying to remote server and creating remote " + "database. This may take a while...") + transport = Transport((str(self.remote_ip), 22)) + transport.connect(username="root", password=self.password) + sftp = SFTPClient.from_transport(transport) + sftp.put(dumpfile.name, dumpfile.name) + sftp.close() + transport.close() + # delete the file + os.unlink(dumpfile.name) + # run the file remotely + # mysql -u root -p < test.sql + stdin, stdout, stderr = self.ssh.exec_command( + "cat {dumpfile} | mysql --defaults-extra-file=/etc/mysql/debian.cnf".format(dumpfile=dumpfile.name)) + err = stderr.read() + if err: + self.add_info("ERROR: {0}".format(err)) + else: + self.add_info("Dumped SQL database on remote server") + # delete remote file + self.ssh.exec_command( + "rm -f {dumpfile}".format(dumpfile=dumpfile.name)) + else: + self.add_info("ERROR: {0}".format(err)) + + # + # Get the position on server1 + # + output, err = self._execute_local_sql("show master status;") + r = p.returncode + if r == 0: + self.add_info(output) + for line in output.split("\n"): + elems = line.split() + if len(elems) > 2 and elems[2] == "pi": + self.file_local = elems[0] + self.position_local = elems[1] + self.add_info("Local File: {0}".format(self.file_local)) + self.add_info("Local Position: {0}".format( + self.position_local)) + + self.add_info("Configuring MySQL on remote server...") + # clean up the old database + output, _ = self._execute_remote_sql('show master status;') + self.add_info(output) + for line in output.split("\n"): + elems = line.split() + if len(elems) > 2 and elems[2] == "pi": + self.file_remote = elems[0] + self.position_remote = elems[1] + self.add_info("Remote File: {0}".format(self.file_remote)) + self.add_info("Remote Position: {0}".format( + self.position_remote)) + + # create everything remote + self.add_info("Add replication on remote server...") + self._execute_remote_sql(""" + stop slave; + CHANGE MASTER TO MASTER_HOST = '{local_ip}', MASTER_USER = 'replicator', + MASTER_PASSWORD = '{replicator_password}', MASTER_LOG_FILE = '{local_file}', MASTER_LOG_POS = {local_position}; + start slave;""".format( + replicator_password=replicator_password, + local_file=self.file_local, + local_position=self.position_local, + local_ip=self.local_ip)) + + # + # Configure replication on LOCAL host + # + self.add_info("Add replication on local server...") + self._execute_local_sql(""" + stop slave; + CHANGE MASTER TO MASTER_HOST = '{remote_ip}', MASTER_USER = 'replicator', + MASTER_PASSWORD = '{replicator_password}', MASTER_LOG_FILE = '{remote_file}', MASTER_LOG_POS = {remote_position}; + start slave;""".format( + replicator_password=replicator_password, + remote_ip=self.remote_ip, + remote_file=self.file_remote, + remote_position=self.position_remote)) + + self.add_info("Starting local webserver") + self.os.restart(service="apache2", action="start") + self.add_info("Starting remote webserver") + self.ssh.exec_command("service apache2 start") + self.add_info("\nRedundant setup complete.") + + self.d.scrollbox(self.info.decode('utf-8'), height=20, width=60) + + +class DBMenu(object): + def __init__(self, app, dialog, pConfig): + self.app = app + self.d = dialog + self.pConfig = pConfig + self.db = MySQLConfig() + self.peer = Peer(self.d, self.pConfig, self.db) + + def menu(self): + bt = "Configure the database connection" + while 1: + current_config = self.pConfig.get_DB() + code, tags = self.d.menu( + "The current database configuration string is %s" % current_config, + choices=[("init tables", "create missing tables"), + ("view redundancy", ""), + ("setup redundancy", "master master replication"), + ("stop redundancy", "revert to single database")], + cancel='Back', + backtitle=bt) + if code == self.d.DIALOG_OK: + if tags.startswith("init"): + self.db_init() + elif tags.startswith("view"): + self.redundancy_status() + elif tags.startswith("stop redundancy"): + code = self.d.yesno( + "Do you really want to stop the redundancy? This " + "server will be reverted to a single master. The " + "other master will not be touched. You can simply " + "shut down the other machine.", width=60, height=10 + ) + if code == self.d.DIALOG_OK: + self.peer.stop_redundancy() + elif tags.startswith("setup"): + if self.peer.get_peer_data(): + # Now we need to check, if the remote machine is + # running privacyIDEA and MySQL. + try: + self.peer.ssh.set_missing_host_key_policy( + AutoAddPolicy()) + self.peer.ssh.connect(str(self.peer.remote_ip), + username="root", + password=self.peer.password) + stdin, stdout, stderr = self.peer.ssh.exec_command( + 'dpkg -l privacyidea-apache2') + output_pi = stdout.read() + error_pi = stderr.read() + + stdin, stdout, stderr = self.peer.ssh.exec_command( + 'dpkg -l mysql-server') + output_mysql = stdout.read() + error_mysql = stderr.read() + self.peer.ssh.close() + if not output_mysql: + self.d.msgbox( + "MySQL server not installed on {0!s}. " + "Please install mysql-server.".format( + self.peer.remote_ip)) + break + if not output_pi: + self.d.msgbox( + "privacyIDEA not installed on " + "{0!s}. Please install " + "privacyidea-apache2.".format( + self.peer.remote_ip)) + break + except SSHException as exx: + self.d.msgbox("{0!s}".format(exx)) + break + + code = self.d.yesno( + "OK. privacyIDEA and MySQL is installed on the " + "remote server. We are ready to setup redundancy. " + "Data will be cloned to the remote server. All " + "privacyIDEA data on the remote server will be " + "lost. Shall we proceed?", width=60) + if code != self.d.DIALOG_OK: + break + else: + self.peer.setup_redundancy() + + else: + break + + def redundancy_status(self): + r, bind, server_id = self.db.is_redundant() + self.d.scrollbox( + u""" + Master-Master replication active: {active!s} + Server ID: {server_id!s} + Bind Address: {bind_address!s} + """.format(active=r, bind_address=bind, server_id=server_id), + width=60, height=20) + + def db_init(self): + db_connect = self.pConfig.get_DB() + code = self.d.yesno("Do you want to recreate the tables? " + "Existing data will not be lost. Only new tables " + "in the database scheme will be created on %s" % + db_connect, + width=70, + backtitle="Create database tables") + if code == self.d.DIALOG_OK: + r = self.pConfig.DB_init() + if r: + self.d.msgbox("Created database tables.") + else: + self.d.scrollbox("Error creating database tables.") + + +class AuditMenu(object): + def __init__(self, app, dialog): + self.app = app + self.d = dialog + self.Audit = Audit() + + def menu(self): + bt = "Audit Log Rotation" + self.Audit.CP.read() + while 1: + code, tags = self.d.menu("Auditlog Rotate", + choices=[("Configure Audit Log", "")], + cancel='Back', + backtitle=bt) + if code == self.d.DIALOG_OK: + if tags.startswith("Configure"): + self.config() + else: + break + + def config(self): + ''' + Display the cronjobs of user privacyidea + ''' + bt = "Define rotation times." + while 1: + cronjobs = self.Audit.get_cronjobs() + choices = [("Add new rotate check date", "")] + for cronjob in cronjobs: + if cronjob.user == CRON_USER and \ + cronjob.command.startswith(AUDIT_CMD): + comment = "audit rotation" + if cronjob.minute != "*": + comment = "hourly audit rotation." + if cronjob.hour != "*": + comment = "daily audit rotation." + if cronjob.dow != "*": + comment = "weekly audit rotation." + if cronjob.dom != "*": + comment = "monthly audit rotation." + if cronjob.month != "*": + comment = "yearly audit rotation." + choices.append(("%s %s %s %s %s" % (cronjob.minute, + cronjob.hour, + cronjob.dom, + cronjob.month, + cronjob.dow), + comment)) + code, tags = self.d.menu("Here you can define times, when " + "to run a audit rotation check.", + cancel='Back', + choices=choices, + backtitle=bt, + width=70) + + if code == self.d.DIALOG_OK: + if tags.startswith("Add"): + self.add() + else: + self.delete(tags) + else: + break + + def add(self): + ''' + Add an audit rotation + ''' + bt = "Add a new Audit rotation" + age = "" + watermark = "" + + code, typ = self.d.menu("You can either rotate the audit log by age " + "or by the number of log entries:", + choices=[("by age", "", + "Audit entries older than certain days will be deleted."), + ("by entries", "", + "If the number of entries exceed " + "the highwatermark, the entries " + "will be deleted to lowwatermark.")], + backtitle=bt, item_help=1) + if code != self.d.DIALOG_OK: + return + + if typ.startswith("by age"): + code, age = self.d.inputbox("Number of days how old the oldest " + "log entry should be:", + width=70, + backtitle=bt) + else: + code, watermark = self.d.inputbox("Please enter ," + ".", + width=70, + backtitle=bt) + + if code != self.d.DIALOG_OK: + return + + code, bdate = self.d.inputbox("The date to run the audit rotation. " + "Please enter it like this:\n" + " " + " \n" + "You may use '*' as wildcard entry.", + width=70, + backtitle=bt) + + if code == self.d.DIALOG_OK: + date_fragments = bdate.split() + if len(date_fragments) == 5: + pass + elif len(date_fragments) == 4: + date_fragments.append('*') + elif len(date_fragments) == 3: + date_fragments.append('*') + date_fragments.append('*') + elif len(date_fragments) == 2: + date_fragments.append('*') + date_fragments.append('*') + date_fragments.append('*') + elif len(date_fragments) == 1: + date_fragments.append('*') + date_fragments.append('*') + date_fragments.append('*') + date_fragments.append('*') + else: + return + params = {"age": age, + "watermark": watermark} + self.Audit.add_rotate(date_fragments, params) + + def delete(self, tag): + ''' + Delete the Audit rotation + ''' + bt = "Delete an audit rotation" + (minute, hour, dom, month, dow) = tag.split() + code = self.d.yesno("Do you want to delete the audit rotation " + "job at time %s:%s. " + "Month:%s, Day of Month: %s, " + "Day of week: %s?" % + (hour, minute, month, dom, dow)) + if code == self.d.DIALOG_OK: + # Delete backup job. + self.Audit.del_rotate(None, hour, minute, month, dom, dow) + + +class BackupMenu(object): + def __init__(self, app, dialog): + self.app = app + self.d = dialog + self.Backup = Backup() + + def menu(self): + bt = "Backup and Restore configuration" + self.Backup.CP.read() + while 1: + code, tags = self.d.menu("Backup and Restore", + choices=[("Configure backup", ""), + ("Backup now", ""), + ("View Backups", "")], + cancel='Back', + backtitle=bt) + if code == self.d.DIALOG_OK: + if tags.startswith("Configure"): + self.config() + elif tags.startswith("Backup"): + self.now() + elif tags.startswith("View Backup"): + self.view() + else: + break + + def config(self): + ''' + Display the cronjobs of user privacyidea + ''' + bt = "Define backup times" + while 1: + cronjobs = self.Backup.get_cronjobs() + choices = [("Add new backup date", "")] + for cronjob in cronjobs: + if cronjob.user == CRON_USER and \ + cronjob.command.startswith(BACKUP_CMD): + comment = "backup job." + if cronjob.minute != "*": + comment = "hourly backup job." + if cronjob.hour != "*": + comment = "daily backup job." + if cronjob.dow != "*": + comment = "weekly backup job." + if cronjob.dom != "*": + comment = "monthly backup job." + if cronjob.month != "*": + comment = "yearly backup job." + choices.append(("%s %s %s %s %s" % (cronjob.minute, + cronjob.hour, + cronjob.dom, + cronjob.month, + cronjob.dow), + comment)) + code, tags = self.d.menu("Here you can define times, when " + "to run a backup.", + cancel='Back', + choices=choices, + backtitle=bt) + + if code == self.d.DIALOG_OK: + if tags.startswith("Add"): + self.add() + else: + self.delete(tags) + else: + break + pass + + def add(self): + ''' + Add a backup date. + ''' + bt = "Add a new backup date" + code, bdate = self.d.inputbox("The date to run the backup. " + "Please enter it like this:\n" + " " + " \n" + "You may use '*' as wildcard entry.\n" + "Please note that the backup will not contain the encryption key.", + width=70, + backtitle=bt) + + if code == self.d.DIALOG_OK: + date_fragments = bdate.split() + if len(date_fragments) == 5: + pass + elif len(date_fragments) == 4: + date_fragments.append('*') + elif len(date_fragments) == 3: + date_fragments.append('*') + date_fragments.append('*') + elif len(date_fragments) == 2: + date_fragments.append('*') + date_fragments.append('*') + date_fragments.append('*') + elif len(date_fragments) == 1: + date_fragments.append('*') + date_fragments.append('*') + date_fragments.append('*') + date_fragments.append('*') + else: + return + self.Backup.add_backup_time(date_fragments) + + def delete(self, tag): + ''' + Delete a backup date + ''' + bt = "Delete a backup date" + (minute, hour, dom, month, dow) = tag.split() + code = self.d.yesno("Do you want to delete the backup " + "job at time %s:%s. " + "Month:%s, Day of Month: %s, " + "Day of week: %s?" % + (hour, minute, month, dom, dow)) + if code == self.d.DIALOG_OK: + # Delete backup job. + self.Backup.del_backup_time(hour, minute, month, dom, dow) + + def restore(self, tag): + ''' + Restore a backup + ''' + bt = "Restore a backup" + code = self.d.yesno("Are you sure you want to restore the backup %s? " + "Current data will be lost. The restore will " + "administrator settings, token database, audit log, RADIUS " + "clients, server certificates... " + "If unsure, please " + "perform a backup before restoring the old one." + % tag, + width=70) + if code == self.d.DIALOG_OK: + # Restore the backup + self.d.gauge_start("Restoring backup %s" % tag, percent=20) + success, stdout, stderr = self.Backup.restore_backup(tag) + self.d.gauge_update(percent=90) + time.sleep(1) + self.d.gauge_stop() + if success: + self.d.scrollbox(u"Backup successfully restored!\n\n{}".format(stdout)) + mark_service_for_restart(SERVICE_APACHE) + else: + text = u""" +Restore failed: + +{} +""".format(stderr) + self.d.scrollbox(text) + + def now(self): + ''' + Run the backup now. + ''' + success, stdout, stderr = self.Backup.backup_now() + if success: + self.d.msgbox(u"Backup successfully created! " + u"Please note that it does not contain the encryption key.") + else: + text = u""" +Backup failed: + +{} +""".format(stderr) + self.d.scrollbox(text) + + def view(self): + ''' + View the saved backup files to restore one. + ''' + bt = "Restore a backup" + while 1: + backups = self.Backup.get_backups() + choices = [] + for bfile in sorted(backups.keys(), reverse=True): + choices.append((bfile, "%s %s" % (backups[bfile].get("size"), + backups[bfile].get("time")))) + if len(choices) == 0: + self.d.msgbox("No backups found!") + break + else: + code, tags = self.d.menu("Choose a backup you wish to " + "restore...", + choices=choices, + backtitle=bt, + width=78) + if code == self.d.DIALOG_OK: + self.restore(tags) + else: + break + + +class RadiusMenu(object): + def __init__(self, app, dialog): + self.app = app + self.d = dialog + try: + self.RadiusConfig = FreeRADIUSConfig() + except: + # No Radius Server available + self.RadiusConfig = None + + def menu(self): + while 1: + code, tags = self.d.menu("Configure FreeRADIUS", + choices=[("client config", ""), + ("sites", + "Enable and disable RADIUS " + "sites") + ], + cancel='Back') + if code == self.d.DIALOG_OK: + if tags.startswith("client"): + self.clients() + if tags.startswith("sites"): + self.sites() + else: + break + + def sites(self): + sites = self.RadiusConfig.get_sites() + code, tags = self.d.checklist("The FreeRADIUS sites you want to " + "enable. You should only enable " + "'privacyidea' unless you know " + "exactly what you are doing!", + choices=sites, + backtitle="Enable sites") + if code == self.d.DIALOG_OK: + self.RadiusConfig.enable_sites(tags) + mark_service_for_restart(SERVICE_FREERADIUS) + + def clients(self): + while 1: + clients = [("Add new client", "Add a new RADIUS client")] + clients_from_file = self.RadiusConfig.clients_get() + for client, v in clients_from_file.items(): + clients.append((client, "%s/%s (%s)" % (v.get("ipaddr"), + v.get("netmask"), + v.get("shortname")))) + code, tags = self.d.menu("You can select an existing RADIUS client " + "to either delete it or change it " + "or create a new client", + choices=clients, + cancel='Back', + backtitle="Manage RADIUS clients") + + if code == self.d.DIALOG_OK: + if tags.startswith("Add new"): + self.add() + else: + self.manage(tags) + else: + break + + def add(self): + bt = "Add a new RADIUS client" + code, clientname = self.d.inputbox("The name of the new client", + backtitle=bt) + if code != self.d.DIALOG_OK: + return + code, ip = self.d.inputbox("The IP address of the new client %s" % + clientname, + backtitle=bt) + if code != self.d.DIALOG_OK: + return + + code, netmask = self.d.radiolist("The netmask of the new client %s." % clientname, + choices=[("32", "255.255.255.255 (" + "single Host)", 0), + ("24", "255.255.255.0", 1), + ("16", "255.255.0.0", 0), + ("8", "255.0.0.0", 0), + ("0", "0.0.0.0 (" + "everything)", 0), + ("1", "128.0.0.0", 0), + ("2", "192.0.0.0", 0), + ("3", "224.0.0.0", 0), + ("4", "240.0.0.0", 0), + ("5", "248.0.0.0", 0), + ("6", "252.0.0.0", 0), + ("7", "254.0.0.0", 0), + ("9", "255.128.0.0", 0), + ("10", "255.192.0.0", 0), + ("11", "255.224.0.0", 0), + ("12", "255.240.0.0", 0), + ("13", "255.248.0.0", 0), + ("14", "255.252.0.0", 0), + ("15", "255.254.0.0", 0), + ("17", "255.255.128.0", 0), + ("18", "255.255.192.0", 0), + ("19", "255.255.224.0", 0), + ("20", "255.255.240.0", 0), + ("21", "255.255.248.0", 0), + ("22", "255.255.252.0", 0), + ("23", "255.255.254.0", 0), + ("25", "255.255.255.128", 0), + ("26", "255.255.255.192", 0), + ("27", "255.255.255.224", 0), + ("28", "255.255.255.240", 0), + ("29", "255.255.255.248", 0), + ("30", "255.255.255.252", 0), + ("31", "255.255.255.254", 0) + ], + backtitle=bt) + if code != self.d.DIALOG_OK: + return + + code, secret = self.d.inputbox("The secret of the new client %s" % + clientname, + backtitle=bt) + + code, shortname = self.d.inputbox("The shortname of the new client %s" % + clientname, + backtitle=bt) + + if code == self.d.DIALOG_OK: + client = {} + if ip: + client["ipaddr"] = ip + if netmask: + client["netmask"] = netmask + if secret: + client["secret"] = secret + if shortname: + client["shortname"] = shortname + self.RadiusConfig.client_add({clientname: client}) + mark_service_for_restart(SERVICE_FREERADIUS) + + def manage(self, clientname): + bt = "Manage client %s" % clientname + code, tags = self.d.menu("Manage client %s." % clientname, + choices=[("Delete client", "")], + backtitle=bt) + if code == self.d.DIALOG_OK: + if tags.startswith("Delete"): + code = self.d.yesno("Do you really want to delete the " + "RADIUS client %s?" % clientname) + if code == self.d.DIALOG_OK: + self.RadiusConfig.client_delete(clientname) + mark_service_for_restart(SERVICE_FREERADIUS) + + +class MainMenu(object): + def __init__(self, config=None): + if config: + self.config_file = config + else: + self.config_file = DEFAULT_CONFIG + + try: + self.pConfig = PrivacyIDEAConfig(self.config_file) + except IOError: + sys.stderr.write("=" * 75) + sys.stderr.write("\nCan not access {0!s}. You need to have read " + "and write access to this " + "file.\n".format(self.config_file)) + sys.exit(5) + + self.app = create_app(config_name="production") + self.d = Dialog(dialog="dialog") + self.radiusDialog = RadiusMenu(self.app, self.d) + self.backupDialog = BackupMenu(self.app, self.d) + self.dbDialog = DBMenu(self.app, self.d, self.pConfig) + self.webserverDialog = WebserverMenu(self.app, self.d) + self.auditDialog = AuditMenu(self.app, self.d) + + def restart_services_if_needed(self): + if services_for_restart: + code = self.d.yesno("Do you want to restart the services for the " + "changes to take effect?") + if code == self.d.DIALOG_OK: + for service in services_for_restart: + OSConfig.restart(service, True) + reset_services_for_restart() + + def main_menu(self): + choices = [("privacyIDEA", "", + "Configure privacyIDEA application " + "stuff like administrators.") + ] + if self.radiusDialog.RadiusConfig: + choices.append(("FreeRADIUS", "", + "Configure RADIUS settings like " + "the RADIUS clients.")) + choices.append(("Database", "", + "Configure database and setup redundancy")) + choices.append(("Webserver", "", + "Restart Webserver")) + choices.append(("Backup and Restore", "", + "Backup or Restore of privacyIDEA " + "configuration and database.")) + choices.append(("Audit Rotation", "", + "Define times when to check if the Audit log should " + "be rotated.")) + + while 1: + code, tags = self.d.menu("Which subject do you want to configure?", + choices=choices, + backtitle="privacyIDEA configuration", + cancel="Exit", + item_help=1) + if code == self.d.DIALOG_OK: + print tags + if tags == "privacyIDEA": + self.privacyidea_menu() + elif tags == "FreeRADIUS": + self.radiusDialog.menu() + elif tags.startswith("Backup"): + self.backupDialog.menu() + elif tags.startswith("Webserver"): + self.webserverDialog.menu() + elif tags.startswith("Database"): + self.dbDialog.menu() + elif tags.startswith("Audit"): + self.auditDialog.menu() + + else: + # End + self.restart_services_if_needed() + break + + def privacyidea_menu(self): + while 1: + code, tags = self.d.menu( + "Configure privacyidea", + choices=[("view config", "Display configuration.", ""), + ("loglevel", "Change log level.", ""), + ("admin realms", "Modify admin realms.", ""), + ("manage local admins", "Modify admins.", ""), + ("Danger zone!", "Enter at your own risk!", + "Here you may recreated your " + "encryption and signing keys.")], + menu_height=22, + cancel='Back', + backtitle="privacyIDEA configuration", + item_help=1) + + if code == self.d.DIALOG_OK: + if tags.startswith("loglevel"): + self.privacyidea_loglevel() + elif tags.startswith("view"): + self.privacyidea_view() + elif tags.startswith("admin realms"): + self.privacyidea_adminrealms() + elif tags.startswith("manage local admins"): + self.privacyidea_admins() + elif tags.startswith("Danger zone"): + self.privacyidea_danger_menu() + else: + break + + def privacyidea_danger_menu(self): + while 1: + code, tags = self.d.menu( + "privacyIDEA Danger Zone", + choices=[("initialize pi.cfg-file", + "Create new pi.cfg-file.", + "This will also create new salt and pepper. Admins " + "will not be able to login anymore!"), + ("encryption key", "Create new encryption key.", + "Token seeds can not be decrypted anymore!"), + ("signing key", "Create new audit signing key.", + "Old audit entries can not be verified anymore.")], + menu_height=22, + cancel='Back', + backtitle="privacyIDEA Danger Zone", + item_help=1) + + if code == self.d.DIALOG_OK: + if tags.startswith("initialize"): + self.privacyidea_initialize() + elif tags.startswith("encryption"): + self.privacyidea_enckey() + elif tags.startswith("signing"): + self.privacyidea_sign() + else: + break + + def privacyidea_admins(self): + while 1: + with self.app.app_context(): + db_admins = get_db_admins() + admins = [("Add new admin", "Add a new administrator")] + for admin in db_admins: + admins.append((admin.username, admin.email or "")) + code, tags = self.d.menu("You can select an existing administrator " + "to either delete it or change the " + "password or create a new admin", + choices=admins, + cancel='Back', + backtitle="Manage administrators") + + if code == self.d.DIALOG_OK: + if tags == "Add new admin": + self.privacyidea_admin_add() + else: + self.privacyidea_admin_manage(tags) + else: + break + + def privacyidea_admin_manage(self, admin_name): + bt = "Manage administrator" + code, tags = self.d.menu("Manage admin %s" % admin_name, + choices=[("Delete admin", ""), + ("Change password", "")], + backtitle=bt) + if code == self.d.DIALOG_OK: + if tags.startswith("Delete"): + code = self.d.yesno("Do you really want to delete the " + "administrator %s?" % admin_name) + if code == self.d.DIALOG_OK: + with self.app.app_context(): + delete_db_admin(admin_name) + + if tags.startswith("Change password"): + password = self.privacyidea_admin_password(admin_name) + pass + + def privacyidea_admin_password(self, admin_name, create=False): + bt = "Setting password for administrator %s" % admin_name + password = None + while 1: + code, password1 = self.d.passwordbox("Enter the password for the " + "administrator %s.\n" + "(Your typing will not be " + "visible)" % + admin_name, + backtitle=bt) + + if code == self.d.DIALOG_OK: + code, password2 = self.d.passwordbox("Repeat the password", + backtitle=bt) + if code == self.d.DIALOG_OK: + if password1 != password2: + self.d.msgbox("The passwords do not match. " + "Please try again.") + else: + password = password1 + with self.app.app_context(): + create_db_admin(self.app, admin_name, + password=password) + break + else: + break + else: + break + return password + + def privacyidea_admin_add(self): + bt = "Add a new administrator" + code, admin_name = self.d.inputbox("The username of the new " + "administrator", + backtitle=bt) + + if code == self.d.DIALOG_OK: + password = self.privacyidea_admin_password(admin_name, + create=True) + + def privacyidea_adminrealms(self): + adminrealms = self.pConfig.get_superusers() + # convert to string + adminrealms = ",".join(adminrealms) + code, tags = self.d.inputbox("You may enter a comma separated list " + "of realms that are recognized as " + "admin realms.", + init=adminrealms, + width=40, + backtitle="configure admin realms") + if code == self.d.DIALOG_OK: + # convert to list with no whitespaces in elemtents + adminrealms = [x.strip() for x in tags.split(",")] + self.pConfig.set_superusers(adminrealms) + self.pConfig.save() + mark_service_for_restart(SERVICE_APACHE) + + def privacyidea_initialize(self): + code = self.d.yesno("Do you want to initialize " + "the config file? Old privacyIDEA " + "configurations will be overwritten!", + backtitle="Initialize privacyIDEA configuration", + defaultno=1) + if code == self.d.DIALOG_OK: + self.pConfig.initialize() + self.pConfig.save() + mark_service_for_restart(SERVICE_APACHE) + + def privacyidea_enckey(self): + code = self.d.yesno("Do you want to create a new encryption key? " + "All token keys will not be readable anymore!", + backtitle="Create a new encryption key.", + defaultno=1) + if code == self.d.DIALOG_OK: + r, f = self.pConfig.create_encryption_key() + if r: + self.d.msgbox("Successfully created new encryption key %s." % + f) + else: + self.d.msgbox("Failed to create new encryption key %s!" % + f) + + def privacyidea_sign(self): + code = self.d.yesno("Do you want to create a new audit trail " + "signing key? " + "Older audit entries can not be verified anymore.", + backtitle="Create a new signing key.", + defaultno=1) + if code == self.d.DIALOG_OK: + r, f = self.pConfig.create_audit_keys() + if r: + self.d.msgbox("Successfully created new audit keys %s." % + f) + else: + self.d.msgbox("Failed to create new audit keys %s!" % f) + + def privacyidea_loglevel(self): + loglevel = self.pConfig.get_loglevel() + code, tags = self.d.radiolist( + "choose a loglevel", + choices=[("logging.DEBUG", "Excessive logging.", + int(loglevel == "logging.DEBUG")), + ("logging.INFO", "Normal logging.", + int(loglevel == "logging.INFO")), + ("logging.WARN", "Only log warnings.", + int(loglevel == "logging.WARN")), + ("logging.ERROR", "Sparse logging.", + int(loglevel == "logging.ERROR"))], + backtitle="privacyIDEA loglevel.") + if code == self.d.DIALOG_OK: + self.pConfig.set_loglevel(tags) + self.pConfig.save() + mark_service_for_restart(SERVICE_APACHE) + + def privacyidea_view(self): + text = u""" + The secret key file : %s + List of the admin realms : %s + Loglevel : %s + """ % (self.pConfig.get_keyfile(), + self.pConfig.get_superusers(), + self.pConfig.get_loglevel()) + self.d.scrollbox(text) + + +def create_arguments(): + parser = argparse.ArgumentParser(description=DESCRIPTION, + fromfile_prefix_chars='@') + parser.add_argument("-f", "--file", + help="The pi.cfg file.", + required=False) + parser.add_argument("-v", "--version", + help="Print the version of the program.", + action='version', version='%(prog)s ' + VERSION) + + args = parser.parse_args() + return args + + +def main(): + locale.setlocale(locale.LC_ALL, '') + args = create_arguments() + pS = MainMenu(config=args.file) + pS.main_menu() + + + diff --git a/authappliance/pi-appliance b/authappliance/pi-appliance index 0cbabbb..a8876c2 100755 --- a/authappliance/pi-appliance +++ b/authappliance/pi-appliance @@ -15,1416 +15,8 @@ # # You should have received a copy of the GNU Affero General Public # License along with this program. If not, see . -""" -text dialog based setup tool to configure the privacyIDEA basics. -""" - -import locale -import argparse -import sys -import time -from dialog import Dialog -from authappliance.lib.appliance import (Backup, Audit, FreeRADIUSConfig, - ApacheConfig, - PrivacyIDEAConfig, - OSConfig, MySQLConfig, - DEFAULT_CONFIG, - CRON_USER, AUDIT_CMD, BACKUP_CMD, RemoteMySQLConfig, SERVICE_APACHE, - SERVICE_FREERADIUS) -from privacyidea.lib.auth import (create_db_admin, get_db_admins, - delete_db_admin) -from privacyidea.models import Admin -from privacyidea.app import create_app -from netaddr import IPAddress -from paramiko.client import SSHClient -from paramiko import SSHException, AutoAddPolicy, SFTPClient, Transport -from subprocess import Popen, PIPE -import random -import os -from tempfile import NamedTemporaryFile - - - -DESCRIPTION = __doc__ -VERSION = "2.0" - - -#: This holds the services that should be restarted -#: at the end of the session. It should be modified -#: using mark_service_for_restart and -#: reset_services_for_restart. -services_for_restart = set() - -def mark_service_for_restart(service): - """ - Add ``service`` to set of services that should be restarted. - """ - services_for_restart.add(service) - -def reset_services_for_restart(): - """ - Clear the set of services that should be restarted. - """ - services_for_restart.clear() - - -class WebserverMenu(object): - - def __init__(self, app, dialog): - self.app = app - self.os = OSConfig() - self.d = dialog - self.apache = ApacheConfig() - - def menu(self): - bt = "Webservice configuration" - while 1: - code, tags = self.d.menu("Configure Webserver", - choices=[("restart services", ""), - ("-" * 40, ""), - ("Generate selfsigned " - "certificate", ""), - ("-" * 40, ""), - ("Generate Certificate " - "Signing Request", ""), - ("Import certificate", ""), - ("-" * 40, ""), - ("Regenerate private key", "") - ], - cancel='Back', - backtitle=bt) - if code == self.d.DIALOG_OK: - if tags.startswith("restart"): - self.restart() - if tags.startswith("Regenerate private"): - self.generate_private_key() - if tags.startswith("Generate self"): - self.generate_self() - if tags.startswith("Generate Certificate"): - self.generate_csr() - if tags.startswith("Import"): - self.import_certificate() - else: - break - - def generate_self(self): - code = self.d.yesno("Do you want to recreate the self signed " - "certificate?") - if code == self.d.DIALOG_OK: - self.apache.create_self_signed() - mark_service_for_restart(SERVICE_APACHE) - - def generate_private_key(self): - code, tags = self.d.menu("Regenerate private key. " - "When regenerating the private key you also " - "need a new certificate! " - "Do not regenerate the private key while " - "you are waiting for a certificate signing " - "request to be signed. In this case " - "the certificate will not match the " - "private key anymore.", - choices=[("2048", "2048 bit"), - ("4096", "4096 bit"), - ("8192", "8192 bit")], - backtitle="Regenerate private key") - if code == self.d.DIALOG_OK: - self.apache.create_private_key(tags) - mark_service_for_restart(SERVICE_APACHE) - - def generate_csr(self): - csr = self.apache.generate_csr() - code = self.d.msgbox("The certificate signing request is written " - "to {0}. \nCopy this file and pass it to your " - "certificate authority for getting it signed. \n" - "Do not regenerate the private " - "key after you have sent the CSR to your " - "certificate authority.".format(csr), - width=70, height=10) - - def import_certificate(self): - cert, keyfile = self.apache.get_certificates() - homedir = os.getenv("HOME") - code = self.d.msgbox("You received a signed certificate from a " - "certificate authority. Copy this file to the " - "directory \n{0}.\n" - "In the following dialog you may choose, " - "which certificate you want to use for your " - "webserver.".format(homedir), - width=70, height=10) - - bt = "Import certificate (.pem, .crt, .der, .cer)" - files = self.apache.get_imports(homedir) - choices = [] - for bfile in sorted(files.keys()): - choices.append((bfile, "%s" % (files[bfile].get("time")))) - if len(choices) == 0: - self.d.msgbox("No certificate found!") - else: - code, tags = self.d.menu("Choose the certificate you want to " - "use for the webserver.", - choices=choices, - backtitle=bt, - width=78) - if code == self.d.DIALOG_OK: - self.apache.import_cert(homedir + "/" + tags, cert) - mark_service_for_restart(SERVICE_APACHE) - - def restart(self): - code = self.d.yesno("Do you want to restart the services for the " - "changes to take effect?") - if code == self.d.DIALOG_OK: - self.os.restart(SERVICE_APACHE) - -class Peer(object): - - files = ["/etc/privacyidea/enckey", "/etc/privacyidea/logging.cfg", - "/etc/privacyidea/private.pem", "/etc/privacyidea/public.pem"] - - def __init__(self, dialog, pConfig, dbConfig, remote_ip=None, - password=None, local_ip=None): - self.remote_ip = remote_ip - self.local_ip = local_ip - self.password = password - self.d = dialog - self.pConfig = pConfig - self.dbConfig = dbConfig - self.ssh = SSHClient() - self.os = OSConfig() - self.info = "" - self.file_local = "" - self.file_remote = "" - self.position_local = "" - self.position_remote = "" - - def get_peer_data(self): - bt = "Add another SQL Master" - code, ip = self.d.inputbox( - "Enter the IP Address of the other privacyIDEA server.", - backtitle=bt) - if code != self.d.DIALOG_OK: - return False - - try: - self.remote_ip = IPAddress(ip) - except: - self.d.msgbox("Invalid IP address") - return False - - code, password = self.d.passwordbox( - "Enter the root password of the other machine. Please NOTE: " - "On an Ubuntu machine Root SSH login with password is forbidden. " - "You need to change 'PermitRootLogin' in the " - "/etc/ssh/sshd_config. Otherwise authentication will fail.", - insecure=True, backtitle=bt) - if code != self.d.DIALOG_OK: - return False - - self.password = password - - code, local_ip = self.d.inputbox( - "Enter the local IP Address of this machine, to which the remote " - "server will connect.", backtitle=bt) - if code != self.d.DIALOG_OK: - return False - - try: - self.local_ip = IPAddress(local_ip) - except: - self.d.msgbox("Invalid IP address") - return False - - return True - - def add_info(self, new_info): - self.info += new_info + "\n" - self.d.infobox(self.info, height=20, width=60) - - def stop_redundancy(self): - self.info = "" - self.add_info("Stopping local webserver") - self.os.restart(service="apache2", action="stop") - - # To stop redundancy we remove - # several fields from the my.cnf file. - self.add_info("Removing config from my.cnf...") - self.dbConfig.set("mysqld", "bind-address", "127.0.0.1") - for setting in ["server-id", "auto_increment_increment", - "auto_increment_offset", "log_bin", "binlog_do_db"]: - self.dbConfig.delete("mysqld", setting) - - # delete the replication user - self.add_info("Dropping replicator user...") - _, err = self._execute_local_sql("drop user 'replicator'@'%';") - if err: - self.add_info("Error dropping replicator user!") - - # stop the slave - self.add_info("Stopping the slave...") - _, err = self._execute_local_sql("stop slave; reset slave;") - if err: - self.add_info("Error stopping slave: {0}".format(err)) - # restart mysql - self.add_info("Restarting MySQL server...") - self.dbConfig.restart() - self.add_info("Starting local webserver...") - self.os.restart(service="apache2", action="start") - self.add_info("done.") - self.d.scrollbox(self.info.decode('utf-8'), height=20, width=60) - - def _execute_local_sql(self, sql): - p = Popen(['mysql', '--defaults-extra-file=/etc/mysql/debian.cnf'], - stdin=PIPE, stdout=PIPE, stderr=PIPE) - output, err = p.communicate(sql) - if err: - self.add_info("====== ERROR =======") - self.add_info(err) - return output, err - - def _escape_for_shell(self, argument): - return "'{}'".format(argument.replace("'", r"'\''")) - - def _execute_remote_sql(self, sql): - assert '"' not in sql - stdin, stdout, stderr = self.ssh.exec_command('echo {} | mysql --defaults-extra-file=/etc/mysql/debian.cnf'.format( - self._escape_for_shell(sql))) - err = stderr.read() - if err: - self.add_info("====== ERROR =======") - self.add_info(err) - output = stdout.read() - return output, err - - def setup_redundancy(self): - # - # Copy files in /etc/privacyidea - # - self.info = "" - self.add_info("copying files to remote server...") - transport = Transport((str(self.remote_ip), 22)) - transport.connect(username="root", password=self.password) - sftp = SFTPClient.from_transport(transport) - for file in self.files: - if os.path.exists(file): - sftp.put(file, file) - - # adjust remote pi.cfg - remote_config = PrivacyIDEAConfig(DEFAULT_CONFIG, opener=sftp.file) - local_config = PrivacyIDEAConfig(DEFAULT_CONFIG) - - new_remote_config = local_config.config.copy() - # copy the local pi.cfg to the remote config except the - # keys listed in ``save_remote_config_keys``. - save_remote_config_keys = ['SQLALCHEMY_DATABASE_URI'] - for key in save_remote_config_keys: - new_remote_config[key] = remote_config.config[key] - remote_config.config = new_remote_config - remote_config.save() - - # - # Setup my.cnf, locally and remotely - # - shared_my_cnf_values = {"auto_increment_increment": 2, - "log_bin": "/var/log/mysql/mysql-bin.log", - "binlog_do_db": "pi", - "bind-address": "0.0.0.0" - } - remote_my_cnf_values = {"server-id": 2, - "auto_increment_offset": 2} - local_my_cnf_values = {"server-id": 1, - "auto_increment_offset": 1} - - self.add_info("Setup my.cnf on local server...") - - for key, value in shared_my_cnf_values.items() + local_my_cnf_values.items(): - self.dbConfig.set('mysqld', key, value) - - self.add_info("Setup my.cnf on remote server...") - - remote_my_cnf = RemoteMySQLConfig(sftp) - for key, value in shared_my_cnf_values.items() + remote_my_cnf_values.items(): - remote_my_cnf.set('mysqld', key, value) - - sftp.close() - transport.close() - - # - # Restart services - # - self.add_info("Restarting local MySQL server...") - self.dbConfig.restart() - self.add_info("Restarting remote MySQL server...") - self.ssh.connect(str(self.remote_ip), username="root", - password=self.password) - stdin, stdout, stderr = self.ssh.exec_command("service mysql restart") - if stderr: - self.add_info(stderr.read()) - # - # Configuring mysql - # - # Before configuring mysql we stop the webserver - self.add_info("Stopping local webserver") - self.os.restart(service="apache2", action="stop") - self.add_info("Stopping remote webserver") - stdin, stdout, stderr = self.ssh.exec_command("service apache2 stop") - if stderr: - self.add_info(stderr.read()) - - - self.add_info("Configuring MySQL on local server...") - # We start at 40, since 39 is "'" which might lead to confusion. - # Create a random password of random length - replicator_password = "".join([random.choice( - "01234567890abcdefghijklmnopqrstuvwxyzABZDEFGHIJKLMNOPQRSTUVWXYZ") - for x in - range(random.randrange(21,31))]) - - # Add the replication users on both machines - self.add_info("Drop and add replication user on local machine...") - self._execute_local_sql("drop user if exists 'replicator'@'%';") - self._execute_local_sql("""create user 'replicator'@'%' identified by '{}'; - grant replication slave on *.* to 'replicator'@'%';""".format(replicator_password)) - - self.add_info("Drop and add replication user on remote machine...") - # Drop user - _ ,err = self._execute_remote_sql("drop user if exists 'replicator'@'%'") - self._execute_remote_sql("""create user 'replicator'@'%' identified by '{}'; - grant replication slave on *.* to 'replicator'@'%';""".format(replicator_password)) - # - # dump it and copy it to the other server - # - self.add_info("Dumping and copying the existing database...") - - dumpfile = NamedTemporaryFile(mode="w", delete=False) - p = Popen(["mysqldump", "--defaults-extra-file=/etc/mysql/debian.cnf", "--databases", "pi"], # TODO: Need explicit password here? - stdout=dumpfile, stderr=PIPE) - output, err = p.communicate() - r = p.wait() - if r == 0: - self.add_info("Saved SQL dump to {0}".format(dumpfile.name)) - # copy the file.name to the remote machine and run the file. - self.add_info("Copying to remote server and creating remote " - "database. This may take a while...") - transport = Transport((str(self.remote_ip), 22)) - transport.connect(username="root", password=self.password) - sftp = SFTPClient.from_transport(transport) - sftp.put(dumpfile.name, dumpfile.name) - sftp.close() - transport.close() - # delete the file - os.unlink(dumpfile.name) - # run the file remotely - # mysql -u root -p < test.sql - stdin, stdout, stderr = self.ssh.exec_command( - "cat {dumpfile} | mysql --defaults-extra-file=/etc/mysql/debian.cnf".format(dumpfile=dumpfile.name)) - err = stderr.read() - if err: - self.add_info("ERROR: {0}".format(err)) - else: - self.add_info("Dumped SQL database on remote server") - # delete remote file - self.ssh.exec_command( - "rm -f {dumpfile}".format(dumpfile=dumpfile.name)) - else: - self.add_info("ERROR: {0}".format(err)) - - # - # Get the position on server1 - # - output, err = self._execute_local_sql("show master status;") - r = p.returncode - if r == 0: - self.add_info(output) - for line in output.split("\n"): - elems = line.split() - if len(elems) > 2 and elems[2] == "pi": - self.file_local = elems[0] - self.position_local = elems[1] - self.add_info("Local File: {0}".format(self.file_local)) - self.add_info("Local Position: {0}".format( - self.position_local)) - - self.add_info("Configuring MySQL on remote server...") - # clean up the old database - output, _ = self._execute_remote_sql('show master status;') - self.add_info(output) - for line in output.split("\n"): - elems = line.split() - if len(elems) > 2 and elems[2] == "pi": - self.file_remote = elems[0] - self.position_remote = elems[1] - self.add_info("Remote File: {0}".format(self.file_remote)) - self.add_info("Remote Position: {0}".format( - self.position_remote)) - - - - # create everything remote - self.add_info("Add replication on remote server...") - self._execute_remote_sql(""" - stop slave; - CHANGE MASTER TO MASTER_HOST = '{local_ip}', MASTER_USER = 'replicator', - MASTER_PASSWORD = '{replicator_password}', MASTER_LOG_FILE = '{local_file}', MASTER_LOG_POS = {local_position}; - start slave;""".format( - replicator_password=replicator_password, - local_file=self.file_local, - local_position=self.position_local, - local_ip=self.local_ip)) - - # - # Configure replication on LOCAL host - # - self.add_info("Add replication on local server...") - self._execute_local_sql(""" - stop slave; - CHANGE MASTER TO MASTER_HOST = '{remote_ip}', MASTER_USER = 'replicator', - MASTER_PASSWORD = '{replicator_password}', MASTER_LOG_FILE = '{remote_file}', MASTER_LOG_POS = {remote_position}; - start slave;""".format( - replicator_password=replicator_password, - remote_ip=self.remote_ip, - remote_file=self.file_remote, - remote_position=self.position_remote)) - - self.add_info("Starting local webserver") - self.os.restart(service="apache2", action="start") - self.add_info("Starting remote webserver") - self.ssh.exec_command("service apache2 start") - self.add_info("\nRedundant setup complete.") - - self.d.scrollbox(self.info.decode('utf-8'), height=20, width=60) - - - -class DBMenu(object): - - def __init__(self, app, dialog, pConfig): - self.app = app - self.d = dialog - self.pConfig = pConfig - self.db = MySQLConfig() - self.peer = Peer(self.d, self.pConfig, self.db) - - def menu(self): - bt = "Configure the database connection" - while 1: - current_config = self.pConfig.get_DB() - code, tags = self.d.menu( - "The current database configuration string is %s" % current_config, - choices=[("init tables", "create missing tables"), - ("view redundancy", ""), - ("setup redundancy", "master master replication"), - ("stop redundancy", "revert to single database")], - cancel='Back', - backtitle=bt) - if code == self.d.DIALOG_OK: - if tags.startswith("init"): - self.db_init() - elif tags.startswith("view"): - self.redundancy_status() - elif tags.startswith("stop redundancy"): - code = self.d.yesno( - "Do you really want to stop the redundancy? This " - "server will be reverted to a single master. The " - "other master will not be touched. You can simply " - "shut down the other machine.", width=60, height=10 - ) - if code == self.d.DIALOG_OK: - self.peer.stop_redundancy() - elif tags.startswith("setup"): - if self.peer.get_peer_data(): - # Now we need to check, if the remote machine is - # running privacyIDEA and MySQL. - try: - self.peer.ssh.set_missing_host_key_policy( - AutoAddPolicy()) - self.peer.ssh.connect(str(self.peer.remote_ip), - username="root", - password=self.peer.password) - stdin, stdout, stderr = self.peer.ssh.exec_command( - 'dpkg -l privacyidea-apache2') - output_pi = stdout.read() - error_pi = stderr.read() - - stdin, stdout, stderr = self.peer.ssh.exec_command( - 'dpkg -l mysql-server') - output_mysql = stdout.read() - error_mysql = stderr.read() - self.peer.ssh.close() - if not output_mysql: - self.d.msgbox( - "MySQL server not installed on {0!s}. " - "Please install mysql-server.".format( - self.peer.remote_ip)) - break - if not output_pi: - self.d.msgbox( - "privacyIDEA not installed on " - "{0!s}. Please install " - "privacyidea-apache2.".format( - self.peer.remote_ip)) - break - except SSHException as exx: - self.d.msgbox("{0!s}".format(exx)) - break - - code = self.d.yesno( - "OK. privacyIDEA and MySQL is installed on the " - "remote server. We are ready to setup redundancy. " - "Data will be cloned to the remote server. All " - "privacyIDEA data on the remote server will be " - "lost. Shall we proceed?", width=60) - if code != self.d.DIALOG_OK: - break - else: - self.peer.setup_redundancy() - - else: - break - - def redundancy_status(self): - r, bind, server_id = self.db.is_redundant() - self.d.scrollbox( -u""" -Master-Master replication active: {active!s} -Server ID: {server_id!s} -Bind Address: {bind_address!s} -""".format(active=r, bind_address=bind, server_id=server_id), - width=60, height=20) - - def db_init(self): - db_connect = self.pConfig.get_DB() - code = self.d.yesno("Do you want to recreate the tables? " - "Existing data will not be lost. Only new tables " - "in the database scheme will be created on %s" % - db_connect, - width=70, - backtitle="Create database tables") - if code == self.d.DIALOG_OK: - r = self.pConfig.DB_init() - if r: - self.d.msgbox("Created database tables.") - else: - self.d.scrollbox("Error creating database tables.") - - -class AuditMenu(object): - - def __init__(self, app, dialog): - self.app = app - self.d = dialog - self.Audit = Audit() - - def menu(self): - bt = "Audit Log Rotation" - self.Audit.CP.read() - while 1: - code, tags = self.d.menu("Auditlog Rotate", - choices=[("Configure Audit Log", "")], - cancel='Back', - backtitle=bt) - if code == self.d.DIALOG_OK: - if tags.startswith("Configure"): - self.config() - else: - break - - def config(self): - ''' - Display the cronjobs of user privacyidea - ''' - bt = "Define rotation times." - while 1: - cronjobs = self.Audit.get_cronjobs() - choices = [("Add new rotate check date", "")] - for cronjob in cronjobs: - if cronjob.user == CRON_USER and \ - cronjob.command.startswith(AUDIT_CMD): - comment = "audit rotation" - if cronjob.minute != "*": - comment = "hourly audit rotation." - if cronjob.hour != "*": - comment = "daily audit rotation." - if cronjob.dow != "*": - comment = "weekly audit rotation." - if cronjob.dom != "*": - comment = "monthly audit rotation." - if cronjob.month != "*": - comment = "yearly audit rotation." - choices.append(("%s %s %s %s %s" % (cronjob.minute, - cronjob.hour, - cronjob.dom, - cronjob.month, - cronjob.dow), - comment)) - code, tags = self.d.menu("Here you can define times, when " - "to run a audit rotation check.", - cancel='Back', - choices=choices, - backtitle=bt, - width=70) - - if code == self.d.DIALOG_OK: - if tags.startswith("Add"): - self.add() - else: - self.delete(tags) - else: - break - - def add(self): - ''' - Add an audit rotation - ''' - bt = "Add a new Audit rotation" - age = "" - watermark = "" - - code, typ = self.d.menu("You can either rotate the audit log by age " - "or by the number of log entries:", - choices=[("by age", "", - "Audit entries older than certain days will be deleted."), - ("by entries", "", - "If the number of entries exceed " - "the highwatermark, the entries " - "will be deleted to lowwatermark.")], - backtitle=bt, item_help=1) - if code != self.d.DIALOG_OK: - return - - if typ.startswith("by age"): - code, age = self.d.inputbox("Number of days how old the oldest " - "log entry should be:", - width=70, - backtitle=bt) - else: - code, watermark = self.d.inputbox("Please enter ," - ".", - width=70, - backtitle=bt) - - if code != self.d.DIALOG_OK: - return - - code, bdate = self.d.inputbox("The date to run the audit rotation. " - "Please enter it like this:\n" - " " - " \n" - "You may use '*' as wildcard entry.", - width=70, - backtitle=bt) - - if code == self.d.DIALOG_OK: - date_fragments = bdate.split() - if len(date_fragments) == 5: - pass - elif len(date_fragments) == 4: - date_fragments.append('*') - elif len(date_fragments) == 3: - date_fragments.append('*') - date_fragments.append('*') - elif len(date_fragments) == 2: - date_fragments.append('*') - date_fragments.append('*') - date_fragments.append('*') - elif len(date_fragments) == 1: - date_fragments.append('*') - date_fragments.append('*') - date_fragments.append('*') - date_fragments.append('*') - else: - return - params = {"age": age, - "watermark": watermark} - self.Audit.add_rotate(date_fragments, params) - - def delete(self, tag): - ''' - Delete the Audit rotation - ''' - bt = "Delete an audit rotation" - (minute, hour, dom, month, dow) = tag.split() - code = self.d.yesno("Do you want to delete the audit rotation " - "job at time %s:%s. " - "Month:%s, Day of Month: %s, " - "Day of week: %s?" % - (hour, minute, month, dom, dow)) - if code == self.d.DIALOG_OK: - # Delete backup job. - self.Audit.del_rotate(None, hour, minute, month, dom, dow) - -class BackupMenu(object): - - def __init__(self, app, dialog): - self.app = app - self.d = dialog - self.Backup = Backup() - - def menu(self): - bt = "Backup and Restore configuration" - self.Backup.CP.read() - while 1: - code, tags = self.d.menu("Backup and Restore", - choices=[("Configure backup", ""), - ("Backup now", ""), - ("View Backups", "")], - cancel='Back', - backtitle=bt) - if code == self.d.DIALOG_OK: - if tags.startswith("Configure"): - self.config() - elif tags.startswith("Backup"): - self.now() - elif tags.startswith("View Backup"): - self.view() - else: - break - - def config(self): - ''' - Display the cronjobs of user privacyidea - ''' - bt = "Define backup times" - while 1: - cronjobs = self.Backup.get_cronjobs() - choices = [("Add new backup date", "")] - for cronjob in cronjobs: - if cronjob.user == CRON_USER and \ - cronjob.command.startswith(BACKUP_CMD): - comment = "backup job." - if cronjob.minute != "*": - comment = "hourly backup job." - if cronjob.hour != "*": - comment = "daily backup job." - if cronjob.dow != "*": - comment = "weekly backup job." - if cronjob.dom != "*": - comment = "monthly backup job." - if cronjob.month != "*": - comment = "yearly backup job." - choices.append(("%s %s %s %s %s" % (cronjob.minute, - cronjob.hour, - cronjob.dom, - cronjob.month, - cronjob.dow), - comment)) - code, tags = self.d.menu("Here you can define times, when " - "to run a backup.", - cancel='Back', - choices=choices, - backtitle=bt) - - if code == self.d.DIALOG_OK: - if tags.startswith("Add"): - self.add() - else: - self.delete(tags) - else: - break - pass - - def add(self): - ''' - Add a backup date. - ''' - bt = "Add a new backup date" - code, bdate = self.d.inputbox("The date to run the backup. " - "Please enter it like this:\n" - " " - " \n" - "You may use '*' as wildcard entry.\n" - "Please note that the backup will not contain the encryption key.", - width=70, - backtitle=bt) - - if code == self.d.DIALOG_OK: - date_fragments = bdate.split() - if len(date_fragments) == 5: - pass - elif len(date_fragments) == 4: - date_fragments.append('*') - elif len(date_fragments) == 3: - date_fragments.append('*') - date_fragments.append('*') - elif len(date_fragments) == 2: - date_fragments.append('*') - date_fragments.append('*') - date_fragments.append('*') - elif len(date_fragments) == 1: - date_fragments.append('*') - date_fragments.append('*') - date_fragments.append('*') - date_fragments.append('*') - else: - return - self.Backup.add_backup_time(date_fragments) - - def delete(self, tag): - ''' - Delete a backup date - ''' - bt = "Delete a backup date" - (minute, hour, dom, month, dow) = tag.split() - code = self.d.yesno("Do you want to delete the backup " - "job at time %s:%s. " - "Month:%s, Day of Month: %s, " - "Day of week: %s?" % - (hour, minute, month, dom, dow)) - if code == self.d.DIALOG_OK: - # Delete backup job. - self.Backup.del_backup_time(hour, minute, month, dom, dow) - - def restore(self, tag): - ''' - Restore a backup - ''' - bt = "Restore a backup" - code = self.d.yesno("Are you sure you want to restore the backup %s? " - "Current data will be lost. The restore will " - "administrator settings, token database, audit log, RADIUS " - "clients, server certificates... " - "If unsure, please " - "perform a backup before restoring the old one." - % tag, - width=70) - if code == self.d.DIALOG_OK: - # Restore the backup - self.d.gauge_start("Restoring backup %s" % tag, percent=20) - success, stdout, stderr = self.Backup.restore_backup(tag) - self.d.gauge_update(percent=90) - time.sleep(1) - self.d.gauge_stop() - if success: - self.d.scrollbox(u"Backup successfully restored!\n\n{}".format(stdout)) - mark_service_for_restart(SERVICE_APACHE) - else: - text = u""" -Restore failed: - -{} -""".format(stderr) - self.d.scrollbox(text) - - def now(self): - ''' - Run the backup now. - ''' - success, stdout, stderr = self.Backup.backup_now() - if success: - self.d.msgbox(u"Backup successfully created! " - u"Please note that it does not contain the encryption key.") - else: - text = u""" -Backup failed: - -{} -""".format(stderr) - self.d.scrollbox(text) - - def view(self): - ''' - View the saved backup files to restore one. - ''' - bt = "Restore a backup" - while 1: - backups = self.Backup.get_backups() - choices = [] - for bfile in sorted(backups.keys(), reverse=True): - choices.append((bfile, "%s %s" % (backups[bfile].get("size"), - backups[bfile].get("time")))) - if len(choices) == 0: - self.d.msgbox("No backups found!") - break - else: - code, tags = self.d.menu("Choose a backup you wish to " - "restore...", - choices=choices, - backtitle=bt, - width=78) - if code == self.d.DIALOG_OK: - self.restore(tags) - else: - break - - - -class RadiusMenu(object): - - def __init__(self, app, dialog): - self.app = app - self.d = dialog - try: - self.RadiusConfig = FreeRADIUSConfig() - except: - # No Radius Server available - self.RadiusConfig = None - - def menu(self): - while 1: - code, tags = self.d.menu("Configure FreeRADIUS", - choices=[("client config", ""), - ("sites", - "Enable and disable RADIUS " - "sites") - ], - cancel='Back') - if code == self.d.DIALOG_OK: - if tags.startswith("client"): - self.clients() - if tags.startswith("sites"): - self.sites() - else: - break - - def sites(self): - sites = self.RadiusConfig.get_sites() - code, tags = self.d.checklist("The FreeRADIUS sites you want to " - "enable. You should only enable " - "'privacyidea' unless you know " - "exactly what you are doing!", - choices=sites, - backtitle="Enable sites") - if code == self.d.DIALOG_OK: - self.RadiusConfig.enable_sites(tags) - mark_service_for_restart(SERVICE_FREERADIUS) - - def clients(self): - while 1: - clients = [("Add new client", "Add a new RADIUS client")] - clients_from_file = self.RadiusConfig.clients_get() - for client, v in clients_from_file.items(): - clients.append((client, "%s/%s (%s)" % (v.get("ipaddr"), - v.get("netmask"), - v.get("shortname")))) - code, tags = self.d.menu("You can select an existing RADIUS client " - "to either delete it or change it " - "or create a new client", - choices=clients, - cancel='Back', - backtitle="Manage RADIUS clients") - - if code == self.d.DIALOG_OK: - if tags.startswith("Add new"): - self.add() - else: - self.manage(tags) - else: - break - - def add(self): - bt = "Add a new RADIUS client" - code, clientname = self.d.inputbox("The name of the new client", - backtitle=bt) - if code != self.d.DIALOG_OK: - return - code, ip = self.d.inputbox("The IP address of the new client %s" % - clientname, - backtitle=bt) - if code != self.d.DIALOG_OK: - return - - code, netmask = self.d.radiolist("The netmask of the new client %s." % clientname, - choices=[("32", "255.255.255.255 (" - "single Host)", 0), - ("24", "255.255.255.0", 1), - ("16", "255.255.0.0", 0), - ("8", "255.0.0.0", 0), - ("0", "0.0.0.0 (" - "everything)", 0), - ("1", "128.0.0.0", 0), - ("2", "192.0.0.0", 0), - ("3", "224.0.0.0", 0), - ("4", "240.0.0.0", 0), - ("5", "248.0.0.0", 0), - ("6", "252.0.0.0", 0), - ("7", "254.0.0.0", 0), - ("9", "255.128.0.0", 0), - ("10", "255.192.0.0", 0), - ("11", "255.224.0.0", 0), - ("12", "255.240.0.0", 0), - ("13", "255.248.0.0", 0), - ("14", "255.252.0.0", 0), - ("15", "255.254.0.0", 0), - ("17", "255.255.128.0", 0), - ("18", "255.255.192.0", 0), - ("19", "255.255.224.0", 0), - ("20", "255.255.240.0", 0), - ("21", "255.255.248.0", 0), - ("22", "255.255.252.0", 0), - ("23", "255.255.254.0", 0), - ("25", "255.255.255.128", 0), - ("26", "255.255.255.192", 0), - ("27", "255.255.255.224", 0), - ("28", "255.255.255.240", 0), - ("29", "255.255.255.248", 0), - ("30", "255.255.255.252", 0), - ("31", "255.255.255.254", 0) - ], - backtitle=bt) - if code != self.d.DIALOG_OK: - return - - code, secret = self.d.inputbox("The secret of the new client %s" % - clientname, - backtitle=bt) - - code, shortname = self.d.inputbox("The shortname of the new client %s" % - clientname, - backtitle=bt) - - if code == self.d.DIALOG_OK: - client = {} - if ip: - client["ipaddr"] = ip - if netmask: - client["netmask"] = netmask - if secret: - client["secret"] = secret - if shortname: - client["shortname"] = shortname - self.RadiusConfig.client_add({clientname: client}) - mark_service_for_restart(SERVICE_FREERADIUS) - - def manage(self, clientname): - bt = "Manage client %s" % clientname - code, tags = self.d.menu("Manage client %s." % clientname, - choices=[("Delete client", "")], - backtitle=bt) - if code == self.d.DIALOG_OK: - if tags.startswith("Delete"): - code = self.d.yesno("Do you really want to delete the " - "RADIUS client %s?" % clientname) - if code == self.d.DIALOG_OK: - self.RadiusConfig.client_delete(clientname) - mark_service_for_restart(SERVICE_FREERADIUS) - - -class MainMenu(object): - - def __init__(self, config=None): - if config: - self.config_file = config - else: - self.config_file = DEFAULT_CONFIG - - try: - self.pConfig = PrivacyIDEAConfig(self.config_file) - except IOError: - sys.stderr.write("="*75) - sys.stderr.write("\nCan not access {0!s}. You need to have read " - "and write access to this " - "file.\n".format(self.config_file)) - sys.exit(5) - - self.app = create_app(config_name="production") - self.d = Dialog(dialog="dialog") - self.radiusDialog = RadiusMenu(self.app, self.d) - self.backupDialog = BackupMenu(self.app, self.d) - self.dbDialog = DBMenu(self.app, self.d, self.pConfig) - self.webserverDialog = WebserverMenu(self.app, self.d) - self.auditDialog = AuditMenu(self.app, self.d) - - def restart_services_if_needed(self): - if services_for_restart: - code = self.d.yesno("Do you want to restart the services for the " - "changes to take effect?") - if code == self.d.DIALOG_OK: - for service in services_for_restart: - OSConfig.restart(service, True) - reset_services_for_restart() - - def main_menu(self): - choices = [("privacyIDEA", "", - "Configure privacyIDEA application " - "stuff like administrators.") - ] - if self.radiusDialog.RadiusConfig: - choices.append(("FreeRADIUS", "", - "Configure RADIUS settings like " - "the RADIUS clients.")) - choices.append(("Database", "", - "Configure database and setup redundancy")) - choices.append(("Webserver", "", - "Restart Webserver")) - choices.append(("Backup and Restore", "", - "Backup or Restore of privacyIDEA " - "configuration and database.")) - choices.append(("Audit Rotation", "", - "Define times when to check if the Audit log should " - "be rotated.")) - - while 1: - code, tags = self.d.menu("Which subject do you want to configure?", - choices=choices, - backtitle="privacyIDEA configuration", - cancel="Exit", - item_help=1) - if code == self.d.DIALOG_OK: - print tags - if tags == "privacyIDEA": - self.privacyidea_menu() - elif tags == "FreeRADIUS": - self.radiusDialog.menu() - elif tags.startswith("Backup"): - self.backupDialog.menu() - elif tags.startswith("Webserver"): - self.webserverDialog.menu() - elif tags.startswith("Database"): - self.dbDialog.menu() - elif tags.startswith("Audit"): - self.auditDialog.menu() - - else: - # End - self.restart_services_if_needed() - break - - def privacyidea_menu(self): - while 1: - code, tags = self.d.menu( - "Configure privacyidea", - choices=[("view config", "Display configuration.",""), - ("loglevel", "Change log level.", ""), - ("admin realms", "Modify admin realms.", ""), - ("manage local admins", "Modify admins.", ""), - ("Danger zone!", "Enter at your own risk!", - "Here you may recreated your " - "encryption and signing keys.")], - menu_height=22, - cancel='Back', - backtitle="privacyIDEA configuration", - item_help=1) - - if code == self.d.DIALOG_OK: - if tags.startswith("loglevel"): - self.privacyidea_loglevel() - elif tags.startswith("view"): - self.privacyidea_view() - elif tags.startswith("admin realms"): - self.privacyidea_adminrealms() - elif tags.startswith("manage local admins"): - self.privacyidea_admins() - elif tags.startswith("Danger zone"): - self.privacyidea_danger_menu() - else: - break - - def privacyidea_danger_menu(self): - while 1: - code, tags = self.d.menu( - "privacyIDEA Danger Zone", - choices=[("initialize pi.cfg-file", - "Create new pi.cfg-file.", - "This will also create new salt and pepper. Admins " - "will not be able to login anymore!"), - ("encryption key", "Create new encryption key.", - "Token seeds can not be decrypted anymore!"), - ("signing key", "Create new audit signing key.", - "Old audit entries can not be verified anymore.")], - menu_height=22, - cancel='Back', - backtitle="privacyIDEA Danger Zone", - item_help=1) - - if code == self.d.DIALOG_OK: - if tags.startswith("initialize"): - self.privacyidea_initialize() - elif tags.startswith("encryption"): - self.privacyidea_enckey() - elif tags.startswith("signing"): - self.privacyidea_sign() - else: - break - - - def privacyidea_admins(self): - while 1: - with self.app.app_context(): - db_admins = get_db_admins() - admins = [("Add new admin", "Add a new administrator")] - for admin in db_admins: - admins.append((admin.username, admin.email or "")) - code, tags = self.d.menu("You can select an existing administrator " - "to either delete it or change the " - "password or create a new admin", - choices=admins, - cancel='Back', - backtitle="Manage administrators") - - if code == self.d.DIALOG_OK: - if tags == "Add new admin": - self.privacyidea_admin_add() - else: - self.privacyidea_admin_manage(tags) - else: - break - - def privacyidea_admin_manage(self, admin_name): - bt = "Manage administrator" - code, tags = self.d.menu("Manage admin %s" % admin_name, - choices=[("Delete admin", ""), - ("Change password", "")], - backtitle=bt) - if code == self.d.DIALOG_OK: - if tags.startswith("Delete"): - code = self.d.yesno("Do you really want to delete the " - "administrator %s?" % admin_name) - if code == self.d.DIALOG_OK: - with self.app.app_context(): - delete_db_admin(admin_name) - - if tags.startswith("Change password"): - password = self.privacyidea_admin_password(admin_name) - pass - - def privacyidea_admin_password(self, admin_name, create=False): - bt = "Setting password for administrator %s" % admin_name - password = None - while 1: - code, password1 = self.d.passwordbox("Enter the password for the " - "administrator %s.\n" - "(Your typing will not be " - "visible)" % - admin_name, - backtitle=bt) - - if code == self.d.DIALOG_OK: - code, password2 = self.d.passwordbox("Repeat the password", - backtitle=bt) - if code == self.d.DIALOG_OK: - if password1 != password2: - self.d.msgbox("The passwords do not match. " - "Please try again.") - else: - password = password1 - with self.app.app_context(): - create_db_admin(self.app, admin_name, - password=password) - break - else: - break - else: - break - return password - - def privacyidea_admin_add(self): - bt = "Add a new administrator" - code, admin_name = self.d.inputbox("The username of the new " - "administrator", - backtitle=bt) - - if code == self.d.DIALOG_OK: - password = self.privacyidea_admin_password(admin_name, - create=True) - - def privacyidea_adminrealms(self): - adminrealms = self.pConfig.get_superusers() - # convert to string - adminrealms = ",".join(adminrealms) - code, tags = self.d.inputbox("You may enter a comma separated list " - "of realms that are recognized as " - "admin realms.", - init=adminrealms, - width=40, - backtitle="configure admin realms") - if code == self.d.DIALOG_OK: - # convert to list with no whitespaces in elemtents - adminrealms = [x.strip() for x in tags.split(",")] - self.pConfig.set_superusers(adminrealms) - self.pConfig.save() - mark_service_for_restart(SERVICE_APACHE) - - def privacyidea_initialize(self): - code = self.d.yesno("Do you want to initialize " - "the config file? Old privacyIDEA " - "configurations will be overwritten!", - backtitle="Initialize privacyIDEA configuration", - defaultno=1) - if code == self.d.DIALOG_OK: - self.pConfig.initialize() - self.pConfig.save() - mark_service_for_restart(SERVICE_APACHE) - - def privacyidea_enckey(self): - code = self.d.yesno("Do you want to create a new encryption key? " - "All token keys will not be readable anymore!", - backtitle="Create a new encryption key.", - defaultno=1) - if code == self.d.DIALOG_OK: - r, f = self.pConfig.create_encryption_key() - if r: - self.d.msgbox("Successfully created new encryption key %s." % - f) - else: - self.d.msgbox("Failed to create new encryption key %s!" % - f) - - def privacyidea_sign(self): - code = self.d.yesno("Do you want to create a new audit trail " - "signing key? " - "Older audit entries can not be verified anymore.", - backtitle="Create a new signing key.", - defaultno=1) - if code == self.d.DIALOG_OK: - r, f = self.pConfig.create_audit_keys() - if r: - self.d.msgbox("Successfully created new audit keys %s." % - f) - else: - self.d.msgbox("Failed to create new audit keys %s!" % f) - - def privacyidea_loglevel(self): - loglevel = self.pConfig.get_loglevel() - code, tags = self.d.radiolist( - "choose a loglevel", - choices=[("logging.DEBUG", "Excessive logging.", - int(loglevel == "logging.DEBUG")), - ("logging.INFO", "Normal logging.", - int(loglevel == "logging.INFO")), - ("logging.WARN", "Only log warnings.", - int(loglevel == "logging.WARN")), - ("logging.ERROR", "Sparse logging.", - int(loglevel == "logging.ERROR"))], - backtitle="privacyIDEA loglevel.") - if code == self.d.DIALOG_OK: - self.pConfig.set_loglevel(tags) - self.pConfig.save() - mark_service_for_restart(SERVICE_APACHE) - - def privacyidea_view(self): - text = u""" - The secret key file : %s - List of the admin realms : %s - Loglevel : %s - """ % (self.pConfig.get_keyfile(), - self.pConfig.get_superusers(), - self.pConfig.get_loglevel()) - self.d.scrollbox(text) - - - -def create_arguments(): - parser = argparse.ArgumentParser(description=DESCRIPTION, - fromfile_prefix_chars='@') - parser.add_argument("-f", "--file", - help="The pi.cfg file.", - required=False) - parser.add_argument("-v", "--version", - help="Print the version of the program.", - action='version', version='%(prog)s ' + VERSION) - - args = parser.parse_args() - return args - - - -def main(): - locale.setlocale(locale.LC_ALL, '') - args = create_arguments() - pS = MainMenu(config=args.file) - pS.main_menu() - +# +from authappliance.menu import main if __name__ == '__main__': main() - - -