/*
   passwd.c - password entry lookup routines
   Parts of this file were part of the nss_ldap library (as ldap-pwd.c)
   which has been forked into the nss-pam-ldapd library.

   Copyright (C) 1997-2005 Luke Howard
   Copyright (C) 2006 West Consulting
   Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 Arthur de Jong

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

   This library 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
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with this library; if not, write to the Free Software
   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
   02110-1301 USA
*/

#include "config.h"

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

#include "common.h"
#include "log.h"
#include "myldap.h"
#include "cfg.h"
#include "attmap.h"
#include "common/dict.h"
#include "compat/strndup.h"

/* ( nisSchema.2.0 NAME 'posixAccount' SUP top AUXILIARY
 *   DESC 'Abstraction of an account with POSIX attributes'
 *   MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory )
 *   MAY ( userPassword $ loginShell $ gecos $ description ) )
 */

/* the search base for searches */
const char *passwd_bases[NSS_LDAP_CONFIG_MAX_BASES] = { NULL };

/* the search scope for searches */
int passwd_scope = LDAP_SCOPE_DEFAULT;

/* the basic search filter for searches */
const char *passwd_filter = "(objectClass=posixAccount)";

/* the attributes used in searches */
const char *attmap_passwd_uid           = "uid";
const char *attmap_passwd_userPassword  = "\"*\"";
const char *attmap_passwd_uidNumber     = "uidNumber";
const char *attmap_passwd_gidNumber     = "gidNumber";
const char *attmap_passwd_gecos         = "\"${gecos:-$cn}\"";
const char *attmap_passwd_homeDirectory = "homeDirectory";
const char *attmap_passwd_loginShell    = "loginShell";

/* special properties for objectSid-based searches
   (these are already LDAP-escaped strings) */
static char *uidSid=NULL;
static char *gidSid=NULL;

/* default values for attributes */
static const char *default_passwd_userPassword     = "*"; /* unmatchable */

/* Note that the resulting password value should be one of:
   <empty> - no password set, allow login without password
   *       - often used to prevent logins
   x       - "valid" encrypted password that does not match any valid password
             often used to indicate that the password is defined elsewhere
   other   - encrypted password, usually in crypt(3) format */

/* the attribute list to request with searches */
static const char **passwd_attrs=NULL;

/* create a search filter for searching a passwd entry
   by name, return -1 on errors */
static int mkfilter_passwd_byname(const char *name,
                                  char *buffer,size_t buflen)
{
  char safename[300];
  /* escape attribute */
  if(myldap_escape(name,safename,sizeof(safename)))
    return -1;
  /* build filter */
  return mysnprintf(buffer,buflen,
                    "(&%s(%s=%s))",
                    passwd_filter,
                    attmap_passwd_uid,safename);
}

/* create a search filter for searching a passwd entry
   by uid, return -1 on errors */
static int mkfilter_passwd_byuid(uid_t uid,
                                 char *buffer,size_t buflen)
{
  if (uidSid!=NULL)
  {
    return mysnprintf(buffer,buflen,
                      "(&%s(%s=%s\\%02x\\%02x\\%02x\\%02x))",
                      passwd_filter,
                      attmap_passwd_uidNumber,uidSid,
                      (int)(uid&0xff),(int)((uid>>8)&0xff),
                      (int)((uid>>16)&0xff),(int)((uid>>24)&0xff));
  }
  else
  {
    return mysnprintf(buffer,buflen,
                      "(&%s(%s=%d))",
                      passwd_filter,
                      attmap_passwd_uidNumber,(int)uid);
  }
}

void passwd_init(void)
{
  int i;
  SET *set;
  /* set up search bases */
  if (passwd_bases[0]==NULL)
    for (i=0;i<NSS_LDAP_CONFIG_MAX_BASES;i++)
      passwd_bases[i]=nslcd_cfg->ldc_bases[i];
  /* set up scope */
  if (passwd_scope==LDAP_SCOPE_DEFAULT)
    passwd_scope=nslcd_cfg->ldc_scope;
  /* special case when uidNumber or gidNumber reference objectSid */
  if (strncasecmp(attmap_passwd_uidNumber,"objectSid:",10)==0)
  {
    uidSid=sid2search(attmap_passwd_uidNumber+10);
    attmap_passwd_uidNumber=strndup(attmap_passwd_uidNumber,9);
  }
  if (strncasecmp(attmap_passwd_gidNumber,"objectSid:",10)==0)
  {
    gidSid=sid2search(attmap_passwd_gidNumber+10);
    attmap_passwd_gidNumber=strndup(attmap_passwd_gidNumber,9);
  }
  /* set up attribute list */
  set=set_new();
  attmap_add_attributes(set,"objectClass"); /* for testing shadowAccount */
  attmap_add_attributes(set,attmap_passwd_uid);
  attmap_add_attributes(set,attmap_passwd_userPassword);
  attmap_add_attributes(set,attmap_passwd_uidNumber);
  attmap_add_attributes(set,attmap_passwd_gidNumber);
  attmap_add_attributes(set,attmap_passwd_gecos);
  attmap_add_attributes(set,attmap_passwd_homeDirectory);
  attmap_add_attributes(set,attmap_passwd_loginShell);
  passwd_attrs=set_tolist(set);
  set_free(set);
}

/* the cache that is used in dn2uid() */
static pthread_mutex_t dn2uid_cache_mutex=PTHREAD_MUTEX_INITIALIZER;
static DICT *dn2uid_cache=NULL;
struct dn2uid_cache_entry
{
  time_t timestamp;
  char *uid;
};
#define DN2UID_CACHE_TIMEOUT (15*60)

/* checks whether the entry has a valid uidNumber attribute
   (>= nss_min_uid) */
static int entry_has_valid_uid(MYLDAP_ENTRY *entry)
{
  int i;
  const char **values;
  char *tmp;
  uid_t uid;
  /* if min_uid is not set any entry should do */
  if (nslcd_cfg->ldc_nss_min_uid==0)
    return 1;
  /* get all uidNumber attributes */
  values=myldap_get_values_len(entry,attmap_passwd_uidNumber);
  if ((values==NULL)||(values[0]==NULL))
  {
    log_log(LOG_WARNING,"%s: %s: missing",
                        myldap_get_dn(entry),attmap_passwd_uidNumber);
    return 0;
  }
  /* check if there is a uidNumber attributes >= min_uid */
  for (i=0;values[i]!=NULL;i++)
  {
    if (uidSid!=NULL)
      uid=(uid_t)binsid2id(values[i]);
    else
    {
      errno=0;
      uid=strtouid(values[i],&tmp,10);
      if ((*(values[i])=='\0')||(*tmp!='\0'))
      {
        log_log(LOG_WARNING,"%s: %s: non-numeric",
                            myldap_get_dn(entry),attmap_passwd_uidNumber);
        continue;
      }
      else if ((errno!=0)||(strchr(values[i],'-')!=NULL))
      {
        log_log(LOG_WARNING,"%s: %s: out of range",
                            myldap_get_dn(entry),attmap_passwd_uidNumber);
        continue;
      }
    }
    if (uid>=nslcd_cfg->ldc_nss_min_uid)
      return 1;
  }
  /* nothing found */
  return 0;
}

/* Perform an LDAP lookup to translate the DN into a uid.
   This function either returns NULL or a strdup()ed string. */
char *lookup_dn2uid(MYLDAP_SESSION *session,const char *dn,int *rcp,char *buf,size_t buflen)
{
  MYLDAP_SEARCH *search;
  MYLDAP_ENTRY *entry;
  static const char *attrs[3];
  int rc=LDAP_SUCCESS;
  const char **values;
  char *uid=NULL;
  if (rcp==NULL)
    rcp=&rc;
  /* we have to look up the entry */
  attrs[0]=attmap_passwd_uid;
  attrs[1]=attmap_passwd_uidNumber;
  attrs[2]=NULL;
  search=myldap_search(session,dn,LDAP_SCOPE_BASE,passwd_filter,attrs,rcp);
  if (search==NULL)
  {
    log_log(LOG_WARNING,"%s: lookup error: %s",dn,ldap_err2string(*rcp));
    return NULL;
  }
  entry=myldap_get_entry(search,rcp);
  if (entry==NULL)
  {
    if (*rcp!=LDAP_SUCCESS)
      log_log(LOG_WARNING,"%s: lookup error: %s",dn,ldap_err2string(*rcp));
    return NULL;
  }
  /* check the uidNumber attribute if min_uid is set */
  if (entry_has_valid_uid(entry))
  {
    /* get uid (just use first one) */
    values=myldap_get_values(entry,attmap_passwd_uid);
    /* check the result for presence and validity */
    if ((values!=NULL)&&(values[0]!=NULL)&&isvalidname(values[0])&&(strlen(values[0])<buflen))
    {
      strcpy(buf,values[0]);
      uid=buf;
    }
  }
  /* clean up and return */
  myldap_search_close(search);
  return uid;
}

/* Translate the DN into a user name. This function tries several aproaches
   at getting the user name, including looking in the DN for a uid attribute,
   looking in the cache and falling back to looking up a uid attribute in a
   LDAP query. */
char *dn2uid(MYLDAP_SESSION *session,const char *dn,char *buf,size_t buflen)
{
  struct dn2uid_cache_entry *cacheentry=NULL;
  char *uid;
  /* check for empty string */
  if ((dn==NULL)||(*dn=='\0'))
    return NULL;
  /* try to look up uid within DN string */
  if (myldap_cpy_rdn_value(dn,attmap_passwd_uid,buf,buflen)!=NULL)
  {
    /* check if it is valid */
    if (!isvalidname(buf))
      return NULL;
    return buf;
  }
  /* see if we have a cached entry */
  pthread_mutex_lock(&dn2uid_cache_mutex);
  if (dn2uid_cache==NULL)
    dn2uid_cache=dict_new();
  if ((dn2uid_cache!=NULL) && ((cacheentry=dict_get(dn2uid_cache,dn))!=NULL))
  {
    /* if the cached entry is still valid, return that */
    if (time(NULL) < (cacheentry->timestamp+DN2UID_CACHE_TIMEOUT))
    {
      if ((cacheentry->uid!=NULL)&&(strlen(cacheentry->uid)<buflen))
        strcpy(buf,cacheentry->uid);
      else
        buf=NULL;
      pthread_mutex_unlock(&dn2uid_cache_mutex);
      return buf;
    }
    /* leave the entry intact, just replace the uid below */
  }
  pthread_mutex_unlock(&dn2uid_cache_mutex);
  /* look up the uid using an LDAP query */
  uid=lookup_dn2uid(session,dn,NULL,buf,buflen);
  /* store the result in the cache */
  pthread_mutex_lock(&dn2uid_cache_mutex);
  /* try to get the entry from the cache here again because it could have
     changed in the meantime */
  cacheentry=dict_get(dn2uid_cache,dn);
  if (cacheentry==NULL)
  {
    /* allocate a new entry in the cache */
    cacheentry=(struct dn2uid_cache_entry *)malloc(sizeof(struct dn2uid_cache_entry));
    if (cacheentry!=NULL)
    {
      cacheentry->uid=NULL;
      dict_put(dn2uid_cache,dn,cacheentry);
    }
  }
  /* update the cache entry */
  if (cacheentry!=NULL)
  {
    cacheentry->timestamp=time(NULL);
    /* copy the uid if needed */
    if (cacheentry->uid==NULL)
      cacheentry->uid=uid!=NULL?strdup(uid):NULL;
    else if ((uid==NULL)||(strcmp(cacheentry->uid,uid)!=0))
    {
      free(cacheentry->uid);
      cacheentry->uid=uid!=NULL?strdup(uid):NULL;
    }
  }
  pthread_mutex_unlock(&dn2uid_cache_mutex);
  /* copy the result into the buffer */
  return uid;
}

MYLDAP_ENTRY *uid2entry(MYLDAP_SESSION *session,const char *uid,int *rcp)
{
  MYLDAP_SEARCH *search=NULL;
  MYLDAP_ENTRY *entry=NULL;
  const char *base;
  int i;
  static const char *attrs[3];
  char filter[4096];
  /* if it isn't a valid username, just bail out now */
  if (!isvalidname(uid))
  {
    if (rcp!=NULL)
      *rcp=LDAP_INVALID_SYNTAX;
    return NULL;
  }
  /* set up attributes (we don't need much) */
  attrs[0]=attmap_passwd_uid;
  attrs[1]=attmap_passwd_uidNumber;
  attrs[2]=NULL;
  /* we have to look up the entry */
  mkfilter_passwd_byname(uid,filter,sizeof(filter));
  for (i=0;(i<NSS_LDAP_CONFIG_MAX_BASES)&&((base=passwd_bases[i])!=NULL);i++)
  {
    search=myldap_search(session,base,passwd_scope,filter,attrs,rcp);
    if (search==NULL)
    {
      if ((rcp!=NULL)&&(*rcp==LDAP_SUCCESS))
        *rcp=LDAP_NO_SUCH_OBJECT;
      return NULL;
    }
    entry=myldap_get_entry(search,rcp);
    if ((entry!=NULL)&&(entry_has_valid_uid(entry)))
      return entry;
  }
  if ((rcp!=NULL)&&(*rcp==LDAP_SUCCESS))
    *rcp=LDAP_NO_SUCH_OBJECT;
  return NULL;
}

char *uid2dn(MYLDAP_SESSION *session,const char *uid,char *buf,size_t buflen)
{
  MYLDAP_ENTRY *entry;
  /* look up the entry */
  entry=uid2entry(session,uid,NULL);
  if (entry==NULL)
    return NULL;
  /* get DN */
  return myldap_cpy_dn(entry,buf,buflen);
}

#ifndef NSS_FLAVOUR_GLIBC

/* only check nsswitch.conf for glibc */
#define check_nsswitch_reload()
#define shadow_uses_ldap() (1)

#else /* NSS_FLAVOUR_GLIBC */

/* the cached value of whether shadow lookups use LDAP in nsswitch.conf */
#define NSSWITCH_FILE "/etc/nsswitch.conf"
#define CACHED_UNKNOWN 22
static int cached_shadow_uses_ldap=CACHED_UNKNOWN;
static time_t cached_shadow_lastcheck=0;
#define CACHED_SHADOW_TIMEOUT (60)
static time_t nsswitch_mtime=0;

/* check whether /etc/nsswitch.conf should be related to update
   cached_shadow_uses_ldap */
static inline void check_nsswitch_reload(void)
{
  struct stat buf;
  time_t t;
  if ((cached_shadow_uses_ldap!=CACHED_UNKNOWN)&&
      ((t=time(NULL)) > (cached_shadow_lastcheck+CACHED_SHADOW_TIMEOUT)))
  {
    cached_shadow_lastcheck=t;
    if (stat(NSSWITCH_FILE,&buf))
    {
      log_log(LOG_ERR,"stat(%s) failed: %s",NSSWITCH_FILE,strerror(errno));
      /* trigger a recheck anyway */
      cached_shadow_uses_ldap=CACHED_UNKNOWN;
      return;
    }
    /* trigger a recheck if file changed */
    if (buf.st_mtime!=nsswitch_mtime)
    {
      nsswitch_mtime=buf.st_mtime;
      cached_shadow_uses_ldap=CACHED_UNKNOWN;
    }
  }
}

/* check whether shadow lookups are configured to use ldap */
static inline int shadow_uses_ldap(void)
{
  if (cached_shadow_uses_ldap==CACHED_UNKNOWN)
  {
    log_log(LOG_INFO,"(re)loading %s",NSSWITCH_FILE);
    cached_shadow_uses_ldap=nsswitch_db_uses_ldap(NSSWITCH_FILE,"shadow");
    cached_shadow_lastcheck=time(NULL);
  }
  return cached_shadow_uses_ldap;
}

#endif /* NSS_FLAVOUR_GLIBC */

/* the maximum number of uidNumber attributes per entry */
#define MAXUIDS_PER_ENTRY 5

static int write_passwd(TFILE *fp,MYLDAP_ENTRY *entry,const char *requser,
                        const uid_t *requid,uid_t calleruid)
{
  int32_t tmpint32;
  const char **tmpvalues;
  char *tmp;
  const char **usernames;
  const char *passwd;
  uid_t uids[MAXUIDS_PER_ENTRY];
  int numuids;
  char gidbuf[32];
  gid_t gid;
  char gecos[1024];
  char homedir[256];
  char shell[64];
  char passbuffer[64];
  int i,j;
  /* get the usernames for this entry */
  usernames=myldap_get_values(entry,attmap_passwd_uid);
  if ((usernames==NULL)||(usernames[0]==NULL))
  {
    log_log(LOG_WARNING,"%s: %s: missing",
                        myldap_get_dn(entry),attmap_passwd_uid);
    return 0;
  }
  /* if we are using shadow maps and this entry looks like it would return
     shadow information, make the passwd entry indicate it */
  if (myldap_has_objectclass(entry,"shadowAccount")&&shadow_uses_ldap())
  {
    passwd="x";
  }
  else
  {
    passwd=get_userpassword(entry,attmap_passwd_userPassword,passbuffer,sizeof(passbuffer));
    if ((passwd==NULL)||(calleruid!=0))
      passwd=default_passwd_userPassword;
  }
  /* get the uids for this entry */
  if (requid!=NULL)
  {
    uids[0]=*requid;
    numuids=1;
  }
  else
  {
    tmpvalues=myldap_get_values_len(entry,attmap_passwd_uidNumber);
    if ((tmpvalues==NULL)||(tmpvalues[0]==NULL))
    {
      log_log(LOG_WARNING,"%s: %s: missing",
                          myldap_get_dn(entry),attmap_passwd_uidNumber);
      return 0;
    }
    for (numuids=0;(numuids<MAXUIDS_PER_ENTRY)&&(tmpvalues[numuids]!=NULL);numuids++)
    {
      if (uidSid!=NULL)
        uids[numuids]=(uid_t)binsid2id(tmpvalues[numuids]);
      else
      {
        errno=0;
        uids[numuids]=strtouid(tmpvalues[numuids],&tmp,10);
        if ((*(tmpvalues[numuids])=='\0')||(*tmp!='\0'))
        {
          log_log(LOG_WARNING,"%s: %s: non-numeric",
                              myldap_get_dn(entry),attmap_passwd_uidNumber);
          return 0;
        }
        else if ((errno!=0)||(strchr(tmpvalues[numuids],'-')!=NULL))
        {
          log_log(LOG_WARNING,"%s: %s: out of range",
                              myldap_get_dn(entry),attmap_passwd_uidNumber);
          return 0;
        }
      }
    }
  }
  /* get the gid for this entry */
  if (gidSid!=NULL)
  {
    tmpvalues=myldap_get_values_len(entry,attmap_passwd_gidNumber);
    if ((tmpvalues==NULL)||(tmpvalues[0]==NULL))
    {
      log_log(LOG_WARNING,"%s: %s: missing",
                          myldap_get_dn(entry),attmap_passwd_gidNumber);
      return 0;
    }
    gid=(gid_t)binsid2id(tmpvalues[0]);
  }
  else
  {
    attmap_get_value(entry,attmap_passwd_gidNumber,gidbuf,sizeof(gidbuf));
    if (gidbuf[0]=='\0')
    {
      log_log(LOG_WARNING,"%s: %s: missing",
                          myldap_get_dn(entry),attmap_passwd_gidNumber);
      return 0;
    }
    errno=0;
    gid=strtogid(gidbuf,&tmp,10);
    if ((gidbuf[0]=='\0')||(*tmp!='\0'))
    {
      log_log(LOG_WARNING,"%s: %s: non-numeric",
                          myldap_get_dn(entry),attmap_passwd_gidNumber);
      return 0;
    }
    else if ((errno!=0)||(strchr(gidbuf,'-')!=NULL))
    {
      log_log(LOG_WARNING,"%s: %s: out of range",
                          myldap_get_dn(entry),attmap_passwd_gidNumber);
      return 0;
    }
  }
  /* get the gecos for this entry */
  attmap_get_value(entry,attmap_passwd_gecos,gecos,sizeof(gecos));
  /* get the home directory for this entry */
  attmap_get_value(entry,attmap_passwd_homeDirectory,homedir,sizeof(homedir));
  if (homedir[0]=='\0')
    log_log(LOG_WARNING,"%s: %s: missing",
                        myldap_get_dn(entry),attmap_passwd_homeDirectory);
  /* get the shell for this entry */
  attmap_get_value(entry,attmap_passwd_loginShell,shell,sizeof(shell));
  /* write the entries */
  for (i=0;usernames[i]!=NULL;i++)
    if ((requser==NULL)||(STR_CMP(requser,usernames[i])==0))
    {
      if (!isvalidname(usernames[i]))
      {
        log_log(LOG_WARNING,"%s: %s: denied by validnames option",
                            myldap_get_dn(entry),attmap_passwd_uid);
      }
      else
      {
        for (j=0;j<numuids;j++)
        {
          if (uids[j]>=nslcd_cfg->ldc_nss_min_uid)
          {
            WRITE_INT32(fp,NSLCD_RESULT_BEGIN);
            WRITE_STRING(fp,usernames[i]);
            WRITE_STRING(fp,passwd);
            WRITE_TYPE(fp,uids[j],uid_t);
            WRITE_TYPE(fp,gid,gid_t);
            WRITE_STRING(fp,gecos);
            WRITE_STRING(fp,homedir);
            WRITE_STRING(fp,shell);
          }
        }
      }
    }
  return 0;
}

NSLCD_HANDLE_UID(
  passwd,byname,
  char name[256];
  char filter[4096];
  READ_STRING(fp,name);
  log_setrequest("passwd=\"%s\"",name);
  if (!isvalidname(name)) {
    log_log(LOG_WARNING,"request denied by validnames option");
    return -1;
  }
  check_nsswitch_reload();,
  NSLCD_ACTION_PASSWD_BYNAME,
  mkfilter_passwd_byname(name,filter,sizeof(filter)),
  write_passwd(fp,entry,name,NULL,calleruid)
)

NSLCD_HANDLE_UID(
  passwd,byuid,
  uid_t uid;
  char filter[4096];
  READ_TYPE(fp,uid,uid_t);
  log_setrequest("passwd=%d",(int)uid);
  if (uid<nslcd_cfg->ldc_nss_min_uid)
  {
    /* return an empty result */
    WRITE_INT32(fp,NSLCD_VERSION);
    WRITE_INT32(fp,NSLCD_ACTION_PASSWD_BYUID);
    WRITE_INT32(fp,NSLCD_RESULT_END);
  }
  check_nsswitch_reload();,
  NSLCD_ACTION_PASSWD_BYUID,
  mkfilter_passwd_byuid(uid,filter,sizeof(filter)),
  write_passwd(fp,entry,NULL,&uid,calleruid)
)

NSLCD_HANDLE_UID(
  passwd,all,
  const char *filter;
  log_setrequest("passwd(all)");
  check_nsswitch_reload();,
  NSLCD_ACTION_PASSWD_ALL,
  (filter=passwd_filter,0),
  write_passwd(fp,entry,NULL,NULL,calleruid)
)