#!/usr/bin/python

#
# Copyright (C) 2023 Nethesis S.r.l.
# SPDX-License-Identifier: GPL-2.0-only
#

# Used env variables:
# - username
# - password (this is the OTP)
# - config

import os
import re
import sys
import pyotp
import binascii
import subprocess
from euci import EUci
from nethsec import users

def debug(msg):
    if os.environ.get("debug", False):
        print(f'[DEBUG] {msg}', file=sys.stderr)

username = os.environ.get("username")
otp = os.environ.get("password")
config_path = os.environ.get("config")

instance = re.sub(r'^openvpn-|\.conf$', '', config_path)

uci = EUci()
try:
    db = uci.get("openvpn", instance, "ns_user_db")
except:
    # user db not set
    sys.exit(5)

debug(f"Username: {username}")
debug(f"Database: {db}")
user = users.get_user_by_name(uci, username, db)
if user is None:
    # user not found
    sys.exit(4)

if not "openvpn_enabled" in user or user["openvpn_enabled"] != "1":
    # user not enabled
    sys.exit(3)

if "openvpn_2fa" not in user:
    # 2fa secret not set
    sys.exit(2)

# If user DB is remote, check the existence of the user
# in the LDAP server
if uci.get("users", db) == "ldap":
    debug("User is remote")
    ldap = uci.get_all("users", db)
    uri = ldap.get("uri", "")

    base_dn = ldap.get("base_dn", "")
    # default for user_dn is the base_dn
    user_dn = ldap.get("user_dn", base_dn)
    user_attr = ldap.get("user_attr", "uid")
    tls_reqcert = ldap.get("tls_reqcert", "never")
    starttls = int(ldap.get("start_tls", 0))
    bind_dn = ldap.get("bind_dn")
    bind_password = ldap.get("bind_password")

    if not uri or not base_dn:
        # no info to connect ldap db
        debug(f"Error: invalid LDAP URI or base DN")
        sys.exit(5)

    ldapsearch_command = [
        "/usr/bin/ldapsearch",
        "-LLL",
        "-H", uri,
        "-b", base_dn,
    ]

    if bind_dn:
        ldapsearch_command.append("-D")
        ldapsearch_command.append(bind_dn)
        ldapsearch_command.append("-w")
        ldapsearch_command.append(bind_password)
    else:
        ldapsearch_command.append("-x")

    if starttls:
        ldapsearch_command.append("-Z")

    # static filter to limit the number of returned attributes and entries
    ldapsearch_command.append(f"{user_attr}={username}")

    env = os.environ.copy()
    env['LDAPTLS_REQCERT'] = tls_reqcert
    debug(f"Command: LDAPTLS_REQCERT={tls_reqcert} " + " ".join(ldapsearch_command))

    try:
        proc = subprocess.run(ldapsearch_command, env=env, check=True, capture_output=True, text=True)
        # Search for a generic field: NS7 AD, Windows AD and LDAP all have different DN format
        if "objectclass" in proc.stdout.lower():
            debug("User found in LDAP")
        else:
            debug("User not found in LDAP")
            sys.exit(6)
        # continue to check the OTP
    except Exception as e:
        debug("Remote bind: failed")
        print(e, file=sys.stderr)
        sys.exit(7)
else:
    debug("User is local")


try:
    if pyotp.totp.TOTP(user.get("openvpn_2fa")).verify(otp):
        sys.exit(0)
except binascii.Error:
    # the user has old migrated 2fa secret
    proc = subprocess.run(["/usr/bin/oathtool", "--totp=SHA1", user.get("openvpn_2fa")], capture_output=True, text=True)
    if proc.stdout.strip() == otp:
        sys.exit(0)

# otp codes do not match
sys.exit(1)
