From 255ed18f83d37d5f9198e66057fe5239ed2671e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cornelius=20K=C3=B6lbel?= Date: Tue, 14 Apr 2020 13:27:12 +0200 Subject: [PATCH] Migrate to click framework The click framework is a new framework for command line interfaces. We use a file for each command in the commands/ directory to keep things easier to manage. We add a new command section "certificate" - although it is a tokentype, but this makes creating certificates, CSRs... easier. Closes #45 Closes #44 --- privacyideautils/clientutils.py | 4 + privacyideautils/commands/__init__.py | 0 privacyideautils/commands/audit.py | 69 + privacyideautils/commands/certificate.py | 103 ++ privacyideautils/commands/config.py | 75 + privacyideautils/commands/machine.py | 219 +++ privacyideautils/commands/realm.py | 84 + privacyideautils/commands/resolver.py | 80 + privacyideautils/commands/securitymodule.py | 60 + privacyideautils/commands/token.py | 762 +++++++++ privacyideautils/commands/user.py | 62 + scripts/privacyidea | 1558 +------------------ setup.py | 11 +- 13 files changed, 1580 insertions(+), 1507 deletions(-) create mode 100644 privacyideautils/commands/__init__.py create mode 100644 privacyideautils/commands/audit.py create mode 100644 privacyideautils/commands/certificate.py create mode 100644 privacyideautils/commands/config.py create mode 100644 privacyideautils/commands/machine.py create mode 100644 privacyideautils/commands/realm.py create mode 100644 privacyideautils/commands/resolver.py create mode 100644 privacyideautils/commands/securitymodule.py create mode 100644 privacyideautils/commands/token.py create mode 100644 privacyideautils/commands/user.py diff --git a/privacyideautils/clientutils.py b/privacyideautils/clientutils.py index 1807273..8cf3e1a 100644 --- a/privacyideautils/clientutils.py +++ b/privacyideautils/clientutils.py @@ -21,9 +21,11 @@ import pprint import requests import gettext +from requests.packages.urllib3.exceptions import InsecureRequestWarning _ = gettext.gettext +__version__ = 3.0 TIMEOUT = 5 etng = False @@ -90,6 +92,8 @@ def __init__(self, username, password, baseuri="http://localhost:5000", self.baseuri = baseuri self.log = logging.getLogger('privacyideaclient') self.verify_ssl = not no_ssl_check + if not self.verify_ssl: + requests.packages.urllib3.disable_warnings(InsecureRequestWarning) # Do the first server communication and retrieve the auth token self.set_credentials(username, password) diff --git a/privacyideautils/commands/__init__.py b/privacyideautils/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/privacyideautils/commands/audit.py b/privacyideautils/commands/audit.py new file mode 100644 index 0000000..a5012ca --- /dev/null +++ b/privacyideautils/commands/audit.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# +# 2020-04-13 Cornelius Kölbel +# migrate to click +# +# This code is free software; you can redistribute it and/or +# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE +# 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 . +# +import click +import datetime +import logging +from privacyideautils.clientutils import (showresult, + dumpresult, + privacyideaclient, + __version__) + +@click.group() +@click.pass_context +def audit(ctx): + """ + Manage the audit log. Basically fetch audit information. + """ + pass + +@audit.command() +@click.pass_context +@click.option("--page", help="The page number to view", type=int) +@click.option("--rp", help="The number of entries per page", type=int) +@click.option("--sortname", help="The name of the column to sort by", default="number") +@click.option("--sortorder", help="The order to sort (desc, asc)", + type=click.Choice(["desc", "asc"]), default="desc") +@click.option("--query", help="A search tearm to search for") +@click.option("--qtype", help="The column to search for") +def list(ctx, page, rp, sortname, sortorder, query, qtype): + """ + List the audit log + """ + client = ctx.obj["pi_client"] + param = {} + if page: + param["page"] = page + if rp: + param["rp"] = rp + if sortname: + param["sortname"] = sortname + if sortorder: + param["sortorder"] = sortorder + if query: + param["query"] = query + if qtype: + param["qtype"] = qtype + resp = client.auditsearch(param) + r1 = resp.data + auditdata = r1.get("result").get("value").get("auditdata") + count = r1.get("result").get("value").get("count") + for row in auditdata: + print(row) + print("Total: {0!s}".format(count)) + diff --git a/privacyideautils/commands/certificate.py b/privacyideautils/commands/certificate.py new file mode 100644 index 0000000..0636821 --- /dev/null +++ b/privacyideautils/commands/certificate.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# +# 2020-04-13 Cornelius Kölbel +# migrate to click +# +# This code is free software; you can redistribute it and/or +# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE +# 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 . +# +import click +import datetime +import logging +from privacyideautils.clientutils import (showresult, + dumpresult, + privacyideaclient, + __version__) +from privacyideautils.clientutils import PrivacyIDEAClientError + +@click.group() +@click.pass_context +def certificate(ctx): + """ + Manage certificates + """ + pass + + +@certificate.command() +@click.pass_context +@click.option("--ca", help="Specify the CA where you want to send the CSR to.") +@click.option("--user", help="The user to whom the certificate should be assigned.") +@click.option("--realm", help="The realm of the user to whom the certificate should be assigned.") +@click.argument("requestfile", type=click.File("rb")) +def sign(ctx, requestfile, ca, user, realm): + """ + Send a certificate signing request to privacyIDEA and have the CSR signed. + """ + client = ctx.obj["pi_client"] + param = {"type": "certificate", + "genkey": 1} + if requestfile: + param["request"] = requestfile.read() + if ca: + param["ca"] = ca + if user: + param["user"] = user + if realm: + param["realm"] = realm + + try: + resp = client.inittoken(param) + print("result: {0!s}".format(resp.status)) + showresult(resp.data) + if resp.status == 200: + if not param.get("serial"): + print("serial: {0!s}".format(resp.data.get("detail", {}).get("serial"))) + except PrivacyIDEAClientError as e: + print(e) + + +@certificate.command() +@click.pass_context +@click.option("--ca", help="Specify the CA where you want to send the CSR to.") +@click.option("--user", help="The user to whom the certificate should be assigned.") +@click.option("--realm", help="The realm of the user to whom the certificate should be assigned.") +@click.option("--pin", help="Set the PIN of the PKCS12 file.") +@click.option("--template", help="Use the specified template.") +def create(ctx, ca, user, realm, pin, template): + """ + Create a key pair and certificate on the server side. + """ + client = ctx.obj["pi_client"] + param = {"type": "certificate", + "genkey": 1} + if template: + param["request"] = requestfile.read() + if ca: + param["ca"] = ca + if user: + param["user"] = user + if realm: + param["realm"] = realm + if pin: + param["pin"] = pin + + try: + resp = client.inittoken(param) + print("result: {0!s}".format(resp.status)) + showresult(resp.data) + if resp.status == 200: + if not param.get("serial"): + print("serial: {0!s}".format(resp.data.get("detail", {}).get("serial"))) + except PrivacyIDEAClientError as e: + print(e) diff --git a/privacyideautils/commands/config.py b/privacyideautils/commands/config.py new file mode 100644 index 0000000..0662090 --- /dev/null +++ b/privacyideautils/commands/config.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# +# 2020-04-13 Cornelius Kölbel +# migrate to click +# +# This code is free software; you can redistribute it and/or +# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE +# 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 . +# +import click +import datetime +import logging +from privacyideautils.clientutils import (showresult, + dumpresult, + privacyideaclient, + __version__) + +@click.group() +@click.pass_context +def config(ctx): + """ + Manage the configuration. + """ + pass + + +@config.command() +@click.pass_context +def list(ctx): + """ + List the configuration of the privacyIDEA server. + """ + client = ctx.obj["pi_client"] + response = client.getconfig({}) + showresult(response.data) + + +@config.command() +@click.pass_context +@click.option('--config', required=True, multiple=True, + help="Set a configuration value. Use it like --config key=value.") +def set(ctx, config): + """ + Set configuration values of privacyIDEA. + """ + client = ctx.obj["pi_client"] + for conf in config: + param = {} + (k, v) = conf.split("=") + param[k] = v + response = client.setconfig(param) + showresult(response.data) + + +@config.command() +@click.pass_context +@click.option('--key', required=True, multiple=True, + help="Delete config values from the privacyIDEA server by key.") +def delete(ctx, key): + """ + Delete a configuration value from the privacyIDEA server. + """ + client = ctx.obj["pi_client"] + for k in key: + response = client.deleteconfig(k) + showresult(response.data) diff --git a/privacyideautils/commands/machine.py b/privacyideautils/commands/machine.py new file mode 100644 index 0000000..d54890c --- /dev/null +++ b/privacyideautils/commands/machine.py @@ -0,0 +1,219 @@ +# -*- coding: utf-8 -*- +# +# 2020-04-13 Cornelius Kölbel +# migrate to click +# +# This code is free software; you can redistribute it and/or +# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE +# 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 . +# +import click +import datetime +import logging +from privacyideautils.clientutils import (showresult, + dumpresult, + privacyideaclient, + __version__) + +KNOWN_APPS = ["ssh", "luks", "offline"] + + +def options_to_dict(Option): + ''' + This takes an array Option consisting of entries like + slot=7, partition=dev3 + and converts it to a dictionary: + { "option_slot": "7", + "option_partition": "dev3" } + + :param Option: array of options + :type Option: array + :return: dictionary + ''' + options = {} + for option in Option: + opt = option.split("=") + if len(opt) == 2: + # There was exactly one equal sign and we have a key and a value + value = opt[1] + if opt[0].startswith("option_"): + key = opt[0] + else: + key = "option_" + opt[0] + options[key] = value + return options + + +@click.group() +@click.pass_context +def machine(ctx): + """ + Machine commands used to list machines + and assign tokens and applications to these machines. + """ + pass + + +@machine.command() +@click.pass_context +def list(ctx): + """ + List the machines found in the machine resolvers. + """ + client = ctx.obj["pi_client"] + response = client.get('/machine/') + showresult(response.data) + + +@machine.command() +@click.pass_context +@click.option("--hostname", help="List the attached tokens of this machine.") +@click.option("--serial", help="List the attachments for this token.") +@click.option("--machineid", help="List the attachments for this machine ID.") +@click.option("--resolver", help="List the machines in this machine resolver.") +def listtoken(ctx, hostname, serial, machineid, resolver): + """ + List the token machine mapping. + You can list all mappings for a token, for a machine or all machines in + a machine resolver. + """ + client = ctx.obj["pi_client"] + param = {} + if hostname: + param["hostname"] = hostname + if serial: + param["serial"] = serial + if machineid: + param["machineid"] = machineid + if resolver: + param["resolver"] = resolver + response = client.get("/machine/token", param) + showresult(response.data) + + +@machine.command() +@click.pass_context +@click.option("--hostname", required=True, + help="The hostname of the machine, for which the authitem should be retrieved.") +@click.option("--application", + help="The application, which authitems should be retrieved.") +@click.option("--challenge", + help="If the application requires a challenge, you can pass a challenge, for" + " which the authitem will be returned.") +def authitem(ctx, application, hostname, challenge): + """ + Get the authentication item for the given machine and application. + """ + client = ctx.obj["pi_client"] + param = {} + if application: + param["application"] = application + if hostname: + param["hostname"] = hostname + if challenge: + param["challenge"] = challenge + if param.get("application"): + response = client.get("/machine/authitem/%s" % param.get( + "application"), param) + else: + response = client.get("/machine/authitem", param) + showresult(response.data) + + +@machine.command() +@click.pass_context +@click.option("--hostname", help="The hostname of the machine", required=True) +@click.option("--serial", required=True, help="The serial of the token.") +@click.option("--application", required=True, help="The application type to attach.", + type=click.Choice(KNOWN_APPS)) +@click.option("--option", help="Option for the application, like key=value.", multiple=True) +def attach(ctx, hostname, serial, application, option): + """ + Attach a token with an application to a machine + """ + client = ctx.obj["pi_client"] + param = {"name": hostname, + "serial": serial, + "application": application} + ret = client.post("/machine/token", param) + showresult(ret) + if len(option) > 0: + options = options_to_dict(option) + param.update(options) + ret = client.post("/machine/tokenoption", param) + showresult(ret) + + +@machine.command() +@click.pass_context +@click.option("--hostname", help="The hostname of the machine", required=True) +@click.option("--serial", required=True, help="The serial of the token.") +@click.option("--application", required=True, help="The application type to attach.", + type=click.Choice(KNOWN_APPS)) +def detach(ctx, hostname, serial, application): + """ + Detach a token from a machine. + """ + client = ctx.obj["pi_client"] + ret = client.post("/machine/deltoken", + {"name": hostname, + "serial": serial, + "application": application}) + showresult(ret) + + +@machine.command() +@click.pass_context +@click.option("--hostname", help="The hostname of the machine", required=True) +@click.option("--serial", required=True, help="The serial of the token.") +@click.option("--application", required=True, help="The application type to attach.", + type=click.Choice(KNOWN_APPS)) +@click.option("--option", help="Option for the application, like key=value.", multiple=True) +def add_option(ctx, hostname, serial, application, option): + """ + Add options to an attached token + """ + client = ctx.obj["pi_client"] + param = {"name": hostname, + "serial": serial, + "application": application} + if len(args.option) > 0: + options = options_to_dict(option) + param.update(options) + ret = client.post("/machine/tokenoption", param) + showresult(ret) + + +@machine.command() +@click.pass_context +@click.option("--hostname", help="The hostname of the machine", required=True) +@click.option("--serial", required=True, help="The serial of the token.") +@click.option("--application", required=True, help="The application type to attach.", + type=click.Choice(KNOWN_APPS)) +@click.option("--option", help="Option for the application, like key=value.", multiple=True) +def delete_option(ctx, hostname, serial, application, option): + """ + Delete options from an attached token + """ + client = ctx.obj["pi_client"] + param = {"name": hostname, + "serial": serial, + "application": application} + if len(args.option) > 0: + options = options_to_dict(option) + for k in options.keys(): + param["key"] = k + ret = client.post("/machine/deloption", param) + showresult(ret) + + + diff --git a/privacyideautils/commands/realm.py b/privacyideautils/commands/realm.py new file mode 100644 index 0000000..6aff177 --- /dev/null +++ b/privacyideautils/commands/realm.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# +# 2020-04-13 Cornelius Kölbel +# migrate to click +# +# This code is free software; you can redistribute it and/or +# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE +# 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 . +# +import click +import datetime +import logging +from privacyideautils.clientutils import (showresult, + dumpresult, + privacyideaclient, + __version__) + +@click.group() +@click.pass_context +def realm(ctx): + """ + Manage realms. + """ + pass + + +@realm.command() +@click.pass_context +def list(ctx): + """ + List all realms + """ + client = ctx.obj["pi_client"] + response = client.getrealms() + showresult(response.data) + +@realm.command() +@click.option("--realm", help="The name of the new realm.", required=True) +@click.option("-r", "--resolver", required=True, multiple=True, + help="The name of the resolver. You can specify several resolvers by " + "using several --resolver arguments.") +@click.pass_context +def set(ctx, realm, resolver): + """ + Create a new realm + """ + client = ctx.obj["pi_client"] + param = {} + param['resolvers'] = ",".join(resolver) + response = client.setrealm(realm, param) + showresult(response.data) + + +@realm.command() +@click.option("--realm", required=True, help="Delete a realm") +@click.pass_context +def delete(ctx, realm): + """ + Delete the specified realm + """ + client = ctx.obj["pi_client"] + response = client.deleterealm(realm) + showresult(response.data) + + +@realm.command() +@click.option("--realm", required=True, help="Set default realm") +@click.pass_context +def default(ctx, realm): + """ + Set the specified realm as default realm + """ + client = ctx.obj["pi_client"] + response = client.setdefaultrealm(realm) + showresult(response.data) diff --git a/privacyideautils/commands/resolver.py b/privacyideautils/commands/resolver.py new file mode 100644 index 0000000..34ebf5a --- /dev/null +++ b/privacyideautils/commands/resolver.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# +# 2020-04-13 Cornelius Kölbel +# migrate to click +# +# This code is free software; you can redistribute it and/or +# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE +# 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 . +# +import click +import datetime +import logging +from privacyideautils.clientutils import (showresult, + dumpresult, + privacyideaclient, + __version__) + +@click.group() +@click.pass_context +def resolver(ctx): + """ + Manage resolvers. + """ + pass + + +@resolver.command() +@click.pass_context +def list(ctx): + """ + List all available resolvers + """ + client = ctx.obj["pi_client"] + response = client.getresolver({}) + showresult(response.data) + + +@resolver.command() +@click.pass_context +@click.option("--resolver", required=True, help="The name of the resolver to delete.") +def deleter(ctx, resolver): + """ + Delete a resolver + """ + client = ctx.obj["pi_client"] + response = client.deleteresolver(resolver) + showresult(response.data) + + +@resolver.command() +@click.pass_context +@click.option("--resolver", required=True, help="The name of the new resolver.") +@click.option("--type", required=True, + type=click.Choice(["LDAP", "SQL", "PASSWD", "SCIM"]), + help="The type of the new resolver") +@click.option("--filename", help="The filename for Passwdresolvers") +def set(ctx, resolver, type, filename): + """ + Create a new resolver + """ + client = ctx.obj["pi_client"] + if args.type.lower() == "passwd": + response = client.setresolver(resolver, {"type": type, "filename": filename}) + else: + print("Resolver Type currently not supported.") + showresult(response.data) + + + + + diff --git a/privacyideautils/commands/securitymodule.py b/privacyideautils/commands/securitymodule.py new file mode 100644 index 0000000..5db6fcf --- /dev/null +++ b/privacyideautils/commands/securitymodule.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# +# 2020-04-13 Cornelius Kölbel +# migrate to click +# +# This code is free software; you can redistribute it and/or +# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE +# 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 . +# +import click +import datetime +import logging +import getpass +from privacyideautils.clientutils import (showresult, + dumpresult, + privacyideaclient, + __version__) + +@click.group() +@click.pass_context +def securitymodule(ctx): + """ + Manage the security module. + """ + pass + + +@securitymodule.command() +@click.pass_context +def init(ctx): + """ + Initialize the module by entering the password + """ + client = ctx.obj["pi_client"] + password = getpass.getpass(prompt="Please enter password for" + " security module:") + print("Setting the password of your security module") + response = client.set_hsm(param={"password": str(password)}) + showresult(response.data) + + +@securitymodule.command() +@click.pass_context +def status(ctx): + """ + Get the status of the security module + """ + client = ctx.obj["pi_client"] + print("This is the configuration of your active Security module:") + response = client.get_hsm() + showresult(response.data) \ No newline at end of file diff --git a/privacyideautils/commands/token.py b/privacyideautils/commands/token.py new file mode 100644 index 0000000..3b2d545 --- /dev/null +++ b/privacyideautils/commands/token.py @@ -0,0 +1,762 @@ +# -*- coding: utf-8 -*- +# +# 2020-04-13 Cornelius Kölbel +# migrate to click +# +# This code is free software; you can redistribute it and/or +# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE +# 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 . +# +import click +import datetime +import logging +import qrcode +from privacyideautils.clientutils import (showresult, + dumpresult, + privacyideaclient, + __version__) +from privacyideautils.etokenng import initetng +from privacyideautils.initdaplug import init_dongle +from privacyideautils.nitrokey import NitroKey +from privacyideautils.yubikey import (enrollYubikey, YubikeyPlug, + create_static_password, MODE_YUBICO, + MODE_OATH, MODE_STATIC) +from email.mime.text import MIMEText +import smtplib + + +mode_string_mapping = {"YUBICO": MODE_YUBICO, + "OATH": MODE_OATH, + "STATIC": MODE_STATIC} + + +def cifs_push(config, text): + ''' + Push the the data text to a cifs share + + :param config: dictionary with the fields cifs_server, cifs_share, + cifs_dir, cifs_user, cifs_password + :type config: dict + :param text: text to be pushed to the windows share + :type text: string + + ''' + FILENAME = datetime.datetime.now().strftime("/tmp/%y%m%d-%H%M%S" + "_privacyideaadm.out") + f = open(FILENAME, 'w') + f.write(text) + f.close() + + filename = os.path.basename(FILENAME) + + print("Pushing %s to %s//%s/%s" % (filename, + config.get("cifs_server"), + config.get("cifs_share", ""), + config.get("cifs_dir"))) + + args = ["smbclient", + "//%s\\%s" % (config.get("cifs_server"), + config.get("cifs_share", "")), + "-U", "%s%%%s" % (config.get("cifs_user"), + config.get("cifs_password")), "-c", + "put %s %s\\%s" % (FILENAME, + config.get("cifs_dir", "."), + filename)] + + p = subprocess.Popen(args, + cwd=None, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=False) + (result, error) = p.communicate() + _rcode = p.returncode + print(result) + print(error) + + try: + os.remove(FILENAME) + except Exception as e: + print ("couldn't remove push test file: %r" % e) + + + +def sendmail(config, text): + ''' + Send an email with the text + + :param config: dictionary with the fields mail_from, mail_to, mail_host, + mail_subject + :type config: dict + :param text: text to be sent via mail + :type text: string + + ''' + if not config.get("mail_to"): + Exception("mail_to required!") + + if not config.get("mail_host"): + Exception("mail_host required!") + + print("sending mail to %s" % config.get("mail_to")) + msg = MIMEText(text) + sender = config.get("mail_from") + recipient = config.get("mail_to") + msg['Subject'] = config.get("mail_subject") + msg['From'] = sender + msg['To'] = recipient + + mail = smtplib.SMTP(config.get("mail_host"), config.get("mail_port") or 25) + mail.ehlo() + if config.get("mail_tls"): + mail.starttls() + if config.get("mail_user"): + mail.login(config.get("mail_user"), config.get("mail_password")) + + mail.sendmail(sender, [recipient], msg.as_string()) + mail.quit() + + +@click.group() +@click.pass_context +def token(ctx): + """ + Manage tokens. + """ + pass + + +@token.command() +@click.pass_context +@click.option('-u', '--user', help="List tokens of this user.") +@click.option('-s', '--serial', help="List tokens with this serial.") +@click.option('-c', '--csv', help="Export as csv.", is_flag=True) +@click.option('-e', '--export_fields', help="comma separated list of additional " + "fields to export into the CSV export.") +@click.option('--cifs_server', help="If exporting as CSV you can save the " + "result to this CIFS server.") +@click.option('--cifs_user', help="If exporting as CSV you can save the " + "result to a CIFS server with this username.") +@click.option('--cifs_password', help="If exporting as CSV you can save the " + "result to a CIFS server with this password.") +@click.option('--mail_host', help="If exporting as CSV you can send the " + "result as mail via this mail host.") +@click.option('--mail_to', help="If exporting as CSV you can send the " + "result to this email address.") +def list(ctx, user, serial, csv, export_fields, mail_host, mail_to, + cifs_server, cifs_user, cifs_password): + """ + List tokens + """ + client = ctx.obj["pi_client"] + param = {} + if user: + param["user"] = user + if serial: + param["serial"] = serial + if csv: + param['outform'] = 'csv' + if export_fields: + param['user_fields'] = export_fields + resp = client.listtoken(param) + r1 = resp.data + if mail_host and mail_to: + sendmail(mail_host, mail_to, r1) + if cifs_server and cifs_user and cifs_password: + cifs_push(cifs_server, cifs_user, cifs_password, r1) + else: + resp = client.listtoken(param) + if resp.status == 200: + r1 = resp.data + result = r1['result'] + dumpresult(result['status'], + result['value']['tokens']) + +@token.command() +@click.pass_context +@click.option("--user", help="If a user is specified, the " + "token is directly assigned to this user.") +@click.option("--serial", help="This is the new serial number " + "of the token") +@click.option("--description", help="The description of the " + "token. This can be used to identify the token " + "more easily.", + default="command line enrolled") +@click.option("--pin", help="The OTP PIN of the token.") +@click.option("--otpkey", help="The OTP key, like the HMAC key") +@click.option("--genkey", help="Generate an HOTP key", is_flag=True) +@click.option("--type", help="The token type", + type=click.Choice(["hotp", "totp", "pw", "spass", "dpw", + "ssh", "sms", "email", "yubico", + "registration"], case_sensitive=False), + default="hotp") +@click.option("--etng", help="If specified, an etoken NG will " + "be initialized", is_flag=True) +def init(ctx, user, serial, description, pin, otpkey, genkey, type, etng): + """ + Initialize a token. I.e. create a new token in privacyidea. + """ + client = ctx.obj["pi_client"] + param = {} + param["type"] = type + param["otpkey"] = otpkey + if genkey: + param["genkey"] = 1 + if user: + param["user"] = user + if serial: + param["serial"] = serial + if description: + param["description"] = description + if pin: + param["pin"] = pin + + if etng: + tokenlabel = user + tdata = initetng({'label': tokenlabel, + 'debug': True}) + if not tdata['userpin'] or not tdata['hmac'] or not tdata['serial']: + print("No token was added to privacyIDEA: ", tdata['error']) + sys.exit(1) + param['serial'] = tdata['serial'] + param['otpkey'] = tdata['hmac'] + param['userpin'] = tdata['userpin'] + param['sopin'] = tdata['sopin'] + print(("FIXME: what shall we do with the eToken password and " + "SO PIN: ", tdata['userpin'], tdata['sopin'])) + + resp = client.inittoken(param) + print("result: {0!s}".format(resp.status)) + showresult(resp.data) + if resp.status == 200: + if not param.get("serial"): + print("serial: {0!s}".format(resp.data.get("detail", {}).get("serial"))) + if param.get("genkey"): + print("otpkey: {0!s}".format(resp.data.get("detail", {}).get("otpkey", {}).get("value"))) + googleurl = resp.data.get("detail", {}).get("googleurl", {}).get("value") + qr = qrcode.QRCode() + qr.add_data(googleurl) + qr.print_ascii(tty=True) + + +@token.command() +@click.pass_context +@click.option("--realm", + help="The realm for which registration tokens should be enrolled.", + required=True) +@click.option("--dump", help="Do not send notification email to the user, but dump " + "the data to stdout.", is_flag=True) +@click.option("--mail_host", help="Mailserver to send notification.", required=True) +@click.option("--mail_from", help="Mail sender address.", required=True) +@click.option("--mail_subject", help="Mail subject.", required=True) +@click.option("--mail_body", help="Mail body. Should contain %%(username)s " + "and %%(registration)s", + required=True) +@click.option("--mail_port", help="Port of the mailserver", type=int, + default=25) +@click.option("--mail_tls", help="If mailserver supports STARTTLS.", default=False, + is_flag=True) +@click.option("--mail_user", help="Username, if required by mailserver.") +@click.option("--mail_password", help="Password, if required by mailserver.") +@click.option("--mail_subject", help="The subject of the email") +def registration(ctx, realm, dump, mail_host, mail_from, mail_subject, + mail_body, mail_port, mail_tls, mail_user, mail_password): + """ + enroll registration tokens for all users in a realm, who do not have a + token, yet. + """ + client = ctx.obj["pi_client"] + response = client.userlist({"realm": realm}) + data = response.data + result = data.get('result') + users = result.get('value') + tokens = [] + for user in users: + username = user.get("username") + email = user.get("email") + # check, if the user has tokens + count = get_users_token_num(client, username, realm) + if count == 0: + # User has no token, create one. + print("Creating token for user %s" % username) + response = client.inittoken({"type": "registration", + "user": username, + "realm": realm}) + result = response.data.get("result") + detail = response.data.get("detail") + registrationcode = detail.get("registrationcode") + serial = detail.get("serial") + tokens.append({"username": username, + "email": email, + "serial": serial, + "registration": registrationcode}) + + for token in tokens: + if dump: + print(token) + else: + print("Sending email to %(email)s" % token) + config = {"mail_to": token.get("email"), + "mail_from": mail_from, + "mail_host": mail_host, + "mail_port": mail_port, + "mail_tls": mail_tls, + "mail_user": mail_user, + "mail_password": mail_password} + sendmail(config, mail_body % token) + +@token.command() +@click.pass_context +@click.option("--yubiprefix", help="A prefix that is outputted " + "by the yubikey", + default="") +@click.option("--yubiprefixrandom", help="A random prefix " + "of length. For YUBICO mode the default will be 6!", + type=int, default=0) +@click.option("--yubiprefixserial", + help="Use the serial number of " + "the yubikey as prefix.", is_flag=True) +@click.option("--yubimode", help="The mode the yubikey should " + "be initialized in. (default=OATH)", + type=click.Choice(["OATH", "YUBICO", "STATIC"]), + default="OATH") +@click.option("--filename", + help="If the initialized yubikeys should not be " + "sent to a privacyIDEA server the otpkeys can " + "be written to a CSV file, to be imported " + "later.") +@click.option("--yubislot", help="The slot of the yubikey, that " + "is initialized (default=1)", + type=click.Choice(["1", "2"]), + default="1") +@click.option("--yubiCR", + help="Initialize the yubikey in challenge/" + "response mode.", + is_flag=True) +@click.option("--description", help="The description of the " + "token. This can be used to identify the token " + "more easily.", + default="command line enrolled") +@click.option("--access", + help="Use this hexlified access key to programm the " + "yubikey") +@click.option("--newaccess", + help="Set a new access key, so that the yubikey will" + "only programmable with this new access key. " + "You can reset the access key by setting the " + "new access key to '000000000000'.") +def yubikey_mass_enroll(ctx, yubiprefix, yubiprefixrandom, yubiprefixserial, + yubimode, filename, yubislot, yubiCR, description, access, newaccess): + """ + Initialize a bunch of yubikeys + """ + client = ctx.obj["pi_client"] + yp = YubikeyPlug() + while True: + print("\nPlease insert the next yubikey.", end=' ') + sys.stdout.flush() + submit_param = {} + _ret = yp.wait_for_new_yubikey() + otpkey, serial, prefix = enrollYubikey( + debug=False, + APPEND_CR=not yubiCR, + prefix_serial=yubiprefixserial, + fixed_string=yubiprefix, + len_fixed_string=yubiprefixrandom, + slot=int(yubislot), + mode=yubimode, + challenge_response=yubiCR, + access_key=access, + new_access_key=newaccess) + if yubimode == MODE_OATH: + # According to http://www.openauthentication.org/oath-id/prefixes/ + # The OMP of Yubico is UB + # As TokenType we use OM (oath mode) + submit_param = {'type': 'HOTP', + 'serial': "UBOM%s_%s" % (serial, yubislot), + 'otpkey': otpkey, + 'description': description, + 'otplen': 6, + 'yubikey.prefix': prefix} + if yubiCR: + submit_param['type'] = 'TOTP' + submit_param['timeStep'] = 30 + + elif yubi_mode == MODE_STATIC: + password = create_static_password(otpkey) + # print "otpkey ", otpkey + # print "password ", password + submit_param = {'serial': "UBSM%s_%s" % (serial, yubi_slot), + 'otpkey': password, + 'type': "pw", + 'description': description, + 'yubikey.prefix': prefix} + + elif yubimode == MODE_YUBICO: + yubi_otplen = 32 + if prefix: + yubi_otplen = 32 + len(prefix) + elif yubiprefixrandom: + # default prefix length for MODE_YUBICO is 6 + if yubiprefixrandom is None: + yubiprefixrandom = 6 + yubi_otplen = 32 + (yubiprefixrandom * 2) + # According to http://www.openauthentication.org/oath-id/prefixes/ + # The OMP of Yubico is UB + # As TokenType we use AM (AES mode) + submit_param = {'type': 'yubikey', + 'serial': "UBAM%s_%s" % (serial, yubislot), + 'otpkey': otpkey, + 'otplen': yubi_otplen, + 'description': description, + 'yubikey.prefix': prefix} + + else: + print("Unknown Yubikey mode") + pass + if 'realm' in proc_params: + submit_param['realm'] = proc_params.get('realm') + + if filename: + # Now we write the data to a file + f = open(filename, mode="a") + f.write("%(serial)s, %(otpkey)s, %(type)s, %(otplen)s\n" % + submit_param) + f.close() + else: + # The token is submitted to the privacyIDEA system + resp = client.inittoken(submit_param) + print(resp.status) + showresult(resp.data) + + +@token.command() +@click.pass_context +@click.option("--nitromode", help="Either HOTP or TOTP", + type=click.Choice(["HOTP", "TOTP"]), default="HOTP") +@click.option("--slot", help="The slot of the Nitrokey, that is initialized (default=0)", + type=click.Choice(["{0!s}".format(x) for x in range(0, 16)]), + default="0") +@click.option("--description", + help="The description of the token. This can be used to identify the token " + "more easily.") +@click.option("--pin", help="The Admin password of the Nitrokey") +@click.option("--digits", help="The number of allowed OTP digits.", + type=click.Choice(["6", "8"]), default=6) +@click.option("--slotname", help="The name of the OTP slot.", default="privacyIDEA") +def nitrokey_mass_enroll(ctx, nitromode, slot, description, pin, digits, slotname): + """ + Initialize a bunch of Nitrokeys + """ + client = ctx.obj["pi_client"] + NK = NitroKey() + if nitromode == "TOTP": + raise Exception("At the moment we only support HOTP.") + slot = int(slot) + digits = int(digits) + print("\nWe are going to initialize your Nitrokeys. Please assure, " + "that no Nitrokey-App is active!\n") + if not password: + # Ask for the Nitrokey administrator password + password = getpass.getpass(prompt="Please enter the Nitrokey " + "Administrator Password:") + + NK.admin_login(password) + while True: + print("Please insert the next Nitrokey!") + input("Press [ENTER] when ready.") + + print("Initializing keys") + otp_key = NK.init_hotp(slot, slotname, digits=digits) + status = NK.status() + serial = "".join(status.get("card_serial", "").split()).upper() + print("Enrolled token with serial: {0!s}.".format(serial)) + + param = {} + param["serial"] = "NK{0!s}_{1!s}".format(serial, slot) + param["otpkey"] = otp_key + param["otplen"] = int(digits) + param["type"] = nitromode + param["description"] = description or slotname + resp = client.inittoken(param) + showresult(resp.data) + + NK.logout() + +@token.command() +@click.pass_context +@click.option("-k", "--keyboard", is_flag=True, + help="If this option is set, the daplug will simulate " + "a keyboard and type the OTP value when plugged in.") +@click.option("--hidmap", help="Specify the HID mapping. The default HID " + "mapping is 05060708090a0b0c0d0e. Only use this, " + "if you know " + "what you are doing!", + default="05060708090a0b0c0d0e") +@click.option("--otplen", type=click.Choice(["6", "8"]), + help="Specify if the OTP length should be 6 or 8.") +def daplug_mass_enroll(ctx, keyboard, hidmap, otplen): + """ + Initialize a bunch of daplug dongles. + """ + client = ctx.obj["pi_client"] + (serial, hotpkey) = init_dongle(keyboard=keyboard, + mapping=hidmap, + otplen=otplen) + if serial: + param = {} + param["serial"] = "DPLG%s" % serial + param["otpkey"] = hotpkey + param["otplen"] = int(otplen) + param["type"] = "daplug" + param["description"] = "daplug dongle" + r1 = client.inittoken(param) + showresult(r1) + +@token.command() +@click.pass_context +@click.option("--label", help="The label of the eToken NG OTP.", + default="privacyIDEAToken") +@click.option("--description", help="Description of the token.", + default="mass enrolled") +def etokenng_mass_enroll(ctx, label, description): + """ + Enroll a bunch of eToken NG OTP. + """ + print("""Mass-Enrolling eToken NG OTP. + !!! Beware the tokencontents of all tokens will be deleted. !!! + + Random User PINs and SO-PINs will be set. + The SO-PIN will be stored in the Token-Database. + """) + client = ctx.obj["pi_client"] + param = {} + while True: + answer = input("Please insert the next eToken NG" + " and press enter (x=Exit): ") + if "x" == answer.lower(): + break + tdata = initetng({'label': label, + 'debug': False, + 'description': description}) + if not tdata['userpin'] or not tdata['hmac'] or not tdata['serial']: + print("No token was added to privacyIDEA:", tdata['error']) + sys.exit(1) + param['serial'] = tdata['serial'] + param['otpkey'] = tdata['hmac'] + param['userpin'] = tdata['userpin'] + param['sopin'] = tdata['sopin'] + r1 = client.inittoken(param) + showresult(r1) + +@token.command() +@click.pass_context +@click.option("--serial", help="Serial number of the token to assign", required=True) +@click.option("--user", help="The user, who should get the token", required=True) +def assigntoken(ctx, serial, user): + """ + Assign a token to a user + """ + client = ctx.obj["pi_client"] + param = {} + param["user"] = user + param["serial"] = serial + response = client.assigntoken(param) + showresult(response.data) + + +@token.command() +@click.pass_context +@click.option("--serial", help="Serial number of the token to unassign", required=True) +def unassigntoken(ctx, serial): + """ + Remove a token from a user + """ + client = ctx.obj["pi_client"] + response = client.unassigntoken({"serial": serial}) + showresult(response.data) + +@token.command() +@click.pass_context +@click.option("-f", "--file", help="The token file to import", required=True) +def importtoken(args, client): + """ + Import a token file + """ + client = ctx.obj["pi_client"] + response = client.importtoken({'file': file}) + showresult(response.data) + + +@token.command() +@click.pass_context +@click.option("--serial", help="serial number of the token to disable") +@click.option("--user", help="The username of the user, whose tokens should be disabled") +def disable(ctx, serial, user): + """ + Disable token by serial or user name + """ + client = ctx.obj["pi_client"] + param = {} + if serial: + param["serial"] = serial + if user: + param["user"] = user + response = client.disabletoken(param) + showresult(response.data) + + +@token.command() +@click.pass_context +@click.option("--serial", help="serial number of the token to enable") +@click.option("--user", help="The username of the user, whose tokens should be enabled") +def enabletoken(ctx, serial, user): + """ + Enable token by serial or user name + """ + client = ctx.obj["pi_client"] + param = {} + if serial: + param["serial"] = serial + if user: + param["user"] = user + response = client.enabletoken(param) + showresult(response.data) + +@token.command() +@click.pass_context +@click.option("--serial", help="serial number of the token to remove") +@click.option("--user", help="The username of the user, whose tokens should be deleted") +@click.option("--realm", help="Delete all tokens of the given type in this realm.") +@click.option("--type", help="Delete all tokens of this type in the given realm.") +def delete(ctx, serial, user, realm, type): + """ + Delete tokens based on serial, user, realm or token type. + """ + client = ctx.obj["pi_client"] + serials = [] + if serial: + serials = [serial] + + elif user: + response = client.listtoken({"user": user, "realm": realm}) + value = response.data.get("result", {}).get("value") + for token in value.get("tokens"): + serials.append(token.get("serial")) + + elif type: + if not realm: + print("If you want to delete a tokentype, you need to specify a realm!") + sys.exit(1) + response = client.listtoken({"tokenrealm": realm, "type": type}) + value = response.data.get("result", {}).get("value") + for token in value.get("tokens"): + serials.append(token.get("serial")) + + for serial in serials: + print("Delete token %s" % serial) + response = client.deletetoken(serial) + showresult(response.data) + + +@token.command() +@click.pass_context +@click.option("--serial", help="Serial number of the token", required=True) +@click.option("--otp1", help="First OTP value", required=True) +@click.option("--otp2", help="Second consecutive OTP value", required=True) +def resync(ctx, serial, otp1, otp2): + """ + Resynchronize the token + """ + client = ctx.obj["pi_client"] + param = {} + param["serial"] = serial + param["otp1"] = otp1 + param["otp2"] = otp2 + response = client.removetoken(param) + showresult(response.data) + + +@token.command() +@click.pass_context +@click.option("--serial", help="Serial number of the token") +@click.option("--user", help="User, whose token should be modified") +@click.option("--pin", help="Set the OTP PIN of the token") +@click.option("--otplen", help="Set the OTP lenght of the token. Usually this is 6 or 8.", + type=click.Choice(["6", "8"])) +@click.option("--syncwindow", help="Set the synchronizatio window of a token.", type=int) +@click.option("--maxfailcount", help="Set the maximum fail counter of a token.", type=int) +@click.option("--counterwindow", help="Set the window of the counter.", type=int) +@click.option("--hashlib", help="Set the hashlib.", + type=click.Choice(["sha1", "sha2", "sha256", "sha384", "sha512"])) +@click.option("--timewindow", help="Set the timewindow.", type=int) +@click.option("--timestep", help="Set the timestep. Usually 30 or 60.", type=int) +@click.option("--timeshift", help="Set the clock drift, the time shift.", type=int) +@click.option("--countauthsuccessmax", help="Set the maximum allowed successful authentications", + type=int) +@click.option("--countauthsuccess", help="Set the number of successful authentications", + type=int) +@click.option("--countauth", help="Set the number of authentications", type=int) +@click.option("--countauthmax", help="Set the maximum allowed of authentications", type=int) +@click.option("--validityperiodstart", help="Set the start date when the token is usable.") +@click.option("--validityperiodend", help="Set the end date till when the token is usable.") +@click.option("--description", help="Set the description of the token.") +@click.option("--phone", help="Set the phone number of the token.") +def set(ctx, serial, user, pin, otplen, syncwindow, maxfailcount, counterwindow, + hashlib, timewindow, timestep, timeshift, countauthsuccessmax, countauthsuccess, + countauth, countauthmax, validityperiodstart, validityperiodend, description, phone): + """ + Set certain attributes of a token + """ + client = ctx.obj["pi_client"] + param = {} + if serial: + param["serial"] = serial + if user: + param["user"] = user + if pin: + param["pin"] = pin + if otplen: + param["OtpLen"] = otplen + if syncwindow: + param["SyncWindow"] = syncwindow + if maxfailcount: + param["MaxFailCount"] = maxfailcount + if counterwindow: + param["CounterWindow"] = counterwindow + if hashlib: + param["hashlib"] = hashlib + if timewindow: + param["timeWindow"] = timewindow + if timestep: + param["timeStep"] = timestep + if timeshift: + param["timeShift"] = timeshift + if countauthsuccessmax: + param["countAuthSuccessMax"] = countauthsuccessmax + if countauthsuccess: + param["countAuthSuccess"] = countauthsuccess + if countauthmax: + param["countAuthMax"] = countauthmax + if countauth: + param["countAuth"] = countauth + if validityperiodstart: + param["validityPeriodStart"] = validityperiodstart + if validityperiodend: + param["validityPeriodEnd"] = validityperiodend + if description: + param["description"] = description + if phone: + param["phone"] = phone + + response = client.set(param) + showresult(response.data) + diff --git a/privacyideautils/commands/user.py b/privacyideautils/commands/user.py new file mode 100644 index 0000000..6ba7d0d --- /dev/null +++ b/privacyideautils/commands/user.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# +# 2020-04-13 Cornelius Kölbel +# migrate to click +# +# This code is free software; you can redistribute it and/or +# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE +# 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 . +# +import click +import datetime +import logging +from privacyideautils.clientutils import (showresult, + dumpresult, + privacyideaclient, + __version__) + +@click.group() +@click.pass_context +def user(ctx): + """ + Manage users. + """ + pass + + +@user.command() +@click.pass_context +def list(ctx): + """ + List all available users + """ + client = ctx.obj["pi_client"] + resp = client.userlist({'username': '*'}) + r1 = resp.data + result = r1['result'] + tabentry = ['username', + 'surname', + 'userid', + 'phone', + 'mobile', + 'email'] + tabsize = [20, 20, 20, 20, 20, 20] + tabstr = ["%20s", "%20s", "%20s", "%20s", "%20s", "%20s"] + tabdelim = '|' + tabvisible = [0, 1, 2, 3, 4, 5] + tabhead = ['login', 'surname', 'Id', 'phone', 'mobile', 'email'] + dumpresult(result['status'], + result['value'], + {'tabsize': tabsize, 'tabstr': tabstr, + 'tabdelim': tabdelim, 'tabvisible': tabvisible, + 'tabhead': tabhead, 'tabentry': tabentry}) + diff --git a/scripts/privacyidea b/scripts/privacyidea index 4ebe4a1..7fb81bf 100755 --- a/scripts/privacyidea +++ b/scripts/privacyidea @@ -53,1529 +53,81 @@ Thus you can avoid exposing secret credentials. """ from __future__ import print_function from six.moves import input -import argparse +import click import sys import os import datetime import subprocess -import getpass -import qrcode + +from privacyideautils.commands.token import token +from privacyideautils.commands.user import user +from privacyideautils.commands.audit import audit +from privacyideautils.commands.resolver import resolver +from privacyideautils.commands.config import config +from privacyideautils.commands.realm import realm +from privacyideautils.commands.machine import machine +from privacyideautils.commands.securitymodule import securitymodule +from privacyideautils.commands.certificate import certificate from privacyideautils.clientutils import (showresult, dumpresult, - privacyideaclient) -from privacyideautils.yubikey import (enrollYubikey, YubikeyPlug, - create_static_password, MODE_YUBICO, - MODE_OATH, MODE_STATIC) -from privacyideautils.etokenng import initetng -from privacyideautils.initdaplug import init_dongle -from privacyideautils.nitrokey import NitroKey -from email.mime.text import MIMEText -import smtplib -try: - import configparser -except ImportError: - import ConfigParser as configparser + privacyideaclient, + __version__) -VERSION = "2.14" -KNOWN_APPS = ["ssh", "luks"] DESCRIPTION = __doc__ -mode_string_mapping = {"YUBICO": MODE_YUBICO, - "OATH": MODE_OATH, - "STATIC": MODE_STATIC} - - -def yubi_mass_enroll(lotpc, - proc_params, - yubi_mode, - yubi_slot, - yubi_prefix_serial, - yubi_prefix, - yubi_prefix_random, - yubi_cr, - yubi_access_key, - yubi_new_access_key): - """ - Do the Yubikey mass enrollment - - :param lotpc: the privacyidea connnection - :param proc_params: all parameters form the command line - :type proc_params: Namespace - :param yubi_mode: yubikey modus: YUBI_STATIC_MODE, YUBI_OATH_MODE, - YUBI_AES_MODE - :param yubi_slot: slot of the yubikey [1,2] - :param yubi_prefix_serial: serial number added to the prefix - :param yubi_prefix: the public prefix - :param yubi_prefix_random: the rendom prefix - :param yubi_cr: boolean - uses as TOTP token a.k.a. Challenge Response mode - :param yubi_access_key: hexkey to programm a yubikey - :param yubi_new_access_key: new access key - """ - yp = YubikeyPlug() - description = proc_params.description - filename = proc_params.filename - print(filename) - while 0 == 0: - print("\nPlease insert the next yubikey.", end=' ') - sys.stdout.flush() - submit_param = {} - _ret = yp.wait_for_new_yubikey() - otpkey, serial, prefix = enrollYubikey( - debug=False, - APPEND_CR=not yubi_cr, - prefix_serial=yubi_prefix_serial, - fixed_string=yubi_prefix, - len_fixed_string=yubi_prefix_random, - slot=yubi_slot, - mode=yubi_mode, - challenge_response=yubi_cr, - access_key=yubi_access_key, - new_access_key=yubi_new_access_key) - if yubi_mode == MODE_OATH: - # According to http://www.openauthentication.org/oath-id/prefixes/ - # The OMP of Yubico is UB - # As TokenType we use OM (oath mode) - submit_param = {'type': 'HOTP', - 'serial': "UBOM%s_%s" % (serial, yubi_slot), - 'otpkey': otpkey, - 'description': description, - 'otplen': 6, - 'yubikey.prefix': prefix} - if yubi_cr: - submit_param['type'] = 'TOTP' - submit_param['timeStep'] = 30 - - elif yubi_mode == MODE_STATIC: - password = create_static_password(otpkey) - # print "otpkey ", otpkey - # print "password ", password - submit_param = {'serial': "UBSM%s_%s" % (serial, yubi_slot), - 'otpkey': password, - 'type': "pw", - 'description': description, - 'yubikey.prefix': prefix} - - elif yubi_mode == MODE_YUBICO: - yubi_otplen = 32 - if prefix: - yubi_otplen = 32 + len(prefix) - elif yubi_prefix_random: - # default prefix length for MODE_YUBICO is 6 - if yubi_prefix_random is None: - yubi_prefix_random = 6 - yubi_otplen = 32 + (yubi_prefix_random * 2) - # According to http://www.openauthentication.org/oath-id/prefixes/ - # The OMP of Yubico is UB - # As TokenType we use AM (AES mode) - submit_param = {'type': 'yubikey', - 'serial': "UBAM%s_%s" % (serial, yubi_slot), - 'otpkey': otpkey, - 'otplen': yubi_otplen, - 'description': description, - 'yubikey.prefix': prefix} - - else: - print("Unknown Yubikey mode") - pass - if 'realm' in proc_params: - submit_param['realm'] = proc_params.get('realm') - - if filename: - # Now we write the data to a file - f = open(filename, mode="a") - f.write("%(serial)s, %(otpkey)s, %(type)s, %(otplen)s\n" % - submit_param) - f.close() - else: - # The token is submitted to the privacyIDEA system - resp = lotpc.inittoken(submit_param) - print(resp.status) - showresult(resp.data) - - -def cifs_push(config, text): - ''' - Push the the data text to a cifs share - - :param config: dictionary with the fields cifs_server, cifs_share, - cifs_dir, cifs_user, cifs_password - :type config: dict - :param text: text to be pushed to the windows share - :type text: string - - ''' - FILENAME = datetime.datetime.now().strftime("/tmp/%y%m%d-%H%M%S" - "_privacyideaadm.out") - f = open(FILENAME, 'w') - f.write(text) - f.close() - - filename = os.path.basename(FILENAME) - - print("Pushing %s to %s//%s/%s" % (filename, - config.get("cifs_server"), - config.get("cifs_share", ""), - config.get("cifs_dir"))) - - args = ["smbclient", - "//%s\\%s" % (config.get("cifs_server"), - config.get("cifs_share", "")), - "-U", "%s%%%s" % (config.get("cifs_user"), - config.get("cifs_password")), "-c", - "put %s %s\\%s" % (FILENAME, - config.get("cifs_dir", "."), - filename)] - - p = subprocess.Popen(args, - cwd=None, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - shell=False) - (result, error) = p.communicate() - _rcode = p.returncode - print(result) - print(error) - - try: - os.remove(FILENAME) - except Exception as e: - print ("couldn't remove push test file: %r" % e) - - -def sendmail(config, text): - ''' - Send an email with the text - - :param config: dictionary with the fields mail_from, mail_to, mail_host, - mail_subject - :type config: dict - :param text: text to be sent via mail - :type text: string - - ''' - if not config.get("mail_to"): - Exception("mail_to required!") - - if not config.get("mail_host"): - Exception("mail_host required!") - - print("sending mail to %s" % config.get("mail_to")) - msg = MIMEText(text) - sender = config.get("mail_from") - recipient = config.get("mail_to") - msg['Subject'] = config.get("mail_subject") - msg['From'] = sender - msg['To'] = recipient - - mail = smtplib.SMTP(config.get("mail_host"), config.get("mail_port") or 25) - mail.ehlo() - if config.get("mail_tls"): - mail.starttls() - if config.get("mail_user"): - mail.login(config.get("mail_user"), config.get("mail_password")) - - mail.sendmail(sender, [recipient], msg.as_string()) - mail.quit() - - -def read_config(config_file): - ''' - Read the configuration/parameters from a config file - ''' - cfg = configparser.SafeConfigParser() - cfg_dict = {} - cfg.read(config_file) - for key, value in cfg.items("Default"): - cfg_dict[key] = value - - return cfg_dict - - -def options_to_dict(Option): - ''' - This takes an array Option consisting of entries like - slot=7, partition=dev3 - and converts it to a dictionary: - { "option_slot": "7", - "option_partition": "dev3" } - - :param Option: array of options - :type Option: array - :return: dictionary - ''' - options = {} - for option in Option: - opt = option.split("=") - if len(opt) == 2: - # There was exactly one equal sign and we have a key and a value - value = opt[1] - if opt[0].startswith("option_"): - key = opt[0] - else: - key = "option_" + opt[0] - options[key] = value - return options - - -def listtoken(args, client): - param = {} - if args.user: - param["user"] = args.user - if args.serial: - param["serial"] = args.serial - if args.csv: - param['outform'] = 'csv' - if args.export_fields: - param['user_fields'] = args.export_fields - resp = client.listtoken(param) - r1 = resp.data - if args.mail_host and args.mail_to: - sendmail(args, r1) - if args.cifs_server and args.cifs_user and args.cifs_password: - cifs_push(args, r1) - else: - resp = client.listtoken(param) - if resp.status == 200: - r1 = resp.data - result = r1['result'] - dumpresult(result['status'], - result['value']['tokens']) - - -def listaudit(args, client): - param = {} - if args.page: - param["page"] = args.page - if args.rp: - param["rp"] = args.rp - if args.sortname: - param["sortname"] = args.sortname - if args.sortorder: - param["sortorder"] = args.sortorder - if args.query: - param["query"] = args.query - if args.qtype: - param["qtype"] = args.qtype - r1 = client.auditsearch(param) - rows = r1.get("rows") - for row in rows: - print(row.get("cell")) - print("Page: ", r1.get("page")) - print("Total: ", r1.get("total")) - - -def listuser(args, client): - resp = client.userlist({'username': '*'}) - r1 = resp.data - result = r1['result'] - tabentry = ['username', - 'surname', - 'userid', - 'phone', - 'mobile', - 'email'] - tabsize = [20, 20, 20, 20, 20, 20] - tabstr = ["%20s", "%20s", "%20s", "%20s", "%20s", "%20s"] - tabdelim = '|' - tabvisible = [0, 1, 2, 3, 4, 5] - tabhead = ['login', 'surname', 'Id', 'phone', 'mobile', 'email'] - dumpresult(result['status'], - result['value'], - {'tabsize': tabsize, 'tabstr': tabstr, - 'tabdelim': tabdelim, 'tabvisible': tabvisible, - 'tabhead': tabhead, 'tabentry': tabentry}) - - -def inittoken(args, client): - param = {} - param["type"] = args.type - param["otpkey"] = args.otpkey - if args.genkey: - param["genkey"] = 1 - if args.user: - param["user"] = args.user - if args.serial: - param["serial"] = args.serial - if args.description: - param["description"] = args.description - if args.pin: - param["pin"] = args.pin - - if args.etng: - tokenlabel = args.user or args.label - tdata = initetng({'label': tokenlabel, - 'debug': True}) - if not tdata['userpin'] or not tdata['hmac'] or not tdata['serial']: - print("No token was added to privacyIDEA: ", tdata['error']) - sys.exit(1) - param['serial'] = tdata['serial'] - param['otpkey'] = tdata['hmac'] - param['userpin'] = tdata['userpin'] - param['sopin'] = tdata['sopin'] - print(("FIXME: what shall we do with the eToken password and " - "SO PIN: ", tdata['userpin'], tdata['sopin'])) - - resp = client.inittoken(param) - print("result: {0!s}".format(resp.status)) - showresult(resp.data) - if resp.status == 200: - if not param.get("serial"): - print("serial: {0!s}".format(resp.data.get("detail", {}).get("serial"))) - if param.get("genkey"): - print("otpkey: {0!s}".format(resp.data.get("detail", {}).get("otpkey", {}).get("value"))) - googleurl = resp.data.get("detail", {}).get("googleurl", {}).get("value") - qr = qrcode.QRCode() - qr.add_data(googleurl) - qr.print_ascii(tty=True) - - -def get_users_token_num(client, user, realm): - """ - Get the number of tokens, the user has assigned to - - :param user: username - :param realm: realmname - :return: int +CLICK_CONTEXT_SETTINGS = dict( + help_option_names=['-h', '--help'], + max_content_width=999 +) + + +def print_version(ctx, param, value): + if not value or ctx.resilient_parsing: + return + click.echo('privacyIDEA admin client version: {}'.format(__version__)) + ctx.exit() + + +@click.group(context_settings=CLICK_CONTEXT_SETTINGS) +@click.option('-v', '--version', is_flag=True, callback=print_version, + expose_value=False, is_eager=True) +@click.option('-U', '--url', required=True) +@click.option('-a', '--admin', required=True, + help='The username to authenticate against privacyIDEA.') +@click.option('-p', '--password', type=str, + prompt="Please enter your password", hide_input=True, + help='The password of the user') +@click.option('-n', '--nosslcheck', + help='Do not check the SSL certificate', is_flag=True) +@click.pass_context +def cli(ctx, url, admin, password, nosslcheck): """ - response = client.listtoken({"user": user, "realm": realm}) - result = response.data.get("result") - count = result.get("value", {}).get("count", 0) - return count + Manage your tokens on the privacyIDEA server + Examples: -def registration_enroll(args, client): - """ - enroll registration tokens for all users in a realm, who do not have a - token, yet. + \b + Enroll a token + $ privacyidea -U https://yourserver -a user token init - :param args: - :param client: - :return: """ - print(args) - response = client.userlist({"realm": args.realm}) - data = response.data - result = data.get('result') - users = result.get('value') - tokens = [] - for user in users: - username = user.get("username") - email = user.get("email") - # check, if the user has tokens - count = get_users_token_num(client, username, args.realm) - if count == 0: - # User has no token, create one. - print("Creating token for user %s" % username) - response = client.inittoken({"type": "registration", - "user": username, - "realm": args.realm}) - result = response.data.get("result") - detail = response.data.get("detail") - registrationcode = detail.get("registrationcode") - serial = detail.get("serial") - tokens.append({"username": username, - "email": email, - "serial": serial, - "registration": registrationcode}) - - for token in tokens: - if args.dump: - print(token) - else: - print("Sending email to %(email)s" % token) - config = {"mail_to": token.get("email"), - "mail_from": args.mail_from, - "mail_host": args.mail_host, - "mail_port": args.mail_port, - "mail_tls": args.mail_tls, - "mail_user": args.mail_user, - "mail_password": args.mail_password} - sendmail(config, args.mail_body % token) - - -def yubikey_mass_enroll(args, client): - yubi_mass_enroll(client, - args, - mode_string_mapping.get(args.yubimode), - int(args.yubislot), - args.yubiprefixserial, - args.yubiprefix, - args.yubiprefixrandom, - args.yubiCR, - args.access, - args.newaccess) - - -def nitrokey_mass_enroll(args, client): - NK = NitroKey() - - if args.nitromode == "TOTP": - raise Exception("At the moment we only support HOTP.") - slot = int(args.slot) - slotname = args.slotname - digits = int(args.digits) - password = args.password - print("\nWe are going to initialize your Nitrokeys. Please assure, " - "that no Nitrokey-App is active!\n") - if not password: - # Ask for the Nitrokey administrator password - password = getpass.getpass(prompt="Please enter the Nitrokey " - "Administrator Password:") - - NK.admin_login(password) - while True: - print("Please insert the next Nitrokey!") - input("Press [ENTER] when ready.") - - print("Initializing keys") - otp_key = NK.init_hotp(slot, slotname, digits=digits) - status = NK.status() - serial = "".join(status.get("card_serial","").split()).upper() - print("Enrolled token with serial: {0!s}.".format(serial)) - - param = {} - param["serial"] = "NK{0!s}_{1!s}".format(serial, args.slot) - param["otpkey"] = otp_key - param["otplen"] = int(digits) - param["type"] = args.nitromode - param["description"] = args.description or args.slotname - resp = client.inittoken(param) - showresult(resp.data) - - NK.logout() - - -def daplug_mass_enroll(args, client): - (serial, hotpkey) = init_dongle(keyboard=args.keyboard, - mapping=args.hidmap, - otplen=args.otplen) - if serial: - param = {} - param["serial"] = "DPLG%s" % serial - param["otpkey"] = hotpkey - param["otplen"] = int(args.otplen) - param["type"] = "daplug" - param["description"] = "daplug dongle" - r1 = client.inittoken(param) - showresult(r1) - - -def etokenng_mass_enroll(args, client): - print("""Mass-Enrolling eToken NG OTP. -!!! Beware the tokencontents of all tokens will be deleted. !!! - -Random User PINs and SO-PINs will be set. -The SO-PIN will be stored in the Token-Database. -""") - - param = {} - while 0 == 0: - answer = input("Please insert the next eToken NG" - " and press enter (x=Exit): ") - if "x" == answer.lower(): - break - tokenlabel = args.label - description = args.description - tdata = initetng({'label': tokenlabel, - 'debug': False, - 'description': description}) - if not tdata['userpin'] or not tdata['hmac'] or not tdata['serial']: - print("No token was added to privacyIDEA:", tdata['error']) - sys.exit(1) - param['serial'] = tdata['serial'] - param['otpkey'] = tdata['hmac'] - param['userpin'] = tdata['userpin'] - param['sopin'] = tdata['sopin'] - r1 = client.inittoken(param) - showresult(r1) - - -def assigntoken(args, client): - param = {} - param["user"] = args.user - param["serial"] = args.serial - response = client.assigntoken(param) - showresult(response.data) - - -def unassigntoken(args, client): - response = client.unassigntoken({"serial": args.serial}) - showresult(response.data) + client = privacyideaclient(admin, password, url, no_ssl_check=nosslcheck) + ctx.obj["pi_client"] = client -def importtoken(args, client): - print(args) - response = client.importtoken({'file': args.file}) - showresult(response.data) +COMMANDS = (token, user, audit, resolver, config, securitymodule, realm, machine, certificate) - -def disabletoken(args, client): - param = {} - if args.serial: - param["serial"] = args.serial - if args.user: - param["user"] = args.user - response = client.disabletoken(param) - showresult(response.data) - - -def enabletoken(args, client): - param = {} - if args.serial: - param["serial"] = args.serial - if args.user: - param["user"] = args.user - response = client.enabletoken(param) - showresult(response.data) - - -def deletetoken(args, client): - """ - Delete tokens based on - * serial number - * the username or - * the tokentype - - :param args: - :param client: - :return: - """ - serials = [] - if args.serial: - serials = [args.serial] - - elif args.user: - response = client.listtoken({"user": args.user, "realm": args.realm}) - value = response.data.get("result", {}).get("value") - for token in value.get("tokens"): - serials.append(token.get("serial")) - - elif args.type: - if not args.realm: - print("If you want to delete a tokentype, you need to specify a " - "realm!") - sys.exit(1) - response = client.listtoken({"tokenrealm": args.realm, "type": - args.type}) - value = response.data.get("result", {}).get("value") - for token in value.get("tokens"): - serials.append(token.get("serial")) - - for serial in serials: - print("Delete token %s" % serial) - response = client.deletetoken(serial) - showresult(response.data) - - -def resynctoken(args, client): - param = {} - param["serial"] = args.serial - param["otp1"] = args.otp1 - param["otp2"] = args.otp2 - response = client.removetoken(param) - showresult(response.data) - - -def settoken(args, client): - param = {} - if args.serial: - param["serial"] = args.serial - if args.user: - param["user"] = args.user - if args.pin: - param["pin"] = args.pin - if args.otplen: - param["OtpLen"] = args.otplen - if args.syncwindow: - param["SyncWindow"] = args.syncwindow - if args.maxfailcount: - param["MaxFailCount"] = args.maxfailcount - if args.counterwindow: - param["CounterWindow"] = args.counterwindow - if args.hashlib: - param["hashlib"] = args.hashlib - if args.timewindow: - param["timeWindow"] = args.timewindow - if args.timestep: - param["timeStep"] = args.timestep - if args.timeshift: - param["timeShift"] = args.timeshift - if args.countauthsuccessmax: - param["countAuthSuccessMax"] = args.countauthsuccessmax - if args.countauthsuccess: - param["countAuthSuccess"] = args.countauthsuccess - if args.countauthmax: - param["countAuthMax"] = args.countauthmax - if args.countauth: - param["countAuth"] = args.countauth - if args.validityperiodstart: - param["validityPeriodStart"] = args.validityperiodstart - if args.validityperiodend: - param["validityPeriodEnd"] = args.validityperiodend - if args.description: - param["description"] = args.description - if args.phone: - param["phone"] = args.phone - - response = client.set(param) - showresult(response.data) - - -def machine_list(args, client): - response = client.get('/machine/') - showresult(response.data) - - -def machine_get_token(args, client): - param = {} - if args.hostname: - param["hostname"] = args.hostname - if args.serial: - param["serial"] = args.serial - if args.machineid: - param["machineid"] = args.machineid - if args.resolver: - param["resolver"] = args.resolver - response = client.get("/machine/token", param) - showresult(response.data) - - -def machine_auth_item(args, client): - param = {} - if args.application: - param["application"] = args.application - if args.hostname: - param["hostname"] = args.hostname - if args.challenge: - param["challenge"] = args.challenge - - if param.get("application"): - response = client.get("/machine/authitem/%s" % param.get( - "application"), param) - else: - response = client.get("/machine/authitem", param) - showresult(response.data) - - -def machine_attachtoken(args, client): - # TODO: Migration - param = {"name": args.name, - "serial": args.serial, - "application": args.app} - ret = client.connect("/machine/addtoken", - {}, - param) - showresult(ret) - if len(args.option) > 0: - options = options_to_dict(args.option) - param.update(options) - ret = client.connect("/machine/addoption", - {}, - param) - showresult(ret) - - -def machine_detachtoken(args, client): - # TODO: Migration - ret = client.connect("/machine/deltoken", - {}, - {"name": args.name, - "serial": args.serial, - "application": args.app}) - showresult(ret) - - -def machine_addoption(args, client): - # TODO: Migration - param = {"name": args.name, - "serial": args.serial, - "application": args.app} - if len(args.option) > 0: - options = options_to_dict(args.option) - param.update(options) - ret = client.connect("/machine/addoption", - {}, - param) - showresult(ret) - - -def machine_deloption(args, client): - # TODO: Migration - param = {"name": args.name, - "serial": args.serial, - "application": args.app} - if len(args.option) > 0: - options = options_to_dict(args.option) - for k in options.keys(): - param["key"] = k - ret = client.connect("/machine/deloption", - {}, - param) - showresult(ret) - - -def getrealms(args, client): - response = client.getrealms() - showresult(response.data) - - -def setrealm(args, client): - param = {} - param['resolvers'] = ",".join(args.resolver) - response = client.setrealm(args.realm, param) - showresult(response.data) - - -def deleterealm(args, client): - response = client.deleterealm(args.realm) - showresult(response.data) - - -def setdefaultrealm(args, client): - response = client.setdefaultrealm(args.realm) - showresult(response.data) - - -def getresolvers(args, client): - response = client.getresolver({}) - showresult(response.data) - - -def deleteresolver(args, client): - response = client.deleteresolver(args.resolver) - showresult(response.data) - - -def setresolver(args, client): - if args.type.lower() == "passwd": - response = client.setresolver(args.resolver, {"type": args.type, - "filename": - args.filename}) - showresult(response.data) - - -def securitymodule(args, client): - if args.init_hsm: - password = getpass.getpass(prompt="Please enter password for" - " security module:") - print("Setting the password of your security module") - response = client.set_hsm(param={"password": str(password)}) - else: - print("This is the configuration of your active Security module:") - response = client.get_hsm() - showresult(response.data) - - -def get_config(args, client): - response = client.getconfig({}) - showresult(response.data) - - -def set_config(args, client): - for config in args.config: - param = {} - (k, v) = config.split("=") - param[k] = v - response = client.setconfig(param) - showresult(response.data) - - -def del_config(args, client): - for k in args.key: - response = client.deleteconfig(k) - showresult(response.data) - - -def create_arguments(): - parser = argparse.ArgumentParser(description=DESCRIPTION, - fromfile_prefix_chars='@') - parser.add_argument("-U", "--url", - help="The URL of the privacyIDEA server including " - "protocol and port like " - "https://localhost:5001") - parser.add_argument("-a", "--admin", - help="The name of an administrator or a normal " - "user like 'admin' or 'user@realm2'. The " - "administrator will be able to perform all " - "tasks, while the user will only be able to " - "enroll tokens and list tokens of his own.") - parser.add_argument("-r", "--adminrealm", - help="The realm of the administrator like " - "'admin'", - default="") - parser.add_argument("-p", "--password", - help="The password of the user. Please " - "avoid to post the password at the command line. " - "You will be asked for it - or you can provide the " - "password in a configuration file. " - "Note, that you can write a file password.txt " - "containing two lines '--password' and the second " - "line the password itself and add this to the " - "command line with @password.txt") - parser.add_argument("-v", "--version", - help="Print the version of the program.", - action='version', version='%(prog)s ' + VERSION) - parser.add_argument("--nosslcheck", - help="Do not check SSL certificates.", - action="store_true") - - subparsers = parser.add_subparsers(help="The available commands. Running " - " -h will give you a detailed " - "help on this command.", - title="COMMANDS", - description="The command line tool " - "requires one command, to know what " - "action it should take") - - ################################################################ - # - # listuser - # - p_listuser = subparsers.add_parser('user', - help="list the available users.") - p_listuser.set_defaults(func=listuser) - - ################################################################ - # - # audit - # - p_audit = subparsers.add_parser('audit', - help="list the audit log.") - p_audit.set_defaults(func=listaudit) - p_audit.add_argument("--page", - help="The page number to view", - type=int) - p_audit.add_argument("--rp", - help="The number of entries per page", - type=int) - p_audit.add_argument("--sortname", - help="The name of the column to sort by", - default="number") - p_audit.add_argument("--sortorder", - help="The order to sort (desc, asc)", - default="desc") - p_audit.add_argument("--query", - help="A search tearm to search for") - p_audit.add_argument("--qtype", - help="The column to search for") - - ################################################################ - # - # token commands - # - token_parser = subparsers.add_parser("token", - help="token commands used to " - "list tokens, assign, enroll, resync " - "...") - token_sub = token_parser.add_subparsers() - - # listtokens - p_listtoken = token_sub.add_parser('list', - help='list the available tokens.') - p_listtoken.set_defaults(func=listtoken) - p_listtoken.add_argument("--serial", help="The serial number of the token " - "to list. May contain wildcards.") - p_listtoken.add_argument("--user", - help="List all tokens of this given user.") - p_listtoken.add_argument('--csv', action="store_true", - help='output as csv format') - p_listtoken.add_argument('--export_fields', - help="comma separated list of additional " - "fields to export into the CSV export.") - p_listtoken.add_argument("--mail_host", - help="If exporting as CSV you can send the " - "result as mail via this mail host.") - p_listtoken.add_argument("--mail_to", - help="If exporting as CSV you can send the " - "result to this email address.") - p_listtoken.add_argument("--cifs_server", - help="If exporting as CSV you can save the " - "result to this CIFS server.") - p_listtoken.add_argument("--cifs_user", - help="If exporting as CSV you can save the " - "result to a CIFS server with this username.") - p_listtoken.add_argument("--cifs_password", - help="If exporting as CSV you can save the " - "result to a CIFS server with this password.") - # inittoken - p_inittoken = token_sub.add_parser("init", - help="Initialize a token. I.e. create " - "a new token in privacyidea.") - p_inittoken.set_defaults(func=inittoken) - p_inittoken.add_argument("--user", help="If a user is specified, the " - "token is directly assigned to this user.") - p_inittoken.add_argument("--serial", help="This is the new serial number " - "of the token") - p_inittoken.add_argument("--description", help="The description of the " - "token. This can be used to identify the token " - "more easily.", - default="command line enrolled") - p_inittoken.add_argument("--pin", help="The OTP PIN of the token.") - p_inittoken.add_argument("--otpkey", help="The OTP key, like the HMAC key") - p_inittoken.add_argument("--genkey", help="Generate an HOTP key", - action="store_true") - p_inittoken.add_argument("--type", help="The token type", - choices=["hotp", "totp", "pw", "spass", "dpw", - "ssh", "sms", "email", "yubico", - "registration"], - default="hotp") - p_inittoken.add_argument("--etng", help="If specified, an etoken NG will " - "be initialized", action="store_true") - # registration_enroll - # Will enroll a registration token to each user in a realm, who has now - # token, yet. - p_registration = token_sub.add_parser("registration", - help="Enroll registration tokens to " - "users in a realm.") - p_registration.set_defaults(func=registration_enroll) - p_registration.add_argument("--realm", help="The realm for which " - "registration tokens should " - "be enrolled.", - required=True) - p_registration.add_argument("--dump", - help="Do not send notification " - "email to the user, but dump " - "the data to stdout.", - action="store_true") - p_registration.add_argument("--mail_host", - help="Mailserver to send notification.", - required=True) - p_registration.add_argument("--mail_from", help="Mail sender address.", - required=True) - p_registration.add_argument("--mail_subject", help="Mail subject.", - required=True) - p_registration.add_argument("--mail_body", - help="Mail body. Should contain %%(username)s " - "and %%(registration)s", - required=True) - p_registration.add_argument("--mail_port", help="Port of the mailserver", - default=25) - p_registration.add_argument("--mail_tls", - help="If mailserver supports STARTTLS.", - default=False, - action="store_true") - p_registration.add_argument("--mail_user", - help="Username, if required by mailserver.") - p_registration.add_argument("--mail_password", - help="Password, if required by mailserver.") - # yubikey_mass_enroll - p_ykmass = token_sub.add_parser("yubikey_mass_enroll", - help="Initialize a bunch of yubikeys") - p_ykmass.set_defaults(func=yubikey_mass_enroll) - p_ykmass.add_argument("--yubiprefix", help="A prefix that is outputted " - "by the yubikey", - default="") - p_ykmass.add_argument("--yubiprefixrandom", help="A random prefix " - "of length. For YUBICO mode the default will be 6!", - metavar="NUMBER", - type=int, - default=None) - p_ykmass.add_argument("--yubiprefixserial", - help="Use the serial number of " - "the yubikey as prefix.", action="store_true") - p_ykmass.add_argument("--yubimode", help="The mode the yubikey should " - "be initialized in. (default=OATH)", - choices=["OATH", "YUBICO", "STATIC"], - default="OATH") - p_ykmass.add_argument("--filename", - help="If the initialized yubikeys should not be " - "sent to a privacyIDEA server the otpkeys can " - "be written to a CSV file, to be imported " - "later.") - p_ykmass.add_argument("--yubislot", help="The slot of the yubikey, that " - "is initialized (default=1)", - choices=["1", "2"], - default="1") - p_ykmass.add_argument("--yubiCR", - help="Initialize the yubikey in challenge/" - "response mode.", - action="store_true") - p_ykmass.add_argument("--description", help="The description of the " - "token. This can be used to identify the token " - "more easily.", - default="command line enrolled") - p_ykmass.add_argument("--access", - help="Use this hexlified access key to programm the " - "yubikey") - p_ykmass.add_argument("--newaccess", - help="Set a new access key, so that the yubikey will" - "only programmable with this new access key. " - "You can reset the access key by setting the " - "new access key to '000000000000'.") - - # nitrokey_mass_enroll - p_nitro = token_sub.add_parser("nitrokey_mass_enroll", - help="Initialize a bunch of Nitrokeys") - p_nitro.set_defaults(func=nitrokey_mass_enroll) - p_nitro.add_argument("--nitromode", help="Either HOTP or TOTP", - choices=["HOTP", "TOTP"], - default="HOTP") - p_nitro.add_argument("--slot", help="The slot of the yubikey, that is " - "initialized (default=0)", - choices=["{0!s}".format(x) for x in range(0, 16)], - default="0") - p_nitro.add_argument("--description", help="The description of the " - "token. This can be used to identify the token " - "more easily.") - p_nitro.add_argument("--pin", - help="The Admin password of the Nitrokey") - p_nitro.add_argument("--digits", - help="The number of allowed OTP digits.", - choices=[6, 8], default=6) - p_nitro.add_argument("--slotname", - help="The name of the OTP slot.", - default="privacyIDEA") - - # daplug_mass_enroll - p_daplug = token_sub.add_parser("daplug_mass_enroll", - help="Initialize a bunch of " - "daplug dongles.") - p_daplug.set_defaults(func=daplug_mass_enroll) - p_daplug.add_argument("-k", "--keyboard", action="store_true", - help="If this option is set, the daplug will " - "simulate " - "a keyboard and type the OTP value when plugged in.") - p_daplug.add_argument("--hidmap", - help="Specify the HID mapping. The default HID " - "mapping is 05060708090a0b0c0d0e. Only use this, " - "if you know " - "what you are doing!", - default="05060708090a0b0c0d0e") - p_daplug.add_argument("--otplen", choices=["6", "8"], - help="Specify if the OTP length should be 6 or 8.") - - # etokenng_mass_enroll - p_etngmass = token_sub.add_parser("etokenng_mass_enroll", - help="Enroll a bunch of eToken NG OTP.") - p_etngmass.set_defaults(func=etokenng_mass_enroll) - p_etngmass.add_argument("--label", - help="The label of the eToken NG OTP.", - default="privacyIDEAToken") - p_etngmass.add_argument("--description", - help="Description of the token.", - default="mass enrolled") - - # assigntoken - p_assigntoken = token_sub.add_parser("assign", - help="Assign a token to a user") - p_assigntoken.set_defaults(func=assigntoken) - p_assigntoken.add_argument("--serial", help="Serial number of the token " - "to assign", - required=True) - p_assigntoken.add_argument("--user", help="The user, who should get " - "the token", - required=True) - - # unassigntoken - p_unassigntoken = token_sub.add_parser("unassign", - help="Assign a token to a user") - p_unassigntoken.set_defaults(func=unassigntoken) - p_unassigntoken.add_argument("--serial", help="Serial number of the token " - "to unassign", - required=True) - - # importtoken - p_import = token_sub.add_parser("import", - help="Import a token file") - p_import.set_defaults(func=importtoken) - p_import.add_argument("-f", "--file", help="The token file to import", - required=True) - - # disabletoken - p_disable = token_sub.add_parser("disable", - help="Disable token by serial or by user") - p_disable.set_defaults(func=disabletoken) - p_disable.add_argument("--serial", - help="serial number of the token to disable") - p_disable.add_argument("--user", - help="The username of the user, whose tokens " - "should be disabled") - - # enabletoken - p_enable = token_sub.add_parser("enable", - help="Enable token by serial or by user") - p_enable.set_defaults(func=enabletoken) - p_enable.add_argument("--serial", - help="serial number of the token to enable") - p_enable.add_argument("--user", - help="The username of the user, whose tokens should " - "be enabled") - - # removetoken - p_remove = token_sub.add_parser("delete", - help="Delete token by serial or by user") - p_remove.set_defaults(func=deletetoken) - p_remove.add_argument("--serial", - help="serial number of the token to remove") - p_remove.add_argument("--user", - help="The username of the user, whose tokens should " - "be deleted") - p_remove.add_argument("--realm", - help="Delete all tokens of the given type in this " - "realm.") - p_remove.add_argument("--type", - help="Delete all tokens of this type in the given " - "realm.") - - # resynctoken - p_resync = token_sub.add_parser("resync", - help="Resynchronize the token") - p_resync.set_defaults(func=resynctoken) - p_resync.add_argument("--serial", help="Serial number of the token", - required=True) - p_resync.add_argument("--otp1", help="First OTP value", - required=True) - p_resync.add_argument("--otp2", help="Second consecutive OTP value", - required=True) - - # set - p_set = token_sub.add_parser("set", - help="Set certain attributes of a token.") - p_set.set_defaults(func=settoken) - p_set.add_argument("--serial", help="Serial number of the token") - p_set.add_argument("--user", help="User, whose token should be modified") - p_set.add_argument("--pin", help="Set the OTP PIN of the token") - p_set.add_argument("--otplen", - help="Set the OTP lenght of the token. Usually this is " - "6 or 8.", - type=int) - p_set.add_argument("--syncwindow", - help="Set the synchronizatio window of a token.", - type=int) - p_set.add_argument("--maxfailcount", - help="Set the maximum fail counter of a token.", - type=int) - p_set.add_argument("--counterwindow", - help="Set the window of the counter.", - type=int) - p_set.add_argument("--hashlib", - help="Set the hashlib.", - choices=["sha1", "sha2", "sha256", "sha384", "sha512"]) - p_set.add_argument("--timewindow", - help="Set the timewindow.", - type=int) - p_set.add_argument("--timestep", - help="Set the timestep. Usually 30 or 60.", - type=int) - p_set.add_argument("--timeshift", - help="Set the clock drift, the time shift.", - type=int) - p_set.add_argument("--countauthsuccessmax", - help="Set the maximum allowed successful " - "authentications", - type=int) - p_set.add_argument("--countauthsuccess", - help="Set the number of successful authentications", - type=int) - p_set.add_argument("--countauth", - help="Set the number of authentications", - type=int) - p_set.add_argument("--countauthmax", - help="Set the maximum allowed of authentications", - type=int) - p_set.add_argument("--validityperiodstart", - help="Set the start date when the token is usable.") - p_set.add_argument("--validityperiodend", - help="Set the end date till when the token is usable.") - p_set.add_argument("--description", - help="Set the description of the token.") - p_set.add_argument("--phone", - help="Set the phone number of the token.") - - ##################################################################### - # - # machine META commands - # - machine_parser = subparsers.add_parser("machine", - help="machine commands used to " - "list machines and assign " - "tokens and applications to these " - "machines") - machine_sub = machine_parser.add_subparsers() - # createmachine - p_createmachine = machine_sub.add_parser("list", - help="List the machines found in " - "the machine resolvers.") - p_createmachine.set_defaults(func=machine_list) - - # getttokenapps - #p_gettapps = machine_sub.add_parser("gettokenapps", - # help="get the application definitions " - # "of " - # "a machine, including the apps, the " - # "serial numbers and the " - # "authentication items") - #p_gettapps.set_defaults(func=gettokenapps) - #p_gettapps.add_argument("--name", help="The name of the machine") - #p_gettapps.add_argument("--app", help="The name of the application", - # choices=KNOWN_APPS) - #p_gettapps.add_argument("--serial", help="The serial number of the token") - #p_gettapps.add_argument("--challenge", - # help="A challenge value, that might " - # "be needed to return an authentication item") - #p_gettapps.add_argument("--challenge_hex", - # help="A challenge value in hexadecimal format, " - # "that " - # "might be needed to return an authentication item") - - # machine_addtoken - #p_maddtoken = machine_sub.add_parser("addtoken", - # help="Add a token with an " - # "application " - # "to a machine") - #p_maddtoken.set_defaults(func=machine_addtoken) - #p_maddtoken.add_argument("--name", help="The name of the machine", - # required=True) - #p_maddtoken.add_argument("--serial", help="The serial number of the token", - # required=True) - #p_maddtoken.add_argument("--app", help="The name of the application", - # required=True, - # choices=KNOWN_APPS) - #p_maddtoken.add_argument("--option", - # help="Special option for the " - # "application. Like the user for SSH or the " - # "partition or slot for LUKS.", - # action="append") - - # machine_showtoken - p_mshowtoken = machine_sub.add_parser("showtoken", - help="List the token machine " - "mapping. " - "You can list all mappings for a " - "token, " - "for a machine or for an " - "application") - p_mshowtoken.set_defaults(func=machine_get_token) - p_mshowtoken.add_argument("--hostname", help="The name of the machine to " - "show") - p_mshowtoken.add_argument("--machineid", help="The machine ID in the " - "machine resolver.") - p_mshowtoken.add_argument("--resolver", help="The name of the machine " - "resolver") - p_mshowtoken.add_argument("--serial", - help="The serial number of the token," - " that is assigned in those mappings.") - - # get authentication item - p_mauthitem = machine_sub.add_parser("authitem", - help="Get the authentication item " - "for the given machine and " - "application") - p_mauthitem.set_defaults(func=machine_auth_item) - p_mauthitem.add_argument("--hostname", required=True, - help="The hostname of the machine, for which the" - "authitem should be retrieved.") - p_mauthitem.add_argument("--application", - help="The application, which authitems should be " - "retrieved.") - p_mauthitem.add_argument("--challenge", - help="If the application requires a challenge, " - "you can pass a challenge, for which the " - "authitem will be returned.") - - # machine_deltoken - #p_mdeltoken = machine_sub.add_parser("deltoken", - # help="Delete a mapping from a " - # "machine. " - # "This does not delete the machine") - #p_mdeltoken.set_defaults(func=machine_deltoken) - #p_mdeltoken.add_argument("--name", - # help="The name of the machine to delete", - # required=True) - #p_mdeltoken.add_argument("--serial", - # help="The serial number of the token, " - # "that is mapped", required=True) - #p_mdeltoken.add_argument("--app", - # help="The name of the application that is " - # "mapped.", required=True, - # choices=KNOWN_APPS) - - # machine_addoption - #p_maddoption = machine_sub.add_parser("addoption", - # help="Add an option to a machine " - # "mapping.") - #p_maddoption.set_defaults(func=machine_addoption) - #p_maddoption.add_argument("--name", help="The name of the machine of the " - # "mapping to add the option to.", - # required=True) - #p_maddoption.add_argument("--serial", - # help="The serial number of the token " - # "of the mapping to add the option to.", - # required=True) - #p_maddoption.add_argument("--app", - # help="The name of the application of the " - # "mapping to add the option to", - # required=True, - # choices=KNOWN_APPS) - #p_maddoption.add_argument("--option", - # help="The option to add. It should be " - # "passed like key=value. You can add several " - # "options " - # "at once.", required=True, - # action="append") - - # machine_deloption - #p_mdeloption = machine_sub.add_parser("deloption", - # help="Delete an option from " - # "a machine mapping.") - #p_mdeloption.set_defaults(func=machine_deloption) - #p_mdeloption.add_argument("--name", help="The name of the machine of the " - # "mapping to delete the option from.", - # required=True) - #p_mdeloption.add_argument("--serial", - # help="The serial number of the token " - # "of the mapping to to delete the option from.", - # required=True) - #p_mdeloption.add_argument("--app", - # help="The name of the application of the " - # "mapping to delete the option from.", - # required=True, - # choices=KNOWN_APPS) - #p_mdeloption.add_argument("--option", - # help="The option to delete. You only " - # "need to pass the key of the option. You can " - # "specify several options at once.", - # required=True, - # action="append") - - ################################################################### - # - # securitymodule - # - p_securitymodule = subparsers.add_parser("securitymodule", - help="Get the status of the " - "securitymodule or set the " - "password " - "of the securitymodule") - p_securitymodule.set_defaults(func=securitymodule) - p_securitymodule.add_argument("--init_hsm", - help="Initialize the module by entering the " - "password", - action="store_true") - - ################################################################ - # - # getconfig - # - config_parser = subparsers.add_parser("config", - help="server configuration") - config_sub = config_parser.add_subparsers() - - p_getconfig = config_sub.add_parser("get", - help="returns the configuration of " - "the privacyIDEA server.") - p_getconfig.set_defaults(func=get_config) - - # setconfig - p_setconfig = config_sub.add_parser("set", - help="set a configuration value of " - "the privacyIDEA server.") - p_setconfig.set_defaults(func=set_config) - p_setconfig.add_argument("--config", required=True, action="append", - help="Use the config like --config=value=key. " - "You can use several --config arguments.") - - # delconfig - p_delconfig = config_sub.add_parser("delete", - help="delete a configuration value of " - "the privacyIDEA server.") - p_delconfig.set_defaults(func=del_config) - p_delconfig.add_argument("--key", required=True, action="append", - help="Specify the config key to delete. " - "You can use several --key arguments.") - - ############################################################### - # - # realm - # - realm_parser = subparsers.add_parser("realm", - help="realm configuration") - realm_sub = realm_parser.add_subparsers() - # get - p_getrealm = realm_sub.add_parser("get", - help="returns a list of the realms") - p_getrealm.set_defaults(func=getrealms) - - # set - p_setrealm = realm_sub.add_parser("set", - help="Create a new realm") - p_setrealm.set_defaults(func=setrealm) - p_setrealm.add_argument("--realm", required=True, - help="The name of the new realm") - p_setrealm.add_argument("--resolver", required=True, action="append", - help="The name of the resolver. You can specify " - "several resolvers by using several --resolver " - "arguments.") - - # delete - p_deleterealm = realm_sub.add_parser("delete", - help="returns a list of the realms") - p_deleterealm.set_defaults(func=deleterealm) - p_deleterealm.add_argument("--realm", required=True, - help="The name of the realm to delete") - - # set default realm - p_defaultrealm = realm_sub.add_parser("default", - help="The the default realm") - p_defaultrealm.set_defaults(func=setdefaultrealm) - p_defaultrealm.add_argument("--realm", required=True, - help="The name of the realm that should be " - "the default realm.") - - ################################################################ - # - # resolver - resolver_parser = subparsers.add_parser("resolver", - help="resolver configuration") - resolver_sub = resolver_parser.add_subparsers() - # get - p_getresolver = resolver_sub.add_parser("get", - help="Returns a list of the " - "resolvers.") - p_getresolver.set_defaults(func=getresolvers) - - # set - p_setresolver = resolver_sub.add_parser("set", - help="Create a new resolver.") - p_setresolver.set_defaults(func=setresolver) - p_setresolver.add_argument("--resolver", required=True, - help="The name of the new resolver.") - p_setresolver.add_argument("--type", required=True, - choices=["LDAP", "SQL", "PASSWD", "SCIM"], - help="The type of the new resolver") - p_setresolver.add_argument("--filename", - help="The filename for Passwdresolvers") - - # delete - p_deleteresolver = resolver_sub.add_parser("delete", - help="Delete a resolver.") - p_deleteresolver.set_defaults(func=deleteresolver) - p_deleteresolver.add_argument("--resolver", required=True, - help="The name of the resolver to delete.") - - args = parser.parse_args() - return args +for cmd in COMMANDS: + cli.add_command(cmd) def main(): - args = create_arguments() - client = None - if args.url and hasattr(args, 'func'): - if not args.password: - password = getpass.getpass(prompt="Please enter password for" - " '%s':" % args.admin) - else: - password = args.password - - # Create the privacyideaclient instance - client = privacyideaclient(args.admin, - password, - args.url, - no_ssl_check=args.nosslcheck) - - args.func(args, client) - else: - print("Missing parameters") + try: + cli(obj={}) + except ValueError as e: + sys.write.err('Error', exc_info=e) + click.echo('Error: ' + str(e)) + sys.exit(1) if __name__ == '__main__': diff --git a/setup.py b/setup.py index a1c862b..95d78df 100644 --- a/setup.py +++ b/setup.py @@ -5,9 +5,9 @@ import os import sys -version = "2.23" +version = "3.0" name = 'privacyideaadm' -release = '2.23.5' +release = '3.0' # Taken from kennethreitz/requests/setup.py package_directory = os.path.realpath(os.path.dirname(__file__)) @@ -39,7 +39,8 @@ def get_file_contents(file_path): author='Cornelius Kölbel', author_email='cornelius@privacyidea.org', url='http://www.privacyidea.org', - packages=['privacyideautils'], + packages=['privacyideautils', + 'privacyideautils.commands'], setup_requires=['sphinx <= 1.8.5;python_version<"3.0"', 'sphinx >= 2.0;python_version>="3.0"'], install_requires=[ @@ -49,7 +50,9 @@ def get_file_contents(file_path): "pyusb", "qrcode", "requests", - 'six' + 'six', + "click", + "python-yubico" ], cmdclass=cmdclass, command_options={