/* PAM Quota module
   Converted for XFS Quotas by David Connolly <david.connolly@gmail.com>
   Written by Brian Masney <masneyb@gftp.org>

   To compile:

   gcc -DLINUX_PAM -fPIC -Dlinux -Di386 -DPAM_DYNAMIC -shared -Xlinker -x -o pam_quota_xfs.so pam_quota_xfs.c

   This module will setup disk quotas for new users. You should use this module
   along with the pam_mkhomedir module. When the user first logs in, it checks
   if their home directory doesn't exist, and if not it will setup quotas for
   that user. So you have to put this module before the pam_mkhomedir module.

   Here is a sample PAM config file:

   auth       required     /lib/security/pam_stack.so service=system-auth
   auth       required     /lib/security/pam_nologin.so
   account    required     /lib/security/pam_stack.so service=system-auth
   password   required     /lib/security/pam_stack.so service=system-auth
   session    required     /lib/security/pam_stack.so service=system-auth
   session    required     /lib/security/pam_limits.so
   session    required     /lib/security/pam_quota_xfs.so bsoftlimit=19000 bhardlimit=20000 isoftlimit=3000 ihardlimit=4000
   session    required     /lib/security/pam_mkhomedir.so skel=/etc/skel umask=022
   session    optional     /lib/security/pam_console.so

   Released under the GNU LGPL version 2 or later

 */

#define _GNU_SOURCE 1 /* For caddr_t */
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pwd.h>
#include <mntent.h>
#include <syslog.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/quota.h>
#include <sys/vfs.h>
#include <sys/stat.h>
#include <xfs/xqm.h>

#define PAM_SM_SESSION

#include <security/pam_modules.h>
#include <security/_pam_macros.h>

static __u64 d_blk_hardlimit = 0,
               d_blk_softlimit = 0,
               d_ino_hardlimit = 0,
               d_ino_softlimit = 0;

static int _pam_parse (int flags, int argc, const char **argv)
{
  int ctrl = 0;

  for (; argc-- > 0; ++argv)
    {
      if (strncmp (*argv, "bhardlimit=", 11) == 0)
        d_blk_hardlimit = strtol (*argv + 11, NULL, 10);
      else if (strncmp (*argv, "bsoftlimit=", 11) == 0)
        d_blk_softlimit = strtol (*argv + 11, NULL, 10);
      else if (strncmp (*argv, "ihardlimit=", 11) == 0)
        d_ino_hardlimit = strtol (*argv + 11, NULL, 10);
      else if (strncmp (*argv, "isoftlimit=", 11) == 0)
        d_ino_softlimit = strtol (*argv + 11, NULL, 10);
    }
  return (ctrl);
}


PAM_EXTERN
int pam_sm_open_session (pam_handle_t * pamh, int flags, int argc,
                         const char **argv)
{
  char mntdevice[BUFSIZ], mntpoint[BUFSIZ], mnttype[BUFSIZ], srvstr[40], *pos;
  size_t mntpoint_len, thislen;
  const char *user, *service;
  const struct passwd *pwd;
  const struct mntent *mnt;
  struct fs_disk_quota quotablk;
  struct stat st;
  FILE * fd;

  _pam_parse (flags, argc, argv);

  if (pam_get_item (pamh, PAM_USER, (const void **) &user) != PAM_SUCCESS)
    return (PAM_USER_UNKNOWN);

  if (user == NULL || *user == '\0')
    return (PAM_USER_UNKNOWN);

  if ((pwd = getpwnam (user)) == NULL)
    return (PAM_CRED_INSUFFICIENT);

  /* If the home directory already exists, don't setup any quotas */
  if (stat (pwd->pw_dir, &st) == 0)
    return (PAM_SUCCESS);

  if (pam_get_item (pamh, PAM_SERVICE, (const void **) &service) != PAM_SUCCESS)
    service = "";

  snprintf (srvstr, sizeof (srvstr), "%s(pam_quota)[%d]", service, getpid ());
  openlog (srvstr, 0, LOG_AUTHPRIV);

  /* See if the base directory exists, if not exit */
  if ((pos = strrchr (pwd->pw_dir, '/')) != NULL)
    {
      *pos = '\0';
      if (stat (pwd->pw_dir, &st) != 0)
        return PAM_PERM_DENIED;
      *pos = '/';
    }

  /* Find out what device the filesystem is hosted on */
  if ((fd = setmntent ("/etc/mtab", "r")) == NULL)
    return PAM_PERM_DENIED;

  *mntpoint = *mntdevice = '\0';
  mntpoint_len = 0;
  while ((mnt = getmntent (fd)) != NULL)
    {
      thislen = strlen (mnt->mnt_dir);
      if (strncmp (pwd->pw_dir, mnt->mnt_dir, thislen) == 0 &&
          thislen > mntpoint_len)
        {
          strncpy (mntpoint, mnt->mnt_dir, sizeof (mntpoint));
          strncpy (mntdevice, mnt->mnt_fsname, sizeof (mntdevice));
          strncpy (mnttype, mnt->mnt_type, sizeof (mnttype));
          mntpoint_len = thislen;
        }
    }

  endmntent (fd);

  if (*mntdevice == '\0')
    return PAM_PERM_DENIED;

   /* Setup user quotas if we were passed any quota arguments */
   if (d_blk_hardlimit > 0 || d_blk_softlimit > 0 ||
       d_ino_hardlimit > 0 || d_ino_softlimit > 0)
     {
       if (quotactl (QCMD (Q_XGETQUOTA, USRQUOTA), mntdevice,
                     pwd->pw_uid, (caddr_t) &quotablk) != 0)
         memset (&quotablk, 0, sizeof (quotablk));

       quotablk.d_blk_hardlimit = d_blk_hardlimit * 2;
       quotablk.d_blk_softlimit = d_blk_softlimit * 2;
       quotablk.d_ino_hardlimit = d_ino_hardlimit;
       quotablk.d_ino_softlimit = d_ino_softlimit;
       quotablk.d_fieldmask = FS_DQ_LIMIT_MASK;

       if (quotactl (QCMD (Q_XSETQLIM, USRQUOTA), mntdevice,
                     pwd->pw_uid, (caddr_t) &quotablk) != 0)
         {
           syslog (LOG_ERR, "Failed to set quotas for UID %d (running as UID %d): %s", pwd->pw_uid, getuid (), strerror (errno));
           return PAM_PERM_DENIED;
         }
       else
         {
           syslog (LOG_ERR, "Successfully setup quotas for UID %d", pwd->pw_uid);
         }
     }

   return (PAM_SUCCESS);
}


PAM_EXTERN
int pam_sm_close_session (pam_handle_t * pamh, int flags, int argc,
                          const char **argv)
{
  return (PAM_SUCCESS);
}


#ifdef PAM_STATIC

/* static module data */
struct pam_module _pam_quota_modstruct =
{
  "pam_quota",
  NULL,
  NULL,
  NULL,
  pam_sm_open_session,
  pam_sm_close_session,
  NULL,
};

#endif