From 756f6cbe6635019b192dae45d8d7e349b20113d1 Mon Sep 17 00:00:00 2001 From: vuko Date: Wed, 5 May 2021 21:08:08 +0200 Subject: [PATCH] initial commit --- README.rst | 2 + build.nix | 3 + default.nix | 16 ++++ hsadmin/cmd.py | 145 ++++++++++++++++++++++++++++++++++ hsadmin/krb5.conf | 13 +++ hsadmin/password_reset.jinja2 | 8 ++ python-kadmin.nix | 24 ++++++ setup.py | 23 ++++++ 8 files changed, 234 insertions(+) create mode 100644 README.rst create mode 100644 build.nix create mode 100644 default.nix create mode 100644 hsadmin/cmd.py create mode 100644 hsadmin/krb5.conf create mode 100644 hsadmin/password_reset.jinja2 create mode 100644 python-kadmin.nix create mode 100644 setup.py diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..31dafdb --- /dev/null +++ b/README.rst @@ -0,0 +1,2 @@ +Command line tool for managing user accounts at Warsaw Hackerspace. For now only +password reset is implemented. diff --git a/build.nix b/build.nix new file mode 100644 index 0000000..825f3e0 --- /dev/null +++ b/build.nix @@ -0,0 +1,3 @@ +{ pkgs ? import {} }: + +pkgs.python3Packages.callPackage (import ./default.nix) {} diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..124d459 --- /dev/null +++ b/default.nix @@ -0,0 +1,16 @@ +{ buildPythonPackage, setuptools, jinja2, ldap3, apg, callPackage }: + +let + python-kadmin = callPackage (import ./python-kadmin.nix) {}; +in buildPythonPackage rec { + pname = "hs-admin-${version}"; + version = "0.0.1"; + propagatedBuildInputs = [ + python-kadmin + setuptools + jinja2 + ldap3 + apg + ]; + src = ./.; +} diff --git a/hsadmin/cmd.py b/hsadmin/cmd.py new file mode 100644 index 0000000..7978133 --- /dev/null +++ b/hsadmin/cmd.py @@ -0,0 +1,145 @@ +import os +from pkg_resources import resource_filename, cleanup_resources, resource_string +import atexit +from pathlib import Path + +atexit.register(cleanup_resources, force=True) +os.environ["KRB5_CONFIG"] = str( + Path(resource_filename(__name__, "krb5.conf")).resolve() +) + +import kadmin +import getpass +import argparse +import secrets +import string +import shutil +import logging + +from ldap3 import Server, Connection, LEVEL +from ldap3.utils.conv import escape_filter_chars +from ldap3.utils.dn import escape_rdn + +import subprocess + +import smtplib +from jinja2 import Template +from email.message import EmailMessage + +FROM_LDAP = object() + +parser = argparse.ArgumentParser() +parser.add_argument("--admin", default=getpass.getuser()) +parser.add_argument("--verbose", action="store_true") +subparsers = parser.add_subparsers(dest="cmd", help="command") +reset_password = subparsers.add_parser( + "reset_password", + help="change user password to newly generated one and send it to his email address from LDAP", +) +reset_password.add_argument("user") +reset_password.add_argument( + "--show-password", action="store_true", help="print generated password" +) +reset_password.add_argument("email_address", default=FROM_LDAP, nargs="?") + +APG_CMD = shutil.which("apg") + + +def generage_password(length=15): + if APG_CMD is None: + logging.warning("apg command not found. Using built in password generator") + pool = string.ascii_lowercase + string.ascii_uppercase + string.digits + password = "".join([secrets.choice(pool) for _ in range(length)]) + else: + password = ( + subprocess.run( + [APG_CMD, "-m", str(length), "-n", "1", "-M", "NCL"], + check=True, + capture_output=True, + ) + .stdout.decode() + .strip() + ) + + if len(password) != length: + raise Exception("Password generation failed") + return password + + +def main(): + args = parser.parse_args() + logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO) + if args.cmd == "reset_password": + admin_pass = getpass.getpass(f"{args.admin}@HACKERSPACE.PL password: ") + logging.debug("initializing kadmin") + k = kadmin.init_with_password(f"{args.admin}/admin", admin_pass) + logging.debug("kadmin initialized") + user = args.user + if user is None: + user = input("User: ") + p = k.get_principal(user) + + password = generage_password() + if args.email_address is not FROM_LDAP: + address = args.email_address + else: + address = get_email_address(args.admin, admin_pass, user) + + if args.show_password: + print(f'password: "{password}"') + + i = input( + f"Type yes to reset {user}'s password and send email to {address}\n" + ).strip() + if i != "yes": + print("Aborted") + return + + p.change_password(password) + print("password changed") + + send_mail(args.admin, admin_pass, password, user, address) + print("email sent") + else: + parser.print_help() + + +def get_email_address(admin, admin_pass, uid): + logging.debug("fetching email address from LDAP") + s = Server("ldap.hackerspace.pl", use_ssl=True) + with Connection( + s, + user=f"uid={escape_rdn(admin)},ou=People,dc=hackerspace,dc=pl", + password=admin_pass, + raise_exceptions=True, + ) as c: + logging.debug("connected to LDAP server") + c.search( + search_base="ou=People,dc=hackerspace,dc=pl", + search_filter=f"(uid={escape_filter_chars(uid)})", + search_scope=LEVEL, + attributes=["mailRoutingAddress"], + ) + if not c.entries: + raise Exception("empty response") + if len(c.entries) > 1: + raise Exception("too many responses") + address = c.entries[0]["mailRoutingAddress"] + logging.debug(f"got mail address from LDAP: {address}") + return address + + +def send_mail(admin, admin_password, password, user, address): + mail_template = Template( + resource_string(__name__, "password_reset.jinja2").decode() + ) + msg = EmailMessage() + config = {"password": password, "user": user, "admin": admin} + msg.set_content(mail_template.render(config)) + msg["Subject"] = f"Password reset for {user}@hackerspace.pl" + msg["From"] = f"{admin}@hackerspace.pl" + msg["To"] = address + + with smtplib.SMTP_SSL("mail.hackerspace.pl") as s: + s.login(admin, admin_password) + s.send_message(msg) diff --git a/hsadmin/krb5.conf b/hsadmin/krb5.conf new file mode 100644 index 0000000..8ed94b9 --- /dev/null +++ b/hsadmin/krb5.conf @@ -0,0 +1,13 @@ +[libdefaults] + default_realm = HACKERSPACE.PL +[realms] + HACKERSPACE.PL = { + admin_server = hackerspace.pl + kdc = kerberos.hackerspace.pl + default_domain = hackerspace.pl + sasl-realm = HACKERSPACE.PL + } + +[domain_realm] + .hackerspace.pl = HACKERSPACE.PL + hackerspace.pl = HACKERSPACE.PL diff --git a/hsadmin/password_reset.jinja2 b/hsadmin/password_reset.jinja2 new file mode 100644 index 0000000..078fbab --- /dev/null +++ b/hsadmin/password_reset.jinja2 @@ -0,0 +1,8 @@ +Hi, + +Password for your Warsaw Hackerspace account has been resetted. + +user: {{ user }} +password: {{ password }} + +You can change it here: https://profile.hackerspace.pl diff --git a/python-kadmin.nix b/python-kadmin.nix new file mode 100644 index 0000000..4fdc9c0 --- /dev/null +++ b/python-kadmin.nix @@ -0,0 +1,24 @@ +{ buildPythonPackage, bison, krb5, fetchFromGitHub }: + +buildPythonPackage rec { + pname = "python-kadmin-${version}"; + version = "0.0.2"; + nativeBuildInputs = [ bison ]; + buildInputs = [ krb5 ]; + src = fetchFromGitHub { + owner = "nightfly19"; + repo = "python-kadmin"; + #rev = "c1acec9d197b79e3f51928aad6df0f99e86283c2"; + #sha256 = "0jb0k998624scy204im068kwhnwa2l6vag69qi3hnmlpr1q3wh0z"; + rev = "31d25f734b926b71e15d4c2f3a2e68decf8a465b"; + sha256 = "1vnmrd9sz08sr3nsg1n1rgwrqai802hba8gqybgabblbza4kg4x1"; + }; + preConfigure = '' + substituteInPlace setup.py --replace '["/usr/include/", "/usr/include/et/"]' '["${krb5.dev}/include"]' + ''; + postInstall = '' + ln -s $src $out + ''; + #doCheck=false; + doCheck=true; +} diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..fbed873 --- /dev/null +++ b/setup.py @@ -0,0 +1,23 @@ +from setuptools import setup + +setup( + name="hsadmin", + version="0.1", + description="Warsaw Hackerspace account managment cli", + author="vuko", + author_email="vuko@hackerspace.pl", + classifiers=[ + "Development Status :: 3 - Alpha", + "License :: OSI Approved :: zlib/libpng License", + "Programming Language :: Python :: 3.8", + ], + packages=["hsadmin"], + python_requires=">=3.8,", + install_requires=["kadmin", "setuptools", "jinja2", "ldap3"], + package_data={"hsadmin": ["krb5.conf", "password_reset.jinja2"]}, + entry_points={ + "console_scripts": [ + "hs-admin=hsadmin.cmd:main", + ] + }, +)