/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright 2008 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

#pragma ident	"%Z%%M%	%I%	%E% SMI"

#include <stdlib.h>
#include <strings.h>
#include <unistd.h>
#include <syslog.h>
#include <thread.h>
#include <synch.h>
#include <grp.h>
#include <assert.h>
#include <libintl.h>
#include <smbsrv/libsmb.h>
#include <smb_sqlite.h>

/*
 * Local domain SID (aka machine SID) is not stored in the domain table
 * therefore the index is 0
 */
#define	SMB_LGRP_LOCAL_IDX	0
#define	SMB_LGRP_BUILTIN_IDX	1

#define	SMB_LGRP_DB_NAME	"/var/smb/smbgroup.db"
#define	SMB_LGRP_DB_TIMEOUT	3000		/* in millisecond */
#define	SMB_LGRP_DB_VERMAJOR	1
#define	SMB_LGRP_DB_VERMINOR	0
#define	SMB_LGRP_DB_MAGIC	0x4C475250	/* LGRP */

#define	SMB_LGRP_DB_ORD		1		/* open read-only */
#define	SMB_LGRP_DB_ORW		2		/* open read/write */

#define	SMB_LGRP_DB_ADDMEMBER	1
#define	SMB_LGRP_DB_DELMEMBER	2

/*
 * members column of the groups table is an array of
 * member structure smb_lgmid_t defined below.
 *
 * privs column of the groups table is an array of bytes
 * where each byte is the id of an enable privilege
 */
#define	SMB_LGRP_DB_SQL \
	"CREATE TABLE db_info ("				\
	"	ver_major INTEGER,"				\
	"	ver_minor INTEGER,"				\
	"	magic     INTEGER"				\
	");"							\
	""							\
	"CREATE TABLE domains ("				\
	"	dom_idx INTEGER PRIMARY KEY,"			\
	"	dom_sid TEXT UNIQUE,"				\
	"       dom_cnt INTEGER"				\
	");"							\
	""							\
	"CREATE UNIQUE INDEX domsid_idx ON domains (dom_sid);"	\
	""							\
	"CREATE TABLE groups ("					\
	"	name      TEXT PRIMARY KEY,"			\
	"	sid_idx   INTEGER,"				\
	"	sid_rid   INTEGER,"				\
	"	sid_type  INTEGER,"				\
	"	sid_attrs INTEGER,"				\
	"	comment   TEXT,"				\
	"	n_privs   INTEGER,"				\
	"	privs     BLOB,"				\
	"	n_members INTEGER,"				\
	"	members   BLOB"					\
	");"							\
	""							\
	"CREATE INDEX grprid_idx ON groups (sid_rid);"

/*
 * Number of groups table columns
 */
#define	SMB_LGRP_GTBL_NCOL	10

#define	SMB_LGRP_GTBL_NAME	0
#define	SMB_LGRP_GTBL_SIDIDX	1
#define	SMB_LGRP_GTBL_SIDRID	2
#define	SMB_LGRP_GTBL_SIDTYP	3
#define	SMB_LGRP_GTBL_SIDATR	4
#define	SMB_LGRP_GTBL_CMNT	5
#define	SMB_LGRP_GTBL_NPRIVS	6
#define	SMB_LGRP_GTBL_PRIVS	7
#define	SMB_LGRP_GTBL_NMEMBS	8
#define	SMB_LGRP_GTBL_MEMBS	9

#define	SMB_LGRP_INFO_NONE	0x00
#define	SMB_LGRP_INFO_NAME	0x01
#define	SMB_LGRP_INFO_CMNT	0x02
#define	SMB_LGRP_INFO_SID	0x04
#define	SMB_LGRP_INFO_PRIV	0x08
#define	SMB_LGRP_INFO_MEMB	0x10
#define	SMB_LGRP_INFO_ALL	0x1F

#define	NULL_MSGCHK(msg)	((msg) ? (msg) : "NULL")

/* Member ID */
typedef struct smb_lgmid {
	uint32_t m_idx;
	uint32_t m_rid;
	uint16_t m_type;
} smb_lgmid_t;

#define	SMB_LGRP_MID_HEXSZ	32

/* Member list */
typedef struct smb_lgmlist {
	uint32_t	m_cnt;
	char		*m_ids;
} smb_lgmlist_t;

/* Privilege ID */
typedef uint8_t smb_lgpid_t;

/* Privilege list */
typedef struct smb_lgplist {
	uint32_t	p_cnt;
	smb_lgpid_t	*p_ids;
} smb_lgplist_t;

static mutex_t smb_lgrp_lsid_mtx;
static nt_sid_t *smb_lgrp_lsid;

static int smb_lgrp_db_init(void);
static sqlite *smb_lgrp_db_open(int);
static void smb_lgrp_db_close(sqlite *);
static int smb_lgrp_db_setinfo(sqlite *);

static boolean_t smb_lgrp_gtbl_exists(sqlite *, char *);
static int smb_lgrp_gtbl_lookup(sqlite *, int, smb_group_t *, int, ...);
static int smb_lgrp_gtbl_insert(sqlite *, smb_group_t *);
static int smb_lgrp_gtbl_update(sqlite *, char *, smb_group_t *, int);
static int smb_lgrp_gtbl_delete(sqlite *, char *);
static int smb_lgrp_gtbl_update_mlist(sqlite *, char *, smb_gsid_t *, int);
static int smb_lgrp_gtbl_update_plist(sqlite *, char *, uint8_t, boolean_t);
static int smb_lgrp_gtbl_count(sqlite *, int, int *);

static int smb_lgrp_dtbl_insert(sqlite *, char *, uint32_t *);
static int smb_lgrp_dtbl_getidx(sqlite *, nt_sid_t *, uint16_t,
    uint32_t *, uint32_t *);
static int smb_lgrp_dtbl_getsid(sqlite *, uint32_t, nt_sid_t **);

static int smb_lgrp_mlist_add(smb_lgmlist_t *, smb_lgmid_t *, smb_lgmlist_t *);
static int smb_lgrp_mlist_del(smb_lgmlist_t *, smb_lgmid_t *, smb_lgmlist_t *);

static int smb_lgrp_plist_add(smb_lgplist_t *, smb_lgpid_t, smb_lgplist_t *);
static int smb_lgrp_plist_del(smb_lgplist_t *, smb_lgpid_t, smb_lgplist_t *);

static void smb_lgrp_encode_privset(smb_group_t *, smb_lgplist_t *);

static int smb_lgrp_decode(smb_group_t *, char **, int, sqlite *);
static int smb_lgrp_decode_privset(smb_group_t *, char *, char *);
static int smb_lgrp_decode_members(smb_group_t *, char *, char *, sqlite *);

static void smb_lgrp_set_default_privs(smb_group_t *);
static boolean_t smb_lgrp_chkname(char *);
static boolean_t smb_lgrp_chkmember(uint16_t);
static int smb_lgrp_getsid(int, uint32_t *, uint16_t, sqlite *, nt_sid_t **);

/*
 * smb_lgrp_add
 *
 * Create a local group with the given name and comment.
 * This new group doesn't have any members and no enabled
 * privileges.
 *
 * No well-known accounts can be added other than Administators,
 * Backup Operators and Power Users. These built-in groups
 * won't have any members when created but a set of default
 * privileges will be enabled for them.
 */
int
smb_lgrp_add(char *gname, char *cmnt)
{
	well_known_account_t *wk_acct;
	struct group *pxgrp;
	smb_group_t grp;
	nt_sid_t *sid = NULL;
	sqlite *db;
	int rc;

	(void) trim_whitespace(gname);
	if (!smb_lgrp_chkname(gname))
		return (SMB_LGRP_INVALID_NAME);

	if (cmnt && (strlen(cmnt) > SMB_LGRP_COMMENT_MAX))
		return (SMB_LGRP_INVALID_ARG);

	bzero(&grp, sizeof (grp));
	grp.sg_name = utf8_strlwr(gname);
	grp.sg_cmnt = cmnt;

	wk_acct = nt_builtin_lookup(gname);
	if (wk_acct == NULL) {
		if ((pxgrp = getgrnam(gname)) == NULL)
			return (SMB_LGRP_NOT_FOUND);

		/*
		 * Make sure a local SID can be obtained
		 */
		if (smb_idmap_getsid(pxgrp->gr_gid, SMB_IDMAP_GROUP, &sid)
		    != IDMAP_SUCCESS)
			return (SMB_LGRP_NO_SID);

		if (!nt_sid_is_indomain(smb_lgrp_lsid, sid)) {
			free(sid);
			return (SMB_LGRP_SID_NOTLOCAL);
		}

		grp.sg_id.gs_type = SidTypeAlias;
		grp.sg_domain = SMB_LGRP_LOCAL;
		grp.sg_rid = pxgrp->gr_gid;
	} else {
		if (wk_acct->flags & LGF_HIDDEN) {
			/* cannot add well-known accounts */
			return (SMB_LGRP_WKSID);
		}

		grp.sg_id.gs_type = wk_acct->sid_name_use;
		if ((sid = nt_sid_strtosid(wk_acct->sid)) == NULL)
			return (SMB_LGRP_NO_MEMORY);
		(void) nt_sid_get_rid(sid, &grp.sg_rid);
		free(sid);
		grp.sg_domain = SMB_LGRP_BUILTIN;

		grp.sg_privs = smb_privset_new();
		smb_lgrp_set_default_privs(&grp);
	}


	grp.sg_attr = SE_GROUP_MANDATORY | SE_GROUP_ENABLED_BY_DEFAULT |
	    SE_GROUP_ENABLED;

	db = smb_lgrp_db_open(SMB_LGRP_DB_ORW);
	rc = smb_lgrp_gtbl_insert(db, &grp);
	smb_lgrp_db_close(db);

	smb_privset_free(grp.sg_privs);
	return (rc);
}

/*
 * smb_lgrp_rename
 *
 * Renames the given group
 */
int
smb_lgrp_rename(char *gname, char *new_gname)
{
	smb_group_t grp;
	sqlite *db;
	int rc;

	(void) trim_whitespace(gname);
	if (!smb_lgrp_chkname(gname))
		return (SMB_LGRP_INVALID_NAME);

	(void) trim_whitespace(new_gname);
	if (!smb_lgrp_chkname(gname))
		return (SMB_LGRP_INVALID_NAME);

	if (utf8_strcasecmp(gname, new_gname) == 0)
		return (SMB_LGRP_SUCCESS);

	/* Cannot rename well-known groups */
	if (nt_builtin_is_wellknown(gname))
		return (SMB_LGRP_WKSID);

	/* Cannot rename to a well-known groups */
	if (nt_builtin_is_wellknown(new_gname))
		return (SMB_LGRP_WKSID);

	grp.sg_name = new_gname;

	db = smb_lgrp_db_open(SMB_LGRP_DB_ORW);
	rc = smb_lgrp_gtbl_update(db, gname, &grp, SMB_LGRP_GTBL_NAME);
	smb_lgrp_db_close(db);

	return (rc);
}

/*
 * smb_lgrp_delete
 *
 * Deletes the specified local group.
 */
int
smb_lgrp_delete(char *gname)
{
	sqlite *db;
	int rc;

	(void) trim_whitespace(gname);
	if (!smb_lgrp_chkname(gname))
		return (SMB_LGRP_INVALID_NAME);

	/* Cannot remove a built-in group */
	if (nt_builtin_is_wellknown(gname))
		return (SMB_LGRP_WKSID);

	db = smb_lgrp_db_open(SMB_LGRP_DB_ORW);
	rc = smb_lgrp_gtbl_delete(db, gname);
	smb_lgrp_db_close(db);

	return (rc);
}

/*
 * smb_lgrp_setcmnt
 *
 * Sets the description for the given group
 */
int
smb_lgrp_setcmnt(char *gname, char *cmnt)
{
	smb_group_t grp;
	sqlite *db;
	int rc;

	(void) trim_whitespace(gname);
	if (!smb_lgrp_chkname(gname))
		return (SMB_LGRP_INVALID_NAME);

	if (cmnt && (strlen(cmnt) > SMB_LGRP_COMMENT_MAX))
		return (SMB_LGRP_INVALID_ARG);

	grp.sg_cmnt = cmnt;

	db = smb_lgrp_db_open(SMB_LGRP_DB_ORW);
	rc = smb_lgrp_gtbl_update(db, gname, &grp, SMB_LGRP_GTBL_CMNT);
	smb_lgrp_db_close(db);

	return (rc);
}

/*
 * smb_lgrp_getcmnt
 *
 * Obtain the description of the specified group
 */
int
smb_lgrp_getcmnt(char *gname, char **cmnt)
{
	smb_group_t grp;
	sqlite *db;
	int rc;

	(void) trim_whitespace(gname);
	if (!smb_lgrp_chkname(gname))
		return (SMB_LGRP_INVALID_NAME);

	if (cmnt == NULL)
		return (SMB_LGRP_INVALID_ARG);

	db = smb_lgrp_db_open(SMB_LGRP_DB_ORD);
	rc = smb_lgrp_gtbl_lookup(db, SMB_LGRP_GTBL_NAME, &grp,
	    SMB_LGRP_INFO_CMNT, gname);
	smb_lgrp_db_close(db);

	if (rc == SMB_LGRP_SUCCESS) {
		*cmnt = grp.sg_cmnt;
		grp.sg_cmnt = NULL;
		smb_lgrp_free(&grp);
	}

	return (rc);
}


/*
 * smb_lgrp_setpriv
 *
 * Enable/disable the specified privilge for the group
 */
int
smb_lgrp_setpriv(char *gname, uint8_t priv_lid, boolean_t enable)
{
	sqlite *db;
	int rc;

	(void) trim_whitespace(gname);
	if (!smb_lgrp_chkname(gname))
		return (SMB_LGRP_INVALID_NAME);

	if ((priv_lid < SE_MIN_LUID) || (priv_lid > SE_MAX_LUID))
		return (SMB_LGRP_NO_SUCH_PRIV);

	db = smb_lgrp_db_open(SMB_LGRP_DB_ORW);
	rc = smb_lgrp_gtbl_update_plist(db, gname, priv_lid, enable);
	smb_lgrp_db_close(db);

	if (enable) {
		if (rc == SMB_LGRP_PRIV_HELD)
			rc = SMB_LGRP_SUCCESS;
	} else {
		if (rc == SMB_LGRP_PRIV_NOT_HELD)
			rc = SMB_LGRP_SUCCESS;
	}

	return (rc);
}

/*
 * smb_lgrp_getpriv
 *
 * Obtain the status of the specified privilge for the group
 */
int
smb_lgrp_getpriv(char *gname, uint8_t priv_lid, boolean_t *enable)
{
	sqlite *db;
	smb_group_t grp;
	int rc;

	(void) trim_whitespace(gname);
	if (!smb_lgrp_chkname(gname))
		return (SMB_LGRP_INVALID_NAME);

	if ((priv_lid < SE_MIN_LUID) || (priv_lid > SE_MAX_LUID))
		return (SMB_LGRP_NO_SUCH_PRIV);

	db = smb_lgrp_db_open(SMB_LGRP_DB_ORD);
	rc = smb_lgrp_gtbl_lookup(db, SMB_LGRP_GTBL_NAME, &grp,
	    SMB_LGRP_INFO_PRIV, gname);
	smb_lgrp_db_close(db);

	if (rc == SMB_LGRP_SUCCESS) {
		*enable = (smb_privset_query(grp.sg_privs, priv_lid) == 1);
		smb_lgrp_free(&grp);
	}

	return (rc);
}

/*
 * smb_lgrp_add_member
 *
 * Add the given account to the specified group as its member.
 */
int
smb_lgrp_add_member(char *gname, nt_sid_t *msid, uint16_t sid_type)
{
	sqlite *db;
	smb_gsid_t mid;
	int rc;

	(void) trim_whitespace(gname);
	if (!smb_lgrp_chkname(gname))
		return (SMB_LGRP_INVALID_NAME);

	if (!nt_sid_is_valid(msid))
		return (SMB_LGRP_INVALID_ARG);

	if (!smb_lgrp_chkmember(sid_type))
		return (SMB_LGRP_INVALID_MEMBER);

	mid.gs_sid = msid;
	mid.gs_type = sid_type;

	db = smb_lgrp_db_open(SMB_LGRP_DB_ORW);
	rc = smb_lgrp_gtbl_update_mlist(db, gname, &mid, SMB_LGRP_DB_ADDMEMBER);
	smb_lgrp_db_close(db);

	return (rc);
}

/*
 * smb_lgrp_del_member
 *
 * Delete the specified member from the given group.
 */
int
smb_lgrp_del_member(char *gname, nt_sid_t *msid, uint16_t sid_type)
{
	sqlite *db;
	smb_gsid_t mid;
	int rc;

	(void) trim_whitespace(gname);
	if (!smb_lgrp_chkname(gname))
		return (SMB_LGRP_INVALID_NAME);

	if (!nt_sid_is_valid(msid))
		return (SMB_LGRP_INVALID_ARG);

	mid.gs_sid = msid;
	mid.gs_type = sid_type;

	db = smb_lgrp_db_open(SMB_LGRP_DB_ORW);
	rc = smb_lgrp_gtbl_update_mlist(db, gname, &mid, SMB_LGRP_DB_DELMEMBER);
	smb_lgrp_db_close(db);

	return (rc);
}

/*
 * smb_lgrp_getbyname
 *
 * Retrieves the information of the group specified by
 * the given name.
 *
 * Note that this function doesn't allocate the group
 * structure itself only the fields, so the given grp
 * pointer has to point to a group structure.
 * Caller must free the allocated memories for the fields
 * by calling smb_lgrp_free().
 */
int
smb_lgrp_getbyname(char *gname, smb_group_t *grp)
{
	sqlite *db;
	int rc;

	(void) trim_whitespace(gname);
	if (!smb_lgrp_chkname(gname))
		return (SMB_LGRP_INVALID_NAME);

	db = smb_lgrp_db_open(SMB_LGRP_DB_ORD);
	rc = smb_lgrp_gtbl_lookup(db, SMB_LGRP_GTBL_NAME, grp,
	    SMB_LGRP_INFO_ALL, gname);
	smb_lgrp_db_close(db);

	return (rc);
}

/*
 * smb_lgrp_getbyrid
 *
 * Retrieves the information of the group specified by
 * the given RID and domain type.
 *
 * Note that this function doesn't allocate the group
 * structure itself only the fields, so the given grp
 * pointer has to point to a group structure.
 * Caller must free the allocated memories for the fields
 * by calling smb_lgrp_free().
 *
 * If grp is NULL no information would be returned. The
 * return value of SMB_LGRP_SUCCESS will indicate that a
 * group with the given information exists.
 */
int
smb_lgrp_getbyrid(uint32_t rid, smb_gdomain_t domtype, smb_group_t *grp)
{
	smb_group_t tmpgrp;
	sqlite *db;
	int infolvl = SMB_LGRP_INFO_ALL;
	int rc;

	if (grp == NULL) {
		grp = &tmpgrp;
		infolvl = SMB_LGRP_INFO_NONE;
	}

	db = smb_lgrp_db_open(SMB_LGRP_DB_ORD);
	rc = smb_lgrp_gtbl_lookup(db, SMB_LGRP_GTBL_SIDRID, grp, infolvl,
	    rid, domtype);
	smb_lgrp_db_close(db);

	return (rc);
}

/*
 * smb_lgrp_numbydomain
 *
 * Returns the number of groups in the given domain in the
 * arg 'count'
 */
int
smb_lgrp_numbydomain(smb_gdomain_t dom_type, int *count)
{
	sqlite *db;
	int dom_idx;
	int rc;

	switch (dom_type) {
	case SMB_LGRP_LOCAL:
		dom_idx = SMB_LGRP_LOCAL_IDX;
		break;
	case SMB_LGRP_BUILTIN:
		dom_idx = SMB_LGRP_BUILTIN_IDX;
		break;
	default:
		*count = 0;
		return (SMB_LGRP_INVALID_ARG);
	}

	db = smb_lgrp_db_open(SMB_LGRP_DB_ORD);
	rc = smb_lgrp_gtbl_count(db, dom_idx, count);
	smb_lgrp_db_close(db);

	return (rc);
}

/*
 * smb_lgrp_numbydomain
 *
 * Returns the number of groups which have the given SID
 * as a member.
 */
int
smb_lgrp_numbymember(nt_sid_t *msid, int *count)
{
	smb_giter_t gi;
	smb_group_t grp;
	int rc;

	*count = 0;
	rc = smb_lgrp_iteropen(&gi);
	if (rc != SMB_LGRP_SUCCESS)
		return (rc);

	while (smb_lgrp_iterate(&gi, &grp) == SMB_LGRP_SUCCESS) {
		if (smb_lgrp_is_member(&grp, msid))
			(*count)++;
		smb_lgrp_free(&grp);
	}
	smb_lgrp_iterclose(&gi);
	return (SMB_LGRP_SUCCESS);
}

/*
 * smb_lgrp_free
 *
 * Frees the allocated memory for the fields of the given
 * group structure. Note that this function doesn't free
 * the group itself.
 */
void
smb_lgrp_free(smb_group_t *grp)
{
	int i;

	if (grp == NULL)
		return;

	free(grp->sg_name);
	free(grp->sg_cmnt);
	free(grp->sg_id.gs_sid);
	smb_privset_free(grp->sg_privs);

	for (i = 0; i < grp->sg_nmembers; i++)
		free(grp->sg_members[i].gs_sid);
	free(grp->sg_members);
}

/*
 * smb_lgrp_iteropen
 *
 * Initializes the given group iterator by opening
 * the group database and creating a virtual machine
 * for iteration.
 */
int
smb_lgrp_iteropen(smb_giter_t *iter)
{
	char *sql;
	char *errmsg = NULL;
	int rc = SMB_LGRP_SUCCESS;

	assert(iter);

	bzero(iter, sizeof (smb_giter_t));

	sql = sqlite_mprintf("SELECT * FROM groups");
	if (sql == NULL)
		return (SMB_LGRP_NO_MEMORY);

	iter->sgi_db = smb_lgrp_db_open(SMB_LGRP_DB_ORD);
	if (iter->sgi_db == NULL) {
		sqlite_freemem(sql);
		return (SMB_LGRP_DBOPEN_FAILED);
	}

	rc = sqlite_compile(iter->sgi_db, sql, NULL, &iter->sgi_vm, &errmsg);
	sqlite_freemem(sql);

	if (rc != SQLITE_OK) {
		syslog(LOG_DEBUG, "failed to create a VM (%s)",
		    NULL_MSGCHK(errmsg));
		rc = SMB_LGRP_DB_ERROR;
	}

	return (rc);
}

/*
 * smb_lgrp_iterclose
 *
 * Closes the given group iterator.
 */
void
smb_lgrp_iterclose(smb_giter_t *iter)
{
	char *errmsg = NULL;
	int rc;

	assert(iter);

	rc = sqlite_finalize(iter->sgi_vm, &errmsg);
	if (rc != SQLITE_OK) {
		syslog(LOG_DEBUG, "failed to destroy a VM (%s)",
		    NULL_MSGCHK(errmsg));
	}

	smb_lgrp_db_close(iter->sgi_db);
}

/*
 * smb_lgrp_iterate
 *
 * Iterate through group database
 * Group information is returned in provided group structure.
 *
 * Note that this function doesn't allocate the group
 * structure itself only the fields, so the given grp
 * pointer has to point to a group structure.
 * Caller must free the allocated memories for the fields
 * by calling smb_lgrp_free().
 */
int
smb_lgrp_iterate(smb_giter_t *iter, smb_group_t *grp)
{
	const char **values;
	int ncol;
	int rc;
	int i;

	if (iter->sgi_vm == NULL || iter->sgi_db == NULL)
		return (SMB_LGRP_INVALID_ARG);

	bzero(grp, sizeof (smb_group_t));
	rc = sqlite_step(iter->sgi_vm, &ncol, &values, NULL);
	if (rc == SQLITE_DONE)
		return (SMB_LGRP_NO_MORE);

	if (rc != SQLITE_ROW)
		return (SMB_LGRP_DBEXEC_FAILED);

	if (ncol != SMB_LGRP_GTBL_NCOL)
		return (SMB_LGRP_DB_ERROR);

	for (i = 0; i < ncol; i++) {
		if (values[i] == NULL)
			return (SMB_LGRP_DB_ERROR);
	}

	return (smb_lgrp_decode(grp, (char **)values, SMB_LGRP_INFO_ALL,
	    iter->sgi_db));
}

/*
 * smb_lgrp_is_member
 *
 * Check to see if the specified account is a member of
 * the given group.
 */
boolean_t
smb_lgrp_is_member(smb_group_t *grp, nt_sid_t *sid)
{
	int i;

	if (grp == NULL || grp->sg_members == NULL || sid == NULL)
		return (B_FALSE);

	for (i = 0; i < grp->sg_nmembers; i++) {
		if (nt_sid_is_equal(grp->sg_members[i].gs_sid, sid))
			return (B_TRUE);
	}

	return (B_FALSE);
}

/*
 * smb_lgrp_strerror
 *
 * Returns a text for the given group error code.
 */
char *
smb_lgrp_strerror(int errno)
{
	switch (errno) {
	case SMB_LGRP_SUCCESS:
		return (dgettext(TEXT_DOMAIN, "success"));
	case SMB_LGRP_INVALID_ARG:
		return (dgettext(TEXT_DOMAIN, "invalid argument"));
	case SMB_LGRP_INVALID_MEMBER:
		return (dgettext(TEXT_DOMAIN, "invalid member type"));
	case SMB_LGRP_INVALID_NAME:
		return (dgettext(TEXT_DOMAIN, "invalid name"));
	case SMB_LGRP_NOT_FOUND:
		return (dgettext(TEXT_DOMAIN, "group not found"));
	case SMB_LGRP_EXISTS:
		return (dgettext(TEXT_DOMAIN, "group exists"));
	case SMB_LGRP_NO_SID:
		return (dgettext(TEXT_DOMAIN, "cannot obtain a SID"));
	case SMB_LGRP_NO_LOCAL_SID:
		return (dgettext(TEXT_DOMAIN, "cannot get the machine SID"));
	case SMB_LGRP_SID_NOTLOCAL:
		return (dgettext(TEXT_DOMAIN, "not a local SID"));
	case SMB_LGRP_WKSID:
		return (dgettext(TEXT_DOMAIN,
		    "operation not permitted on well-known accounts"));
	case SMB_LGRP_NO_MEMORY:
		return (dgettext(TEXT_DOMAIN, "not enough memory"));
	case SMB_LGRP_DB_ERROR:
		return (dgettext(TEXT_DOMAIN, "database operation error"));
	case SMB_LGRP_DBINIT_ERROR:
		return (dgettext(TEXT_DOMAIN, "database initialization error"));
	case SMB_LGRP_INTERNAL_ERROR:
		return (dgettext(TEXT_DOMAIN, "internal error"));
	case SMB_LGRP_MEMBER_IN_GROUP:
		return (dgettext(TEXT_DOMAIN, "member already in the group"));
	case SMB_LGRP_MEMBER_NOT_IN_GROUP:
		return (dgettext(TEXT_DOMAIN, "not a member"));
	case SMB_LGRP_NO_SUCH_PRIV:
		return (dgettext(TEXT_DOMAIN, "no such privilege"));
	case SMB_LGRP_NO_SUCH_DOMAIN:
		return (dgettext(TEXT_DOMAIN, "no such domain SID"));
	case SMB_LGRP_PRIV_HELD:
		return (dgettext(TEXT_DOMAIN, "already holds the privilege"));
	case SMB_LGRP_PRIV_NOT_HELD:
		return (dgettext(TEXT_DOMAIN, "privilege not held"));
	case SMB_LGRP_BAD_DATA:
		return (dgettext(TEXT_DOMAIN, "bad data"));
	case SMB_LGRP_NO_MORE:
		return (dgettext(TEXT_DOMAIN, "no more groups"));
	case SMB_LGRP_DBOPEN_FAILED:
		return (dgettext(TEXT_DOMAIN, "failed openning database"));
	case SMB_LGRP_DBEXEC_FAILED:
		return (dgettext(TEXT_DOMAIN,
		    "failed executing database operation"));
	case SMB_LGRP_DBINIT_FAILED:
		return (dgettext(TEXT_DOMAIN, "failed initializing database"));
	case SMB_LGRP_DOMLKP_FAILED:
		return (dgettext(TEXT_DOMAIN, "failed getting the domain SID"));
	case SMB_LGRP_DOMINS_FAILED:
		return (dgettext(TEXT_DOMAIN,
		    "failed inserting the domain SID"));
	case SMB_LGRP_INSERT_FAILED:
		return (dgettext(TEXT_DOMAIN, "failed inserting the group"));
	case SMB_LGRP_DELETE_FAILED:
		return (dgettext(TEXT_DOMAIN, "failed deleting the group"));
	case SMB_LGRP_UPDATE_FAILED:
		return (dgettext(TEXT_DOMAIN, "failed updating the group"));
	case SMB_LGRP_LOOKUP_FAILED:
		return (dgettext(TEXT_DOMAIN, "failed looking up the group"));
	}

	return (dgettext(TEXT_DOMAIN, "unknown error code"));
}

/*
 * smb_lgrp_chkmember
 *
 * Determines valid account types for being member of
 * a local group.
 *
 * Currently, we just support users as valid members.
 */
static boolean_t
smb_lgrp_chkmember(uint16_t sid_type)
{
	return (sid_type == SidTypeUser);
}

/*
 * smb_lgrp_start
 *
 * Initializes the library private global variables.
 * If the database doesn't exist, it'll create it and adds the
 * predefined builtin groups.
 */
int
smb_lgrp_start(void)
{
	char *supported_bg[] =
	    {"Administrators", "Backup Operators", "Power Users"};
	well_known_account_t *wka;
	int rc, i, ngrp;
	char *lsid_str;

	(void) mutex_init(&smb_lgrp_lsid_mtx, USYNC_THREAD, NULL);
	(void) mutex_lock(&smb_lgrp_lsid_mtx);
	lsid_str = smb_config_get_localsid();
	if (lsid_str == NULL) {
		(void) mutex_unlock(&smb_lgrp_lsid_mtx);
		return (SMB_LGRP_NO_LOCAL_SID);
	}

	smb_lgrp_lsid = nt_sid_strtosid(lsid_str);
	free(lsid_str);
	if (!nt_sid_is_valid(smb_lgrp_lsid)) {
		free(smb_lgrp_lsid);
		smb_lgrp_lsid = NULL;
		(void) mutex_unlock(&smb_lgrp_lsid_mtx);
		return (SMB_LGRP_NO_LOCAL_SID);
	}

	rc = smb_lgrp_db_init();
	if (rc != SMB_LGRP_SUCCESS) {
		free(smb_lgrp_lsid);
		smb_lgrp_lsid = NULL;
		(void) mutex_unlock(&smb_lgrp_lsid_mtx);
		return (rc);
	}
	(void) mutex_unlock(&smb_lgrp_lsid_mtx);

	ngrp = sizeof (supported_bg) / sizeof (supported_bg[0]);
	for (i = 0; i < ngrp; i++) {
		wka = nt_builtin_lookup(supported_bg[i]);
		if (wka == NULL)
			continue;
		rc = smb_lgrp_add(wka->name, wka->desc);
		if (rc != SMB_LGRP_SUCCESS)
			syslog(LOG_DEBUG, "failed to add %s", wka->name);
	}

	return (SMB_LGRP_SUCCESS);
}

/*
 * smb_lgrp_stop
 *
 * Unintialize the library global private variables.
 */
void
smb_lgrp_stop(void)
{
	(void) mutex_lock(&smb_lgrp_lsid_mtx);
	free(smb_lgrp_lsid);
	smb_lgrp_lsid = NULL;
	(void) mutex_unlock(&smb_lgrp_lsid_mtx);
	(void) mutex_destroy(&smb_lgrp_lsid_mtx);
}

/*
 * smb_lgrp_db_open
 *
 * Opens group database with the given mode.
 */
static sqlite *
smb_lgrp_db_open(int mode)
{
	sqlite *db;
	char *errmsg = NULL;

	db = sqlite_open(SMB_LGRP_DB_NAME, mode, &errmsg);
	if (db == NULL) {
		syslog(LOG_ERR, "failed to open group database (%s)",
		    NULL_MSGCHK(errmsg));
		sqlite_freemem(errmsg);
	}

	return (db);
}

/*
 * smb_lgrp_db_close
 *
 * Closes the given database handle
 */
static void
smb_lgrp_db_close(sqlite *db)
{
	if (db) {
		sqlite_close(db);
	}
}

/*
 * smb_lgrp_db_init
 *
 * Creates the group database based on the defined SQL statement.
 * It also initializes db_info and domain tables.
 */
static int
smb_lgrp_db_init(void)
{
	int dbrc = SQLITE_OK;
	int rc = SMB_LGRP_SUCCESS;
	sqlite *db = NULL;
	char *errmsg = NULL;

	db = sqlite_open(SMB_LGRP_DB_NAME, 0600, &errmsg);
	if (db == NULL) {
		syslog(LOG_ERR, "failed to create group database (%s)",
		    NULL_MSGCHK(errmsg));
		sqlite_freemem(errmsg);
		return (SMB_LGRP_DBOPEN_FAILED);
	}

	sqlite_busy_timeout(db, SMB_LGRP_DB_TIMEOUT);
	dbrc = sqlite_exec(db, "BEGIN TRANSACTION;", NULL, NULL, &errmsg);
	if (dbrc != SQLITE_OK) {
		syslog(LOG_DEBUG, "failed to begin database transaction (%s)",
		    NULL_MSGCHK(errmsg));
		sqlite_freemem(errmsg);
		sqlite_close(db);
		return (SMB_LGRP_DBEXEC_FAILED);
	}

	switch (sqlite_exec(db, SMB_LGRP_DB_SQL, NULL, NULL, &errmsg)) {
	case SQLITE_ERROR:
		/*
		 * This is the normal situation: CREATE probably failed because
		 * tables already exist. It may indicate an error in SQL as well
		 * but we cannot tell.
		 */
		sqlite_freemem(errmsg);
		dbrc = sqlite_exec(db, "ROLLBACK TRANSACTION", NULL, NULL,
		    &errmsg);
		rc = SMB_LGRP_SUCCESS;
		break;

	case SQLITE_OK:
		dbrc = sqlite_exec(db, "COMMIT TRANSACTION", NULL, NULL,
		    &errmsg);
		if (dbrc != SQLITE_OK)
			break;
		rc = smb_lgrp_dtbl_insert(db, NT_BUILTIN_DOMAIN_SIDSTR,
		    NULL);
		if (rc == SMB_LGRP_SUCCESS)
			rc = smb_lgrp_db_setinfo(db);
		if (rc != SMB_LGRP_SUCCESS) {
			(void) sqlite_close(db);
			(void) unlink(SMB_LGRP_DB_NAME);
			return (rc);
		}
		break;

	default:
		syslog(LOG_ERR,
		    "failed to initialize group database (%s)", errmsg);
		sqlite_freemem(errmsg);
		dbrc = sqlite_exec(db, "ROLLBACK TRANSACTION", NULL, NULL,
		    &errmsg);
		rc = SMB_LGRP_DBINIT_FAILED;
		break;
	}

	if (dbrc != SQLITE_OK) {
		/* this is bad - database may be left in a locked state */
		syslog(LOG_DEBUG, "failed to close a transaction (%s)",
		    NULL_MSGCHK(errmsg));
		sqlite_freemem(errmsg);
	}

	(void) sqlite_close(db);
	return (rc);
}

/*
 * smb_lgrp_gtbl_lookup
 *
 * This is a flexible lookup function for the group database.
 * The key type can be specified by the 'key' arg and the actual key
 * values can be passed after the 'infolvl' arg. 'infolvl' arg specifies
 * what information items for the specified group is needed.
 *
 * Note that the function assumes the given key is unique and only
 * specifies one or 0 group. The keys that are supported now are
 * the group name and the group SID
 *
 * Note that this function doesn't allocate the group
 * structure itself only the fields, so the given grp
 * pointer has to point to a group structure.
 * Caller must free the allocated memories for the fields
 * by calling smb_lgrp_free().
 */
static int
smb_lgrp_gtbl_lookup(sqlite *db, int key, smb_group_t *grp, int infolvl, ...)
{
	char *errmsg = NULL;
	char *sql;
	char **result;
	int nrow, ncol;
	int rc, dom_idx;
	smb_group_t kgrp;
	va_list ap;

	if (db == NULL)
		return (SMB_LGRP_DBOPEN_FAILED);

	bzero(grp, sizeof (smb_group_t));
	va_start(ap, infolvl);

	switch (key) {
	case SMB_LGRP_GTBL_NAME:
		kgrp.sg_name = va_arg(ap, char *);
		sql = sqlite_mprintf("SELECT * FROM groups WHERE name = '%s'",
		    kgrp.sg_name);
		break;

	case SMB_LGRP_GTBL_SIDRID:
		kgrp.sg_rid = va_arg(ap, uint32_t);
		kgrp.sg_domain = va_arg(ap, smb_gdomain_t);
		dom_idx = (kgrp.sg_domain == SMB_LGRP_LOCAL)
		    ? SMB_LGRP_LOCAL_IDX : SMB_LGRP_BUILTIN_IDX;
		sql = sqlite_mprintf("SELECT * FROM groups"
		    "WHERE (sid_idx = %d) AND (sid_rid = %u)",
		    dom_idx, kgrp.sg_rid);
		break;

	default:
		va_end(ap);
		return (SMB_LGRP_INVALID_ARG);
	}

	va_end(ap);
	if (sql == NULL)
		return (SMB_LGRP_NO_MEMORY);

	rc = sqlite_get_table(db, sql, &result, &nrow, &ncol, &errmsg);
	sqlite_freemem(sql);

	if (rc != SQLITE_OK) {
		syslog(LOG_DEBUG, "failed to lookup (%s)", NULL_MSGCHK(errmsg));
		sqlite_freemem(errmsg);
		return (SMB_LGRP_LOOKUP_FAILED);
	}

	if (nrow == 0)  {
		/* group not found */
		sqlite_free_table(result);
		return (SMB_LGRP_NOT_FOUND);
	}

	if (nrow != 1 || ncol != SMB_LGRP_GTBL_NCOL) {
		sqlite_free_table(result);
		return (SMB_LGRP_DB_ERROR);
	}

	rc = smb_lgrp_decode(grp, &result[SMB_LGRP_GTBL_NCOL], infolvl, db);
	sqlite_free_table(result);
	return (rc);
}

/*
 * smb_lgrp_gtbl_exists
 *
 * Checks to see if the given group exists or not.
 */
static boolean_t
smb_lgrp_gtbl_exists(sqlite *db, char *gname)
{
	char *errmsg = NULL;
	char *sql;
	char **result;
	int nrow, ncol;
	int rc;

	if (db == NULL)
		return (NULL);

	sql = sqlite_mprintf("SELECT name FROM groups WHERE name = '%s'",
	    gname);
	rc = sqlite_get_table(db, sql, &result, &nrow, &ncol, &errmsg);
	sqlite_freemem(sql);

	if (rc != SQLITE_OK) {
		syslog(LOG_DEBUG, "failed to lookup %s (%s)",
		    gname, NULL_MSGCHK(errmsg));
		sqlite_freemem(errmsg);
		return (B_FALSE);
	}

	sqlite_free_table(result);
	return (nrow != 0);
}

/*
 * smb_lgrp_gtbl_count
 *
 * Counts the number of groups in the domain specified by
 * 'dom_idx'
 */
static int
smb_lgrp_gtbl_count(sqlite *db, int dom_idx, int *count)
{
	char *errmsg = NULL;
	char *sql;
	char **result;
	int nrow, ncol;
	int rc;

	*count = 0;
	if (db == NULL)
		return (SMB_LGRP_DBOPEN_FAILED);

	sql = sqlite_mprintf("SELECT sid_idx FROM groups WHERE sid_idx = %d",
	    dom_idx);
	rc = sqlite_get_table(db, sql, &result, &nrow, &ncol, &errmsg);
	sqlite_freemem(sql);

	if (rc != SQLITE_OK) {
		syslog(LOG_DEBUG, "failed to count (%s)", NULL_MSGCHK(errmsg));
		sqlite_freemem(errmsg);
		return (SMB_LGRP_LOOKUP_FAILED);
	}

	sqlite_free_table(result);
	if (ncol > 1)
		return (SMB_LGRP_DB_ERROR);

	*count = nrow;
	return (SMB_LGRP_SUCCESS);
}

/*
 * smb_lgrp_gtbl_insert
 *
 * Insert a record for the given group in the group database.
 *
 * NOTE: this function assumes that this group has no members
 * at this time.
 */
static int
smb_lgrp_gtbl_insert(sqlite *db, smb_group_t *grp)
{
	smb_lgpid_t privs[SE_MAX_LUID + 1];
	smb_lgplist_t plist;
	char *errmsg = NULL;
	char *sql;
	int dom_idx;
	int rc;

	if (db == NULL)
		return (SMB_LGRP_DBOPEN_FAILED);

	dom_idx = (grp->sg_domain == SMB_LGRP_LOCAL)
	    ? SMB_LGRP_LOCAL_IDX : SMB_LGRP_BUILTIN_IDX;

	plist.p_cnt = SE_MAX_LUID;
	plist.p_ids = privs;
	smb_lgrp_encode_privset(grp, &plist);

	sql = sqlite_mprintf("INSERT INTO groups"
	    "(name, sid_idx, sid_rid, sid_type, sid_attrs, comment, "
	    "n_privs, privs, n_members, members) "
	    "VALUES('%s', %u, %u, %u, %u, '%q', %u, '%q', %u, '%q')",
	    grp->sg_name, dom_idx, grp->sg_rid, grp->sg_id.gs_type,
	    grp->sg_attr, (grp->sg_cmnt) ? grp->sg_cmnt : "",
	    plist.p_cnt, (char *)plist.p_ids, 0, "");

	if (sql == NULL)
		return (SMB_LGRP_NO_MEMORY);

	rc = sqlite_exec(db, sql, NULL, NULL, &errmsg);
	sqlite_freemem(sql);

	if (rc != SQLITE_OK) {
		syslog(LOG_DEBUG, "failed to insert %s (%s)",
		    grp->sg_name, NULL_MSGCHK(errmsg));
		sqlite_freemem(errmsg);
		rc = SMB_LGRP_INSERT_FAILED;
	} else {
		rc = SMB_LGRP_SUCCESS;
	}

	return (rc);
}

/*
 * smb_lgrp_gtbl_delete
 *
 * Removes the specified group from the database
 */
static int
smb_lgrp_gtbl_delete(sqlite *db, char *gname)
{
	char *errmsg = NULL;
	char *sql;
	int rc;

	if (db == NULL)
		return (SMB_LGRP_DBOPEN_FAILED);

	sql = sqlite_mprintf("DELETE FROM groups WHERE name = '%s'", gname);
	if (sql == NULL)
		return (SMB_LGRP_NO_MEMORY);

	rc = sqlite_exec(db, sql, NULL, NULL, &errmsg);
	sqlite_freemem(sql);

	if (rc != SQLITE_OK) {
		syslog(LOG_DEBUG, "failed to delete %s (%s)",
		    gname, NULL_MSGCHK(errmsg));
		sqlite_freemem(errmsg);
		rc = SMB_LGRP_DELETE_FAILED;
	} else {
		rc = SMB_LGRP_SUCCESS;
	}

	return (rc);
}

/*
 * smb_lgrp_gtbl_update
 *
 * Updates the specified group information, the supported items
 * are group name and comment
 */
static int
smb_lgrp_gtbl_update(sqlite *db, char *gname, smb_group_t *grp, int col_id)
{
	char *errmsg = NULL;
	char *sql;
	int rc;

	if (db == NULL)
		return (SMB_LGRP_DBOPEN_FAILED);

	/* UPDATE doesn't fail if gname doesn't exist */
	if (!smb_lgrp_gtbl_exists(db, gname))
		return (SMB_LGRP_NOT_FOUND);

	switch (col_id) {
	case SMB_LGRP_GTBL_NAME:
		if (smb_lgrp_gtbl_exists(db, grp->sg_name))
			return (SMB_LGRP_EXISTS);
		sql = sqlite_mprintf("UPDATE groups SET name = '%s' "
		    "WHERE name = '%s'", grp->sg_name, gname);
		break;

	case SMB_LGRP_GTBL_CMNT:
		sql = sqlite_mprintf("UPDATE groups SET comment = '%q' "
		"WHERE name = '%s'", grp->sg_cmnt, gname);
		break;

	default:
		return (SMB_LGRP_INVALID_ARG);
	}

	if (sql == NULL)
		return (SMB_LGRP_NO_MEMORY);

	rc = sqlite_exec(db, sql, NULL, NULL, &errmsg);
	sqlite_freemem(sql);

	if (rc != SQLITE_OK) {
		syslog(LOG_DEBUG, "failed to update %s (%s)",
		    gname, NULL_MSGCHK(errmsg));
		sqlite_freemem(errmsg);
		rc = SMB_LGRP_UPDATE_FAILED;
	} else {
		rc = SMB_LGRP_SUCCESS;
	}

	return (rc);
}

/*
 * smb_lgrp_gtbl_update_mlist
 *
 * Adds/removes the specified member from the member list of the
 * given group
 */
static int
smb_lgrp_gtbl_update_mlist(sqlite *db, char *gname, smb_gsid_t *member,
    int flags)
{
	smb_lgmlist_t new_members;
	smb_lgmlist_t members;
	smb_lgmid_t mid;
	char *errmsg = NULL;
	char *sql;
	char **result;
	int nrow, ncol;
	int rc;

	if (db == NULL)
		return (SMB_LGRP_DBOPEN_FAILED);

	sql = sqlite_mprintf("SELECT n_members, members FROM groups "
	    "WHERE name = '%s'", gname);

	if (sql == NULL)
		return (SMB_LGRP_NO_MEMORY);

	rc = sqlite_get_table(db, sql, &result, &nrow, &ncol, &errmsg);
	sqlite_freemem(sql);

	if (rc != SQLITE_OK) {
		syslog(LOG_DEBUG, "failed to lookup %s (%s)",
		    gname, NULL_MSGCHK(errmsg));
		sqlite_freemem(errmsg);
		return (SMB_LGRP_LOOKUP_FAILED);
	}

	if (nrow == 0)  {
		/* group not found */
		sqlite_free_table(result);
		return (SMB_LGRP_NOT_FOUND);
	}

	if (nrow != 1 || ncol != 2) {
		sqlite_free_table(result);
		return (SMB_LGRP_DB_ERROR);
	}

	bzero(&mid, sizeof (mid));
	mid.m_type = member->gs_type;
	rc = smb_lgrp_dtbl_getidx(db, member->gs_sid, mid.m_type,
	    &mid.m_idx, &mid.m_rid);
	if (rc != SMB_LGRP_SUCCESS) {
		sqlite_free_table(result);
		return (rc);
	}

	members.m_cnt = atoi(result[2]);
	members.m_ids = result[3];

	switch (flags) {
	case SMB_LGRP_DB_ADDMEMBER:
		rc = smb_lgrp_mlist_add(&members, &mid, &new_members);
		break;
	case SMB_LGRP_DB_DELMEMBER:
		rc = smb_lgrp_mlist_del(&members, &mid, &new_members);
		break;
	default:
		rc = SMB_LGRP_INVALID_ARG;
	}

	sqlite_free_table(result);
	if (rc != SMB_LGRP_SUCCESS)
		return (rc);

	sql = sqlite_mprintf("UPDATE groups SET n_members = %u, members = '%s'"
	    " WHERE name = '%s'", new_members.m_cnt, new_members.m_ids, gname);

	free(new_members.m_ids);

	if (sql == NULL)
		return (SMB_LGRP_NO_MEMORY);

	rc = sqlite_exec(db, sql, NULL, NULL, &errmsg);
	sqlite_freemem(sql);

	if (rc != SQLITE_OK) {
		syslog(LOG_DEBUG, "failed to update %s (%s)", gname,
		    NULL_MSGCHK(errmsg));
		sqlite_freemem(errmsg);
		rc = SMB_LGRP_UPDATE_FAILED;
	} else {
		rc = SMB_LGRP_SUCCESS;
	}

	return (rc);
}

/*
 * smb_lgrp_gtbl_update_plist
 *
 * Adds/removes the specified privilege from the privilege list of the
 * given group
 */
static int
smb_lgrp_gtbl_update_plist(sqlite *db, char *gname, uint8_t priv_id,
    boolean_t enable)
{
	char *sql;
	char *errmsg = NULL;
	char **result;
	int nrow, ncol;
	int rc;
	smb_lgplist_t privs;
	smb_lgplist_t new_privs;

	if (db == NULL)
		return (SMB_LGRP_DBOPEN_FAILED);

	sql = sqlite_mprintf("SELECT n_privs, privs FROM groups "
	    "WHERE name = '%s'", gname);

	if (sql == NULL)
		return (SMB_LGRP_NO_MEMORY);

	rc = sqlite_get_table(db, sql, &result, &nrow, &ncol, &errmsg);
	sqlite_freemem(sql);

	if (rc != SQLITE_OK) {
		syslog(LOG_DEBUG, "failed to lookup %s (%s)",
		    gname, NULL_MSGCHK(errmsg));
		sqlite_freemem(errmsg);
		return (SMB_LGRP_LOOKUP_FAILED);
	}

	if (nrow == 0)  {
		/* group not found */
		sqlite_free_table(result);
		return (SMB_LGRP_NOT_FOUND);
	}

	if (nrow != 1 || ncol != 2) {
		sqlite_free_table(result);
		return (SMB_LGRP_DB_ERROR);
	}

	privs.p_cnt = atoi(result[2]);
	privs.p_ids = (smb_lgpid_t *)result[3];

	if (enable)
		rc = smb_lgrp_plist_add(&privs, priv_id, &new_privs);
	else
		rc = smb_lgrp_plist_del(&privs, priv_id, &new_privs);

	sqlite_free_table(result);
	if (rc != SMB_LGRP_SUCCESS)
		return (rc);

	sql = sqlite_mprintf("UPDATE groups SET n_privs = %u, privs = '%q'"
	    " WHERE name = '%s'", new_privs.p_cnt, (char *)new_privs.p_ids,
	    gname);

	free(new_privs.p_ids);

	if (sql == NULL)
		return (SMB_LGRP_NO_MEMORY);

	rc = sqlite_exec(db, sql, NULL, NULL, &errmsg);
	sqlite_freemem(sql);

	if (rc != SQLITE_OK) {
		syslog(LOG_DEBUG, "failed to update %s (%s)",
		    gname, NULL_MSGCHK(errmsg));
		sqlite_freemem(errmsg);
		rc = SMB_LGRP_UPDATE_FAILED;
	} else {
		rc = SMB_LGRP_SUCCESS;
	}

	return (rc);
}

/*
 * smb_lgrp_dtbl_insert
 *
 * Inserts the specified domain SID in the dmain table.
 * Upon successful insert the index will be returned in
 * 'dom_idx' arg.
 */
static int
smb_lgrp_dtbl_insert(sqlite *db, char *dom_sid, uint32_t *dom_idx)
{
	char *errmsg = NULL;
	char *sql;
	int rc;

	sql = sqlite_mprintf("INSERT INTO domains (dom_sid, dom_cnt)"
	    " VALUES('%s', 1);", dom_sid);
	if (sql == NULL)
		return (SMB_LGRP_NO_MEMORY);

	rc = sqlite_exec(db, sql, NULL, NULL, &errmsg);
	sqlite_freemem(sql);

	if (rc != SQLITE_OK) {
		syslog(LOG_DEBUG, "failed to insert domain SID (%s)",
		    NULL_MSGCHK(errmsg));
		sqlite_freemem(errmsg);
		return (SMB_LGRP_DOMINS_FAILED);
	}

	if (dom_idx)
		*dom_idx = sqlite_last_insert_rowid(db);
	return (SMB_LGRP_SUCCESS);
}

/*
 * smb_lgrp_dtbl_getidx
 *
 * Searches the domain table for the domain SID of the
 * given member SID. If it finds the domain SID it'll
 * return the index and the RID, otherwise it'll insert
 * it in the domain table as a new SID.
 */
static int
smb_lgrp_dtbl_getidx(sqlite *db, nt_sid_t *sid, uint16_t sid_type,
    uint32_t *dom_idx, uint32_t *rid)
{
	char sidstr[NT_SID_FMTBUF_SIZE];
	nt_sid_t *dom_sid;
	char **result;
	int nrow, ncol;
	char *errmsg = NULL;
	char *sql;
	int rc;

	if (nt_sid_is_indomain(smb_lgrp_lsid, sid)) {
		/* This is a local SID */
		int id_type = (sid_type == SidTypeUser)
		    ? SMB_IDMAP_USER : SMB_IDMAP_GROUP;
		*dom_idx = SMB_LGRP_LOCAL_IDX;
		if (smb_idmap_getid(sid, rid, &id_type) != IDMAP_SUCCESS)
			return (SMB_LGRP_INTERNAL_ERROR);

		return (SMB_LGRP_SUCCESS);
	}

	dom_sid = nt_sid_dup(sid);
	if (dom_sid == NULL)
		return (SMB_LGRP_NO_MEMORY);

	(void) nt_sid_split(dom_sid, rid);
	nt_sid_format2(dom_sid, sidstr);
	free(dom_sid);

	sql = sqlite_mprintf("SELECT dom_idx FROM domains WHERE dom_sid = '%s'",
	    sidstr);
	if (sql == NULL)
		return (SMB_LGRP_NO_MEMORY);

	rc = sqlite_get_table(db, sql, &result, &nrow, &ncol, &errmsg);
	sqlite_freemem(sql);

	if (rc != SQLITE_OK) {
		syslog(LOG_DEBUG, "failed to lookup domain SID (%s)",
		    NULL_MSGCHK(errmsg));
		sqlite_freemem(errmsg);
		return (SMB_LGRP_DOMLKP_FAILED);
	}

	switch (nrow) {
	case 0:
		/* new domain SID; insert it into the domains table */
		sqlite_free_table(result);
		return (smb_lgrp_dtbl_insert(db, sidstr, dom_idx));

	case 1:
		*dom_idx = atoi(result[1]);
		sqlite_free_table(result);
		return (SMB_LGRP_SUCCESS);
	}

	sqlite_free_table(result);
	return (SMB_LGRP_DB_ERROR);
}

/*
 * smb_lgrp_dtbl_getsid
 *
 * Searchs the domain table for the given domain index.
 * Converts the found domain SID to binary format and
 * returns it in the 'sid' arg.
 *
 * Caller must free the returned SID by calling free().
 */
static int
smb_lgrp_dtbl_getsid(sqlite *db, uint32_t dom_idx, nt_sid_t **sid)
{
	char **result;
	int nrow, ncol;
	char *errmsg = NULL;
	char *sql;
	int rc;

	sql = sqlite_mprintf("SELECT dom_sid FROM domains WHERE dom_idx = %u",
	    dom_idx);
	if (sql == NULL)
		return (SMB_LGRP_NO_MEMORY);

	rc = sqlite_get_table(db, sql, &result, &nrow, &ncol, &errmsg);
	sqlite_freemem(sql);

	if (rc != SQLITE_OK) {
		syslog(LOG_DEBUG, "failed to lookup domain index (%s)",
		    NULL_MSGCHK(errmsg));
		sqlite_freemem(errmsg);
		return (SMB_LGRP_DOMLKP_FAILED);
	}

	switch (nrow) {
	case 0:
		rc = SMB_LGRP_NO_SUCH_DOMAIN;
		break;

	case 1:
		*sid = nt_sid_strtosid(result[1]);
		rc = (*sid == NULL)
		    ? SMB_LGRP_INTERNAL_ERROR : SMB_LGRP_SUCCESS;
		break;

	default:
		rc = SMB_LGRP_DB_ERROR;
		break;
	}

	sqlite_free_table(result);
	return (rc);
}

/*
 * smb_lgrp_db_setinfo
 *
 * Initializes the db_info table upon database creation.
 */
static int
smb_lgrp_db_setinfo(sqlite *db)
{
	char *errmsg = NULL;
	char *sql;
	int rc;

	sql = sqlite_mprintf("INSERT INTO db_info (ver_major, ver_minor,"
	    " magic) VALUES (%d, %d, %u)", SMB_LGRP_DB_VERMAJOR,
	    SMB_LGRP_DB_VERMINOR, SMB_LGRP_DB_MAGIC);

	if (sql == NULL)
		return (SMB_LGRP_NO_MEMORY);

	rc = sqlite_exec(db, sql, NULL, NULL, &errmsg);
	sqlite_freemem(sql);
	if (rc != SQLITE_OK) {
		syslog(LOG_DEBUG, "failed to insert database information (%s)",
		    NULL_MSGCHK(errmsg));
		sqlite_freemem(errmsg);
		rc = SMB_LGRP_DBINIT_ERROR;
	} else {
		rc = SMB_LGRP_SUCCESS;
	}

	return (rc);
}

/*
 * smb_lgrp_mlist_add
 *
 * Adds the given member (newm) to the input member list (in_members)
 * if it's not already there. The result list will be returned in
 * out_members. The caller must free the allocated memory for
 * out_members by calling free().
 *
 * in_members and out_members are hex strings.
 */
static int
smb_lgrp_mlist_add(smb_lgmlist_t *in_members, smb_lgmid_t *newm,
    smb_lgmlist_t *out_members)
{
	char mid_hex[SMB_LGRP_MID_HEXSZ];
	char *in_list;
	char *out_list;
	int in_size;
	int out_size;
	int mid_hexsz;
	int i;

	out_members->m_cnt = 0;
	out_members->m_ids = NULL;

	bzero(mid_hex, sizeof (mid_hex));
	mid_hexsz = bintohex((const char *)newm, sizeof (smb_lgmid_t),
	    mid_hex, sizeof (mid_hex));

	/*
	 * Check to see if this is already a group member
	 */
	in_list = in_members->m_ids;
	for (i = 0; i < in_members->m_cnt; i++) {
		if (strncmp(in_list, mid_hex, mid_hexsz) == 0)
			return (SMB_LGRP_MEMBER_IN_GROUP);
		in_list += mid_hexsz;
	}

	in_size = (in_members->m_ids) ? strlen(in_members->m_ids) : 0;
	out_size = in_size + sizeof (mid_hex) + 1;
	out_list = malloc(out_size);
	if (out_list == NULL)
		return (SMB_LGRP_NO_MEMORY);

	bzero(out_list, out_size);
	if (in_members->m_ids)
		(void) strlcpy(out_list, in_members->m_ids, out_size);
	(void) strcat(out_list, mid_hex);

	out_members->m_cnt = in_members->m_cnt + 1;
	out_members->m_ids = out_list;

	return (SMB_LGRP_SUCCESS);
}

/*
 * smb_lgrp_mlist_del
 *
 * Removes the given member (msid) from the input member list
 * (in_members) if it's already there. The result list will b
 * returned in out_members. The caller must free the allocated
 * memory for out_members by calling free().
 *
 * in_members and out_members are hex strings.
 */
static int
smb_lgrp_mlist_del(smb_lgmlist_t *in_members, smb_lgmid_t *mid,
    smb_lgmlist_t *out_members)
{
	char mid_hex[SMB_LGRP_MID_HEXSZ];
	char *in_list;
	char *out_list;
	int in_size;
	int out_size;
	int mid_hexsz;
	int out_cnt;
	int i;

	out_members->m_cnt = 0;
	out_members->m_ids = NULL;

	if ((in_members == NULL) || (in_members->m_cnt == 0))
		return (SMB_LGRP_MEMBER_NOT_IN_GROUP);

	in_size = strlen(in_members->m_ids);
	out_size = in_size + sizeof (mid_hex) + 1;
	out_list = malloc(out_size);
	if (out_list == NULL)
		return (SMB_LGRP_NO_MEMORY);

	*out_list = '\0';

	bzero(mid_hex, sizeof (mid_hex));
	mid_hexsz = bintohex((const char *)mid, sizeof (smb_lgmid_t),
	    mid_hex, sizeof (mid_hex));

	in_list = in_members->m_ids;
	for (i = 0, out_cnt = 0; i < in_members->m_cnt; i++) {
		if (strncmp(in_list, mid_hex, mid_hexsz)) {
			(void) strncat(out_list, in_list, mid_hexsz);
			out_cnt++;
		}
		in_list += mid_hexsz;
	}

	if (out_cnt == in_members->m_cnt) {
		free(out_list);
		return (SMB_LGRP_MEMBER_NOT_IN_GROUP);
	}

	out_members->m_cnt = out_cnt;
	out_members->m_ids = out_list;
	return (SMB_LGRP_SUCCESS);
}

/*
 * smb_lgrp_plist_add
 *
 * Adds the given privilege to the input list (in_privs)
 * if it's not already there. The result list is returned
 * in out_privs. The caller must free the allocated memory
 * for out_privs by calling free().
 */
static int
smb_lgrp_plist_add(smb_lgplist_t *in_privs, smb_lgpid_t priv_id,
    smb_lgplist_t *out_privs)
{
	int i, size;
	smb_lgpid_t *pbuf;

	out_privs->p_cnt = 0;
	out_privs->p_ids = NULL;

	for (i = 0; i < in_privs->p_cnt; i++) {
		if (in_privs->p_ids[i] == priv_id)
			return (SMB_LGRP_PRIV_HELD);
	}

	size = (in_privs->p_cnt + 1) * sizeof (smb_lgpid_t) + 1;
	pbuf = malloc(size);
	if (pbuf == NULL)
		return (SMB_LGRP_NO_MEMORY);

	bzero(pbuf, size);
	bcopy(in_privs->p_ids, pbuf, in_privs->p_cnt * sizeof (smb_lgpid_t));
	pbuf[in_privs->p_cnt] = priv_id;

	out_privs->p_cnt = in_privs->p_cnt + 1;
	out_privs->p_ids = pbuf;

	return (SMB_LGRP_SUCCESS);
}

/*
 * smb_lgrp_plist_del
 *
 * Removes the given privilege from the input list (in_privs)
 * if it's already there. The result list is returned
 * in out_privs. The caller must free the allocated memory
 * for out_privs by calling free().
 */
static int
smb_lgrp_plist_del(smb_lgplist_t *in_privs, smb_lgpid_t priv_id,
    smb_lgplist_t *out_privs)
{
	int i, size;

	out_privs->p_cnt = 0;
	out_privs->p_ids = NULL;

	if ((in_privs == NULL) || (in_privs->p_cnt == 0))
		return (SMB_LGRP_PRIV_NOT_HELD);

	size = (in_privs->p_cnt - 1) * sizeof (smb_lgpid_t) + 1;
	out_privs->p_ids = malloc(size);
	if (out_privs->p_ids == NULL)
		return (SMB_LGRP_NO_MEMORY);

	bzero(out_privs->p_ids, size);

	for (i = 0; i < in_privs->p_cnt; i++) {
		if (in_privs->p_ids[i] != priv_id)
			out_privs->p_ids[out_privs->p_cnt++] =
			    in_privs->p_ids[i];
	}

	if (out_privs->p_cnt == in_privs->p_cnt) {
		free(out_privs->p_ids);
		out_privs->p_cnt = 0;
		out_privs->p_ids = NULL;
		return (SMB_LGRP_PRIV_NOT_HELD);
	}

	return (SMB_LGRP_SUCCESS);
}

/*
 * smb_lgrp_encode_privset
 *
 * Encodes given privilege set into a buffer to be stored in the group
 * database. Each entry of the encoded buffer contains the privilege ID
 * of an enable privilege. The returned buffer is null-terminated.
 */
static void
smb_lgrp_encode_privset(smb_group_t *grp, smb_lgplist_t *plist)
{
	smb_privset_t *privs;
	uint32_t pcnt = plist->p_cnt;
	int i;

	bzero(plist->p_ids, sizeof (smb_lgpid_t) * plist->p_cnt);
	plist->p_cnt = 0;

	privs = grp->sg_privs;
	if ((privs == NULL) || (privs->priv_cnt == 0))
		return;

	if (pcnt < privs->priv_cnt) {
		assert(0);
	}

	for (i = 0; i < privs->priv_cnt; i++) {
		if (privs->priv[i].attrs == SE_PRIVILEGE_ENABLED) {
			plist->p_ids[plist->p_cnt++] =
			    (uint8_t)privs->priv[i].luid.lo_part;
		}
	}
}

/*
 * smb_lgrp_decode_privset
 *
 * Decodes the privilege information read from group table
 * (nprivs, privs) into a binray format specified by the
 * privilege field of smb_group_t
 */
static int
smb_lgrp_decode_privset(smb_group_t *grp, char *nprivs, char *privs)
{
	smb_lgplist_t plist;
	int i;

	plist.p_cnt = atoi(nprivs);
	if (strlen(privs) != plist.p_cnt)
		return (SMB_LGRP_BAD_DATA);

	plist.p_ids = (smb_lgpid_t *)privs;
	grp->sg_privs = smb_privset_new();
	if (grp->sg_privs == NULL)
		return (SMB_LGRP_NO_MEMORY);

	for (i = 0; i < plist.p_cnt; i++)
		smb_privset_enable(grp->sg_privs, plist.p_ids[i]);

	return (SMB_LGRP_SUCCESS);
}

/*
 * smb_lgrp_decode_members
 *
 * Decodes the members information read from group table
 * (nmembers, members) into a binray format specified by the
 * member fields of smb_group_t
 */
static int
smb_lgrp_decode_members(smb_group_t *grp, char *nmembers, char *members,
    sqlite *db)
{
	smb_lgmid_t *m_ids;
	smb_lgmid_t *mid;
	smb_gsid_t *member;
	int mids_size;
	int i, rc;

	grp->sg_nmembers = atoi(nmembers);
	mids_size = grp->sg_nmembers * sizeof (smb_lgmid_t);
	m_ids = malloc(mids_size);
	if (m_ids == NULL)
		return (SMB_LGRP_NO_MEMORY);

	grp->sg_members = malloc(grp->sg_nmembers * sizeof (smb_gsid_t));
	if (grp->sg_members == NULL) {
		free(m_ids);
		return (SMB_LGRP_NO_MEMORY);
	}

	(void) hextobin(members, strlen(members), (char *)m_ids, mids_size);

	mid = m_ids;
	member = grp->sg_members;
	for (i = 0; i < grp->sg_nmembers; i++, mid++, member++) {
		rc = smb_lgrp_getsid(mid->m_idx, &mid->m_rid, mid->m_type, db,
		    &member->gs_sid);
		if (rc != SMB_LGRP_SUCCESS) {
			free(m_ids);
			return (SMB_LGRP_DB_ERROR);
		}

		member->gs_type = mid->m_type;
	}

	free(m_ids);
	return (SMB_LGRP_SUCCESS);
}

/*
 * smb_lgrp_decode
 *
 * Fills out the fields of the given group (grp) based in the
 * string information read from the group table. infolvl determines
 * which fields are requested and need to be decoded.
 *
 * Allocated memories must be freed by calling smb_lgrp_free()
 * upon successful return.
 */
static int
smb_lgrp_decode(smb_group_t *grp, char **values, int infolvl, sqlite *db)
{
	uint32_t sid_idx;
	int rc;

	if (infolvl == SMB_LGRP_INFO_NONE)
		return (SMB_LGRP_SUCCESS);

	if (infolvl & SMB_LGRP_INFO_NAME) {
		grp->sg_name = strdup(values[SMB_LGRP_GTBL_NAME]);
		if (grp->sg_name == NULL)
			return (SMB_LGRP_NO_MEMORY);
	}

	if (infolvl & SMB_LGRP_INFO_CMNT) {
		grp->sg_cmnt = strdup(values[SMB_LGRP_GTBL_CMNT]);
		if (grp->sg_cmnt == NULL) {
			smb_lgrp_free(grp);
			return (SMB_LGRP_NO_MEMORY);
		}
	}


	if (infolvl & SMB_LGRP_INFO_SID) {
		sid_idx = atoi(values[SMB_LGRP_GTBL_SIDIDX]);
		grp->sg_rid = atoi(values[SMB_LGRP_GTBL_SIDRID]);
		grp->sg_attr = atoi(values[SMB_LGRP_GTBL_SIDATR]);
		grp->sg_id.gs_type = atoi(values[SMB_LGRP_GTBL_SIDTYP]);
		rc = smb_lgrp_getsid(sid_idx, &grp->sg_rid, grp->sg_id.gs_type,
		    db, &grp->sg_id.gs_sid);
		if (rc != SMB_LGRP_SUCCESS) {
			smb_lgrp_free(grp);
			return (SMB_LGRP_NO_MEMORY);
		}
		grp->sg_domain = (sid_idx == SMB_LGRP_LOCAL_IDX)
		    ? SMB_LGRP_LOCAL : SMB_LGRP_BUILTIN;
	}

	if (infolvl & SMB_LGRP_INFO_PRIV) {
		rc = smb_lgrp_decode_privset(grp, values[SMB_LGRP_GTBL_NPRIVS],
		    values[SMB_LGRP_GTBL_PRIVS]);

		if (rc != SMB_LGRP_SUCCESS) {
			smb_lgrp_free(grp);
			return (rc);
		}
	}

	if (infolvl & SMB_LGRP_INFO_MEMB) {
		rc = smb_lgrp_decode_members(grp, values[SMB_LGRP_GTBL_NMEMBS],
		    values[SMB_LGRP_GTBL_MEMBS], db);
		if (rc != SMB_LGRP_SUCCESS) {
			smb_lgrp_free(grp);
			return (rc);
		}
	}

	return (SMB_LGRP_SUCCESS);
}

/*
 * smb_lgrp_chkname
 *
 * User account names are limited to 20 characters and group names are
 * limited to 256 characters. In addition, account names cannot be terminated
 * by a period and they cannot include commas or any of the following printable
 * characters: ", /, \, [, ], :, |, <, >, +, =, ;, ?, *.
 * Names also cannot include characters in the range 1-31, which are
 * nonprintable.
 *
 * Source: MSDN, description of NetLocalGroupAdd function.
 */
static boolean_t
smb_lgrp_chkname(char *name)
{
	static char *invalid_chars =
	    "\001\002\003\004\005\006\007\010\011\012\013\014\015\016\017"
	    "\020\021\022\023\024\025\026\027\030\031"
	    "\"/\\[]:|<>+=;,*?";
	int len, i;

	if (name == NULL || *name == '\0')
		return (B_FALSE);

	len = strlen(name);
	if (len > SMB_LGRP_NAME_MAX)
		return (B_FALSE);

	if (name[len - 1] == '.')
		return (B_FALSE);

	for (i = 0; i < len; i++)
		if (strchr(invalid_chars, name[i]))
			return (B_FALSE);

	(void) utf8_strlwr(name);
	return (B_TRUE);
}

/*
 * smb_lgrp_set_default_privs
 *
 * set default privileges for Administrators and Backup Operators
 */
static void
smb_lgrp_set_default_privs(smb_group_t *grp)
{
	if (utf8_strcasecmp(grp->sg_name, "Administrators") == 0) {
		smb_privset_enable(grp->sg_privs, SE_TAKE_OWNERSHIP_LUID);
		return;
	}

	if (utf8_strcasecmp(grp->sg_name, "Backup Operators") == 0) {
		smb_privset_enable(grp->sg_privs, SE_BACKUP_LUID);
		smb_privset_enable(grp->sg_privs, SE_RESTORE_LUID);
		return;
	}
}

/*
 * smb_lgrp_getsid
 *
 * Returns a SID based on the provided information
 * If dom_idx is 0, it means 'rid' contains a UID/GID and the
 * returned SID will be a local SID. If dom_idx is not 0 then
 * the domain SID will be fetched from the domain table.
 */
static int
smb_lgrp_getsid(int dom_idx, uint32_t *rid, uint16_t sid_type,
    sqlite *db, nt_sid_t **sid)
{
	nt_sid_t *dom_sid = NULL;
	nt_sid_t *res_sid = NULL;
	int id_type;
	int rc;

	*sid = NULL;
	if (dom_idx == SMB_LGRP_LOCAL_IDX) {
		id_type = (sid_type == SidTypeUser)
		    ? SMB_IDMAP_USER : SMB_IDMAP_GROUP;
		if (smb_idmap_getsid(*rid, id_type, &res_sid) != IDMAP_SUCCESS)
			return (SMB_LGRP_NO_SID);

		/*
		 * Make sure the returned SID is local
		 */
		if (!nt_sid_is_indomain(smb_lgrp_lsid, res_sid)) {
			free(res_sid);
			return (SMB_LGRP_SID_NOTLOCAL);
		}

		(void) nt_sid_get_rid(res_sid, rid);
		*sid = res_sid;
		return (SMB_LGRP_SUCCESS);
	}

	rc = smb_lgrp_dtbl_getsid(db, dom_idx, &dom_sid);
	if (rc != SMB_LGRP_SUCCESS)
		return (SMB_LGRP_DB_ERROR);

	res_sid = nt_sid_splice(dom_sid, *rid);
	free(dom_sid);
	if (res_sid == NULL)
		return (SMB_LGRP_NO_MEMORY);

	*sid = res_sid;
	return (SMB_LGRP_SUCCESS);
}