"""
Provide authentication using local files

.. versionadded:: 2018.3.0

The `file` auth module allows simple authentication via local files. Different
filetypes are supported, including:

  1. Text files, with passwords in plaintext or hashed
  2. Apache-style htpasswd files
  3. Apache-style htdigest files

.. note::

    The ``python-passlib`` library is required when using a ``^filetype`` of
    ``htpasswd`` or ``htdigest``.

The simplest example is a plaintext file with usernames and passwords:

.. code-block:: yaml

    external_auth:
      file:
        ^filename: /etc/insecure-user-list.txt
        gene:
          - .*
        dean:
          - test.*

In this example the ``/etc/insecure-user-list.txt`` file would be formatted
as so:

.. code-block:: text

    dean:goneFishing
    gene:OceanMan

``^filename`` is the only required parameter. Any parameter that begins with
a ``^`` is passed directly to the underlying file authentication function
via ``kwargs``, with the leading ``^`` being stripped.

The text file option is configurable to work with legacy formats:

.. code-block:: yaml

    external_auth:
      file:
        ^filename: /etc/legacy_users.txt
        ^filetype: text
        ^hashtype: md5
        ^username_field: 2
        ^password_field: 3
        ^field_separator: '|'
        trey:
          - .*

This would authenticate users against a file of the following format:

.. code-block:: text

    46|trey|16a0034f90b06bf3c5982ed8ac41aab4
    555|mike|b6e02a4d2cb2a6ef0669e79be6fd02e4
    2001|page|14fce21db306a43d3b680da1a527847a
    8888|jon|c4e94ba906578ccf494d71f45795c6cb

.. note::

    The :py:func:`hashutil.digest <salt.modules.hashutil.digest>` execution
    function is used for comparing hashed passwords, so any algorithm
    supported by that function will work.

There is also support for Apache-style ``htpasswd`` and ``htdigest`` files:

.. code-block:: yaml

    external_auth:
      file:
        ^filename: /var/www/html/.htusers
        ^filetype: htpasswd
        cory:
          - .*

When using ``htdigest`` the ``^realm`` must be set:

.. code-block:: yaml

    external_auth:
      file:
        ^filename: /var/www/html/.htdigest
        ^filetype: htdigest
        ^realm: MySecureRealm
        cory:
          - .*

"""

import logging
import os

# Import salt utils
import salt.utils.files
import salt.utils.versions

log = logging.getLogger(__name__)

__virtualname__ = "file"


def __virtual__():
    return __virtualname__


def _get_file_auth_config():
    """
    Setup defaults and check configuration variables for auth backends
    """

    config = {
        "filetype": "text",
        "hashtype": "plaintext",
        "field_separator": ":",
        "username_field": 1,
        "password_field": 2,
    }

    for opt in __opts__["external_auth"][__virtualname__]:
        if opt.startswith("^"):
            config[opt[1:]] = __opts__["external_auth"][__virtualname__][opt]

    if "filename" not in config:
        log.error(
            "salt.auth.file: An authentication file must be specified "
            "via external_auth:file:^filename"
        )
        return False

    if not os.path.exists(config["filename"]):
        log.error(
            "salt.auth.file: The configured external_auth:file:^filename (%s)"
            "does not exist on the filesystem",
            config["filename"],
        )
        return False

    config["username_field"] = int(config["username_field"])
    config["password_field"] = int(config["password_field"])

    return config


def _text(username, password, **kwargs):
    """
    The text file function can authenticate plaintext and digest methods
    that are available in the :py:func:`hashutil.digest <salt.modules.hashutil.digest>`
    function.
    """

    filename = kwargs["filename"]
    hashtype = kwargs["hashtype"]
    field_separator = kwargs["field_separator"]
    username_field = kwargs["username_field"] - 1
    password_field = kwargs["password_field"] - 1

    with salt.utils.files.fopen(filename, "r") as pwfile:
        for line in pwfile.readlines():
            fields = line.strip().split(field_separator)

            try:
                this_username = fields[username_field]
            except IndexError:
                log.error(
                    "salt.auth.file: username field (%s) does not exist in file %s",
                    username_field,
                    filename,
                )
                return False
            try:
                this_password = fields[password_field]
            except IndexError:
                log.error(
                    "salt.auth.file: password field (%s) does not exist in file %s",
                    password_field,
                    filename,
                )
                return False

            if this_username == username:
                if hashtype == "plaintext":
                    if this_password == password:
                        return True
                else:
                    # Exceptions for unknown hash types will be raised by hashutil.digest
                    if this_password == __salt__["hashutil.digest"](password, hashtype):
                        return True

                # Short circuit if we've already found the user but the password was wrong
                return False
    return False


def _htpasswd(username, password, **kwargs):
    """
    Provide authentication via Apache-style htpasswd files
    """

    from passlib.apache import HtpasswdFile

    pwfile = HtpasswdFile(kwargs["filename"])

    # passlib below version 1.6 uses 'verify' function instead of 'check_password'
    if salt.utils.versions.version_cmp(kwargs["passlib_version"], "1.6") < 0:
        return pwfile.verify(username, password)
    else:
        return pwfile.check_password(username, password)


def _htdigest(username, password, **kwargs):
    """
    Provide authentication via Apache-style htdigest files
    """

    realm = kwargs.get("realm", None)
    if not realm:
        log.error(
            "salt.auth.file: A ^realm must be defined in "
            "external_auth:file for htdigest filetype"
        )
        return False

    from passlib.apache import HtdigestFile

    pwfile = HtdigestFile(kwargs["filename"])

    # passlib below version 1.6 uses 'verify' function instead of 'check_password'
    if salt.utils.versions.version_cmp(kwargs["passlib_version"], "1.6") < 0:
        return pwfile.verify(username, realm, password)
    else:
        return pwfile.check_password(username, realm, password)


def _htfile(username, password, **kwargs):
    """
    Gate function for _htpasswd and _htdigest authentication backends
    """

    filetype = kwargs.get("filetype", "htpasswd").lower()

    try:
        import passlib

        kwargs["passlib_version"] = passlib.__version__
    except ImportError:
        log.error(
            "salt.auth.file: The python-passlib library is required for %s filetype",
            filetype,
        )
        return False

    if filetype == "htdigest":
        return _htdigest(username, password, **kwargs)
    else:
        return _htpasswd(username, password, **kwargs)


FILETYPE_FUNCTION_MAP = {"text": _text, "htpasswd": _htfile, "htdigest": _htfile}


def auth(username, password):
    """
    File based authentication

    ^filename
        The path to the file to use for authentication.

    ^filetype
        The type of file: ``text``, ``htpasswd``, ``htdigest``.

        Default: ``text``

    ^realm
        The realm required by htdigest authentication.

    .. note::
        The following parameters are only used with the ``text`` filetype.

    ^hashtype
        The digest format of the password. Can be ``plaintext`` or any digest
        available via :py:func:`hashutil.digest <salt.modules.hashutil.digest>`.

        Default: ``plaintext``

    ^field_separator
        The character to use as a delimiter between fields in a text file.

        Default: ``:``

    ^username_field
        The numbered field in the text file that contains the username, with
        numbering beginning at 1 (one).

        Default: ``1``

    ^password_field
        The numbered field in the text file that contains the password, with
        numbering beginning at 1 (one).

        Default: ``2``
    """

    config = _get_file_auth_config()

    if not config:
        return False

    auth_function = FILETYPE_FUNCTION_MAP.get(config["filetype"], "text")

    return auth_function(username, password, **config)
