/* This file is part of GNU Pies.
   Copyright (C) 2015-2022 Sergey Poznyakoff

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

   GNU Pies 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 GNU Pies.  If not, see <http://www.gnu.org/licenses/>. */

#include "ident.h"
#include <grp.h>
#include <security/pam_appl.h>

struct pam_identity_provider_data
{
  char *service;
};

struct pam_cred
{
  const char *user;
  const char *pass;
};

#define COPY_STRING(s) (s) ? strdup(s) : NULL

#define overwrite_and_free(ptr)                 \
    do {                                        \
        char *s = ptr;                          \
        while (*s)                              \
            *s++ = 0;                           \
	free (ptr);				\
    } while (0)

#ifndef PAM_AUTHTOK_RECOVER_ERR
# define PAM_AUTHTOK_RECOVER_ERR PAM_CONV_ERR
#endif

static int
pies_conv (int num_msg, const struct pam_message **msg,
	   struct pam_response **resp, void *appdata_ptr)
{
  int status = PAM_SUCCESS;
  int i;
  struct pam_response *reply = NULL;
  struct pam_cred *cred = appdata_ptr;

  reply = calloc (num_msg, sizeof (*reply));
  if (!reply)
    return PAM_CONV_ERR;

  for (i = 0; i < num_msg && status == PAM_SUCCESS; i++)
    {
      switch (msg[i]->msg_style)
	{
        case PAM_PROMPT_ECHO_ON:
	  reply[i].resp_retcode = PAM_SUCCESS;
	  reply[i].resp = COPY_STRING (cred->user);
	  /* PAM frees resp */
	  break;

        case PAM_PROMPT_ECHO_OFF:
	  if (cred->pass)
	    {
	      reply[i].resp_retcode = PAM_SUCCESS;
	      reply[i].resp = COPY_STRING (cred->pass);
	      /* PAM frees resp */
            } else
	      status = PAM_AUTHTOK_RECOVER_ERR;
          break;

        case PAM_TEXT_INFO:
        case PAM_ERROR_MSG:
	  reply[i].resp_retcode = PAM_SUCCESS;
	  reply[i].resp = NULL;
	  break;

        default:
	  status = PAM_CONV_ERR;
        }
    }

  if (status != PAM_SUCCESS)
    {
      for (i = 0; i < num_msg; i++)
	if (reply[i].resp)
	  {
	    switch (msg[i]->msg_style)
	      {
              case PAM_PROMPT_ECHO_ON:
              case PAM_PROMPT_ECHO_OFF:
		overwrite_and_free (reply[i].resp);
		break;

              case PAM_ERROR_MSG:
              case PAM_TEXT_INFO:
		free (reply[i].resp);
              }
          }
      free (reply);
    } else
      *resp = reply;
    return status;
}

static int
authenticate (pies_identity_provider_t provider,
	      pies_identity_t id, char const *passwd)
{
  struct pam_identity_provider_data *pamdata = provider->data;
  pam_handle_t *pamh;
  int pamerror;
  struct pam_cred cred = { id->username, passwd };
  struct pam_conv pam_conv = { &pies_conv, &cred };
  char const *service = pamdata->service ? pamdata->service : "pies";

  do
    {
      pamerror = pam_start (service, id->username, &pam_conv, &pamh);
      if (pamerror != PAM_SUCCESS)
	break;

      pamerror = pam_authenticate (pamh, 0);
      if (pamerror != PAM_SUCCESS)
	break;

      pamerror = pam_acct_mgmt (pamh, 0);
      if (pamerror != PAM_SUCCESS)
	break;

      pamerror = pam_setcred (pamh, PAM_ESTABLISH_CRED);
    }
  while (0);

  pam_end (pamh, PAM_SUCCESS);

  switch (pamerror)
    {
    case PAM_SUCCESS:
      return 0;
      
    case PAM_AUTH_ERR:
      return -1;
    }
  
  //FIXME _log(L_ERR, 0, "PAM authentication error");
  return -1;
}

static int
is_group_member (pies_identity_provider_t p,
		 pies_identity_t id, char * const * groups)
{
  struct group *gr;
  int result = 0;
  
  setgrent ();
  while (!result && (gr = getgrent ()))
    {
      if (is_array_member (groups, gr->gr_name))
	result = is_array_member (gr->gr_mem, id->username);
    }
  endgrent ();
  return result;
}

static struct grecs_keyword pam_kw[] = {
  { "type", "'pam", "Set mechanism type", grecs_type_null },
  { "service", NULL, "Service name",
    grecs_type_string, GRECS_DFLT, NULL,
    offsetof(struct pam_identity_provider_data, service) },
  { NULL }
};

static int
configure (struct grecs_node *node, pies_identity_provider_t provider)
{
  int i;
  struct pam_identity_provider_data *data = grecs_calloc (1, sizeof (*data));
  provider->data = data;
  for (i = 0; pam_kw[i].ident; i++)
    pam_kw[i].varptr = data;
  if (grecs_tree_process (node->down, pam_kw))
    {
      //FIXME: memory leak
      return -1;
    }
  provider->locus = node->locus;
  return 0;
}

static void
confhelp (void)
{
  static struct grecs_keyword top[] = {
    { "identity-provider", "name: string",
      "Configuration for system identity provider",
      grecs_type_section, GRECS_INAC, NULL, 0, NULL, NULL,
      pam_kw },
    { NULL }
  };
  grecs_print_statement_array (top, 1, 0, stdout);
}

struct pies_identity_mechanism pam_identity_mechanism = {
  "pam",
  authenticate,  
  is_group_member,
  NULL,
  configure,
  confhelp
};

  
  
