/*
  This file is part of TALER
  (C) 2014--2025 Taler Systems SA

  TALER is free software; you can redistribute it and/or modify it under the
  terms of the GNU Lesser General Public License as published by the Free Software
  Foundation; either version 3, or (at your option) any later version.

  TALER 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 General Public License for more details.

  You should have received a copy of the GNU General Public License along with
  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file taler-merchant-httpd_auth.c
 * @brief client authentication logic
 * @author Martin Schanzenbach
 * @author Christian Grothoff
 */
#include "taler/platform.h"
#include <gnunet/gnunet_util_lib.h>
#include <gnunet/gnunet_db_lib.h>
#include <taler/taler_json_lib.h>
#include "taler-merchant-httpd_auth.h"
#include "taler-merchant-httpd_helper.h"

/**
 * Maximum length of a permissions string of a scope
 */
#define TMH_MAX_SCOPE_PERMISSIONS_LEN 4096

/**
 * Maximum length of a name of a scope
 */
#define TMH_MAX_NAME_LEN 255

/**
 * Represents a hard-coded set of default scopes with their
 * permissions and names
 */
struct ScopePermissionMap
{
  /**
   * The scope enum value
   */
  enum TMH_AuthScope as;

  /**
   * The scope name
   */
  char name[TMH_MAX_NAME_LEN];

  /**
   * The scope permissions string.
   * Comma-separated.
   */
  char permissions[TMH_MAX_SCOPE_PERMISSIONS_LEN];
};

/**
 * The default scopes array for merchant
 */
static struct ScopePermissionMap scope_permissions[] = {
  /* Deprecated since v19 */
  {
    .as = TMH_AS_ALL,
    .name = "write",
    .permissions = "*"
  },
  /* Full access for SPA */
  {
    .as = TMH_AS_ALL,
    .name = "all",
    .permissions = "*"
  },
  /* Full access for SPA */
  {
    .as = TMH_AS_SPA,
    .name = "spa",
    .permissions = "*"
  },
  /* Read-only access */
  {
    .as = TMH_AS_READ_ONLY,
    .name = "readonly",
    .permissions = "*-read"
  },
  /* Simple order management */
  {
    .as = TMH_AS_ORDER_SIMPLE,
    .name = "order-simple",
    .permissions = "orders-read,orders-write"
  },
  /* Simple order management for PoS, also allows inventory locking */
  {
    .as = TMH_AS_ORDER_POS,
    .name = "order-pos",
    .permissions = "orders-read,orders-write,inventory-lock"
  },
  /* Simple order management, also allows refunding */
  {
    .as = TMH_AS_ORDER_MGMT,
    .name = "order-mgmt",
    .permissions = "orders-read,orders-write,orders-refund"
  },
  /* Full order management, allows inventory locking and refunds */
  {
    .as = TMH_AS_ORDER_FULL,
    .name = "order-full",
    .permissions = "orders-read,orders-write,inventory-lock,orders-refund"
  },
  /* No permissions, dummy scope */
  {
    .as = TMH_AS_NONE,
  }
};


/**
 * Get permissions string for scope.
 * Also extracts the leftmost bit into the @a refreshable
 * output parameter.
 *
 * @param as the scope to get the permissions string from
 * @param[out] refreshable true if the token associated with this scope is refreshable.
 * @return the permissions string, or NULL if no such scope found
 */
static const char*
get_scope_permissions (enum TMH_AuthScope as,
                       bool *refreshable)
{
  *refreshable = as & TMH_AS_REFRESHABLE;
  for (unsigned int i = 0; TMH_AS_NONE != scope_permissions[i].as; i++)
  {
    /* We ignore the TMH_AS_REFRESHABLE bit */
    if ( (as & ~TMH_AS_REFRESHABLE)  ==
         (scope_permissions[i].as & ~TMH_AS_REFRESHABLE) )
      return scope_permissions[i].permissions;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
              "Failed to find required permissions for scope %d\n",
              as);
  return NULL;
}


/**
 * Extract the token from authorization header value @a auth.
 * The @a auth value can be a bearer token or a Basic
 * authentication header. In both cases, this function
 * updates @a auth to point to the actual credential,
 * skipping spaces.
 *
 * NOTE: We probably want to replace this function with MHD2
 * API calls in the future that are more robust.
 *
 * @param[in,out] auth pointer to authorization header value,
 *        will be updated to point to the start of the token
 *        or set to NULL if header value is invalid
 * @param[out] is_basic_auth will be set to true if the
 *        authorization header uses basic authentication,
 *        otherwise to false
 */
static void
extract_auth (const char **auth,
              bool *is_basic_auth)
{
  const char *bearer = "Bearer ";
  const char *basic = "Basic ";
  const char *tok = *auth;
  size_t offset = 0;
  bool is_bearer = false;

  *is_basic_auth = false;
  if (0 == strncmp (tok,
                    bearer,
                    strlen (bearer)))
  {
    offset = strlen (bearer);
    is_bearer = true;
  }
  else if (0 == strncmp (tok,
                         basic,
                         strlen (basic)))
  {
    offset = strlen (basic);
    *is_basic_auth = true;
  }
  else
  {
    *auth = NULL;
    return;
  }
  tok += offset;
  while (' ' == *tok)
    tok++;
  if ( (is_bearer) &&
       (0 != strncasecmp (tok,
                          RFC_8959_PREFIX,
                          strlen (RFC_8959_PREFIX))) )
  {
    *auth = NULL;
    return;
  }
  *auth = tok;
}


/**
 * Check if @a userpass grants access to @a instance.
 *
 * @param userpass base64 encoded "$USERNAME:$PASSWORD" value
 *        from HTTP Basic "Authentication" header
 * @param instance the access controlled instance
 */
static enum GNUNET_GenericReturnValue
check_auth_instance (const char *userpass,
                     struct TMH_MerchantInstance *instance)
{
  char *tmp;
  char *colon;
  const char *instance_name;
  const char *password;
  const char *target_instance = "admin";
  enum GNUNET_GenericReturnValue ret;

  /* implicitly a zeroed out hash means no authentication */
  if (GNUNET_is_zero (&instance->auth.auth_hash))
    return GNUNET_OK;
  if (NULL == userpass)
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  if (0 ==
      GNUNET_STRINGS_base64_decode (userpass,
                                    strlen (userpass),
                                    (void**) &tmp))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  colon = strchr (tmp,
                  ':');
  if (NULL == colon)
  {
    GNUNET_break_op (0);
    GNUNET_free (tmp);
    return GNUNET_SYSERR;
  }
  *colon = '\0';
  instance_name = tmp;
  password = colon + 1;
  /* instance->settings.id can be NULL if there is no instance yet */
  if (NULL != instance->settings.id)
    target_instance = instance->settings.id;
  if (0 != strcmp (instance_name,
                   target_instance))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Somebody tried to login to instance %s with username %s (login failed).\n",
                target_instance,
                instance_name);
    GNUNET_free (tmp);
    return GNUNET_SYSERR;
  }
  ret = TMH_check_auth (password,
                        &instance->auth.auth_salt,
                        &instance->auth.auth_hash);
  GNUNET_free (tmp);
  if (GNUNET_OK != ret)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Password provided does not match credentials for %s\n",
                target_instance);
  }
  return ret;
}


void
TMH_compute_auth (const char *token,
                  struct TALER_MerchantAuthenticationSaltP *salt,
                  struct TALER_MerchantAuthenticationHashP *hash)
{
  GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE,
                              salt,
                              sizeof (*salt));
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Computing initial auth using token with salt %s\n",
              TALER_B2S (salt));
  TALER_merchant_instance_auth_hash_with_salt (hash,
                                               salt,
                                               token);
}


/**
 * Function used to process Basic authorization header value.
 * Sets correct scope in the auth_scope parameter of the
 * #TMH_HandlerContext.
 *
 * @param hc the handler context
 * @param authn_s the value of the authorization header
 */
static void
process_basic_auth (struct TMH_HandlerContext *hc,
                    const char *authn_s)
{
  /* Handle token endpoint slightly differently: Only allow
   * instance password (Basic auth) to retrieve access token.
   * We need to handle authorization with Basic auth here first
   * The only time we need to handle authentication like this is
   * for the token endpoint!
   */
  if ( (0 != strncmp (hc->rh->url_prefix,
                      "/token",
                      strlen ("/token"))) ||
       (0 != strncmp (MHD_HTTP_METHOD_POST,
                      hc->rh->method,
                      strlen (MHD_HTTP_METHOD_POST))) ||
       (NULL == hc->instance))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Called endpoint `%s' with Basic authentication. Rejecting...\n",
                hc->rh->url_prefix);
    hc->auth_scope = TMH_AS_NONE;
    return;
  }
  if (GNUNET_OK ==
      check_auth_instance (authn_s,
                           hc->instance))
  {
    hc->auth_scope = TMH_AS_ALL;
  }
  else
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Basic authentication failed!\n");
    hc->auth_scope = TMH_AS_NONE;
  }
}


/**
 * Function used to process Bearer authorization header value.
 * Sets correct scope in the auth_scope parameter of the
 * #TMH_HandlerContext..
 *
 * @param hc the handler context
 * @param authn_s the value of the authorization header
 * @return TALER_EC_NONE on success.
 */
static enum TALER_ErrorCode
process_bearer_auth (struct TMH_HandlerContext *hc,
                     const char *authn_s)
{
  if (NULL == hc->instance)
  {
    hc->auth_scope = TMH_AS_NONE;
    return TALER_EC_NONE;
  }
  if (GNUNET_is_zero (&hc->instance->auth.auth_hash))
  {
    /* hash zero means no authentication for instance */
    hc->auth_scope = TMH_AS_ALL;
    return TALER_EC_NONE;
  }
  {
    enum TALER_ErrorCode ec;

    ec = TMH_check_token (authn_s,
                          hc->instance->settings.id,
                          &hc->auth_scope);
    if (TALER_EC_NONE != ec)
    {
      char *dec;
      size_t dec_len;
      const char *token;

      /* NOTE: Deprecated, remove sometime after v1.1 */
      if (0 != strncasecmp (authn_s,
                            RFC_8959_PREFIX,
                            strlen (RFC_8959_PREFIX)))
      {
        GNUNET_break_op (0);
        hc->auth_scope = TMH_AS_NONE;
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    "Authentication token invalid: %d\n",
                    (int) ec);
        return ec;
      }
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "Trying deprecated secret-token:password API authN\n");
      token = authn_s + strlen (RFC_8959_PREFIX);
      dec_len = GNUNET_STRINGS_urldecode (token,
                                          strlen (token),
                                          &dec);
      if ( (0 == dec_len) ||
           (GNUNET_OK !=
            TMH_check_auth (dec,
                            &hc->instance->auth.auth_salt,
                            &hc->instance->auth.auth_hash)) )
      {
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    "Login failed\n");
        hc->auth_scope = TMH_AS_NONE;
        GNUNET_free (dec);
        return TALER_EC_NONE;
      }
      hc->auth_scope = TMH_AS_ALL;
      GNUNET_free (dec);
    }
  }
  return TALER_EC_NONE;
}


/**
 * Checks if @a permission_required is in permissions of
 * @a scope.
 *
 * @param permission_required the permission to check.
 * @param scope the scope to check.
 * @return true if @a permission_required is in the permissions set of @a scope.
 */
static bool
permission_in_scope (const char *permission_required,
                     enum TMH_AuthScope scope)
{
  char *permissions;
  const char *perms_tmp;
  bool is_read_perm = false;
  bool is_write_perm = false;
  bool refreshable;
  const char *last_dash;

  perms_tmp = get_scope_permissions (scope,
                                     &refreshable);
  if (NULL == perms_tmp)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Permission check failed: scope %d not understood\n",
                (int) scope);
    return false;
  }
  last_dash = strrchr (permission_required,
                       '-');
  if (NULL != last_dash)
  {
    is_write_perm = (0 == strcmp (last_dash,
                                  "-write"));
    is_read_perm = (0 == strcmp (last_dash,
                                 "-read"));
  }

  if (0 == strcmp ("token-refresh",
                   permission_required))
  {
    if (! refreshable)
    {
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "Permission check failed: token not refreshable\n");
    }
    return refreshable;
  }
  permissions = GNUNET_strdup (perms_tmp);
  {
    const char *perm = strtok (permissions,
                               ",");

    if (NULL == perm)
    {
      GNUNET_free (permissions);
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "Permission check failed: empty permission set\n");
      return false;
    }
    while (NULL != perm)
    {
      if (0 == strcmp ("*",
                       perm))
      {
        GNUNET_free (permissions);
        return true;
      }
      if ( (0 == strcmp ("*-write",
                         perm)) &&
           (is_write_perm) )
      {
        GNUNET_free (permissions);
        return true;
      }
      if ( (0 == strcmp ("*-read",
                         perm)) &&
           (is_read_perm) )
      {
        GNUNET_free (permissions);
        return true;
      }
      if (0 == strcmp (permission_required,
                       perm))
      {
        GNUNET_free (permissions);
        return true;
      }
      perm = strtok (NULL,
                     ",");
    }
  }
  GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
              "Permission check failed: %s not found in %s\n",
              permission_required,
              permissions);
  GNUNET_free (permissions);
  return false;
}


bool
TMH_scope_is_subset (enum TMH_AuthScope as,
                     enum TMH_AuthScope candidate)
{
  const char *as_perms;
  const char *candidate_perms;
  char *permissions;
  bool as_refreshable;
  bool cand_refreshable;

  as_perms = get_scope_permissions (as,
                                    &as_refreshable);
  candidate_perms = get_scope_permissions (candidate,
                                           &cand_refreshable);
  if (! as_refreshable && cand_refreshable)
    return false;
  if ( (NULL == as_perms) &&
       (NULL != candidate_perms) )
    return false;
  if ( (NULL == candidate_perms) ||
       (0 == strcmp ("*",
                     as_perms)))
    return true;
  permissions = GNUNET_strdup (candidate_perms);
  {
    const char *perm;

    perm = strtok (permissions,
                   ",");
    if (NULL == perm)
    {
      GNUNET_free (permissions);
      return true;
    }
    while (NULL != perm)
    {
      if (! permission_in_scope (perm,
                                 as))
      {
        GNUNET_free (permissions);
        return false;
      }
      perm = strtok (NULL,
                     ",");
    }
  }
  GNUNET_free (permissions);
  return true;
}


enum TMH_AuthScope
TMH_get_scope_by_name (const char *name)
{
  if (NULL == name)
    return TMH_AS_NONE;
  for (unsigned int i = 0; TMH_AS_NONE != scope_permissions[i].as; i++)
  {
    if (0 == strcasecmp (scope_permissions[i].name,
                         name))
      return scope_permissions[i].as;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
              "Name `%s' does not match any scope we understand\n",
              name);
  return TMH_AS_NONE;
}


const char*
TMH_get_name_by_scope (enum TMH_AuthScope scope,
                       bool *refreshable)
{
  *refreshable = scope & TMH_AS_REFRESHABLE;
  for (unsigned int i = 0; TMH_AS_NONE != scope_permissions[i].as; i++)
  {
    /* We ignore the TMH_AS_REFRESHABLE bit */
    if ( (scope & ~TMH_AS_REFRESHABLE)  ==
         (scope_permissions[i].as & ~TMH_AS_REFRESHABLE) )
      return scope_permissions[i].name;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
              "Scope #%d does not match any scope we understand\n",
              (int) scope);
  return NULL;
}


enum GNUNET_GenericReturnValue
TMH_check_auth (const char *password,
                struct TALER_MerchantAuthenticationSaltP *salt,
                struct TALER_MerchantAuthenticationHashP *hash)
{
  struct TALER_MerchantAuthenticationHashP val;

  if (GNUNET_is_zero (hash))
    return GNUNET_OK;
  if (NULL == password)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Denying access: empty password provided\n");
    return GNUNET_SYSERR;
  }
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "Checking against token with salt %s\n",
              TALER_B2S (salt));
  TALER_merchant_instance_auth_hash_with_salt (&val,
                                               salt,
                                               password);
  if (0 !=
      GNUNET_memcmp (&val,
                     hash))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Access denied: password does not match\n");
    return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}


/**
 * Check if the client has provided the necessary credentials
 * to access the selected endpoint of the selected instance.
 *
 * @param[in,out] hc handler context
 * @return #GNUNET_OK on success,
 *         #GNUNET_NO if an error was queued (return #MHD_YES)
 *         #GNUNET_SYSERR to close the connection (return #MHD_NO)
 */
enum GNUNET_GenericReturnValue
TMH_perform_access_control (struct TMH_HandlerContext *hc)
{
  const char *auth;
  bool is_basic_auth = false;
  bool auth_malformed = false;

  auth = MHD_lookup_connection_value (hc->connection,
                                      MHD_HEADER_KIND,
                                      MHD_HTTP_HEADER_AUTHORIZATION);

  if (NULL != auth)
  {
    extract_auth (&auth,
                  &is_basic_auth);
    if (NULL == auth)
      auth_malformed = true;
    hc->auth_token = auth;
  }

  /* If we have zero configured instances (not even ones that have been
     purged) or explicitly disabled authentication, THEN we accept anything
     (no access control), as we then also have no data to protect. */
  if ((0 == GNUNET_CONTAINER_multihashmap_size (TMH_by_id_map)) ||
      (GNUNET_YES == TMH_auth_disabled))
  {
    hc->auth_scope = TMH_AS_ALL;
  }
  else if (is_basic_auth)
  {
    process_basic_auth (hc,
                        auth);
  }
  else   /* Check bearer token */
  {
    enum TALER_ErrorCode ec;

    ec = process_bearer_auth (hc,
                              auth);
    if (TALER_EC_NONE != ec)
    {
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "Bearer authentication failed: %d\n",
                  (int) ec);
      return (MHD_YES ==
              TALER_MHD_reply_with_ec (hc->connection,
                                       ec,
                                       NULL))
          ? GNUNET_NO
          : GNUNET_SYSERR;
    }
  }
  /* We grant access if:
     - Endpoint does not require permissions
     - Authorization scope of bearer token contains permissions
       required by endpoint.
   */
  if ( (NULL != hc->rh->permission) &&
       (! permission_in_scope (hc->rh->permission,
                               hc->auth_scope)))
  {
    if (auth_malformed &&
        (TMH_AS_NONE == hc->auth_scope) )
    {
      GNUNET_break_op (0);
      return (MHD_YES ==
              TALER_MHD_reply_with_error (
                hc->connection,
                MHD_HTTP_UNAUTHORIZED,
                TALER_EC_GENERIC_PARAMETER_MALFORMED,
                "'" RFC_8959_PREFIX
                "' prefix or 'Bearer' missing in 'Authorization' header"))
          ? GNUNET_NO
          : GNUNET_SYSERR;
    }
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                "Credentials provided are %d which are insufficient for access to `%s'\n",
                (int) hc->auth_scope,
                hc->rh->permission);
    return (MHD_YES ==
            TALER_MHD_reply_with_error (
              hc->connection,
              MHD_HTTP_UNAUTHORIZED,
              TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED,
              "Check credentials in 'Authorization' header"))
        ? GNUNET_NO
        : GNUNET_SYSERR;
  }
  return GNUNET_OK;
}
