# (c) 2009-2024 Martin Wendt and contributors; see WsgiDAV https://github.com/mar10/wsgidav
# Original PyFileServer (c) 2005 Ho Chun Wei.
# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
"""
Implementation of a domain controller that allows users to authenticate against
a Windows NT domain or a local computer.

Used by HTTPAuthenticator. Only available on linux and macOS.

See also https://wsgidav.readthedocs.io/en/latest/user_guide_configure.html

Purpose
-------

Usage::

   from wsgidav.dc.nt_dc import NTDomainController
   domain_controller = NTDomainController(wsgidav_app, config)

where:

+ domain_controller object corresponds to that in ``wsgidav.yaml`` or
  as input into ``wsgidav.http_authenticator.HTTPAuthenticator``.

+ preset_domain allows the admin to specify a domain to be used (instead of any domain that
  may come as part of the user_name in domain\\user). This is useful only if there
  is one domain to be authenticated against and you want to spare users from typing the
  domain name

+ preset_server allows the admin to specify the NETBIOS name of the domain controller to
  be used (complete with the preceding \\\\). if absent, it will look for trusted
  domain controllers on the localhost.

This class allows the user to authenticate against a Windows NT domain or a local computer,
requires NT or beyond (2000, XP, 2003, etc).

This class requires Mark Hammond's Win32 extensions for Python at here_ or sourceforge_

.. _here : http://starship.python.net/crew/mhammond/win32/Downloads.html
.. _sourceforge : http://sourceforge.net/projects/pywin32/

Information on Win32 network authentication was from the following resources:

+ http://ejabberd.jabber.ru/node/55

+ http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/81402


Testability and caveats
-----------------------

**Digest Authentication**
   Digest authentication requires the password to be retrieve from the system to compute
   the correct digest for comparison. This is so far impossible (and indeed would be a
   big security loophole if it was allowed), so digest authentication WILL not work
   with this class.

   Highly recommend basic authentication over SSL support.

**User Login**
   Authentication will count as a user login attempt, so any security in place for
   invalid password attempts may be triggered.

   Also note that, even though the user is logged in, the application does not impersonate
   the user - the application will continue to run under the account and permissions it
   started with. The user has the read/write permissions to the share of the running account
   and not his own account.

**Using on a local computer**
   This class has been tested on a local computer (Windows XP). Leave domain as None and
   do not specify domain when entering user_name in this case.

**Using for a network domain**
   This class is being tested for a network domain (I'm setting one up to test).

"""

import win32net
import win32netcon
import win32security

from wsgidav import util
from wsgidav.dc.base_dc import BaseDomainController

__docformat__ = "reStructuredText"
_logger = util.get_module_logger(__name__)


class NTDomainController(BaseDomainController):
    def __init__(self, wsgidav_app, config):
        super().__init__(wsgidav_app, config)

        dc_conf = util.get_dict_value(config, "nt_dc", as_dict=True)

        self.preset_domain = dc_conf.get("preset_domain")
        self.preset_server = dc_conf.get("preset_server")

    def __str__(self):
        return (
            f"{self.__class__.__name__}({self.preset_domain!r}, {self.preset_server!r})"
        )

    def get_domain_realm(self, path_info, environ):
        return "Windows Domain Authentication"

    def require_authentication(self, realm, environ):
        return True

    def basic_auth_user(self, realm, user_name, password, environ):
        domain, user = self._get_domain_username(user_name)
        dc_name = self._get_domain_controller_name(domain)
        return self._auth_user(user, password, domain, dc_name)

    def supports_http_digest_auth(self):
        # We don't have access to a plaintext password (or stored hash)
        return False

    # def is_realm_user(self, realm, user_name, environ):
    #     (domain, usern) = self._get_domain_username(user_name)
    #     dc_name = self._get_domain_controller_name(domain)
    #     return self._is_user(usern, domain, dc_name)

    # def digest_auth_user(self, realm, user_name, environ):
    #     """Computes digest hash A1 part."""
    #     password = self._get_realm_user_password(realm, user_name)
    #     return self._compute_http_digest_a1(realm, user_name, password)

    # def _get_realm_user_password(self, realm, user_name):
    #     (domain, user) = self._get_domain_username(user_name)
    #     dc_name = self._get_domain_controller_name(domain)

    #     try:
    #         userdata = win32net.NetUserGetInfo(dc_name, user, 1)
    #     except Exception:
    #         _logger.exception("NetUserGetInfo")
    #         userdata = {}
    #     return userdata.get("password")

    def _get_domain_username(self, user_name):
        user_data = user_name.split("\\", 1)
        if len(user_data) == 1:
            domain = None
            user = user_data[0]
        else:
            domain = user_data[0]
            user = user_data[1]

        if self.preset_domain is not None:
            domain = self.preset_domain

        return (domain, user)

    def _get_domain_controller_name(self, domain):
        if self.preset_server is not None:
            return self.preset_server

        try:
            # execute this on the localhost
            pdc = win32net.NetGetAnyDCName(None, domain)
        except Exception:
            pdc = None

        return pdc

    def _is_user(self, user_name, domain, server):
        # TODO: implement some kind of caching here?
        resume = "init"
        while resume:
            if resume == "init":
                resume = 0
            try:
                users, _total, resume = win32net.NetUserEnum(
                    server, 0, win32netcon.FILTER_NORMAL_ACCOUNT, 0
                )
                # Make sure, we compare unicode
                un = util.to_str(user_name).lower()
                for userinfo in users:
                    uiname = userinfo.get("name")
                    assert uiname
                    assert util.is_str(uiname)
                    if un == userinfo["name"].lower():
                        return True
            except win32net.error as e:
                _logger.exception(f"NetUserEnum: {e}")
                return False
        _logger.info(f"User {user_name!r} not found on server {server!r}")
        return False

    def _auth_user(self, user_name, password, domain, server):
        # TODO: implement caching?

        # TODO: is this pre-test efficient, or should we simply try LogonUser()?
        #       (Could this trigger account locking?)
        if not self._is_user(user_name, domain, server):
            return False

        htoken = None
        try:
            htoken = win32security.LogonUser(
                user_name,
                domain,
                password,
                win32security.LOGON32_LOGON_NETWORK,
                win32security.LOGON32_PROVIDER_DEFAULT,
            )
            if not htoken:
                _logger.warning(f"LogonUser({user_name!r}, {domain!r}, '***') failed.")
                return False
        except win32security.error as err:
            _logger.warning(
                f"LogonUser({user_name!r}, {domain!r}, '***') failed: {err}"
            )
            return False
        finally:
            if htoken:
                htoken.Close()

        _logger.debug(f"User {user_name!r} logged on.")
        return True
