/* Copyright © 2007, 2008 Red Hat, Inc. All rights reserved.
   Red Hat author: Miloslav Trmač <mitr@redhat.com>

   Redistribution and use in source and binary forms of Linux-PAM, with
   or without modification, are permitted provided that the following
   conditions are met:

   1. Redistributions of source code must retain any existing copyright
      notice, and this entire permission notice in its entirety,
      including the disclaimer of warranties.

   2. Redistributions in binary form must reproduce all prior and current
      copyright notices, this list of conditions, and the following
      disclaimer in the documentation and/or other materials provided
      with the distribution.

   3. The name of any author may not be used to endorse or promote
      products derived from this software without their specific prior
      written permission.

   ALTERNATIVELY, this product may be distributed under the terms of the
   GNU General Public License, in which case the provisions of the GNU
   GPL are required INSTEAD OF the above restrictions.  (This clause is
   necessary due to a potential conflict between the GNU GPL and the
   restrictions contained in a BSD-style copyright.)

   THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
   WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
   MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
   IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
   INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
   BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
   OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
   ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
   TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
   USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
   DAMAGE. */

#include "config.h"
#include <errno.h>
#include <fnmatch.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <sys/socket.h>
#include <unistd.h>

#include <libaudit.h>
#include <linux/netlink.h>

#define PAM_SM_SESSION

#include <security/pam_ext.h>
#include <security/pam_modules.h>
#include <security/pam_modutil.h>

#define DATANAME "pam_tty_audit_last_state"

/* Open an audit netlink socket */
static int
nl_open (void)
{
  return socket (AF_NETLINK, SOCK_RAW, NETLINK_AUDIT);
}

static int
nl_send (int fd, unsigned type, unsigned flags, const void *data, size_t size)
{
  struct sockaddr_nl addr;
  struct msghdr msg;
  struct nlmsghdr nlm;
  struct iovec iov[2];
  ssize_t res;

  nlm.nlmsg_len = NLMSG_LENGTH (size);
  nlm.nlmsg_type = type;
  nlm.nlmsg_flags = NLM_F_REQUEST | flags;
  nlm.nlmsg_seq = 0;
  nlm.nlmsg_pid = 0;
  iov[0].iov_base = &nlm;
  iov[0].iov_len = sizeof (nlm);
  iov[1].iov_base = (void *)data;
  iov[1].iov_len = size;
  addr.nl_family = AF_NETLINK;
  addr.nl_pid = 0;
  addr.nl_groups = 0;
  msg.msg_name = &addr;
  msg.msg_namelen = sizeof (addr);
  msg.msg_iov = iov;
  msg.msg_iovlen = 2;
  msg.msg_control = NULL;
  msg.msg_controllen = 0;
  msg.msg_flags = 0;
  res = sendmsg (fd, &msg, 0);
  if (res == -1)
    return -1;
  if ((size_t)res != nlm.nlmsg_len)
    {
      errno = EIO;
      return -1;
    }
  return 0;
}

static int
nl_recv (int fd, unsigned type, void *buf, size_t size)
{
  struct sockaddr_nl addr;
  struct msghdr msg;
  struct nlmsghdr nlm;
  struct iovec iov[2];
  ssize_t res, resdiff;

 again:
  iov[0].iov_base = &nlm;
  iov[0].iov_len = sizeof (nlm);
  msg.msg_name = &addr;
  msg.msg_namelen = sizeof (addr);
  msg.msg_iov = iov;
  msg.msg_iovlen = 1;
  msg.msg_control = NULL;
  msg.msg_controllen = 0;
  msg.msg_flags = 0;
  if (type != NLMSG_ERROR)
    {
      res = recvmsg (fd, &msg, MSG_PEEK);
      if (res == -1)
	return -1;
      if (res != NLMSG_LENGTH (0))
	{
	  errno = EIO;
	  return -1;
	}
      if (nlm.nlmsg_type == NLMSG_ERROR)
	{
	  struct nlmsgerr err;

	  iov[1].iov_base = &err;
	  iov[1].iov_len = sizeof (err);
	  msg.msg_iovlen = 2;
	  res = recvmsg (fd, &msg, 0);
	  if (res == -1)
	    return -1;
	  if ((size_t)res != NLMSG_LENGTH (sizeof (err))
	      || nlm.nlmsg_type != NLMSG_ERROR)
	    {
	      errno = EIO;
	      return -1;
	    }
	  if (err.error == 0)
	    goto again;
	  errno = -err.error;
	  return -1;
	}
    }
  if (size != 0)
    {
      iov[1].iov_base = buf;
      iov[1].iov_len = size;
      msg.msg_iovlen = 2;
    }
  res = recvmsg (fd, &msg, 0);
  if (res == -1)
    return -1;
  resdiff = NLMSG_LENGTH(size) - (size_t)res;
  if (resdiff < 0
      || nlm.nlmsg_type != type)
    {
      errno = EIO;
      return -1;
    }
  else if (resdiff > 0)
    {
      memset((char *)buf + size - resdiff, 0, resdiff);
    }
  return 0;
}

static int
nl_recv_ack (int fd)
{
  struct nlmsgerr err;

  if (nl_recv (fd, NLMSG_ERROR, &err, sizeof (err)) != 0)
    return -1;
  if (err.error != 0)
    {
      errno = -err.error;
      return -1;
    }
  return 0;
}

static void
cleanup_old_status (pam_handle_t *pamh, void *data, int error_status)
{
  (void)pamh;
  (void)error_status;
  free (data);
}

enum uid_range { UID_RANGE_NONE, UID_RANGE_MM, UID_RANGE_MIN,
    UID_RANGE_ONE, UID_RANGE_ERR };

static enum uid_range
parse_uid_range(pam_handle_t *pamh, const char *s,
                uid_t *min_uid, uid_t *max_uid)
{
    const char *range = s;
    const char *pmax;
    char *endptr;
    enum uid_range rv = UID_RANGE_MM;

    if ((pmax=strchr(range, ':')) == NULL)
        return UID_RANGE_NONE;
    ++pmax;

    if (range[0] == ':')
        rv = UID_RANGE_ONE;
    else {
            errno = 0;
            *min_uid = strtoul (range, &endptr, 10);
            if (errno != 0 || (range == endptr) || *endptr != ':') {
                pam_syslog(pamh, LOG_DEBUG,
                           "wrong min_uid value in '%s'", s);
                return UID_RANGE_ERR;
            }
    }

    if (*pmax == '\0') {
        if (rv == UID_RANGE_ONE)
            return UID_RANGE_ERR;

        return UID_RANGE_MIN;
    }

    errno = 0;
    *max_uid = strtoul (pmax, &endptr, 10);
    if (errno != 0 || (pmax == endptr) || *endptr != '\0') {
        pam_syslog(pamh, LOG_DEBUG,
                   "wrong max_uid value in '%s'", s);
        return UID_RANGE_ERR;
    }

    if (rv == UID_RANGE_ONE)
        *min_uid = *max_uid;
    return rv;
}

int
pam_sm_open_session (pam_handle_t *pamh, int flags, int argc, const char **argv)
{
  enum command { CMD_NONE, CMD_ENABLE, CMD_DISABLE };

  enum command command;
  struct audit_tty_status *old_status, new_status;
  const char *user;
  int i, fd, open_only;
  struct passwd *pwd;
#ifdef HAVE_STRUCT_AUDIT_TTY_STATUS_LOG_PASSWD
  int log_passwd;
#endif /* HAVE_STRUCT_AUDIT_TTY_STATUS_LOG_PASSWD */

  (void)flags;

  if (pam_get_user (pamh, &user, NULL) != PAM_SUCCESS)
    {
      pam_syslog (pamh, LOG_ERR, "error determining target user's name");
      return PAM_SESSION_ERR;
    }

  pwd = pam_modutil_getpwnam(pamh, user);
  if (pwd == NULL)
    {
      pam_syslog(pamh, LOG_WARNING,
                 "open_session unknown user '%s'", user);
      return PAM_SESSION_ERR;
    }

  command = CMD_NONE;
  open_only = 0;
#ifdef HAVE_STRUCT_AUDIT_TTY_STATUS_LOG_PASSWD
  log_passwd = 0;
#endif /* HAVE_STRUCT_AUDIT_TTY_STATUS_LOG_PASSWD */
  for (i = 0; i < argc; i++)
    {
      if (strncmp (argv[i], "enable=", 7) == 0
	  || strncmp (argv[i], "disable=", 8) == 0)
	{
	  enum command this_command;
	  char *copy, *tok_data, *tok;

	  this_command = *argv[i] == 'e' ? CMD_ENABLE : CMD_DISABLE;
	  copy = strdup (strchr (argv[i], '=') + 1);
	  if (copy == NULL)
	    return PAM_SESSION_ERR;
	  for (tok = strtok_r (copy, ",", &tok_data);
	       tok != NULL && command != this_command;
	       tok = strtok_r (NULL, ",", &tok_data))
	    {
	      uid_t min_uid = 0, max_uid = 0;
	      switch (parse_uid_range(pamh, tok, &min_uid, &max_uid))
		{
		case UID_RANGE_NONE:
		    if (fnmatch (tok, user, 0) == 0)
			command = this_command;
		    break;
		case UID_RANGE_MM:
		    if (pwd->pw_uid >= min_uid && pwd->pw_uid <= max_uid)
			command = this_command;
		    break;
		case UID_RANGE_MIN:
		    if (pwd->pw_uid >= min_uid)
			command = this_command;
		    break;
		case UID_RANGE_ONE:
		    if (pwd->pw_uid == max_uid)
			command = this_command;
		    break;
		case UID_RANGE_ERR:
		    break;
		}
	    }
	  free (copy);
	}
      else if (strcmp (argv[i], "open_only") == 0)
	open_only = 1;
      else if (strcmp (argv[i], "log_passwd") == 0)
#ifdef HAVE_STRUCT_AUDIT_TTY_STATUS_LOG_PASSWD
        log_passwd = 1;
#else /* HAVE_STRUCT_AUDIT_TTY_STATUS_LOG_PASSWD */
        pam_syslog (pamh, LOG_WARNING,
                    "The log_passwd option was not available at compile time.");
#warning "pam_tty_audit: The log_passwd option is not available.  Please upgrade your headers/kernel."
#endif /* HAVE_STRUCT_AUDIT_TTY_STATUS_LOG_PASSWD */
      else
	{
	  pam_syslog (pamh, LOG_ERR, "unknown option `%s'", argv[i]);
	}
    }
  if (command == CMD_NONE)
    return PAM_SUCCESS;

  old_status = malloc (sizeof (*old_status));
  if (old_status == NULL)
    return PAM_SESSION_ERR;

  fd = nl_open ();
  if (fd == -1
      || nl_send (fd, AUDIT_TTY_GET, 0, NULL, 0) != 0
      || nl_recv (fd, AUDIT_TTY_GET, old_status, sizeof (*old_status)) != 0)
    {
      pam_syslog (pamh, LOG_ERR, "error reading current audit status: %m");
      if (fd != -1)
	close (fd);
      free (old_status);
      return PAM_SESSION_ERR;
    }

  memcpy(&new_status, old_status, sizeof(new_status));

  new_status.enabled = (command == CMD_ENABLE ? 1 : 0);
#ifdef HAVE_STRUCT_AUDIT_TTY_STATUS_LOG_PASSWD
  new_status.log_passwd = log_passwd;
#endif /* HAVE_STRUCT_AUDIT_TTY_STATUS_LOG_PASSWD */
  if (old_status->enabled == new_status.enabled
#ifdef HAVE_STRUCT_AUDIT_TTY_STATUS_LOG_PASSWD
      && old_status->log_passwd == new_status.log_passwd
#endif /* HAVE_STRUCT_AUDIT_TTY_STATUS_LOG_PASSWD */
     )
    {
      open_only = 1; /* to clean up old_status */
      goto ok_fd;
    }

  if (open_only == 0
      && pam_set_data (pamh, DATANAME, old_status, cleanup_old_status)
      != PAM_SUCCESS)
    {
      pam_syslog (pamh, LOG_ERR, "error saving old audit status");
      close (fd);
      free (old_status);
      return PAM_SESSION_ERR;
    }

  if (nl_send (fd, AUDIT_TTY_SET, NLM_F_ACK, &new_status,
	       sizeof (new_status)) != 0
      || nl_recv_ack (fd) != 0)
    {
      pam_syslog (pamh, LOG_ERR, "error setting current audit status: %m");
      close (fd);
      if (open_only != 0)
	free (old_status);
      return PAM_SESSION_ERR;
    }
  /* Fall through */
 ok_fd:
  close (fd);
  pam_syslog (pamh, LOG_DEBUG, "changed status from %d to %d",
	      old_status->enabled, new_status.enabled);
  if (open_only != 0)
    free (old_status);
  return PAM_SUCCESS;
}

int
pam_sm_close_session (pam_handle_t *pamh, int flags, int argc,
		      const char **argv)
{
  const void *status_;

  (void)flags;
  (void)argc;
  (void)argv;
  if (pam_get_data (pamh, DATANAME, &status_) == PAM_SUCCESS)
    {
      const struct audit_tty_status *status;
      int fd;

      status = status_;

      fd = nl_open ();
      if (fd == -1
	  || nl_send (fd, AUDIT_TTY_SET, NLM_F_ACK, status,
		      sizeof (*status)) != 0
	  || nl_recv_ack (fd) != 0)
	{
	  pam_syslog (pamh, LOG_ERR, "error restoring audit status: %m");
	  if (fd != -1)
	    close (fd);
	  return PAM_SESSION_ERR;
	}
      close (fd);
      pam_syslog (pamh, LOG_DEBUG, "restored status to %d", status->enabled);
    }
  return PAM_SUCCESS;
}
