/*
 * Copyright 2003 Sun Microsystems, Inc. All rights reserved.
 * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */

/*
 * auth_dbm.c: Sample ACP API authentication modue using Netscape DBM file 
 * format.
 *
 * DBM functions based on work by Rob McCool & Brian Behlendorf.
 */

#include "nsacl/aclapi.h"
#include "base/ereport.h" /* for ereport() */

#include <fcntl.h>

#if defined(LINUX)
#include <gdbm/ndbm.h>
#elif defined(SOLARIS)
#include <ndbm.h>
#else
#include <ndbm.h>
#endif

#ifdef XP_WIN32
char * crypt(const char *, const char *);
#endif

#define DBMPREFIX "dbmdb"
#define MODULE "DBMACL"

static CRITICAL auth_crit = NULL;
ACLDbType_t ACL_DbTypeDBM = ACL_DBTYPE_INVALID;    /* Handle to the dbm database type */

#ifdef XP_WIN32
#define NSAPI_PUBLIC __declspec(dllexport)
#else /* !XP_WIN32 */
#define NSAPI_PUBLIC
#endif /* !XP_WIN32 */

/* ------------------ ACL API DBM Authentication Module ----------------- 

   Compiling:
      Be sure that ndbm.h is in the include search path (-I).

      In order to create/manage Netscape DBM databases you need
      to link your programs with the built-in version of DBM at
      [server-root]/bin/https/lib/liblibdbm.so

      So add something like: -L[server-root]/bin/https/lib -llibdbm
      where [server-root] points to where you installed the server.

      No special linkage is necessary for the plug-in itself, this is
      automagic.

      The DBM file format is as follows:
         key=username value=password":"group1,group2,...,groupn[":" extra data]
           anything past the 2nd colon is discared by the server

   Usage:

      To enable the DBM plugin, add this to the end of magnus.conf:
         Init fn="load-modules" shlib=auth_dbm.<ext> funcs="las-dbm-init"
         Init fn="acl-register-module" module="lasdbm" func="las-dbm-init"

   <ext> = so on UNIX
   <ext> = dll on NT.

   The shared library must either be in the LD_LIBRARY_PATH of the server
   or the path to the file must be included with auth_dbm.[so|dll]

   The dbswitch.conf entry should look like:

   directory dbm dbmdb:/path/to/your/dbm/file

   NOTE: Since you are adding this to a global file that affects all
         instances so you will need to add this registration code to
         all instances, including the admin server, otherwise those 
         servers will fail to start.
*/

static char * get_entry(char * item, char * pwfile)
{
    DBM *db;
    datum data, key;
    char *dbmpw = NULL;

    key.dptr = item;
    key.dsize = strlen(key.dptr) + 1;

    if (!(db = dbm_open(pwfile, O_RDWR, 0660))) {
        ereport(LOG_SECURITY, "%s: could not open DBM file: %s", MODULE, pwfile);
        return NULL;
    }

    data = dbm_fetch(db, key);

    if (data.dptr) {
        dbmpw = STRDUP(data.dptr);
    }
    else { 
        ereport(LOG_VERBOSE, "%s: Entry %s not found", MODULE, key.dptr);
        dbmpw = NULL;
    }

    dbm_close(db);

    return(dbmpw);
}

/* Get the user's password from the DBM file and check to see if it is
 * correct.  Only supports crypt() passwords.
 */
#ifdef __cplusplus
extern "C"
#endif
NSAPI_PUBLIC int dbm_check_auth(NSErr_t *errp, PList_t subject, 
                                PList_t resource, PList_t auth_info, 
                                PList_t global_auth, void *arg)
{
    char *pwfile = 0;
    char *pw =  0;
    char *user =  0;
    int rv, i;
    char *dbname;
    ACLDbType_t ACL_DbTypeDBM;	/* Handle to the dbm database type */
    VirtualServer *vs;		/* pointer to the current virtual server */
    UserDB *userdb;		/* pointer to USERDB entry in server.xml */

    char * password, * colon_pw;

    /* Get raw user value, which was extracted by the system from
       Authorization header */
    rv = ACL_GetAttribute(errp, ACL_ATTR_RAW_USER, (void **)&user,
                          subject, resource, auth_info, global_auth);
    if ( rv != LAS_EVAL_TRUE )
        return rv;

    /* Get raw password value, which was extracted by the system from
       Authorization header */
    rv = ACL_GetAttribute(errp, ACL_ATTR_RAW_PASSWORD, (void **)&pw,
                          subject, resource, auth_info, global_auth);
    if ( rv != LAS_EVAL_TRUE )
        return rv;

    /* Find a handle to our database */
    rv = ACL_AuthInfoGetDbname(auth_info, &dbname);
    if ( rv != 0 ) {
        ereport(LOG_SECURITY, "%s: Unable to get database name", MODULE);
        return LAS_EVAL_FALSE;
    }

    /* get the VS's idea of the user database */
    if (PListGetValue(resource, ACL_ATTR_VS_INDEX, (void **)&vs, NULL) < 0) {
        /* oops, no VS */
        return LAS_EVAL_FAIL;
    }

    rv = ACL_UserDBLookup(errp, vs, dbname, &userdb);
    if (rv != LAS_EVAL_TRUE) {
        ereport(LOG_SECURITY, "%s: Unable to get parsed database name.", 
                    MODULE);
        return LAS_EVAL_FAIL;
    }

    ACL_UserDBGetDbHandle(errp, userdb, (void **)&pwfile); 

    /* Now we can actually authenticate the user, get the password first */
    if (!(password = get_entry(user, pwfile))) {
          ereport(LOG_SECURITY, "%s: user %s does not exist in file %s", 
                      MODULE, user, pwfile);
        return LAS_EVAL_FALSE;
    }

    /* Password is up to first : if it exists */
    colon_pw = strchr(password, ':');
    if (colon_pw) {
        *colon_pw = '\0';
    }

    /* Ensure that we are thread-safe */
    crit_enter(auth_crit);
    rv = strcmp(password, (char *)crypt((char *)pw,(char *)password));
    crit_exit(auth_crit);

    if (rv != 0) {
          ereport(LOG_SECURITY, "%s: user %s password %s does not match.", 
                      MODULE, user, password);
        rv = LAS_EVAL_FALSE;
    } else {
        PListInitProp(subject, ACL_ATTR_IS_VALID_PASSWORD_INDEX,
                      ACL_ATTR_IS_VALID_PASSWORD, user, 0);
        rv = LAS_EVAL_TRUE;
    }

    if (password) FREE(password);

    return rv;
}

/*
 * Perform group-based authentication.
 *
 * NOTE: This only works when one group appears in the ACL. Handling
 *       more than one allowed group is left up to the reader. So
 *       group = "foo, bar, baz" won't work but you can use the format
 *       (group = "foo") or (group = "bar") or (group = "baz")
 */

#ifdef __cplusplus
extern "C"
#endif
NSAPI_PUBLIC int dbm_check_group(NSErr_t *errp, PList_t subject, 
                                PList_t resource, PList_t auth_info, 
                                PList_t global_auth, void *arg)
{
    char *user = 0;
    char *group = 0;
    char *pwfile = 0;
    char *group_colon;
    char *group_colon2;
    char *groups_entry;
    char *groups, *x;
    char *dbname;
    ACLDbType_t ACL_DbTypeDBM;	/* Handle to the dbm database type */
    VirtualServer *vs;		/* pointer to the current virtual server */
    UserDB *userdb;		/* pointer to USERDB entry in server.xml */

    int rv = 0;

    /* Get raw user value, which was extracted by the system from
       Authorization header */
    rv = ACL_GetAttribute(errp, ACL_ATTR_RAW_USER, (void **)&user,
                          subject, resource, auth_info, global_auth);
    if ( rv != LAS_EVAL_TRUE )
        return rv;

    /* Get the group list */
    if ((rv = ACL_GetAttribute(errp, ACL_ATTR_GROUPS, (void **)&group, subject,
                          resource, auth_info, global_auth)) != LAS_EVAL_TRUE)
        return rv;

    /* Find a handle to our database */
    rv = ACL_AuthInfoGetDbname(auth_info, &dbname);
    if ( rv != 0 ) {
        ereport(LOG_SECURITY, "%s: Unable to get database name", MODULE);
        return LAS_EVAL_FALSE;
    }

    /* get the VS's idea of the user database */
    if (PListGetValue(resource, ACL_ATTR_VS_INDEX, (void **)&vs, NULL) < 0) {
        /* oops, no VS */
        return LAS_EVAL_FAIL;
    }

    rv = ACL_UserDBLookup(errp, vs, dbname, &userdb);
    if (rv != LAS_EVAL_TRUE) {
        ereport(LOG_SECURITY, "%s: Unable to get parsed database name.", 
                    MODULE);
        return LAS_EVAL_FAIL;
    }

    ACL_UserDBGetDbHandle(errp, userdb, (void **)&pwfile); 

    if (!(groups_entry = get_entry(user, pwfile))) {
          ereport(LOG_SECURITY, "%s: entry %s does not exist in file %s", 
                      MODULE, user, pwfile);
        return LAS_EVAL_FALSE;
    }
    groups = groups_entry;
    
    /* Determine which format is being used */
    if ((group_colon = strchr(groups, ':')) != NULL) {
        group_colon2 = strchr(++group_colon, ':');
        if (group_colon2)
            *group_colon2 = '\0';
        groups = group_colon;
    }

    x = strtok(groups, ",");
    while (x) {
        if (!strcmp(group, x)) {
            FREE(groups_entry);
            PListInitProp(subject, ACL_ATTR_USER_ISMEMBER_INDEX,
                          ACL_ATTR_USER_ISMEMBER,
                          pool_strdup(PListGetPool(subject),x), 0);

            return LAS_EVAL_TRUE;
        }
        x = strtok(NULL, ",");
       
    }
    FREE(groups_entry);

    return LAS_EVAL_FALSE;
}

/*
 * parse_dbm_url - parser for dbm database urls
 *
 * parses a dbswitch.conf entry and generates the database handle
 */

#ifdef __cplusplus
extern "C"
#endif
NSAPI_PUBLIC int parse_dbm_url(NSErr_t *errp, ACLDbType_t d, const char *dbname,
                const char *url, PList_t plist, void **db)
{
    const char *dbmfile = 0; 
    char *dbentry = 0; 

    *db = 0;

    if (!url || !*url) {
        ereport(LOG_SECURITY, "dbm: URL is missing.");
        return -1;
    }

    if (!dbname || !*dbname) {
        ereport(LOG_SECURITY, "dbm: database name is missing.");
        return -1;
    }

    /* skip past the prefix and colon */
    dbmfile = url + strlen(DBMPREFIX) + 1;

    /* If you had additional arguments to get, like binddn for ldap, you
     * would use PListFindValue(plist, "value", (void **)&value, NULL);
     */

    dbentry = STRDUP(dbmfile);

    *db = (void *)dbentry; /* set the path of the physical dbm file */

    return 0;
}

/* 
 * Add the new dbm ACL type and register the URL parser and attribute getter.
 */
#ifdef __cplusplus
extern "C"
#endif
NSAPI_PUBLIC int las_dbm_init(NSErr_t *errp)
{
    int rv;
    ACLMethod_t ACL_MethodBasic;  /* Handle to the "basic" method */

    /* we want to do basic authentication, but with our database */
    if (ACL_MethodFind(errp, "basic", &ACL_MethodBasic) < 0)
        return -1;

    /* our database is a new type of data base, let the system know how
       it can handle it (by providing a parsing function) */
    if (ACL_DbTypeRegister(errp, DBMPREFIX, parse_dbm_url, &ACL_DbTypeDBM) < 0)
        return -1;

    /* register the attribute getter function for authentication */
    /* Initialize getters for method=="basic", dbtype == "dbmdb" */
    rv = ACL_AttrGetterRegister(errp, ACL_ATTR_IS_VALID_PASSWORD,
                                dbm_check_auth,
                                ACL_MethodBasic,
                                ACL_DbTypeDBM,
                                ACL_AT_END, NULL);

    if ( rv < 0 )
        return -1;

    rv = ACL_AttrGetterRegister(errp, ACL_ATTR_USER_ISMEMBER,
                                dbm_check_group,
                                ACL_MethodBasic, 
                                ACL_DbTypeDBM, 
                                ACL_AT_END, NULL);

    if ( rv < 0 )
        return -1;

    auth_crit = crit_init();
    if (auth_crit == 0) {
        return -1;
    }

    return 0;
}
