/**
 * nsswitch lib for RSBAC user management
 *
 * Copyright (c) 2001 by Joerg Wendland, Bret Mogilefsky
 * see included file COPYING for details
 *
 * Copyright (c) 2004-2010 by Amon Ott
 * see included file COPYING for license details
 *
 */

#include "nss-rsbac.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <rsbac/types.h>
#include <rsbac/syscalls.h>
#include <shadow.h>

static pthread_mutex_t  lock;

static int user_index = 0;
static rsbac_uid_t * user_array = NULL;
static int user_num = 0;
static int group_index = 0;
static rsbac_gid_t * group_array = NULL;
static int group_num = 0;

#define ROOM 20

enum nss_status
_nss_rsbac_setspent_locked(void)
{
	if(user_array)
	  free(user_array);
	user_num = rsbac_um_get_user_list(0, RSBAC_UM_VIRTUAL_KEEP, NULL, 0);
	if(user_num < 0)
          return NSS_STATUS_UNAVAIL;
        user_num += ROOM;
        user_array = malloc(user_num * sizeof(*user_array));
	if(!user_array)
          return NSS_STATUS_UNAVAIL;
	
	user_num = rsbac_um_get_user_list(0, RSBAC_UM_VIRTUAL_KEEP, user_array, user_num);
	if(user_num < 0)
          return NSS_STATUS_UNAVAIL;
        if(user_num > 0)
          qsort(user_array, user_num, sizeof(*user_array), rsbac_user_compare);
	user_index = 0;
	return NSS_STATUS_SUCCESS;
}

enum nss_status
_nss_rsbac_setspent(void)
{
	enum nss_status status;

	pthread_mutex_lock(&lock);
	status = _nss_rsbac_setspent_locked();
	pthread_mutex_unlock(&lock);
	return status;
}

/*
 * passwd functions
 */
enum nss_status
_nss_rsbac_setpwent_locked(void)
{
	if(user_array)
	  free(user_array);
	user_num = rsbac_um_get_user_list(0, RSBAC_UM_VIRTUAL_KEEP, NULL, 0);
	if(user_num < 0)
          return NSS_STATUS_UNAVAIL;
        user_num += ROOM;
        user_array = malloc(user_num * sizeof(*user_array));
	if(!user_array)
          return NSS_STATUS_UNAVAIL;

	user_num = rsbac_um_get_user_list(0, RSBAC_UM_VIRTUAL_KEEP, user_array, user_num);
	if(user_num < 0)
          return NSS_STATUS_UNAVAIL;
        if(user_num > 0)
          qsort(user_array, user_num, sizeof(*user_array), rsbac_user_compare);
	user_index = 0;
	return NSS_STATUS_SUCCESS;
}

enum nss_status
_nss_rsbac_setpwent(void)
{
	enum nss_status status;

	pthread_mutex_lock(&lock);
	status = _nss_rsbac_setpwent_locked();
	pthread_mutex_unlock(&lock);
	return status;
}

enum nss_status
_nss_rsbac_endpwent(void)
{
	pthread_mutex_lock(&lock);
	if(user_array)
	  {
            free(user_array);
            user_array = NULL;
          }
	user_index = 0;
	user_num = 0;
	pthread_mutex_unlock(&lock);
	return NSS_STATUS_SUCCESS;
}

enum nss_status
_nss_rsbac_endspent(void)
{
	pthread_mutex_lock(&lock);
	if(user_array)
	  {
            free(user_array);
            user_array = NULL;
          }
	user_index = 0;
	user_num = 0;
	pthread_mutex_unlock(&lock);
	return NSS_STATUS_SUCCESS;
}

static enum nss_status get_copy_user_string(rsbac_uid_t user,
                                            enum rsbac_um_mod_t mod,
                                            char ** pw_item_p,
                                            char ** buffer_p,
                                            int * buflen_p,
                                            int * errnop)
  {
    int res;
    int len;
    union rsbac_um_mod_data_t data;

    if(!pw_item_p || !buffer_p || !buflen_p || !errnop)
      return NSS_STATUS_UNAVAIL;

    res = rsbac_um_get_user_item(0, user, mod, &data);
    if(res < 0)
      {
        *errnop = -res;
        return NSS_STATUS_UNAVAIL;
      }
    len = strlen(data.string);
    if ((mod == UM_name) && (RSBAC_UID_SET(user) > 0) && (RSBAC_UID_SET(user) <= RSBAC_UM_VIRTUAL_MAX))
      {
        if((*buflen_p) < len+11)
          {
            *errnop = ERANGE;
            return NSS_STATUS_TRYAGAIN;
          }
        len=sprintf((*buffer_p), "%u/%s", RSBAC_UID_SET(user), data.string);
      }
    else
      {
        if((*buflen_p) <= len)
          {
            *errnop = ERANGE;
            return NSS_STATUS_TRYAGAIN;
          }
        strncpy((*buffer_p), data.string, len);
      }
    (*buffer_p)[len] = 0;
    (*pw_item_p) = (*buffer_p);
    (*buffer_p) += len+1;
    (*buflen_p) -=len+1;

    *errnop = 0;
    return NSS_STATUS_SUCCESS;
  }

static enum nss_status
  fill_passwd(  rsbac_uid_t user,
  		struct passwd *result,
		char *buffer,
		int buflen,
		int *errnop)
{
	enum nss_status retval = NSS_STATUS_UNAVAIL;
	int res;
        union rsbac_um_mod_data_t data;

	if(!result || !buffer || !errnop || (buflen <= 0))
	  return NSS_STATUS_UNAVAIL;
	*errnop = 0;
	result->pw_uid = RSBAC_UID_NUM(user);
	buffer[0] = 0;
	result->pw_passwd = buffer;
	buffer++;
	buflen--;
	retval = get_copy_user_string(user, UM_name, &result->pw_name, &buffer, &buflen, errnop);
	if(retval != NSS_STATUS_SUCCESS)
	  return retval;
	retval = get_copy_user_string(user, UM_fullname, &result->pw_gecos, &buffer, &buflen, errnop);
	if(retval != NSS_STATUS_SUCCESS)
	  return retval;
	retval = get_copy_user_string(user, UM_homedir, &result->pw_dir, &buffer, &buflen, errnop);
	if(retval != NSS_STATUS_SUCCESS)
	  return retval;
	retval = get_copy_user_string(user, UM_shell, &result->pw_shell, &buffer, &buflen, errnop);
	if(retval != NSS_STATUS_SUCCESS)
	  return retval;
        res = rsbac_um_get_user_item(0, user, UM_group, &data);
        if(res < 0)
          {
            *errnop = -res;
            return NSS_STATUS_UNAVAIL;
          }
        memcpy(buffer, &data.group, sizeof(data.group));
        buffer[sizeof(data.group)] = 0;
        buffer += sizeof(data.group)+1;
        buflen -= sizeof(data.group)+1;
        result->pw_gid = data.group;

        return NSS_STATUS_SUCCESS;
}

static enum nss_status
  fill_spwd(  rsbac_uid_t user,
  		struct spwd *result,
		char *buffer,
		int buflen,
		int *errnop)
{
	enum nss_status retval = NSS_STATUS_UNAVAIL;
	int res;
        union rsbac_um_mod_data_t data;

	if(!result || !buffer || !errnop || !buflen)
	  return NSS_STATUS_UNAVAIL;
	*errnop = 0;
	buffer[0] = 0;
	result->sp_pwdp = buffer;
	buffer++;
	buflen--;
	retval = get_copy_user_string(user, UM_name, &result->sp_namp, &buffer, &buflen, errnop);
	if(retval != NSS_STATUS_SUCCESS)
	  return retval;
	res = rsbac_um_get_user_item(0, user, UM_lastchange, &data);
	if(res < 0)
	   {
	    *errnop = -res;
	    return NSS_STATUS_UNAVAIL;
	   }
	result->sp_lstchg = data.days;
	res = rsbac_um_get_user_item(0, user, UM_minchange, &data);
	if(res < 0)
	   {
	    *errnop = -res;
	    return NSS_STATUS_UNAVAIL;
	   }
	result->sp_min = data.days;
	res = rsbac_um_get_user_item(0, user, UM_maxchange, &data);
	if(res < 0)
	   {
	    *errnop = -res;
	    return NSS_STATUS_UNAVAIL;
	   }
	result->sp_max = data.days;
	res = rsbac_um_get_user_item(0, user, UM_warnchange, &data);
	if(res < 0)
	   {
	    *errnop = -res;
	    return NSS_STATUS_UNAVAIL;
	   }
	result->sp_warn = data.days;
	res = rsbac_um_get_user_item(0, user, UM_inactive, &data);
	if(res < 0)
	   {
	    *errnop = -res;
	    return NSS_STATUS_UNAVAIL;
	   }
	result->sp_inact = data.days;
	res = rsbac_um_get_user_item(0, user, UM_expire, &data);
	if(res < 0)
	   {
	    *errnop = -res;
	    return NSS_STATUS_UNAVAIL;
	   }
	result->sp_expire = data.days;
	   
        return NSS_STATUS_SUCCESS;
}


enum nss_status
_nss_rsbac_getpwent_r(struct passwd *result,
			 char *buffer,
			 size_t buflen,
			 int *errnop)
{
	enum nss_status retval = NSS_STATUS_UNAVAIL;
	int res;

	pthread_mutex_lock(&lock);

        if(!user_array)
          {
            res = _nss_rsbac_setpwent_locked();
            if(res != NSS_STATUS_SUCCESS)
              {
                *errnop = ERANGE;
                pthread_mutex_unlock(&lock);
                return res;
	      }
          }
          
        if(user_index < user_num)
          {
            retval = fill_passwd(user_array[user_index],
                                 result,
                                 buffer,
                                 buflen,
                                 errnop);
	    user_index++;
          }
        else
          retval = NSS_STATUS_NOTFOUND;

        pthread_mutex_unlock(&lock);

	return retval;
}

enum nss_status
_nss_rsbac_getspent_r(struct spwd *result,
			 char *buffer,
			 size_t buflen,
			 int *errnop)
{
	enum nss_status retval = NSS_STATUS_UNAVAIL;
	int res;

	pthread_mutex_lock(&lock);

        if(!user_array)
          {
            res = _nss_rsbac_setspent_locked();
            if(res != NSS_STATUS_SUCCESS)
              {
                *errnop = ERANGE;
                pthread_mutex_unlock(&lock);
                return res;
	      }
          }
          
        if(user_index < user_num)
          {
            retval = fill_spwd(user_array[user_index],
                                 result,
                                 buffer,
                                 buflen,
                                 errnop);
	    user_index++;
          }
        else
          retval = NSS_STATUS_NOTFOUND;

        pthread_mutex_unlock(&lock);

	return retval;
}

enum nss_status
_nss_rsbac_getpwnam_r(char *pwnam,
			 struct passwd *result,
			 char *buffer,
			 size_t buflen,
			 int *errnop)
{
	enum nss_status retval = NSS_STATUS_UNAVAIL;
	rsbac_uid_t user = RSBAC_GEN_UID(RSBAC_UM_VIRTUAL_KEEP, RSBAC_NO_USER);
	int res;

        res = rsbac_um_get_uid(0, pwnam, &user);
        if(res < 0)
          {
            *errnop = -res;
            return retval;
          }
        retval = fill_passwd(user,
                             result,
	                     buffer,
		             buflen,
		             errnop);

	return retval;
}

enum nss_status
_nss_rsbac_getpwuid_r(uid_t uid,
			 struct passwd *result,
			 char *buffer,
			 size_t buflen,
			 int *errnop)
{
	enum nss_status retval = NSS_STATUS_UNAVAIL;

        retval = fill_passwd(RSBAC_GEN_UID(RSBAC_UM_VIRTUAL_KEEP,uid),
                             result,
	                     buffer,
		             buflen,
		             errnop);
	return retval;
}

enum nss_status
_nss_rsbac_getspnam_r(char *pwnam,
			 struct spwd *result,
			 char *buffer,
			 size_t buflen,
			 int *errnop)
{
	enum nss_status retval = NSS_STATUS_UNAVAIL;
	rsbac_uid_t user = RSBAC_GEN_UID(RSBAC_UM_VIRTUAL_KEEP, RSBAC_NO_USER);
	int res;

        res = rsbac_um_get_uid(0, pwnam, &user);
        if(res < 0)
          {
            *errnop = -res;
            return retval;
          }
        retval = fill_spwd(user,
                           result,
                           buffer,
                           buflen,
	                   errnop);
	return retval;
}


/*
 * group functions
 */

static enum nss_status get_copy_group_string(rsbac_gid_t group,
                                            enum rsbac_um_mod_t mod,
                                            char ** gr_item_p,
                                            char ** buffer_p,
                                            int * buflen_p,
                                            int * errnop)
  {
    int res;
    int len;
    union rsbac_um_mod_data_t data;

    if(!gr_item_p || !buffer_p || !buflen_p || !errnop)
      return NSS_STATUS_UNAVAIL;

    res = rsbac_um_get_group_item(0, group, mod, &data);
    if(res < 0)
      {
        *errnop = -res;
        return NSS_STATUS_UNAVAIL;
      }
    len = strlen(data.string);
    if((*buflen_p) < len+1)
      {
        *errnop = ERANGE;
        return NSS_STATUS_TRYAGAIN;
      }
    strncpy((*buffer_p), data.string, len);
    (*buffer_p)[len] = 0;
    (*gr_item_p) = (*buffer_p);
    (*buffer_p) += len+1;
    (*buflen_p) -=len+1;

    *errnop = 0;
    return NSS_STATUS_SUCCESS;
  }

static enum nss_status
  fill_group(rsbac_gid_t group,
  		struct group *result,
		char *buffer,
		int buflen,
		int *errnop)
{
	enum nss_status retval = NSS_STATUS_UNAVAIL;
	rsbac_uid_num_t * g_user_array;
	int member_count;

	if(!result || !buffer || !errnop || (buflen <= 0))
	  return retval;
	result->gr_gid = RSBAC_GID_NUM(group);
	buffer[0] = 0;
	result->gr_passwd = buffer;
	buffer++;
	buflen--;
	retval = get_copy_group_string(group, UM_name, &result->gr_name, &buffer, &buflen, errnop);
	if(retval != NSS_STATUS_SUCCESS)
	  return retval;
	member_count = rsbac_um_get_gm_user_list(0, group, NULL, 0);
	if(member_count > 0)
	  {
	    /* some extra space */
	    member_count += 10;
	    g_user_array = malloc(member_count * sizeof(*g_user_array));
	    if(!g_user_array)
	      {
                memset(buffer, 0, sizeof(char *));
                result->gr_mem = (char **) buffer;
                buffer += sizeof(char *);
                buflen -= sizeof(char *);
	      }
	    else
	      {
                member_count = rsbac_um_get_gm_user_list(0, group,
                                                         g_user_array,
                                                         member_count);
		if(member_count > 0)
		  {
		    int i;
		    int res;
		    int len;
		    int count = 0;
		    char ** pointers = (char **) buffer;
                    union rsbac_um_mod_data_t data;

                    if(buflen < (member_count + 1) * sizeof(char *))
                      {
                        *errnop = ERANGE;
                        return NSS_STATUS_TRYAGAIN;
                      }
                    memset(pointers, 0, (member_count + 1) * sizeof(char *));
                    buffer += (member_count + 1) * sizeof(char *);
                    buflen -= (member_count + 1) * sizeof(char *);
                    for(i=0; i<member_count; i++)
                      {
                        res = rsbac_um_get_user_item(
                        	0,
                        	RSBAC_GEN_UID(RSBAC_GID_SET(group),g_user_array[i]),
                        	UM_name,
                        	&data);
                        if(res < 0)
                          continue;
                        len = strlen(data.string);
                        if(buflen < len+1)
                          {
                            *errnop = ERANGE;
                            return NSS_STATUS_TRYAGAIN;
                          }
                        strncpy(buffer, data.string, len);
                        buffer[len] = 0;
                        pointers[count] = buffer;
                        count++;
                        buffer += len+1;
                        buflen -= len+1;
                      }
                    pointers[count] = NULL;
                    result->gr_mem = pointers;
		  }
                else
                  {
                    memset(buffer, 0, sizeof(char *));
                    result->gr_mem = (char **) buffer;
                    buffer += sizeof(char *);
                    buflen -= sizeof(char *);
                  }
		free(g_user_array);
	      }
	  }
	else
	  {
            memset(buffer, 0, sizeof(char *));
            result->gr_mem = (char **) buffer;
            buffer += sizeof(char *);
            buflen -= sizeof(char *);
	  }
        return NSS_STATUS_SUCCESS;
}

enum nss_status
_nss_rsbac_setgrent_locked(void)
{
	if(group_array)
	  free(group_array);
	group_num = rsbac_um_get_group_list(0, RSBAC_UM_VIRTUAL_KEEP, NULL, 0);
	if(group_num < 0)
          return NSS_STATUS_UNAVAIL;
        group_num += ROOM;
        group_array = malloc(group_num * sizeof(*group_array));
	if(!group_array)
          return NSS_STATUS_UNAVAIL;
	
	group_num = rsbac_um_get_group_list(0, RSBAC_UM_VIRTUAL_KEEP, group_array, group_num);
	if(group_num < 0)
          return NSS_STATUS_UNAVAIL;
        if(group_num > 0)
          qsort(group_array, group_num, sizeof(*group_array), rsbac_group_compare);
	group_index = 0;

	return NSS_STATUS_SUCCESS;
}

enum nss_status
_nss_rsbac_setgrent(void)
{
	enum nss_status status;

	pthread_mutex_lock(&lock);
	status = _nss_rsbac_setgrent_locked();
	pthread_mutex_unlock(&lock);

	return status;
}

enum nss_status
_nss_rsbac_endgrent(void)
{
	pthread_mutex_lock(&lock);
	if(group_array)
	  {
            free(group_array);
            group_array = NULL;
          }
	group_index = 0;
	group_num = 0;
	pthread_mutex_unlock(&lock);

	return NSS_STATUS_SUCCESS;
}

enum nss_status
_nss_rsbac_getgrent_r(struct group *result,
			 char *buffer,
			 size_t buflen,
			 int *errnop)
{
	enum nss_status retval = NSS_STATUS_UNAVAIL;

	pthread_mutex_lock(&lock);

        if(!group_array)
          {
            retval = _nss_rsbac_setgrent_locked();
            if(retval != NSS_STATUS_SUCCESS)
              {
                pthread_mutex_unlock(&lock);
                return retval;
	      }
          }
          
        if(group_index < group_num)
          {
            retval = fill_group(group_array[group_index],
                                result,
                                buffer,
                                buflen,
                                errnop);
	    group_index++;
          }
        else
          retval = NSS_STATUS_NOTFOUND;

	pthread_mutex_unlock(&lock);

	return retval;
}

enum nss_status
_nss_rsbac_getgrnam_r(const char *grnam,
			 struct group *result,
			 char *buffer,
			 size_t buflen,
			 int *errnop)
{
	enum nss_status retval = NSS_STATUS_UNAVAIL;
	rsbac_gid_t group = RSBAC_GEN_GID(RSBAC_UM_VIRTUAL_KEEP, RSBAC_NO_GROUP);

        if(rsbac_um_get_gid(0, (char *) grnam, &group))
          return retval;
        retval = fill_group(group,
                            result,
	                    buffer,
		            buflen,
		            errnop);
	return retval;
}

enum nss_status
_nss_rsbac_getgrgid_r(gid_t gid,
			 struct group *result,
			 char *buffer,
			 size_t buflen,
			 int *errnop)
{
	enum nss_status retval = NSS_STATUS_UNAVAIL;

        retval = fill_group(RSBAC_GEN_GID(RSBAC_UM_VIRTUAL_KEEP, gid),
                            result,
	                    buffer,
		            buflen,
		            errnop);
	return retval;
}

enum nss_status
_nss_rsbac_initgroups_dyn(char *user,
			  gid_t group,
			  long int *start,
			  long int *size,
			  gid_t **groupsp,
			  long int limit,
			  int *errnop)
{
	rsbac_uid_t   uid = RSBAC_GEN_UID(RSBAC_UM_VIRTUAL_KEEP, RSBAC_NO_USER);
	rsbac_gid_num_t * gm_array;
	gid_t *groups = *groupsp;
	int gm_num;

        if(rsbac_um_get_uid(0, user, &uid))
          return NSS_STATUS_UNAVAIL;
	gm_num = rsbac_um_get_gm_list(0, uid, NULL, 0);
	if(gm_num < 0)
          return NSS_STATUS_UNAVAIL;
        gm_num += ROOM;
        gm_array = malloc(gm_num * sizeof(*gm_array));
	if(!gm_array)
          return NSS_STATUS_UNAVAIL;
	
	gm_num = rsbac_um_get_gm_list(0, uid, gm_array, gm_num);
	if(gm_num < 0)
	  {
	    free(gm_array);
            return NSS_STATUS_UNAVAIL;
          }
	if(!gm_num)
	  {
	    free(gm_array);
            return NSS_STATUS_NOTFOUND;
          }

        if(gm_num + (*start) > *size)
          {
            // Have to make the result buffer bigger
            long int newsize = gm_num + (*start);
            newsize = (limit > 0) ? rsbac_min(limit, newsize) : newsize;
            *groupsp = groups = realloc(groups, newsize * sizeof(*groups));
            *size = newsize;
          }
        gm_num = (limit > 0) ? rsbac_min(gm_num, limit - *start) : gm_num;

        while(gm_num--)
          {
            groups[*start] = gm_array[gm_num];
            *start += 1;
          }
        free(gm_array);

	return NSS_STATUS_SUCCESS;
}
