"Fossies" - the Fresh Open Source Software Archive

Member "privacyidea-3.6.2/pi-manage" (22 Jul 2021, 62878 Bytes) of package /linux/misc/privacyidea-3.6.2.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "pi-manage": 3.6.1_vs_3.6.2.

    1 #!/usr/bin/env python
    2 # -*- coding: utf-8 -*-
    3 #
    4 # 2020-11-18 Henning Hollermann <henning.hollermann@netknights.it>
    5 #            Allow import and export of events, resolvers and policies
    6 # 2018-08-07 Cornelius Kölbel <cornelius.koelbel@netknights.it>
    7 #            Allow creation of HSM keys
    8 # 2017-10-08 Cornelius Kölbel <cornelius.koelbel@netknights.it>
    9 #            Allow cleaning up different actions with different
   10 #            retention times.
   11 # 2017-07-12 Cornelius Kölbel <cornelius.koelbel@netknights.it>
   12 #            Add generation of PGP keys
   13 # 2017-02-23 Cornelius Kölbel <cornelius.koelbel@netknights.it>
   14 #            Add CA sub commands
   15 # 2017-01-27 Diogenes S. Jesus
   16 #            Cornelius Kölbel <cornelius.koelbel@netknights.it>
   17 #            Add creation of more detailed policy
   18 # 2016-04-15 Cornelius Kölbel <cornelius@privacyidea.org>
   19 #            Add backup for pymysql driver
   20 # 2016-01-29 Cornelius Kölbel <cornelius@privacyidea.org>
   21 #            Add profiling
   22 # 2015-10-09 Cornelius Kölbel <cornelius@privacyidea.org>
   23 #            Set file permissions
   24 # 2015-09-24 Cornelius Kölbel <cornelius@privacyidea.org>
   25 #            Add validate call
   26 # 2015-06-16 Cornelius Kölbel <cornelius@privacyidea.org>
   27 #            Add creation of JWT token
   28 # 2015-03-27 Cornelius Kölbel, cornelius@privacyidea.org
   29 #            Add sub command for policies
   30 # 2014-12-15 Cornelius Kölbel, info@privacyidea.org
   31 #            Initial creation
   32 #
   33 # (c) Cornelius Kölbel
   34 # Info: http://www.privacyidea.org
   35 #
   36 # This code is free software; you can redistribute it and/or
   37 # modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
   38 # License as published by the Free Software Foundation; either
   39 # version 3 of the License, or any later version.
   40 #
   41 # This code is distributed in the hope that it will be useful,
   42 # but WITHOUT ANY WARRANTY; without even the implied warranty of
   43 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   44 # GNU AFFERO GENERAL PUBLIC LICENSE for more details.
   45 #
   46 # You should have received a copy of the GNU Affero General Public
   47 # License along with this program.  If not, see <http://www.gnu.org/licenses/>.
   48 #
   49 # ./manage.py db init
   50 # ./manage.py db migrate
   51 # ./manage.py createdb
   52 #
   53 from __future__ import print_function
   54 import os
   55 import sys
   56 import datetime
   57 from datetime import timedelta
   58 import re
   59 from subprocess import call, Popen, PIPE
   60 from getpass import getpass
   61 import gnupg
   62 import yaml
   63 import contextlib
   64 import argparse
   65 from cryptography.hazmat.backends import default_backend
   66 from cryptography.hazmat.primitives.asymmetric import rsa
   67 from cryptography.hazmat.primitives import serialization
   68 import flask
   69 from six.moves import shlex_quote
   70 import importlib_metadata
   71 
   72 from privacyidea.lib.sqlutils import delete_matching_rows
   73 from privacyidea.lib.security.default import DefaultSecurityModule
   74 from privacyidea.lib.crypto import geturandom
   75 from privacyidea.lib.auth import (create_db_admin, list_db_admin,
   76                                   delete_db_admin)
   77 from privacyidea.lib.policy import (delete_policy, enable_policy,
   78                                     PolicyClass, set_policy)
   79 from privacyidea.lib.event import (delete_event, enable_event,
   80                                    set_event, EventConfiguration)
   81 from privacyidea.lib.resolver import get_resolver_list, save_resolver
   82 from privacyidea.lib.caconnector import (get_caconnector_list,
   83                                          get_caconnector_class,
   84                                          get_caconnector_object,
   85                                          save_caconnector)
   86 from privacyidea.app import create_app
   87 from privacyidea.lib.auth import ROLE
   88 from flask_script import Manager, Command, Option
   89 from privacyidea.app import db
   90 from flask_migrate import MigrateCommand, stamp as fm_stamp
   91 # Wee need to import something, so that the models will be created.
   92 from privacyidea.models import Audit
   93 from sqlalchemy import create_engine, desc, MetaData
   94 from sqlalchemy.orm import sessionmaker
   95 from privacyidea.lib.auditmodules.sqlaudit import LogEntry
   96 from privacyidea.lib.audit import getAudit
   97 from privacyidea.lib.authcache import cleanup as authcache_cleanup
   98 from privacyidea.lib.utils import parse_timedelta, get_version_number
   99 from privacyidea.lib.crypto import create_hsm_object
  100 from privacyidea.lib.importotp import parseOATHcsv
  101 from privacyidea.lib.token import import_token
  102 from privacyidea.lib.utils.export import EXPORT_FUNCTIONS, IMPORT_FUNCTIONS
  103 import jwt
  104 import ast
  105 import base64
  106 import tarfile
  107 
  108 SILENT = True
  109 MYSQL_DIALECTS = ["mysql", "pymysql", "mysql+pymysql"]
  110 DEFAULT_CONFTYPE_LIST = ("policy", "resolver", "event")
  111 
  112 app = create_app(config_name='production', silent=SILENT)
  113 manager = Manager(app)
  114 admin_manager = Manager(usage='Create new administrators or modify existing '
  115                                     'ones.')
  116 backup_manager = Manager(usage='Create database backup and restore')
  117 realm_manager = Manager(usage='Create new realms, delete existing realms '
  118                                     'or set the default realm')
  119 resolver_manager = Manager(usage='Create new resolver')
  120 policy_manager = Manager(usage='Manage policies')
  121 event_manager = Manager(usage='Manage events')
  122 api_manager = Manager(usage="Manage API keys")
  123 ca_manager = Manager(usage="Manage Certificate Authorities")
  124 audit_manager = Manager(usage="Manage Audit log")
  125 authcache_manager = Manager(usage="Manage AuthCache")
  126 hsm_manager = Manager(usage="Manage HSM")
  127 token_manager = Manager(usage="Manage tokens")
  128 config_manager = Manager(usage="Manage the privacyIDEA configuration")
  129 config_import_manager = Manager(usage="import configuration")
  130 config_export_manager = Manager(usage="export configuration")
  131 manager.add_command('db', MigrateCommand)
  132 manager.add_command('admin', admin_manager)
  133 manager.add_command('backup', backup_manager)
  134 manager.add_command('realm', realm_manager)
  135 manager.add_command('resolver', resolver_manager)
  136 manager.add_command('policy', policy_manager)
  137 manager.add_command('event', event_manager)
  138 manager.add_command('api', api_manager)
  139 manager.add_command('ca', ca_manager)
  140 manager.add_command('audit', audit_manager)
  141 manager.add_command('authcache', authcache_manager)
  142 manager.add_command('hsm', hsm_manager)
  143 manager.add_command('token', token_manager)
  144 manager.add_command('config', config_manager)
  145 config_manager.add_command('import', config_import_manager)
  146 config_manager.add_command('export', config_export_manager)
  147 
  148 
  149 @hsm_manager.command
  150 def create_keys():
  151     """
  152     Create new encryption keys on the HSM. Be sure to first setup the HSM module, the PKCS11
  153     module and the slot/password for the given HSM in your pi.cfg.
  154     Set the variables PI_HSM_MODULE, PI_HSM_MODULE_MODULE, PI_HSM_MODULE_SLOT,
  155                       PI_HSM_MODULE_PASSWORD.
  156     """
  157     hsm_object = create_hsm_object(app.config)
  158     r = hsm_object.create_keys()
  159     print("Please add the following to your pi.cfg:")
  160     print("PI_HSM_MODULE_KEY_LABEL_TOKEN = '{0}'".format(r.get("token")))
  161     print("PI_HSM_MODULE_KEY_LABEL_CONFIG = '{0}'".format(r.get("config")))
  162     print("PI_HSM_MODULE_KEY_LABEL_VALUE = '{0}'".format(r.get("value")))
  163 
  164 
  165 def list_ca(verbose=False):
  166     """
  167     List the Certificate Authorities.
  168     """
  169     lst = get_caconnector_list()
  170     for ca in lst:
  171         print("{ca!s} (type {typ!s})".format(ca=ca.get("connectorname"),
  172                                              typ=ca.get("type")))
  173         if verbose:
  174             for (k, v) in ca.get("data").items():
  175                 print("\t{key!s:20}: {value!s}".format(key=k, value=v))
  176 
  177 
  178 ca_manager.add_command('list', Command(list_ca))
  179 
  180 
  181 @ca_manager.command
  182 def create_crl(ca, force=False):
  183     ca_obj = get_caconnector_object(ca)
  184     r = ca_obj.create_crl(check_validity=not force)
  185     if not r:
  186         print("The CRL was not created.")
  187     else:
  188         print("The CRL {name!s} was created.".format(name=r))
  189 
  190 
  191 @ca_manager.option('name', help='The name of the new CA')
  192 @ca_manager.option('-t', '--type',
  193                    help='The type of the new CA. The default is "local"',
  194                    dest='catype')
  195 def create(name, catype='local'):
  196     """
  197     Create a new CA connector. In case of the "localca" also the directory
  198     structure, the openssl.cnf and the key pair is created.
  199     """
  200     ca = get_caconnector_object(name)
  201     if ca:
  202         print("A CA connector with the name '{0!s}' already exists.".format(
  203             name))
  204         sys.exit(1)
  205     if not catype:
  206         catype = "local"
  207     print("Creating CA connector of type {0!s}.".format(catype))
  208     ca_class = get_caconnector_class(catype)
  209     ca_params = ca_class.create_ca(name)
  210     r = save_caconnector(ca_params)
  211     if r:
  212         print("Saved CA Connector with ID {0!s}.".format(r))
  213     else:
  214         print("Error saving CA connector.")
  215 
  216 
  217 @admin_manager.command
  218 def add(username, email=None, password=None):
  219     """
  220     Register a new administrator in the database.
  221     """
  222     db.create_all()
  223     if not password:
  224         password = getpass()
  225         password2 = getpass(prompt='Confirm: ')
  226         if password != password2:
  227             import sys
  228             sys.exit('Error: passwords do not match.')
  229 
  230     create_db_admin(app, username, email, password)
  231     print('Admin {0} was registered successfully.'.format(username))
  232 
  233 
  234 def list_admins():
  235     """
  236     List all administrators.
  237     """
  238     list_db_admin()
  239 
  240 
  241 admin_manager.add_command('list', Command(list_admins))
  242 
  243 
  244 @admin_manager.command
  245 def delete(username):
  246     """
  247     Delete an existing administrator.
  248     """
  249     delete_db_admin(username)
  250 
  251 
  252 @admin_manager.command
  253 def change(username, email=None, password_prompt=False):
  254     """
  255     Change the email address or the password of an existing administrator.
  256     """
  257     if password_prompt:
  258         password = getpass()
  259         password2 = getpass(prompt='Confirm: ')
  260         if password != password2:
  261             import sys
  262             sys.exit('Error: passwords do not match.')
  263     else:
  264         password = None
  265 
  266     create_db_admin(app, username, email, password)
  267 
  268 
  269 @backup_manager.command
  270 def create(directory="/var/lib/privacyidea/backup/",
  271            conf_dir="/etc/privacyidea/",
  272            radius_directory=None,
  273            enckey=False):
  274     """
  275     Create a new backup of the database and the configuration. The default
  276     does not include the encryption key. Use the 'enckey' option to also
  277     backup the encryption key. Then you should make sure, that the backups
  278     are stored safely.
  279 
  280     If you want to also include the RADIUS configuration into the backup
  281     specify a directory using 'radius_directory'.
  282     """
  283     CONF_DIR = conf_dir
  284     DATE = datetime.datetime.now().strftime("%Y%m%d-%H%M")
  285     BASE_NAME = "privacyidea-backup"
  286 
  287     directory = os.path.abspath(directory)
  288     call(["mkdir", "-p", directory])
  289     # set correct owner, if possible
  290     if os.geteuid() == 0:
  291         encfile_stat = os.stat(app.config.get("PI_ENCFILE"))
  292         os.chown(directory, encfile_stat.st_uid, encfile_stat.st_gid)
  293 
  294     sqlfile = "%s/dbdump-%s.sql" % (directory, DATE)
  295     backup_file = "%s/%s-%s.tgz" % (directory, BASE_NAME, DATE)
  296 
  297     sqluri = app.config.get("SQLALCHEMY_DATABASE_URI")
  298     sqltype = sqluri.split(":")[0]
  299     if sqltype == "sqlite":
  300         productive_file = sqluri[len("sqlite:///"):]
  301         print("Backup SQLite %s" % productive_file)
  302         sqlfile = "%s/db-%s.sqlite" % (directory, DATE)
  303         call(["cp", productive_file, sqlfile])
  304     elif sqltype in MYSQL_DIALECTS:
  305         m = re.match(r".*mysql://(.*):(.*)@(.*)/(\w*)\??(.*)", sqluri)
  306         username = m.groups()[0]
  307         password = m.groups()[1]
  308         datahost = m.groups()[2]
  309         database = m.groups()[3]
  310         # We strip parameters, but we do not use them
  311         _parameters = m.groups()[4]
  312         defaults_file = "{0!s}/mysql.cnf".format(conf_dir)
  313         _write_mysql_defaults(defaults_file, username, password)
  314         call("mysqldump --defaults-file=%s -h %s %s > %s" % (
  315             shlex_quote(defaults_file),
  316             shlex_quote(datahost),
  317             shlex_quote(database),
  318             shlex_quote(sqlfile)), shell=True)
  319     else:
  320         print("unsupported SQL syntax: %s" % sqltype)
  321         sys.exit(2)
  322     enc_file = app.config.get("PI_ENCFILE")
  323 
  324     backup_call = ["tar", "-zcf",
  325                    backup_file, CONF_DIR, sqlfile]
  326 
  327     if radius_directory:
  328         # Simply append the radius directory to the backup command
  329         backup_call.append(radius_directory)
  330 
  331     if not enckey:
  332         # Exclude enckey from backup
  333         # since tar v1.30 --exclude cannot be appended
  334         backup_call.insert(1, "--exclude={0!s}".format(enc_file))
  335 
  336     call(backup_call)
  337     os.unlink(sqlfile)
  338     os.chmod(backup_file, 0o600)
  339 
  340 
  341 def _write_mysql_defaults(filename, username, password):
  342     """
  343     Write the defaults_file for mysql commands
  344 
  345     :param filename: THe name of the file
  346     :param username: The username to connect to the database
  347     :param password: The password to connect to the database
  348     :return:
  349     """
  350     with open(filename, "w") as f:
  351         f.write("""[client]
  352 user={0!s}
  353 password={1!s}
  354 [mysqldump]
  355 no-tablespaces=True""".format(username, password))
  356 
  357     os.chmod(filename, 0o600)
  358     # set correct owner, if possible
  359     if os.geteuid() == 0:
  360         directory_stat = os.stat(os.path.dirname(filename))
  361         os.chown(filename, directory_stat.st_uid, directory_stat.st_gid)
  362 
  363 
  364 @backup_manager.command
  365 def restore(backup_file):
  366     """
  367     Restore a previously made backup. You need to specify the tgz file.
  368     """
  369     sqluri = None
  370     config_file = None
  371     sqlfile = None
  372     enckey_contained = False
  373 
  374     p = Popen(["tar", "-ztf", backup_file], stdout=PIPE, universal_newlines=True)
  375     std_out, err_out = p.communicate()
  376     for line in std_out.split("\n"):
  377         if re.search(r"/pi.cfg$", line):
  378             config_file = "/{0!s}".format(line.strip())
  379         elif re.search(r"\.sql", line):
  380             sqlfile = "/{0!s}".format(line.strip())
  381         elif re.search(r"/enckey", line):
  382             enckey_contained = True
  383 
  384     if not config_file:
  385         raise Exception("Missing config file pi.cfg in backup file.")
  386     if not sqlfile:
  387         raise Exception("Missing database dump in backup file.")
  388 
  389     if enckey_contained:
  390         print("Also restoring encryption key 'enckey'")
  391     else:
  392         print("NO FILE 'enckey' CONTAINED! BE SURE TO RESTORE THE ENCRYPTION "
  393               "KEY MANUALLY!")
  394     print("Restoring to {0!s} with data from {1!s}".format(config_file,
  395                                                            sqlfile))
  396 
  397     call(["tar", "-zxf", backup_file, "-C", "/"])
  398     print(60*"=")
  399     with open(config_file, "r") as f:
  400         # Determine the SQLAlchemy URI
  401         for line in f:
  402             if re.search("^SQLALCHEMY_DATABASE_URI", line):
  403                 key, value = line.split("=", 1)
  404                 # Strip whitespaces, and ' "
  405                 sqluri = value.strip().strip("'").strip('"')
  406 
  407     if sqluri is None:
  408         print("No SQLALCHEMY_DATABASE_URI found in {0!s}".format(config_file))
  409         sys.exit(2)
  410     sqltype = sqluri.split(":")[0]
  411     if sqltype == "sqlite":
  412         productive_file = sqluri[len("sqlite:///"):]
  413         print("Restore SQLite %s" % productive_file)
  414         call(["cp", sqlfile, productive_file])
  415         os.unlink(sqlfile)
  416     elif sqltype in MYSQL_DIALECTS:
  417         m = re.match(r".*mysql://(.*):(.*)@(.*)/(\w*)\??(.*)", sqluri)
  418         username = m.groups()[0]
  419         password = m.groups()[1]
  420         datahost = m.groups()[2]
  421         database = m.groups()[3]
  422         defaults_file = "/etc/privacyidea/mysql.cnf"
  423         _write_mysql_defaults(defaults_file, username, password)
  424         # Rewriting database
  425         print("Restoring database.")
  426         call("mysql --defaults-file=%s -h %s %s < %s" % (shlex_quote(defaults_file),
  427                                                          shlex_quote(datahost),
  428                                                          shlex_quote(database),
  429                                                          shlex_quote(sqlfile)), shell=True)
  430         os.unlink(sqlfile)
  431     else:
  432         print("unsupported SQL syntax: %s" % sqltype)
  433         sys.exit(2)
  434 
  435 
  436 @manager.command
  437 def test():
  438     """
  439     Run all nosetests.
  440     """
  441     call(['nosetests', '-v',
  442           '--with-coverage', '--cover-package=privacyidea', '--cover-branches',
  443           '--cover-erase', '--cover-html', '--cover-html-dir=cover'])
  444 
  445 
  446 @manager.command
  447 def encrypt_enckey(encfile):
  448     """
  449     You will be asked for a password and the encryption key in the specified
  450     file will be encrypted with an AES key derived from your password.
  451 
  452     The encryption key in the file is a 96 bit binary key.
  453 
  454     The password based encrypted encryption key is a hex combination of an IV
  455     and the encrypted data.
  456 
  457     The result can be piped to a new enckey file.
  458     """
  459     # TODO we just print out a string here and assume, the user pipes it into a file.
  460     #      Maybe we should write the file here so we know what is in there
  461     password = getpass()
  462     password2 = getpass(prompt='Confirm: ')
  463     if password != password2:
  464         import sys
  465         sys.exit('Error: passwords do not match.')
  466     with open(encfile, "rb") as f:
  467         enckey = f.read()
  468     res = DefaultSecurityModule.password_encrypt(enckey, password)
  469     print(res)
  470 
  471 
  472 @manager.command
  473 def create_enckey(enckey_b64=None):
  474     """
  475     If the key of the given configuration does not exist, it will be created.
  476 
  477 
  478     :param enckey_b64: (Optional) base64 encoded plain text key
  479     :return:
  480     """
  481     print()
  482     filename = app.config.get("PI_ENCFILE")
  483     if os.path.isfile(filename):
  484         print("The file \n\t%s\nalready exist. We do not overwrite it!" %
  485               filename)
  486         sys.exit(1)
  487     with open(filename, "wb") as f:
  488         if enckey_b64 is None:
  489             f.write(DefaultSecurityModule.random(96))
  490         else:
  491             print("Warning: Passing enckey via cli input is considered harmful.")
  492             bin_enckey = base64.b64decode(enckey_b64)
  493             if len(bin_enckey) != 96:
  494                 print("Error: enckey must be 96 bytes length")
  495                 sys.exit(1)
  496             f.write(bin_enckey)
  497     print("Encryption key written to %s" % filename)
  498     os.chmod(filename, 0o400)
  499     print("The file permission of %s was set to 400!" % filename)
  500     print("Please ensure, that it is owned by the right user.   ")
  501 
  502 
  503 @manager.command
  504 def create_pgp_keys(keysize=2048, force=False):
  505     """
  506     Generate PGP keys to allow encrypted token import.
  507     """
  508     GPG_HOME = app.config.get("PI_GNUPG_HOME", "/etc/privacyidea/gpg")
  509     gpg = gnupg.GPG(gnupghome=GPG_HOME)
  510     keys = gpg.list_keys(True)
  511     if len(keys) and not force:
  512         print("There are already private keys. If you want to "
  513               "generate a new private key, use the parameter --force.")
  514         print(keys)
  515         sys.exit(1)
  516     input_data = gpg.gen_key_input(key_type="RSA", key_length=keysize,
  517                                    name_real="privacyIDEA Server",
  518                                    name_comment="Import")
  519     inputs = input_data.split("\n")
  520     if inputs[-2] == "%commit":
  521         del(inputs[-1])
  522         del(inputs[-1])
  523         inputs.append("%no-protection")
  524         inputs.append("%commit")
  525         inputs.append("")
  526         input_data = "\n".join(inputs)
  527     gpg.gen_key(input_data)
  528 
  529 
  530 @manager.command
  531 def create_audit_keys(keysize=2048):
  532     """
  533     Create the RSA signing keys for the audit log.
  534     You may specify an additional keysize.
  535     The default keysize is 2048 bit.
  536     """
  537     filename = app.config.get("PI_AUDIT_KEY_PRIVATE")
  538     if os.path.isfile(filename):
  539         print("The file \n\t%s\nalready exist. We do not overwrite it!" %
  540               filename)
  541         sys.exit(1)
  542     new_key = rsa.generate_private_key(public_exponent=65537,
  543                                        key_size=keysize,
  544                                        backend=default_backend())
  545     priv_pem = new_key.private_bytes(
  546         encoding=serialization.Encoding.PEM,
  547         format=serialization.PrivateFormat.TraditionalOpenSSL,
  548         encryption_algorithm=serialization.NoEncryption())
  549     with open(filename, "wb") as f:
  550         f.write(priv_pem)
  551 
  552     pub_key = new_key.public_key()
  553     pub_pem = pub_key.public_bytes(
  554         encoding=serialization.Encoding.PEM,
  555         format=serialization.PublicFormat.SubjectPublicKeyInfo)
  556     with open(app.config.get("PI_AUDIT_KEY_PUBLIC"), "wb") as f:
  557         f.write(pub_pem)
  558 
  559     print("Signing keys written to %s and %s" %
  560           (filename, app.config.get("PI_AUDIT_KEY_PUBLIC")))
  561     os.chmod(filename, 0o400)
  562     print("The file permission of %s was set to 400!" % filename)
  563     print("Please ensure, that it is owned by the right user.")
  564 
  565 
  566 @manager.option('--stamp', '-s', help='Stamp database to current head revision.',
  567                 default=False, action='store_true')
  568 def createdb(stamp=False):
  569     """
  570     Initially create the tables in the database. The database must exist
  571     (an SQLite database will be created).
  572     """
  573     print(db)
  574     db.create_all()
  575     if stamp:
  576         # get the path to the migration directory from the distribution
  577         p = [x.locate() for x in importlib_metadata.files('privacyidea') if
  578              'migrations/env.py' in str(x)]
  579         migration_dir = os.path.dirname(os.path.abspath(p[0]))
  580         fm_stamp(directory=migration_dir)
  581     db.session.commit()
  582 
  583 
  584 @manager.command
  585 def dropdb(dropit=None):
  586     """
  587     This drops all the privacyIDEA database tables (except audit table).
  588     Use with caution! All data will be lost!
  589 
  590     For safety reason you need to pass
  591         --dropit==yes
  592     Otherwise the command will not drop anything.
  593     """
  594     if dropit == "yes":
  595         print("Dropping all database tables!")
  596         db.drop_all()
  597     else:
  598         print("Not dropping anything!")
  599 
  600 
  601 @manager.command
  602 def validate(user, password, realm=None):
  603     """
  604     Do an authentication request
  605     """
  606     from privacyidea.lib.user import get_user_from_param
  607     from privacyidea.lib.token import check_user_pass
  608     try:
  609         user = get_user_from_param({"user": user, "realm": realm})
  610         auth, details = check_user_pass(user, password)
  611         print("RESULT=%s" % auth)
  612         print("DETAILS=%s" % details)
  613     except Exception as exx:
  614         print("RESULT=Error")
  615         print("ERROR=%s" % exx)
  616 
  617 
  618 class CommandOutsideRequestContext(Command):
  619     """
  620     In contrast to flask_script's `Command`, this command class does
  621     not push a request context before running the command.
  622     """
  623     def __call__(self, app=None, *args, **kwargs):
  624         return self.run(*args, **kwargs)
  625 
  626     def run(self, *arg, **kwargs):
  627         pass
  628 
  629 
  630 def profile(length=30, profile_dir=None):
  631     """
  632     Start the application in profiling mode.
  633     """
  634     from werkzeug.middleware.profiler import ProfilerMiddleware
  635     if flask.has_request_context():
  636         print("WARNING: The app may behave unrealistically during profiling.")
  637     app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[length],
  638                                       profile_dir=profile_dir)
  639     app.run()
  640 
  641 
  642 # If flask_script's `Command` is used here instead of `CommandOutsideRequestContext`,
  643 # the request context will persist over requests. Thus, `g` is not
  644 # cleared properly between requests, which makes privacyIDEA behave unrealistically.
  645 try:
  646     manager.add_command('profile', CommandOutsideRequestContext(profile))
  647 except TypeError:
  648     # Apparently, we are using Flask-Script 0.6.7, which does not support the API used above.
  649     # So we just add the `profile` command without using `CommandOutsideRequestContext`.
  650     profile = manager.command(profile)
  651 
  652 
  653 @authcache_manager.command
  654 def cleanup(minutes=480):
  655     """
  656     Remove entries from the authcache, where last_auth entry is older than
  657     the given number of minutes.
  658     """
  659     r = authcache_cleanup(int(minutes))
  660     print(u"Entries deleted: {0!s}".format(r))
  661 
  662 
  663 @manager.option('--highwatermark', '--hw', help="If entries exceed this value, "
  664                                                 "old entries are deleted.")
  665 @manager.option('--lowwatermark', '--lw', help="Keep this number of entries.")
  666 @manager.option('--age', help="Delete audit entries older than these number "
  667                               "of days.")
  668 @manager.option('--config', help="Read config from the specified yaml file.")
  669 @manager.option('--dryrun', help="Do not actually delete, only show "
  670                                  "what would be done.", action="store_true")
  671 @manager.option('--chunksize', help="Delete entries in chunks of the given size "
  672                                     "to avoid deadlocks")
  673 @audit_manager.option('--highwatermark', '--hw', help="If entries exceed this value, "
  674                                                       "old entries are deleted.")
  675 @audit_manager.option('--lowwatermark', '--lw', help="Keep this number of entries.")
  676 @audit_manager.option('--age', help="Delete audit entries older than these number "
  677                                     "of days.")
  678 @audit_manager.option('--config', help="Read config from the specified yaml file.")
  679 @audit_manager.option('--dryrun', help="Do not actually delete, only show "
  680                                        "what would be done.", action="store_true")
  681 @audit_manager.option('--chunksize', help="Delete entries in chunks of the given size "
  682                                           "to avoid deadlocks")
  683 def rotate_audit(highwatermark=10000, lowwatermark=5000, age=0, config=None,
  684                  dryrun=False, chunksize=None):
  685     """
  686     Clean the SQL audit log.
  687 
  688     You can either clean the audit log based on the number of entries of
  689     based on the age of the entries.
  690 
  691     Cleaning based on number of entries:
  692 
  693     If more than 'highwatermark' entries are in the audit log old entries
  694     will be deleted, so that 'lowwatermark' entries remain.
  695 
  696     Cleaning based on age:
  697 
  698     Entries older than the specified number of days are deleted.
  699 
  700     Cleaning based on config file:
  701 
  702     You can clean different type of entries with different ages or watermark.
  703     See the documentation for the format of the config file
  704     """
  705     metadata = MetaData()
  706     highwatermark = int(highwatermark or 10000)
  707     lowwatermark = int(lowwatermark or 5000)
  708     if chunksize is not None:
  709         chunksize = int(chunksize)
  710 
  711     default_module = "privacyidea.lib.auditmodules.sqlaudit"
  712     token_db_uri = app.config.get("SQLALCHEMY_DATABASE_URI")
  713     audit_db_uri = app.config.get("PI_AUDIT_SQL_URI", token_db_uri)
  714     audit_module = app.config.get("PI_AUDIT_MODULE", default_module)
  715     if audit_module != default_module:
  716         raise Exception("We only rotate SQL audit module. You are using %s" %
  717                         audit_module)
  718     if config:
  719         print("Cleaning up with config file.")
  720     elif age:
  721         age = int(age)
  722         print("Cleaning up with age: {0!s}.".format(age))
  723     else:
  724         print("Cleaning up with high: {0!s}, low: {1!s}.".format(highwatermark,
  725                                                                  lowwatermark))
  726 
  727     engine = create_engine(audit_db_uri)
  728     # create a configured "Session" class
  729     session = sessionmaker(bind=engine)()
  730     # create a Session
  731     metadata.create_all(engine)
  732     if config:
  733         with open(config, 'r') as f:
  734             yml_config = yaml.load(f)
  735         auditlogs = session.query(LogEntry).all()
  736         delete_list = []
  737         for log in auditlogs:
  738             print("investigating log entry {0!s}".format(log.id))
  739             for rule in yml_config:
  740                 age = int(rule.get("rotate"))
  741                 rotate_date = datetime.datetime.now() - datetime.timedelta(days=age)
  742 
  743                 match = False
  744                 for key in rule.keys():
  745                     if key not in ["rotate"]:
  746                         search_value = rule.get(key)
  747                         print(" + searching for {0!r} in {1!s}".format(search_value,
  748                                                                        getattr(LogEntry, key)))
  749                         audit_value = getattr(log, key) or ""
  750                         m = re.search(search_value, audit_value)
  751                         if m:
  752                             # it matches!
  753                             print(" + -- found {0!r}".format(audit_value))
  754                             match = True
  755                         else:
  756                             # It does not match, we continue to next rule
  757                             print(" + NO MATCH - SKIPPING rest of conditions!")
  758                             match = False
  759                             break
  760 
  761                 if match:
  762                     if log.date < rotate_date:
  763                         # Delete it!
  764                         print(" + Deleting {0!s} due to rule {1!s}".format(log.id, rule))
  765                         # Delete it
  766                         delete_list.append(log.id)
  767                     # skip all other rules and go to the next log entry
  768                     break
  769         if dryrun:
  770             print("If you only would let me I would clean up "
  771                   "{0!s} entries!".format(len(delete_list)))
  772         else:
  773             print("Cleaning up {0!s} entries.".format(len(delete_list)))
  774             delete_matching_rows(session, LogEntry.__table__,
  775                                  LogEntry.id.in_(delete_list), chunksize)
  776     elif age:
  777         now = datetime.datetime.now() - datetime.timedelta(days=age)
  778         print("Deleting entries older than {0!s}".format(now))
  779         criterion = LogEntry.date < now
  780         if dryrun:
  781             r = LogEntry.query.filter(criterion).count()
  782             print("Would delete {0!s} entries.".format(r))
  783         else:
  784             r = delete_matching_rows(session, LogEntry.__table__, criterion, chunksize)
  785             print("{0!s} entries deleted.".format(r))
  786     else:
  787         count = session.query(LogEntry.id).count()
  788         last_id = 0
  789         for l in session.query(LogEntry.id).order_by(desc(LogEntry.id)).limit(1):
  790             last_id = l[0]
  791         print("The log audit log has %i entries, the last one is %i" % (count,
  792                                                                         last_id))
  793         # deleting old entries
  794         if count > highwatermark:
  795             print("More than %i entries, deleting..." % highwatermark)
  796             cut_id = last_id - lowwatermark
  797             # delete all entries less than cut_id
  798             print("Deleting entries smaller than %i" % cut_id)
  799             criterion = LogEntry.id < cut_id
  800             if dryrun:
  801                 r = LogEntry.query.filter(criterion).count()
  802             else:
  803                 r = delete_matching_rows(session, LogEntry.__table__, criterion, chunksize)
  804             print("{0!s} entries deleted.".format(r))
  805 
  806 
  807 @contextlib.contextmanager
  808 def smartopen(filename):
  809     if filename and filename != '-':
  810         fh = open(filename, 'w')
  811     else:
  812         fh = sys.stdout
  813 
  814     try:
  815         yield fh
  816     finally:
  817         if fh is not sys.stdout:
  818             fh.close()
  819 
  820 
  821 @audit_manager.option('--timelimit', '-t', help="Limit the dumped audit entries to a certain "
  822                                                 "period (i.e. '5d' or '3h' for the entries from "
  823                                                 "the last five days or three hours. By default "
  824                                                 "all audit entries will be dumped.")
  825 @audit_manager.option('--filename', '-f', help="Name of the 'csv' file to dump the audit entries "
  826                                                "into. By default write to stdout.", default='-')
  827 def dump(filename, timelimit=None):
  828     """Dump the audit log in csv format."""
  829     audit = getAudit(app.config)
  830     tl = parse_timedelta(timelimit) if timelimit else None
  831     with smartopen(filename) as fh:
  832         for line in audit.csv_generator(timelimit=tl):
  833             fh.write(line)
  834 
  835 
  836 @resolver_manager.command
  837 def create(name, rtype, filename):
  838     """
  839     Create a new resolver with name and type (ldapresolver, sqlresolver).
  840     Read the necessary resolver parameters from the filename. The file should
  841     contain a python dictionary.
  842 
  843     :param name: The name of the resolver
  844     :param rtype: The type of the resolver like ldapresolver or sqlresolver
  845     :param filename: The name of the config file.
  846     :return:
  847     """
  848     from privacyidea.lib.resolver import save_resolver
  849 
  850     with open(filename, 'r') as f:
  851         contents = f.read()
  852 
  853     params = ast.literal_eval(contents)
  854     params["resolver"] = name
  855     params["type"] = rtype
  856     save_resolver(params)
  857 
  858 
  859 @resolver_manager.command
  860 def create_internal(name):
  861     """
  862     This creates a new internal, editable sqlresolver. The users will be
  863     stored in the token database in a table called 'users_<name>'. You can then
  864     add this resolver to a new real using the command 'pi-manage.py realm'.
  865     """
  866     from privacyidea.lib.resolver import save_resolver
  867     sqluri = app.config.get("SQLALCHEMY_DATABASE_URI")
  868     sqlelements = sqluri.split("/")
  869     # mysql://user:password@localhost/pi
  870     # sqlite:////home/cornelius/src/privacyidea/data.sqlite
  871     sql_driver = sqlelements[0][:-1]
  872     user_pw_host = sqlelements[2]
  873     database = "/".join(sqlelements[3:])
  874     username = ""
  875     password = ""
  876     host = ""
  877     # determine host and user
  878     hostparts = user_pw_host.split("@")
  879     if len(hostparts) > 2:
  880         print("Invalid database URI: %s" % sqluri)
  881         sys.exit(2)
  882     elif len(hostparts) == 1:
  883         host = hostparts[0] or "/"
  884     elif len(hostparts) == 2:
  885         host = hostparts[1] or "/"
  886         # split hostname and password
  887         userparts = hostparts[0].split(":")
  888         if len(userparts) == 2:
  889             username = userparts[0]
  890             password = userparts[1]
  891         elif len(userparts) == 1:
  892             username = userparts[0]
  893         else:
  894             print("Invalid username and password in database URI: %s" % sqluri)
  895             sys.exit(3)
  896     # now we can create the resolver
  897     params = {'resolver': name,
  898               'type': "sqlresolver",
  899               'Server': host,
  900               'Driver': sql_driver,
  901               'User': username,
  902               'Password': password,
  903               'Database': database,
  904               'Table': 'users_' + name,
  905               'Limit': '500',
  906               'Editable': '1',
  907               'Map': '{"userid": "id", "username": "username", '
  908                      '"email":"email", "password": "password", '
  909                      '"phone":"phone", "mobile":"mobile", "surname":"surname", '
  910                      '"givenname":"givenname", "description": "description"}'}
  911     save_resolver(params)
  912 
  913     # Now we create the database table
  914     from sqlalchemy import create_engine
  915     from sqlalchemy import Table, MetaData, Column
  916     from sqlalchemy import Integer, String
  917     engine = create_engine(sqluri)
  918     metadata = MetaData()
  919     Table('users_%s' % name,
  920           metadata,
  921           Column('id', Integer, primary_key=True),
  922           Column('username', String(40), unique=True),
  923           Column('email', String(80)),
  924           Column('password', String(255)),
  925           Column('phone', String(40)),
  926           Column('mobile', String(40)),
  927           Column('surname', String(40)),
  928           Column('givenname', String(40)),
  929           Column('description', String(255)))
  930     metadata.create_all(engine)
  931 
  932 
  933 def r_export(filename=None, name=None, print_passwords=False):
  934     """
  935     Export the resolver, specified by 'resolver' to a file. If no resolver name
  936     is given, all resolver configurations are exported. By default, the content is censored.
  937     This behavior may be changed by 'print_passwords'.
  938     If the filename is omitted, the resolvers are written to stdout.
  939     """
  940     conf_export(conftype="resolver", filename=filename, name=name,
  941                 print_passwords=print_passwords)
  942 
  943 
  944 def r_import(filename=None, cleanup=None, update=False):
  945     """
  946     Import the resolvers from a json file. Existing resolvers are skipped by default.
  947     If 'update' is specified the configuration of any existing resolver is updated.
  948     Values given as __CENSORED__ (like e.g. passwords) are not touched during the update.
  949     """
  950     # Todo: Support the cleanup option to remove all resolvers which do not exist in the imported file
  951     conf_import(conftype="resolver", filename=filename, cleanup=cleanup, update=update)
  952 
  953 
  954 # unfortunately it is not possible in flask_script to add a command with a
  955 # different name and options. So we create an appropriate command class.
  956 class ListResolver(Command):
  957     """
  958     Command class to list the available resolvers and the type.
  959     """
  960     option_list = (
  961         Option('-v', '--verbose',
  962                help="Verbose output - also print the configuration of the resolvers.",
  963                dest="verbose", action="store_true"),
  964     )
  965 
  966     def run(self, verbose=False):
  967         from privacyidea.lib.resolver import get_resolver_list
  968         resolver_list = get_resolver_list()
  969 
  970         if not verbose:
  971             for (name, resolver) in resolver_list.items():
  972                 print("{0!s:16} - ({1!s})".format(name, resolver.get("type")))
  973         else:
  974             for (name, resolver) in resolver_list.items():
  975                 print("{0!s:16} - ({1!s})".format(name, resolver.get("type")))
  976                 print("."*32)
  977                 data = resolver.get("data", {})
  978                 for (k, v) in data.items():
  979                     if k.lower() in ["bindpw", "password"]:
  980                         v = "xxxxx"
  981                     print("{0!s:>24}: {1!r}".format(k, v))
  982                 print("")
  983 
  984 
  985 resolver_manager.add_command('list', ListResolver)
  986 
  987 
  988 def list_realms():
  989     """
  990     list the available realms
  991     """
  992     from privacyidea.lib.realm import get_realms
  993     realm_list = get_realms()
  994     for (name, realm_data) in realm_list.items():
  995         resolvernames = [x.get("name") for x in realm_data.get("resolver")]
  996         print("%16s: %s" % (name, resolvernames))
  997 
  998 
  999 realm_manager.add_command('list', Command(list_realms))
 1000 
 1001 
 1002 @realm_manager.command
 1003 def create(name, resolvers):
 1004     """
 1005     Create a new realm.
 1006     This will create a new realm with the given resolver
 1007     or a comma-separated list of resolvers. An existing realm
 1008     with the same name will be replaced.
 1009     """
 1010     from privacyidea.lib.realm import set_realm
 1011     resolvers = resolvers.split(",")
 1012     set_realm(name, resolvers)
 1013 
 1014 
 1015 @realm_manager.command
 1016 def delete(realm):
 1017     """
 1018     Delete the given realm
 1019     """
 1020     from privacyidea.lib.realm import delete_realm
 1021     delete_realm(realm)
 1022 
 1023 
 1024 @realm_manager.command
 1025 def set_default(realm):
 1026     """
 1027     Set the given realm to default
 1028     """
 1029     from privacyidea.lib.realm import set_default_realm
 1030     set_default_realm(realm)
 1031 
 1032 
 1033 @realm_manager.command
 1034 def clear_default():
 1035     """
 1036     Unset the default realm
 1037     """
 1038     from privacyidea.lib.realm import set_default_realm
 1039     set_default_realm(None)
 1040 
 1041 
 1042 # Event interface
 1043 
 1044 
 1045 def list_events():
 1046     """
 1047     List events
 1048     """
 1049     conf = EventConfiguration()
 1050     events = conf.events
 1051     print("{0:7} {4:4} {1:30}\t{2:20}\t{3}".format("Active", "Name", "Module", "Action", "ID"))
 1052     print(90*"=")
 1053     for event in events:
 1054         print("[{0!s:>5}] {4:4} {1:30}\t{2:20}\t{3}".format(event.get("active"),
 1055                                                             event.get("name")[0:30],
 1056                                                             event.get("handlermodule"),
 1057                                                             event.get("action"), event.get("id"),))
 1058 
 1059 
 1060 event_manager.add_command('list', Command(list_events))
 1061 
 1062 
 1063 @event_manager.command
 1064 def enable(eid):
 1065     """
 1066     enable en event by ID
 1067     """
 1068     r = enable_event(eid)
 1069     print(r)
 1070 
 1071 
 1072 @event_manager.command
 1073 def disable(eid):
 1074     """
 1075     disable an event by ID
 1076     """
 1077     r = enable_event(eid, enable=False)
 1078     print(r)
 1079 
 1080 
 1081 @event_manager.command
 1082 def delete(eid):
 1083     """
 1084     delete an event by ID
 1085     """
 1086     r = delete_event(eid)
 1087     print(r)
 1088 
 1089 
 1090 @event_manager.command
 1091 def e_export(filename=None, name=None):
 1092     """
 1093     Export the specified event or all events to a file.
 1094     If the filename is omitted, the event configurations are written to stdout.
 1095     """
 1096     conf_export(conftype="event", filename=filename, name=name)
 1097 
 1098 
 1099 @event_manager.command
 1100 def e_import(filename=None, cleanup=False, update=False):
 1101     """
 1102     Import the events from a file.
 1103     If 'cleanup' is specified the existing events are deleted before the
 1104     events from the file are imported.
 1105     """
 1106     conf_import(conftype="event", filename=filename, cleanup=cleanup, update=update)
 1107 
 1108 
 1109 # Policy interface
 1110 
 1111 def list_policies():
 1112     """
 1113     list the policies
 1114     """
 1115     pol_cls = PolicyClass()
 1116     policies = pol_cls.list_policies()
 1117     print("Active \t Name \t Scope")
 1118     print(40*"=")
 1119     for policy in policies:
 1120         print("%s \t %s \t %s" % (policy.get("active"), policy.get("name"),
 1121                                   policy.get("scope")))
 1122 
 1123 
 1124 policy_manager.add_command('list', Command(list_policies))
 1125 
 1126 
 1127 @policy_manager.command
 1128 def enable(name):
 1129     """
 1130     enable a policy by name
 1131     """
 1132     r = enable_policy(name)
 1133     print(r)
 1134 
 1135 
 1136 @policy_manager.command
 1137 def disable(name):
 1138     """
 1139     disable a policy by name
 1140     """
 1141     r = enable_policy(name, False)
 1142     print(r)
 1143 
 1144 
 1145 @policy_manager.command
 1146 def delete(name):
 1147     """
 1148     delete a policy by name
 1149     """
 1150     r = delete_policy(name)
 1151     print(r)
 1152 
 1153 
 1154 @policy_manager.command
 1155 def p_export(filename=None, name=None):
 1156     """
 1157     Export the specified policy or all policies to a file.
 1158     If the filename is omitted, the policies are written to stdout.
 1159     """
 1160     conf_export(conftype="policy", filename=filename, name=name)
 1161 
 1162 
 1163 @policy_manager.command
 1164 def p_import(filename=None, cleanup=False, update=False):
 1165     """
 1166     Import the policies from a file.
 1167     If 'cleanup' is specified the existing policies are deleted before the
 1168     policies from the file are imported.
 1169     """
 1170     conf_import(conftype="policy", filename=filename, cleanup=cleanup, update=update)
 1171 
 1172 
 1173 @policy_manager.command
 1174 def create(name, scope, action, filename=None):
 1175     """
 1176     create a new policy. 'FILENAME' must contain a dictionary and its content
 1177     takes precedence over CLI parameters.
 1178     I.e. if you are specifying a FILENAME,
 1179     the parameters name, scope and action need to be specified, but are ignored.
 1180 
 1181     Note: This will only create one policy per file.
 1182     """
 1183     if filename:
 1184         try:
 1185             with open(filename, 'r') as f:
 1186                 contents = f.read()
 1187 
 1188             params = ast.literal_eval(contents)
 1189 
 1190             if params.get("name") and params.get("name") != name:
 1191                 print("Found name '{0!s}' in file, will use that instead of "
 1192                       "'{1!s}'.".format(params.get("name"), name))
 1193             else:
 1194                 print("name not defined in file, will use the cli value "
 1195                       "{0!s}.".format(name))
 1196                 params["name"] = name
 1197 
 1198             if params.get("scope") and params.get("scope") != scope:
 1199                 print("Found scope '{0!s}' in file, will use that instead of "
 1200                       "'{1!s}'.".format(params.get("scope"), scope))
 1201             else:
 1202                 print("scope not defined in file, will use the cli value "
 1203                       "{0!s}.".format(scope))
 1204                 params["scope"] = scope
 1205 
 1206             if params.get("action") and params.get("action") != action:
 1207                 print("Found action in file: '{0!s}', will use that instead "
 1208                       "of: '{1!s}'.".format(params.get("action"), action))
 1209             else:
 1210                 print("action not defined in file, will use the cli value "
 1211                       "{0!s}.".format(action))
 1212                 params["action"] = action
 1213 
 1214             r = set_policy(params.get("name"),
 1215                            scope=params.get("scope"),
 1216                            action=params.get("action"),
 1217                            realm=params.get("realm"),
 1218                            resolver=params.get("resolver"),
 1219                            user=params.get("user"),
 1220                            time=params.get("time"),
 1221                            client=params.get("client"),
 1222                            active=params.get("active", True),
 1223                            adminrealm=params.get("adminrealm"),
 1224                            adminuser=params.get("adminuser"),
 1225                            check_all_resolvers=params.get(
 1226                                "check_all_resolvers", False))
 1227             return r
 1228 
 1229         except Exception as _e:
 1230             print("Unexpected error: {0!s}".format(sys.exc_info()[1]))
 1231 
 1232     else:
 1233         r = set_policy(name, scope, action)
 1234         return r
 1235 
 1236 
 1237 @api_manager.option('-r', '--role',
 1238                     help="The role of the API key can either be "
 1239                          "'admin' or 'validate' to access the admin "
 1240                          "API or the validate API.",
 1241                     default=ROLE.ADMIN)
 1242 @api_manager.option('-d', '--days',
 1243                     help='The number of days the access token should be valid.'
 1244                          ' Defaults to 365.',
 1245                     default=365)
 1246 @api_manager.option('-R', '--realm',
 1247                     help='The realm of the admin. Defaults to "API"',
 1248                     default="API")
 1249 @api_manager.option('-u', '--username',
 1250                     help='The username of the admin.')
 1251 def createtoken(role, days, realm, username):
 1252     """
 1253     Create an API authentication token
 1254     for administrative or validate use.
 1255     Possible roles are "admin" or "validate".
 1256     """
 1257     if role not in ["admin", "validate"]:
 1258         print("ERROR: The role must be 'admin' or 'validate'!")
 1259         sys.exit(1)
 1260     username = username or geturandom(hex=True)
 1261     secret = app.config.get("SECRET_KEY")
 1262     authtype = "API"
 1263     validity = timedelta(days=int(days))
 1264     token = jwt.encode({"username": username,
 1265                         "realm": realm,
 1266                         "nonce": geturandom(hex=True),
 1267                         "role": role,
 1268                         "authtype": authtype,
 1269                         "exp": datetime.datetime.utcnow() + validity,
 1270                         "rights": "TODO"},
 1271                        secret)
 1272     print("Username:   {0!s}".format(username))
 1273     print("Realm:      {0!s}".format(realm))
 1274     print("Role:       {0!s}".format(role))
 1275     print("Validity:   {0!s} days".format(days))
 1276     print("Auth-Token: {0!s}".format(token))
 1277 
 1278 
 1279 def import_tokens(file, tokenrealm=None):
 1280     """
 1281     Import Tokens from a CSV file
 1282     """
 1283     contents = ""
 1284     with open(file, "r") as f:
 1285         contents = f.read()
 1286     tokens = parseOATHcsv(contents)
 1287     tokenrealms = [tokenrealm] if tokenrealm else []
 1288     i = 0
 1289     for serial in tokens:
 1290         i += 1
 1291         print(u"{0!s}/{1!s} Importing token {2!s}".format(i, len(tokens), serial))
 1292 
 1293         import_token(serial, tokens[serial], tokenrealms=tokenrealm)
 1294 
 1295 
 1296 token_manager.add_command('import', Command(import_tokens))
 1297 
 1298 
 1299 from functools import partial
 1300 import json
 1301 
 1302 exp_fmt_dict = {'python': str,
 1303                 'json': partial(json.dumps, indent=2),
 1304                 'yaml': yaml.safe_dump}
 1305 
 1306 
 1307 @config_manager.option('-o', '--output', type=argparse.FileType('w'),
 1308                        default=sys.stdout,
 1309                        help='The filename to export the data to. Write to '
 1310                             '<stdout> if this argument is not given.')
 1311 @config_manager.option('-f', '--format', default='python', dest='fmt',
 1312                        choices=exp_fmt_dict.keys(),
 1313                        help='Output format, default is \'python\'')
 1314 # TODO: we need to have an eye on the help output, it might get less readable
 1315 #  when more exporter functions are added
 1316 @config_manager.option('-t', '--types', nargs='*', default=['all'],
 1317                        choices=['all'] + list(EXPORT_FUNCTIONS.keys()),
 1318                        help='The types of configuration to export. By default create '
 1319                             'export using all available exporter types. Currently registered '
 1320                             'exporter types are: '
 1321                             '{0!s}'.format(', '.join(['all'] + list(EXPORT_FUNCTIONS.keys()))))
 1322 def exporter(output, fmt, types):
 1323     """
 1324     Export server configuration using specific or all registered exporter types.
 1325     """
 1326     exp_types = EXPORT_FUNCTIONS.keys() if 'all' in types else types
 1327 
 1328     out = {}
 1329     for typ in exp_types:
 1330         out.update({typ: EXPORT_FUNCTIONS[typ]()})
 1331 
 1332     if out:
 1333         res = exp_fmt_dict.get(fmt.lower())(out) + '\n'
 1334         output.write(res)
 1335 
 1336 
 1337 imp_fmt_dict = {'python': ast.literal_eval,
 1338                 'json': json.loads,
 1339                 'yaml': yaml.safe_load}
 1340 
 1341 
 1342 @config_manager.option('-i', '--input', type=argparse.FileType('r'),
 1343                        default=sys.stdin, dest='infile',
 1344                        help='The filename to import the data from. Try to read '
 1345                             'from <stdin> if this argument is not given.')
 1346 @config_manager.option('-t', '--types', nargs='*', default=['all'],
 1347                        choices=['all'] + list(IMPORT_FUNCTIONS.keys()),
 1348                        help='The types of configuration to import. By default import all '
 1349                             'available data if a corresponding importer type exists. '
 1350                             'Currently registered importer types are: '
 1351                             '{0!s}'.format(', '.join(['all'] + list(IMPORT_FUNCTIONS.keys()))))
 1352 def importer(infile, types):
 1353     """
 1354     Import server configuration using specific or all registered importer types.
 1355     """
 1356     data = None
 1357     imp_types = IMPORT_FUNCTIONS.keys() if 'all' in types else types
 1358 
 1359     content = infile.read()
 1360 
 1361     for fmt in imp_fmt_dict:
 1362         try:
 1363             data = imp_fmt_dict[fmt](content)
 1364             break
 1365         except (SyntaxError, json.decoder.JSONDecodeError, yaml.error.YAMLError) as _e:
 1366             continue
 1367     if not data:
 1368         print('Could not read input format! '
 1369               'Accepting: {0!s}.'.format(', '.join(imp_fmt_dict.keys())),
 1370               file=sys.stderr)
 1371         sys.exit(1)
 1372 
 1373     # we need to go through the importer functions based on priority
 1374     for typ, value in sorted(IMPORT_FUNCTIONS.items(), key=lambda x: x[1]['prio']):
 1375         if typ in imp_types:
 1376             if typ in data:
 1377                 print('Importing configuration type "{0!s}".'.format(typ))
 1378                 value['func'](data[typ])
 1379 
 1380 
 1381 # conf export menu
 1382 def _get_conf_event(name=None, print_passwords=None):
 1383     """ helper function for conf_export """
 1384     event_cls = EventConfiguration()
 1385     if name:
 1386         conf = [e for e in event_cls.events if (e.get("name") == name)]
 1387     else:
 1388         conf = event_cls.events
 1389     return conf
 1390 
 1391 
 1392 def _get_conf_resolver(name=None, print_passwords=False):
 1393     """ helper function for conf_export """
 1394     from privacyidea.lib.resolver import get_resolver_list
 1395     resolver_dict = get_resolver_list(filter_resolver_name=name,
 1396                                       censor=not print_passwords)
 1397     return list(resolver_dict.values())
 1398 
 1399 
 1400 def _get_conf_policy(name=None, print_passwords=None):
 1401     """ helper function for conf_export """
 1402     pol_cls = PolicyClass()
 1403     return pol_cls.list_policies(name=name)
 1404 
 1405 
 1406 def conf_export(conftype=DEFAULT_CONFTYPE_LIST, filename=None, name=None, print_passwords=False):
 1407     """
 1408     Export configurations to a file or write them to stdout if no filename is given.
 1409     """
 1410     import pprint
 1411     pp = pprint.PrettyPrinter(indent=4)
 1412 
 1413     ret_dict = {}
 1414     conf = None
 1415     if isinstance(conftype, list) or isinstance(conftype, tuple):
 1416         conftype_list = conftype
 1417     else:
 1418         conftype_list = [conftype]
 1419 
 1420     for conftype in conftype_list:
 1421         if '_get_conf_' + conftype in globals():
 1422             conf = globals()['_get_conf_' + conftype](name=name, print_passwords=print_passwords)
 1423         if not conf:
 1424             print("The requested {0!s} configuration is empty.".format(conftype), file=sys.stderr)
 1425         ret_dict[conftype] = conf
 1426 
 1427     ret_str = pp.pformat(ret_dict)
 1428     if filename:
 1429         with open(filename, 'w') as f:
 1430             f.write(ret_str)
 1431     if not filename:
 1432         print(ret_str)
 1433 
 1434 
 1435 class FullExport(Command):
 1436     """
 1437     This action exports resolvers, policies and event handlers to standard output or to a file and optionally
 1438     compresses them as tar.gz archive
 1439     """
 1440     option_list = (
 1441         Option("--print_passwords", "-p", action="store_true",
 1442                               help="Print the passwords used in the resolver configuration. "
 1443                                    "This will overwrite existing passwords on import."),
 1444         Option("--archive", "-a", action="store_true",
 1445                               help="Compress the created config-backup as tar.gz archive instead "
 1446                                    "of printing to standard out."),
 1447         Option("--directory", "-d", action="store_true",
 1448                               help="Directory where the backup will be stored.")
 1449     )
 1450 
 1451     def run(self, directory=None, archive=False, print_passwords=False):
 1452         print("Exporting privacyIDEA configuration.", file=sys.stderr)
 1453         if archive or directory:
 1454             from socket import gethostname
 1455             DATE = datetime.datetime.now().strftime("%Y%m%d-%H%M")
 1456             BASE_NAME = "privacyidea-config-backup"
 1457             HOSTNAME = gethostname()
 1458             if not directory:
 1459                 directory = './'
 1460             else:
 1461                 call(["mkdir", "-p", directory])
 1462             config_backup_file_base = "%s/%s-%s-%s" % (directory, BASE_NAME, HOSTNAME, DATE)
 1463             config_backup_file = config_backup_file_base + ".py"
 1464             conf_export(filename=config_backup_file, print_passwords=print_passwords)
 1465             if archive:
 1466                 config_backup_archive = config_backup_file_base + ".tar.gz"
 1467                 tar = tarfile.open(config_backup_archive, "w:gz")
 1468                 tar.add(config_backup_file)
 1469                 tar.close()
 1470                 # cleanup
 1471                 if tarfile.is_tarfile(config_backup_archive):
 1472                     os.remove(config_backup_file)
 1473         else:
 1474             conf_export(filename=None, print_passwords=print_passwords)
 1475 
 1476 
 1477 config_export_manager.add_command('full', FullExport)
 1478 config_export_manager.add_command('policy', Command(p_export))
 1479 config_export_manager.add_command('resolver', Command(r_export))
 1480 config_export_manager.add_command('event', Command(e_export))
 1481 
 1482 
 1483 # conf import menu
 1484 
 1485 def _import_conf_resolver(config_list, cleanup=False, update=False):
 1486     """
 1487     import resolver configuration from a resolver list
 1488     """
 1489     if cleanup:
 1490         print("No cleanup for resolvers implemented")
 1491 
 1492     for config in config_list:
 1493         action_str = "Added"
 1494 
 1495         name = config.get("resolvername")
 1496         exists = get_resolver_list(filter_resolver_name=name)
 1497         if exists:
 1498             if not update:
 1499                 print("Resolver {0!s} exists and -u is not specified, "
 1500                       "skipping import.".format(name))
 1501                 continue
 1502             else:
 1503                 action_str = "Updated"
 1504 
 1505         resolvertype = config.get("type")
 1506         data = config.get("data")
 1507         # now we can create the resolver
 1508         params = {'resolver': name, 'type': resolvertype}
 1509         for key in data.keys():
 1510             params.update({key: data.get(key)})
 1511         r = save_resolver(params)
 1512         print("{0!s} resolver {1!s} with result {2!s}".format(action_str, name, r))
 1513 
 1514 
 1515 def _import_conf_event(config_list, cleanup=False, update=False):
 1516     """
 1517     import event configuration from an event list
 1518     """
 1519     cls = EventConfiguration()
 1520     if cleanup:
 1521         print("Cleanup old events.")
 1522         events = cls.events
 1523         for event in events:
 1524             name = event.get("name")
 1525             r = delete_event(event.get("id"))
 1526             print("Deleted event '{0!s}' with result {1!s}".format(name, r),
 1527                   file=sys.stderr)
 1528 
 1529     for event in config_list:
 1530         action_str = "Added"
 1531         # Todo: This check does not work properly. The event is created nevertheless
 1532         name = event.get("name")
 1533         events_with_name = [e for e in cls.events if (name in e.get("name"))]
 1534         if events_with_name:
 1535             exists = True
 1536             event_id = events_with_name[0].get("id")
 1537         else:
 1538             exists = False
 1539             event_id = None
 1540         if exists:
 1541             if not update:
 1542                 print("Event {0!s} exists and -u is not specified, "
 1543                       "skipping import.".format(name))
 1544                 continue
 1545             else:
 1546                 action_str = "Updated"
 1547         r = set_event(name, event.get("event"), event.get("handlermodule"),
 1548                       event.get("action"),
 1549                       conditions=event.get("conditions"),
 1550                       ordering=event.get("ordering"),
 1551                       options=event.get("options"),
 1552                       active=event.get("active"),
 1553                       position=event.get("position", "post"),
 1554                       id=event_id)
 1555         print("{0!s} event {1!s} with result {2!s}".format(action_str, name, r))
 1556 
 1557 
 1558 def _import_conf_policy(config_list, cleanup=False, update=False):
 1559     """
 1560     import policy configuration from a policy list
 1561     """
 1562 
 1563     cls = PolicyClass()
 1564 
 1565     if cleanup:
 1566         print("Cleanup old policies.")
 1567         policies = cls.list_policies()
 1568         for policy in policies:
 1569             name = policy.get("name")
 1570             r = delete_policy(name)
 1571             print("Deleted policy {0!s} with result {1!s}".format(name, r))
 1572 
 1573     for policy in config_list:
 1574         action_str = "Added"
 1575         name = policy.get("name")
 1576         exists = cls.list_policies(name=name)
 1577         if exists:
 1578             if not update:
 1579                 print("Policy {0!s} exists and -u is not specified, "
 1580                       "skipping import.".format(name))
 1581                 continue
 1582             else:
 1583                 action_str = "Updated"
 1584         r = set_policy(name,
 1585                        action=policy.get("action"),
 1586                        active=policy.get("active", True),
 1587                        adminrealm=policy.get("adminrealm"),
 1588                        adminuser=policy.get("adminuser"),
 1589                        check_all_resolvers=policy.get(
 1590                            "check_all_resolvers", False),
 1591                        client=policy.get("client"),
 1592                        conditions=policy.get("conditions"),
 1593                        pinode=policy.get("pinode"),
 1594                        priority=policy.get("priority"),
 1595                        realm=policy.get("realm"),
 1596                        resolver=policy.get("resolver"),
 1597                        scope=policy.get("scope"),
 1598                        time=policy.get("time"),
 1599                        user=policy.get("user"))
 1600         print("{0!s} policy {1!s} with result {2!s}".format(action_str, name, r))
 1601 
 1602 
 1603 def conf_import(filename=None, conftype=None, cleanup=False, update=False):
 1604     """
 1605     import privacyIDEA configuration from file
 1606     """
 1607     if filename:
 1608         with open(filename, 'r') as f:
 1609             contents = f.read()
 1610     else:
 1611         filename = "Standard input"
 1612         contents = sys.stdin.read()
 1613 
 1614     contents_var = ast.literal_eval(contents)
 1615 
 1616     # be backwards-compatible. In old versions of pi-manage config were exported to
 1617     # individual files as python list without dict key
 1618     if isinstance(contents_var, list):
 1619         conftype_list = [conftype]
 1620         contents_var = {conftype: contents_var}
 1621     else:
 1622         if conftype:
 1623             conftype_list = [conftype]
 1624         else:
 1625             conftype_list = list(contents_var.keys())
 1626 
 1627     for conftype in conftype_list:
 1628 
 1629         print("Importing {0!s} from {1!s}".format(conftype, filename))
 1630 
 1631         config_list = contents_var[conftype]
 1632         if '_import_conf_' + conftype in globals():
 1633             globals()['_import_conf_' + conftype](config_list,
 1634                                                   cleanup=cleanup, update=update)
 1635 
 1636 
 1637 class FullImport(Command):
 1638     """
 1639     This option reads configuration-backups from a plain file, a tar.gz archive or from standard input and imports
 1640     the contained resolvers, policies and event handlers.
 1641     """
 1642     option_list = (
 1643         Option("--file", "-f", dest="file",
 1644                               help="The file to import. It can be a plain python file or a tar.gz archive "
 1645                                    "containing a configuration backup file with a name containing "
 1646                                    "'privacyidea-config-backup'."),
 1647         Option("--update", "-u", action="store_true",
 1648                               help="Update the existing configuration. New policies, resolvers and events will also "
 1649                                    "be added."),
 1650         Option("--cleanup", "-c", action="store_true",
 1651                               help="The configuration on the target machine will be wiped before the import."),
 1652         Option("--wipe", "-w", action="store_true", dest="cleanup",
 1653                               help="Wipe is an alias for cleanup."),
 1654     )
 1655 
 1656     def run(self, file=None, cleanup=False, update=False):
 1657         if file:
 1658             if os.path.isfile(file):
 1659                 if tarfile.is_tarfile(file):
 1660                     tarinfo_objects = []
 1661                     tar = tarfile.open(file)
 1662                     for member in tar.members:
 1663                         if re.search(r"privacyidea-config-backup", member.name):
 1664                             tarinfo_objects.append(member)
 1665                     tar.extractall(members=tarinfo_objects)
 1666                     tar.close()
 1667                     for tarinfo in tarinfo_objects:
 1668                         conf_import(filename=tarinfo.name, cleanup=cleanup, update=update)
 1669                         # cleanup extracted files
 1670                         os.remove(tarinfo.name)
 1671                 else:
 1672                     conf_import(filename=file, cleanup=cleanup, update=update)
 1673         else:
 1674             conf_import(cleanup=cleanup, update=update)
 1675 
 1676 config_import_manager.add_command('full', FullImport)
 1677 config_import_manager.add_command('policy', Command(p_import))
 1678 config_import_manager.add_command('resolver', Command(r_import))
 1679 config_import_manager.add_command('event', Command(e_import))
 1680 
 1681 
 1682 if __name__ == '__main__':
 1683     # We add one blank line, to separate the messages from the initialization
 1684     print("""
 1685              _                    _______  _______
 1686    ___  ____(_)  _____ _______ __/  _/ _ \/ __/ _ |
 1687   / _ \/ __/ / |/ / _ `/ __/ // // // // / _// __ |
 1688  / .__/_/ /_/|___/\_,_/\__/\_, /___/____/___/_/ |_|
 1689 /_/                       /___/
 1690 {0!s:>51}
 1691     """.format('v{0!s}'.format(get_version_number())), file=sys.stderr)
 1692     manager.run()