/* This file is part of GNU Pies.
   Copyright (C) 2007-2025 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/>. */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <stdlib.h>
#include <string.h>
#include <pwd.h>
#include <grp.h>
#include <unistd.h>
#include <sysexits.h>
#include <stdbool.h>
#include "pies.h"

struct grecs_list *
get_user_groups (struct grecs_list *init_list, const char *user)
{
  int rc;
  struct group *gr;
  struct grecs_list *list;

  list = grecs_list_create ();
  
  if (init_list)
    {
      struct grecs_list_entry *ep;

      for (ep = init_list->head; ep; ep = ep->next)
	{
	  grecs_list_append (list, grecs_strdup ((char*)ep->data));
	}
    }

  setgrent ();
  for (rc = 0; rc == 0 && (gr = getgrent ());)
    {
      char **p;
      for (p = gr->gr_mem; *p; p++)
	if (strcmp (*p, user) == 0)
	  {
	    grecs_list_append (list, grecs_strdup (gr->gr_name));
	    break;
	  }
    }
  endgrent ();
  return list;
}

/* Switch to the given UID/GID */
int
switch_to_privs (uid_t uid, gid_t gid, struct grecs_list *retain_groups)
{
  int rc = 0;
  gid_t *emptygidset;
  size_t size = 1, j = 1;

  /* Create a list of supplementary groups */
  size = 1 + (retain_groups ? grecs_list_size (retain_groups) : 0);
  emptygidset = grecs_calloc (size, sizeof emptygidset[0]);
  emptygidset[0] = gid ? gid : getegid ();

  if (retain_groups)
    {
      struct grecs_list_entry *ep;

      for (ep = retain_groups->head; ep; ep = ep->next)
	{
	  const char *grname = ep->data;
	  struct group *group = getgrnam (grname);
	  if (!group)
	    {
	      logmsg (LOG_ERR, _("unknown group: %s"), grname);
	      free (emptygidset);
	      return 1;
	    }
	  emptygidset[j++] = group->gr_gid;
	}
    }

  /* Reset group permissions */
  if (geteuid () == 0 && setgroups (j, emptygidset))
    {
      logmsg (LOG_ERR, _("setgroups(1, %lu) failed: %s"),
	      (unsigned long) emptygidset[0], strerror (errno));
      rc = 1;
    }
  grecs_free (emptygidset);

  /* Switch to the user's gid. On some OSes the effective gid must
     be reset first */

#if defined(HAVE_SETEGID)
  if ((rc = setegid (gid)) < 0)
    logmsg (LOG_ERR, _("setegid(%lu) failed: %s"),
	      (unsigned long) gid, strerror (errno));
#elif defined(HAVE_SETREGID)
  if ((rc = setregid (gid, gid)) < 0)
    logmsg (LOG_ERR, _("setregid(%lu,%lu) failed: %s"),
	      (unsigned long) gid, (unsigned long) gid, strerror (errno));
#elif defined(HAVE_SETRESGID)
  if ((rc = setresgid (gid, gid, gid)) < 0)
    logmsg (LOG_ERR, _("setresgid(%lu,%lu,%lu) failed: %s"),
	      (unsigned long) gid,
	      (unsigned long) gid, (unsigned long) gid, strerror (errno));
#endif

  if (rc == 0 && gid != 0)
    {
      if ((rc = setgid (gid)) < 0 && getegid () != gid)
	logmsg (LOG_ERR, _("setgid(%lu) failed: %s"),
		  (unsigned long) gid, strerror (errno));
      if (rc == 0 && getegid () != gid)
	{
	  logmsg (LOG_ERR, _("cannot set effective gid to %lu"),
		    (unsigned long) gid);
	  rc = 1;
	}
    }

  /* Now reset uid */
  if (rc == 0 && uid != 0)
    {
      uid_t euid;

      if (setuid (uid)
	  || geteuid () != uid
	  || (getuid () != uid && (geteuid () == 0 || getuid () == 0)))
	{

#if defined(HAVE_SETREUID)
	  if (geteuid () != uid)
	    {
	      if (setreuid (uid, -1) < 0)
		{
		  logmsg (LOG_ERR, _("setreuid(%lu,-1) failed: %s"),
			    (unsigned long) uid, strerror (errno));
		  rc = 1;
		}
	      if (setuid (uid) < 0)
		{
		  logmsg (LOG_ERR, _("second setuid(%lu) failed: %s"),
			    (unsigned long) uid, strerror (errno));
		  rc = 1;
		}
	    }
	  else
#endif
	    {
	      logmsg (LOG_ERR, _("setuid(%lu) failed: %s"),
			(unsigned long) uid, strerror (errno));
	      rc = 1;
	    }
	}

      euid = geteuid ();
      if (uid != 0 && setuid (0) == 0)
	{
	  logmsg (LOG_ERR, _("seteuid(0) succeeded when it should not"));
	  rc = 1;
	}
      else if (uid != euid && setuid (euid) == 0)
	{
	  logmsg (LOG_ERR, _("cannot drop non-root setuid privileges"));
	  rc = 1;
	}

    }

  return rc;
}


void
pies_priv_setup (struct pies_privs *privs)
{
  struct passwd *pw;
  struct grecs_list *grplist = 0;

  if (!privs || !privs->user)
    return;

  pw = getpwnam (privs->user);
  if (!pw)
    {
      logmsg (LOG_ERR, _("no such user: %s"), privs->user);
      exit (EX_CONFIG);
    }

  if (privs->allgroups)
    grplist = get_user_groups (privs->groups, privs->user);
  if (switch_to_privs (pw->pw_uid, pw->pw_gid,
		       grplist ? grplist : privs->groups))
    exit (EX_SOFTWARE);
  if (grplist)
    grecs_list_free (grplist);
}


void
pies_epriv_setup (struct pies_privs *privs)
{
  uid_t uid;
  gid_t gid;

  if (privs)
    {
      struct passwd *pw;
      if (!privs->user)
	return;

      pw = getpwnam (privs->user);
      if (!pw)
	{
	  logmsg (LOG_ERR, _("no such user: %s"), privs->user);
	  exit (EX_CONFIG);
	}
      uid = pw->pw_uid;
      gid = pw->pw_gid;
    }
  else
    {
      uid = 0;
      gid = 0;
    }

  if (setegid (gid))
    {
      logmsg (LOG_ERR, _("cannot switch to EGID %lu: %s"),
		(unsigned long) gid, strerror (errno));
      exit (EX_USAGE);
    }
  if (seteuid (uid))
    {
      logmsg (LOG_ERR, _("cannot switch to EUID %lu: %s"),
	      (unsigned long) uid, strerror (errno));
      exit (EX_USAGE);
    }
}

int
pies_privs_cmp (struct pies_privs const *a, struct pies_privs const *b)
{
  if (!a)
    return !!b;
  else if (!b)
    return 1;
  if (safe_strcmp (a->user, b->user))
    return 1;
  if (a->allgroups != b->allgroups)
    return 1;
  return grecs_list_compare (a->groups, b->groups);
}

void
pies_privs_free (struct pies_privs *p)
{
  grecs_free (p->user);
  grecs_list_free (p->groups);
}

  
