/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 4 -*-
 * ex: set softtabstop=4 tabstop=8 expandtab shiftwidth=4: *
 * Editor Settings: expandtabs and use 4 spaces for indentation */

/*
 * Copyright Likewise Software
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or (at
 * your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.  You should have received a copy of the GNU General
 * Public License along with this program.  If not, see
 * <http://www.gnu.org/licenses/>.
 *
 * LIKEWISE SOFTWARE MAKES THIS SOFTWARE AVAILABLE UNDER OTHER LICENSING
 * TERMS AS WELL.  IF YOU HAVE ENTERED INTO A SEPARATE LICENSE AGREEMENT
 * WITH LIKEWISE SOFTWARE, THEN YOU MAY ELECT TO USE THE SOFTWARE UNDER THE
 * TERMS OF THAT SOFTWARE LICENSE AGREEMENT INSTEAD OF THE TERMS OF THE GNU
 * GENERAL PUBLIC LICENSE, NOTWITHSTANDING THE ABOVE NOTICE.  IF YOU
 * HAVE QUESTIONS, OR WISH TO REQUEST A COPY OF THE ALTERNATE LICENSING
 * TERMS OFFERED BY LIKEWISE SOFTWARE, PLEASE CONTACT LIKEWISE SOFTWARE AT
 * license@likewisesoftware.com
 */

/*
 * Copyright (C) Likewise Software. All rights reserved.
 *
 * Module Name:
 *
 *        samdbmember.c
 *
 * Abstract:
 *
 *
 *      Likewise SAM Database Provider
 *
 *      Group membership handling routines
 *
 * Authors: Rafal Szczesniak (rafal@likewise.com)
 *
 */

#include "includes.h"


typedef DWORD
(*PFN_BUILD_MODIFY_MEMBERS_STATEMENT)(
    PSAMDB_CONTEXT DbContext,
    LONG64 LocalGroupId,
    LONG64 MemberId,
    sqlite3_stmt** pSqlStatement,
    PSAMDB_ATTRIBUTE* pInputAttributes,
    PDWORD pInputAttributesCount
    );

static
DWORD
SamDbAddOrRemoveMembersInternal(
    IN PSAMDB_CONTEXT DbContext,
    IN PSID Sid,
    IN DWORD MembersCount,
    IN PSID* Members,
    IN PFN_BUILD_MODIFY_MEMBERS_STATEMENT BuildStatementFn
    );

typedef DWORD
(*PFN_BUILD_QUERY_MEMBERSHIP_STATEMENT)(
    PSAMDB_CONTEXT DbContext,
    LONG64 RecordId,
    sqlite3_stmt** pSqlStatement,
    PSAMDB_ATTRIBUTE* pInputAttributes,
    PDWORD pInputAttributesCount,
    PSAMDB_ATTRIBUTE* pOutputAttributes,
    PDWORD pOutputAttributesCount
    );

static
DWORD
SamDbQueryMembership(
    PSID Sid,
    PDWORD pCount,
    PSID** pSids,
    PFN_BUILD_QUERY_MEMBERSHIP_STATEMENT BuiltStatementFn
    );

static
DWORD
SamDbBuildSelectMembershipsStatement(
    PSAMDB_CONTEXT DbContext,
    LONG64 RecordId,
    sqlite3_stmt** pSqlStatement,
    PSAMDB_ATTRIBUTE* pInputAttributes,
    PDWORD pInputAttributesCount,
    PSAMDB_ATTRIBUTE* pOutputAttributes,
    PDWORD pOutputAttributesCount
    );

static
DWORD
SamDbFindObjectRecord(
    PSAMDB_CONTEXT DbContext,
    PSID Sid,
    PLONG64 pRecordId,
    PSAMDB_OBJECT_CLASS pObjectClass
    );

static
DWORD
SamDbBuildSelectRecordStatement(
    PSAMDB_CONTEXT DbContext,
    PSID Sid,
    sqlite3_stmt** pSqlStatement,
    PSAMDB_ATTRIBUTE* pInputAttributes,
    PDWORD pInputAttributesCount,
    PSAMDB_ATTRIBUTE* pOutputAttributes,
    PDWORD pOutputAttributesCount
    );

static
DWORD
SamDbBuildInsertMembershipStatement(
    PSAMDB_CONTEXT DbContext,
    LONG64 LocalGroupId,
    LONG64 MemberId,
    sqlite3_stmt** pSqlStatement,
    PSAMDB_ATTRIBUTE* pInputAttributes,
    PDWORD pInputAttributesCount
    );

static
DWORD
SamDbBuildDeleteMembershipStatement(
    PSAMDB_CONTEXT DbContext,
    LONG64 LocalGroupId,
    LONG64 MemberId,
    sqlite3_stmt** pSqlStatement,
    PSAMDB_ATTRIBUTE* pInputAttributes,
    PDWORD pInputAttributesCount
    );

static
BOOLEAN
SamDbIsBuiltinMembership(
    PSID LocalGroupSid,
    PSID LocalDomainSid,
    PSID MemberSid
    );

static
DWORD
SamDbBuildSelectMembershipsStatement(
    PSAMDB_CONTEXT DbContext,
    LONG64 RecordId,
    sqlite3_stmt** pSqlStatement,
    PSAMDB_ATTRIBUTE* pInputAttributes,
    PDWORD pInputAttributesCount,
    PSAMDB_ATTRIBUTE* pOutputAttributes,
    PDWORD pOutputAttributesCount
    );

static
DWORD
SamDbBuildSelectMembersStatement(
    PSAMDB_CONTEXT DbContext,
    LONG64 RecordId,
    sqlite3_stmt** pSqlStatement,
    PSAMDB_ATTRIBUTE* pInputAttributes,
    PDWORD pInputAttributesCount,
    PSAMDB_ATTRIBUTE* pOutputAttributes,
    PDWORD pOutputAttributesCount
    );


DWORD
SamDbUpdateMembers(
    IN PSID Sid,
    IN DWORD AddMembersCount,
    IN OPTIONAL PSID* AddMembers,
    IN DWORD RemoveMembersCount,
    IN OPTIONAL PSID* RemoveMembers
    )
{
    DWORD error = ERROR_SUCCESS;
    PSAMDB_CONTEXT dbContext = NULL;
    BOOLEAN transactionStarted = FALSE;
    DWORD currentMembersCount = 0;
    PSID* currentMembers = NULL;
    PSID localDomainSid = NULL;
    DWORD currentMemberIndex = 0;
    DWORD memberIndex = 0;
    SAMDB_QUERY query = {0};
    PSAMDB_OBJECT_INFO userObject = NULL;

    error = SamDbQueryMembership(
                  Sid,
                  &currentMembersCount,
                  &currentMembers,
                  SamDbBuildSelectMembersStatement);
    if (error == ERROR_FILE_NOT_FOUND)
    {
        error = ERROR_NO_SUCH_GROUP;
    }
    BAIL_ON_SAMDB_ERROR(error);

    error = SamDbAcquireContext(
                  &dbContext);
    BAIL_ON_SAMDB_ERROR(error);

    SAMDB_BEGIN_TRANSACTION(transactionStarted, dbContext, error);

    if (AddMembers)
    {
        //
        // Before adding new members, verify if any of them is already
        // a member if the local group
        //
        for (memberIndex = 0; memberIndex < AddMembersCount; memberIndex++)
        {
            for (currentMemberIndex = 0;
                 currentMemberIndex < currentMembersCount;
                 currentMemberIndex++)
            {
                if (RtlEqualSid(AddMembers[memberIndex],
                                currentMembers[currentMemberIndex]))
                {
                    error = ERROR_MEMBER_IN_ALIAS;
                    BAIL_ON_SAMDB_ERROR(error);
                }
            }
        }

        error = SamDbAddMembersInternal(
                      dbContext,
                      Sid,
                      AddMembersCount,
                      AddMembers);
        BAIL_ON_SAMDB_ERROR(error);
    }

    if (RemoveMembers)
    {
        error = SamDbGetLocalDomainSid(&localDomainSid);
        BAIL_ON_SAMDB_ERROR(error);

        //
        // Before removing members, verify whether all of them are
        // already members of the local group
        //
        for (memberIndex = 0; memberIndex < RemoveMembersCount; memberIndex++)
        {
            BOOLEAN memberFound = FALSE;

            for (currentMemberIndex = 0;
                 currentMemberIndex < currentMembersCount;
                 currentMemberIndex++)
            {
                if (RtlEqualSid(RemoveMembers[memberIndex],
                                currentMembers[currentMemberIndex]))
                {
                    memberFound = TRUE;
                    break;
                }
            }

            if (!memberFound)
            {
                error = ERROR_MEMBER_NOT_IN_ALIAS;
                BAIL_ON_SAMDB_ERROR(error);
            }

            //
            // Check if this could be a member's primary group.
            //
            query.Sid = currentMembers[currentMemberIndex];
            error = SamDbFindObjectInternal(
                                  dbContext,
                                  SAMDB_OBJECT_CLASS_USER,
                                  SAMDB_QUERY_TYPE_SID,
                                  &query,
                                  &userObject);
            if (error == ERROR_SUCCESS)
            {
                if (userObject->Info.User.PrimaryGroupRid ==
                    Sid->SubAuthority[Sid->SubAuthorityCount - 1] &&
                    RtlIsStrictPrefixSid(localDomainSid, Sid) &&
                    RtlIsStrictPrefixSid(localDomainSid, userObject->Sid))
                {
                    error = ERROR_MEMBERS_PRIMARY_GROUP;
                    BAIL_ON_SAMDB_ERROR(error);
                }

                if (userObject)
                {
                    SamDbFreeObject(userObject);
                    userObject = NULL;
                }
            }
            else if (error == ERROR_FILE_NOT_FOUND)
            {
                // Member being checked is probably a foreign security principal
                // so ignore returned error.
                error = ERROR_SUCCESS;
            }
            else if (error != ERROR_SUCCESS)
            {
                BAIL_ON_SAMDB_ERROR(error);
            }

            //
            // Check if this is one of builtin memberships that has
            // to be kept.
            //
            if (SamDbIsBuiltinMembership(
                              Sid,
                              localDomainSid,
                              RemoveMembers[memberIndex]))
            {
                error = ERROR_SPECIAL_ACCOUNT;
                BAIL_ON_SAMDB_ERROR(error);
            }
        }

        error = SamDbRemoveMembersInternal(
                      dbContext,
                      Sid,
                      RemoveMembersCount,
                      RemoveMembers);
        BAIL_ON_SAMDB_ERROR(error);
    }

    //
    // Increase the respective domain's sequence number because
    // the local group's members list has changed
    //
    error = SamDbIncrementSequenceNumber(
                  dbContext,
                  Sid);
    BAIL_ON_SAMDB_ERROR(error);

cleanup:
    SAMDB_END_TRANSACTION(transactionStarted, dbContext, error);

    if (error == ERROR_SUCCESS)
    {
        SamDbNotifyCallbacks(Sid);
    }

    if (currentMembers)
    {
        SamDbFreeSidList(
                 currentMembersCount,
                 currentMembers);
    }

    if (userObject)
    {
        SamDbFreeObject(userObject);
    }

    if (dbContext)
    {
        SamDbReleaseContext(dbContext);
    }

    LW_SAFE_FREE_MEMORY(localDomainSid);

    return error;
}


DWORD
SamDbAddMembersInternal(
    PSAMDB_CONTEXT DbContext,
    PSID Sid,
    DWORD MembersCount,
    PSID* Members
    )
{
    return SamDbAddOrRemoveMembersInternal(
                   DbContext,
                   Sid,
                   MembersCount,
                   Members,
                   SamDbBuildInsertMembershipStatement);
}


static
DWORD
SamDbAddOrRemoveMembersInternal(
    IN PSAMDB_CONTEXT DbContext,
    IN PSID Sid,
    IN DWORD MembersCount,
    IN PSID* Members,
    IN PFN_BUILD_MODIFY_MEMBERS_STATEMENT BuildStatementFn
    )
{
    DWORD error = ERROR_SUCCESS;
    NTSTATUS ntStatus = STATUS_SUCCESS;
    LONG64 localGroupRecordId = 0;
    SAMDB_OBJECT_CLASS objectClass = SAMDB_OBJECT_CLASS_UNKNOWN;
    DWORD memberIndex = 0;
    PLONG64 memberRecordIds = NULL;
    PSTR memberSidStr = NULL;
    sqlite3_stmt* sqlStatement = NULL;
    PSAMDB_ATTRIBUTE inputAttributes = NULL;
    DWORD inputAttributesCount = 0;

    error = SamDbFindObjectRecord(
                     DbContext,
                     Sid,
                     &localGroupRecordId,
                     &objectClass);
    BAIL_ON_SAMDB_ERROR(error);

    if (objectClass != SAMDB_OBJECT_CLASS_LOCAL_GROUP)
    {
        error = ERROR_NO_SUCH_ALIAS;
        BAIL_ON_SAMDB_ERROR(error);
    }

    error = LwAllocateMemory(
                     sizeof(*memberRecordIds) * MembersCount,
                     OUT_PPVOID(&memberRecordIds));
    BAIL_ON_SAMDB_ERROR(error);

    for (memberIndex = 0;
         memberIndex < MembersCount;
         memberIndex++)
    {
        error = SamDbFindObjectRecord(
                         DbContext,
                         Members[memberIndex],
                         &memberRecordIds[memberIndex],
                         &objectClass);
        if (error == ERROR_FILE_NOT_FOUND)
        {
            error = SamDbAddForeignSecurityPrincipalObject(
                             DbContext,
                             Members[memberIndex]);
            BAIL_ON_SAMDB_ERROR(error);

            error = SamDbFindObjectRecord(
                             DbContext,
                             Members[memberIndex],
                             &memberRecordIds[memberIndex],
                             &objectClass);
            if (error == ERROR_FILE_NOT_FOUND)
            {
                error = ERROR_INTERNAL_ERROR;

                SAMDB_LOG_ERROR("Foreign Pecurity Principal couldn't be found "
                                "after being created");
                goto cleanup;
            }
            else if (objectClass != SAMDB_OBJECT_CLASS_FOREIGN_SECURITY_PRINCIPAL)
            {
                error = ERROR_INTERNAL_ERROR;

                SAMDB_LOG_ERROR("Object class = %u found while foreign security "
                                "principal was expected",
                                (unsigned int)objectClass);
                goto cleanup;
            }

        }
        else if (error != ERROR_SUCCESS)
        {
            BAIL_ON_SAMDB_ERROR(error);
        }

        if (objectClass != SAMDB_OBJECT_CLASS_USER &&
            objectClass != SAMDB_OBJECT_CLASS_FOREIGN_SECURITY_PRINCIPAL)
        {
            ntStatus = RtlAllocateCStringFromSid(
                              &memberSidStr,
                              Members[memberIndex]);
            BAIL_ON_NT_STATUS(ntStatus);

            error = ERROR_INVALID_PARAMETER;

            SAMDB_LOG_WARNING("Request to add SID %s which is not "
                              "a local user or foreign SID to local group",
                              memberSidStr);
            goto cleanup;
        }
    }

    for (memberIndex = 0;
         memberIndex < MembersCount;
         memberIndex++)
    {
        error = BuildStatementFn(
                          DbContext,
                          localGroupRecordId,
                          memberRecordIds[memberIndex],
                          &sqlStatement,
                          &inputAttributes,
                          &inputAttributesCount);
        BAIL_ON_SAMDB_ERROR(error);

        error = SamDbBindAttributeValues(
                          DbContext,
                          sqlStatement,
                          inputAttributes,
                          inputAttributesCount);
        BAIL_ON_SAMDB_ERROR(error);

        error = sqlite3_step(sqlStatement);
        if (error == SQLITE_DONE)
        {
            error = ERROR_SUCCESS;
        }
        else
        {
            SAMDB_LOG_ERROR("Error code %u returned from sqlite3_step() "
                            "while %u was expected",
                            error,
                            SQLITE_DONE);
            BAIL_ON_SAMDB_SQLITE_ERROR_STMT(error, sqlStatement);
        }
        if (sqlStatement)
        {
            sqlite3_finalize(sqlStatement);
            sqlStatement = NULL;
        }

        if (inputAttributes)
        {
            SamDbFreeAttributes(
                      inputAttributes,
                      inputAttributesCount);
            inputAttributes = NULL;
            inputAttributesCount = 0;
        }

    }

cleanup:
error:
    if (sqlStatement)
    {
        sqlite3_finalize(sqlStatement);
    }

    if (inputAttributes)
    {
        SamDbFreeAttributes(
                    inputAttributes,
                    inputAttributesCount);
    }

    LW_SAFE_FREE_MEMORY(memberRecordIds);
    LW_SAFE_FREE_MEMORY(memberSidStr);

    if (error == ERROR_SUCCESS &&
        ntStatus != STATUS_SUCCESS)
    {
        error = LwNtStatusToWin32Error(ntStatus);
    }

    return error;
}


static
DWORD
SamDbFindObjectRecord(
    PSAMDB_CONTEXT DbContext,
    PSID Sid,
    PLONG64 pRecordId,
    PSAMDB_OBJECT_CLASS pObjectClass
    )
{
    DWORD error = ERROR_SUCCESS;
    NTSTATUS ntStatus = STATUS_SUCCESS;
    sqlite3_stmt* sqlStatement = NULL;
    PSAMDB_ATTRIBUTE inputAttributes = NULL;
    DWORD inputAttributesCount = 0;
    PSAMDB_ATTRIBUTE outputAttributes = NULL;
    DWORD outputAttributesCount = 0;
    DWORD attributeIndex = 0;
    CHAR recordIdAttrName[] = SAMDB_ATTR_RECORD_ID;
    CHAR objectClassAttrName[] = SAMDB_ATTR_OBJECT_CLASS;
    LONG64 recordId = 0;
    SAMDB_OBJECT_CLASS objectClass = SAMDB_OBJECT_CLASS_UNKNOWN;

    error = SamDbBuildSelectRecordStatement(
                       DbContext,
                       Sid,
                       &sqlStatement,
                       &inputAttributes,
                       &inputAttributesCount,
                       &outputAttributes,
                       &outputAttributesCount);
    BAIL_ON_SAMDB_ERROR(error);

    error = SamDbBindAttributeValues(
                  DbContext,
                  sqlStatement,
                  inputAttributes,
                  inputAttributesCount);
    BAIL_ON_SAMDB_ERROR(error);

    error = sqlite3_step(sqlStatement);
    if (error == SQLITE_ROW)
    {
        error = ERROR_SUCCESS;
    }
    else if (error == SQLITE_DONE)
    {
        // No objects found so return error code
        error = ERROR_FILE_NOT_FOUND;
        goto cleanup;
    }
    else
    {
        SAMDB_LOG_ERROR("Error code %u returned from sqlite3_step() "
                        "while %u or %u was expected",
                        error,
                        SQLITE_ROW,
                        SQLITE_DONE);
        BAIL_ON_SAMDB_SQLITE_ERROR_STMT(error, sqlStatement);
    }

    error = SamDbGetOutputAttributeValues(
                 DbContext,
                 sqlStatement,
                 outputAttributes,
                 outputAttributesCount);
    BAIL_ON_SAMDB_ERROR(error);

    for (attributeIndex = 0;
         attributeIndex < outputAttributesCount;
         attributeIndex++)
    {
        if (RtlCStringIsEqual(
                     outputAttributes[attributeIndex].DbColumnName,
                     recordIdAttrName,
                     TRUE))
        {
            PLONG64 pRecordId = (PLONG64)outputAttributes[attributeIndex].Value;
            recordId = *pRecordId;
        }
        else if (RtlCStringIsEqual(
                     outputAttributes[attributeIndex].DbColumnName,
                     objectClassAttrName,
                     TRUE))
        {
            PSAMDB_OBJECT_CLASS pObjectClass =
                (PSAMDB_OBJECT_CLASS)outputAttributes[attributeIndex].Value;
            objectClass = *pObjectClass;
        }
    }

cleanup:
    if (error)
    {
        recordId = 0;
        objectClass = SAMDB_OBJECT_CLASS_UNKNOWN;
    }

    if (sqlStatement)
    {
        sqlite3_finalize(sqlStatement);
    }

    if (inputAttributes)
    {
        SamDbFreeAttributes(
                      inputAttributes,
                      inputAttributesCount);
    }

    if (outputAttributes)
    {
        SamDbFreeAttributes(
                      outputAttributes,
                      outputAttributesCount);
    }

    if (error == ERROR_SUCCESS &&
        ntStatus != STATUS_SUCCESS)
    {
        error = LwNtStatusToWin32Error(ntStatus);
    }

    *pRecordId = recordId;
    *pObjectClass = objectClass;

    return error;
}


#define SAMDB_SQL_SELECT_RECORD_PREFIX    "SELECT "
#define SAMDB_SQL_SELECT_RECORD_FROM      " FROM " SAM_DB_OBJECTS_TABLE " WHERE "


static
DWORD
SamDbBuildSelectRecordStatement(
    PSAMDB_CONTEXT DbContext,
    PSID Sid,
    sqlite3_stmt** pSqlStatement,
    PSAMDB_ATTRIBUTE* pInputAttributes,
    PDWORD pInputAttributesCount,
    PSAMDB_ATTRIBUTE* pOutputAttributes,
    PDWORD pOutputAttributesCount
    )
{
    DWORD error = ERROR_SUCCESS;
    NTSTATUS ntStatus = STATUS_SUCCESS;
    DWORD inputAttributeIndex = 0;
    PSAMDB_ATTRIBUTE inputAttributes = NULL;
    DWORD inputAttributesCount = 0;
    DWORD outputAttributeIndex = 0;
    PSAMDB_ATTRIBUTE outputAttributes = NULL;
    DWORD outputAttributesCount = 0;
    size_t sqlStatementSize = 64;
    size_t sqlStatementLength = 0;
    size_t maxSqlStatementSize = 4096;
    PSTR sqlStatementString = NULL;
    sqlite3_stmt* sqlStatement = NULL;
    PWSTR* ppwszSid = NULL;

    enum _INPUT_ATTR_INDEX {
        ATTR_IDX_OBJECT_SID = 0,
    };

    SAMDB_ATTRIBUTE inputAttrDef[] = {
        {
            SAMDB_ATTR_OBJECT_SID,
            SAMDB_ATTR_TYPE_TEXT,
            NULL
        },
    };

    SAMDB_ATTRIBUTE outputAttrDef[] = {
        {
            SAMDB_ATTR_OBJECT_CLASS,
            SAMDB_ATTR_TYPE_INT32,
            NULL
        },
        {
            SAMDB_ATTR_RECORD_ID,
            SAMDB_ATTR_TYPE_INT64,
            NULL
        },
    };

    error = LwAllocateMemory(
                    sizeof(outputAttrDef),
                    OUT_PPVOID(&outputAttributes));
    BAIL_ON_SAMDB_ERROR(error);

    outputAttributesCount = sizeof(outputAttrDef)/sizeof(outputAttrDef[0]);

    error = LwAllocateMemory(
                    sizeof(*ppwszSid),
                    OUT_PPVOID(&ppwszSid));
    BAIL_ON_SAMDB_ERROR(error);

    ntStatus = RtlAllocateWC16StringFromSid(
                    ppwszSid,
                    Sid);
    BAIL_ON_NT_STATUS(ntStatus);

    error = LwAllocateMemory(
                    sizeof(inputAttrDef),
                    OUT_PPVOID(&inputAttributes));
    BAIL_ON_SAMDB_ERROR(error);

    inputAttributes[inputAttributeIndex] = inputAttrDef[ATTR_IDX_OBJECT_SID];

    inputAttributes[inputAttributeIndex].Value = ppwszSid;
    inputAttributes[inputAttributeIndex].FreeFn = SamDbAttributeFreeSidString;

    //
    // Create SQL statement string (SELECT ...)
    //
    error = LwAllocateMemory(
                    sizeof(sqlStatementString[0]) * sqlStatementSize,
                    OUT_PPVOID(&sqlStatementString));
    BAIL_ON_SAMDB_ERROR(error);

    error = SamDbPrintfString(
                    &sqlStatementString,
                    &sqlStatementSize,
                    &sqlStatementLength,
                    maxSqlStatementSize,
                    "%s",
                    SAMDB_SQL_SELECT_RECORD_PREFIX);
    BAIL_ON_SAMDB_ERROR(error);

    for (outputAttributeIndex = 0;
         outputAttributeIndex < outputAttributesCount;
         outputAttributeIndex++)
    {
        //
        // Append SQL column names separated with ","
        //

        outputAttributes[outputAttributeIndex] =
            outputAttrDef[outputAttributeIndex];

        error = SamDbPrintfString(
                      &sqlStatementString,
                      &sqlStatementSize,
                      &sqlStatementLength,
                      maxSqlStatementSize,
                      (outputAttributeIndex < outputAttributesCount - 1) ? "%s," : "%s",
                      outputAttributes[outputAttributeIndex].DbColumnName);
        BAIL_ON_SAMDB_ERROR(error);
    }

    //
    // Append SQL statement (FROM samdbobjects WHERE ...)
    //
    error = SamDbPrintfString(
                    &sqlStatementString,
                    &sqlStatementSize,
                    &sqlStatementLength,
                    maxSqlStatementSize,
                    "%s%s = ?1",
                    SAMDB_SQL_SELECT_RECORD_FROM,
                    inputAttributes[inputAttributeIndex].DbColumnName);
    BAIL_ON_SAMDB_ERROR(error);

    inputAttributeIndex++;
    inputAttributesCount = inputAttributeIndex;

    error = sqlite3_prepare_v2(
                      DbContext->DbHandle,
                      sqlStatementString,
                      -1,
                      &sqlStatement,
                      NULL);
    if (error)
    {
        SAMDB_LOG_DEBUG("Error when preparing SQL statement:\n%s",
                        sqlStatementString);
    }
    BAIL_ON_SAMDB_SQLITE_ERROR_DB(error, DbContext->DbHandle);

cleanup:
error:
    if (error)
    {
        if (inputAttributes)
        {
            SamDbFreeAttributes(
                     inputAttributes,
                     inputAttributesCount);
            inputAttributes = NULL;
            inputAttributesCount = 0;
        }

        if (outputAttributes)
        {
            SamDbFreeAttributes(
                     outputAttributes,
                     outputAttributesCount);
            outputAttributes = NULL;
            outputAttributesCount = 0;
        }

        if (sqlStatement)
        {
            sqlite3_finalize(sqlStatement);
            sqlStatement = NULL;
        }
    }

    LW_SAFE_FREE_MEMORY(sqlStatementString);

    if (error == ERROR_SUCCESS &&
        ntStatus != STATUS_SUCCESS)
    {
        error = LwNtStatusToWin32Error(ntStatus);
    }

    *pSqlStatement = sqlStatement;
    *pInputAttributes = inputAttributes;
    *pInputAttributesCount = inputAttributesCount;
    *pOutputAttributes = outputAttributes;
    *pOutputAttributesCount = outputAttributesCount;

    return error;
}


#define SAMDB_SQL_INSERT_MEMBERSHIP \
    "INSERT INTO " SAM_DB_MEMBERS_TABLE " (" \
    SAM_DB_COL_GROUP_RECORD_ID "," SAM_DB_COL_MEMBER_RECORD_ID ") " \
    "VALUES (?1, ?2);"


static
DWORD
SamDbBuildInsertMembershipStatement(
    PSAMDB_CONTEXT DbContext,
    LONG64 LocalGroupId,
    LONG64 MemberId,
    sqlite3_stmt** pSqlStatement,
    PSAMDB_ATTRIBUTE* pInputAttributes,
    PDWORD pInputAttributesCount
    )
{
    DWORD error = ERROR_SUCCESS;
    PSAMDB_ATTRIBUTE inputAttributes = NULL;
    DWORD inputAttributeIndex = 0;
    DWORD inputAttributesCount = 0;
    size_t sqlStatementSize = 64;
    size_t sqlStatementLength = 0;
    size_t maxSqlStatementSize = 4096;
    PSTR sqlStatementString = NULL;
    sqlite3_stmt* sqlStatement = NULL;
    PLONG64 pGroupRecordId = NULL;
    PLONG64 pMemberRecordId = NULL;

    enum _MEMBERSHIP_ATTR_INDEX {
        ATTR_IDX_GROUP_RECORD_ID = 0,
        ATTR_IDX_MEMBER_RECORD_ID
    };

    SAMDB_ATTRIBUTE membershipAttributes[] = {
        {
            SAMDB_ATTR_GROUP_RECORD_ID,
            SAMDB_ATTR_TYPE_INT64,
            NULL
        },
        {
            SAMDB_ATTR_MEMBER_RECORD_ID,
            SAMDB_ATTR_TYPE_INT64,
            NULL
        },
    };

    error = LwAllocateMemory(
                    sizeof(*pGroupRecordId),
                    OUT_PPVOID(&pGroupRecordId));
    BAIL_ON_SAMDB_ERROR(error);

    *pGroupRecordId = LocalGroupId;

    error = LwAllocateMemory(
                    sizeof(*pMemberRecordId),
                    OUT_PPVOID(&pMemberRecordId));
    BAIL_ON_SAMDB_ERROR(error);

    *pMemberRecordId = MemberId;

    error = LwAllocateMemory(
                    sizeof(membershipAttributes),
                    OUT_PPVOID(&inputAttributes));
    BAIL_ON_SAMDB_ERROR(error);

    inputAttributes[inputAttributeIndex] =
        membershipAttributes[ATTR_IDX_GROUP_RECORD_ID];
    inputAttributes[inputAttributeIndex].Value = pGroupRecordId;
    inputAttributes[inputAttributeIndex].FreeFn = SamDbAttributeFreePointer;
    inputAttributeIndex++;

    inputAttributes[inputAttributeIndex] =
        membershipAttributes[ATTR_IDX_MEMBER_RECORD_ID];
    inputAttributes[inputAttributeIndex].Value = pMemberRecordId;
    inputAttributes[inputAttributeIndex].FreeFn = SamDbAttributeFreePointer;
    inputAttributeIndex++;

    inputAttributesCount = inputAttributeIndex;

    //
    // Create SQL statement string (INSERT INTO samdbmembers ...)
    //
    error = LwAllocateMemory(
                    sizeof(sqlStatementString[0]) * sqlStatementSize,
                    OUT_PPVOID(&sqlStatementString));
    BAIL_ON_SAMDB_ERROR(error);

    error = SamDbPrintfString(
                    &sqlStatementString,
                    &sqlStatementSize,
                    &sqlStatementLength,
                    maxSqlStatementSize,
                    "%s",
                    SAMDB_SQL_INSERT_MEMBERSHIP);
    BAIL_ON_SAMDB_ERROR(error);

    error = sqlite3_prepare_v2(
                      DbContext->DbHandle,
                      sqlStatementString,
                      -1,
                      &sqlStatement,
                      NULL);
    if (error)
    {
        SAMDB_LOG_DEBUG("Error when preparing SQL statement:\n%s",
                        sqlStatementString);
    }
    BAIL_ON_SAMDB_SQLITE_ERROR_DB(error, DbContext->DbHandle);

cleanup:
    if (error)
    {
        if (inputAttributes)
        {
            SamDbFreeAttributes(
                     inputAttributes,
                     inputAttributesCount);
            inputAttributes = NULL;
            inputAttributesCount = 0;
        }

        if (sqlStatement)
        {
            sqlite3_finalize(sqlStatement);
            sqlStatement = NULL;
        }
    }

    LW_SAFE_FREE_MEMORY(sqlStatementString);

    *pSqlStatement = sqlStatement;
    *pInputAttributes = inputAttributes;
    *pInputAttributesCount = inputAttributesCount;

    return error;
}


DWORD
SamDbRemoveMembersInternal(
    PSAMDB_CONTEXT DbContext,
    PSID Sid,
    DWORD MembersCount,
    PSID* Members
    )
{
    return SamDbAddOrRemoveMembersInternal(
                   DbContext,
                   Sid,
                   MembersCount,
                   Members,
                   SamDbBuildDeleteMembershipStatement);
}


#define SAMDB_SQL_DELETE_MEMBERSHIP \
    "DELETE FROM " SAM_DB_MEMBERS_TABLE " WHERE "


static
DWORD
SamDbBuildDeleteMembershipStatement(
    PSAMDB_CONTEXT DbContext,
    LONG64 LocalGroupId,
    LONG64 MemberId,
    sqlite3_stmt** pSqlStatement,
    PSAMDB_ATTRIBUTE* pInputAttributes,
    PDWORD pInputAttributesCount
    )
{
    DWORD error = ERROR_SUCCESS;
    PSAMDB_ATTRIBUTE inputAttributes = NULL;
    DWORD inputAttributeIndex = 0;
    DWORD inputAttributesCount = 0;
    size_t sqlStatementSize = 64;
    size_t sqlStatementLength = 0;
    size_t maxSqlStatementSize = 4096;
    PSTR sqlStatementString = NULL;
    sqlite3_stmt* sqlStatement = NULL;
    PLONG64 pGroupRecordId = NULL;
    PLONG64 pMemberRecordId = NULL;

    enum _MEMBERSHIP_ATTR_INDEX {
        ATTR_IDX_GROUP_RECORD_ID = 0,
        ATTR_IDX_MEMBER_RECORD_ID
    };

    SAMDB_ATTRIBUTE membershipAttributes[] = {
        {
            SAMDB_ATTR_GROUP_RECORD_ID,
            SAMDB_ATTR_TYPE_INT64,
            NULL
        },
        {
            SAMDB_ATTR_MEMBER_RECORD_ID,
            SAMDB_ATTR_TYPE_INT64,
            NULL
        },
    };

    error = LwAllocateMemory(
                    sizeof(*pGroupRecordId),
                    OUT_PPVOID(&pGroupRecordId));
    BAIL_ON_SAMDB_ERROR(error);

    *pGroupRecordId = LocalGroupId;

    error = LwAllocateMemory(
                    sizeof(*pMemberRecordId),
                    OUT_PPVOID(&pMemberRecordId));
    BAIL_ON_SAMDB_ERROR(error);

    *pMemberRecordId = MemberId;

    error = LwAllocateMemory(
                    sizeof(membershipAttributes),
                    OUT_PPVOID(&inputAttributes));
    BAIL_ON_SAMDB_ERROR(error);

    inputAttributes[inputAttributeIndex] =
        membershipAttributes[ATTR_IDX_GROUP_RECORD_ID];
    inputAttributes[inputAttributeIndex].Value = pGroupRecordId;
    inputAttributes[inputAttributeIndex].FreeFn = SamDbAttributeFreePointer;
    inputAttributeIndex++;

    inputAttributes[inputAttributeIndex] =
        membershipAttributes[ATTR_IDX_MEMBER_RECORD_ID];
    inputAttributes[inputAttributeIndex].Value = pMemberRecordId;
    inputAttributes[inputAttributeIndex].FreeFn = SamDbAttributeFreePointer;
    inputAttributeIndex++;

    inputAttributesCount = inputAttributeIndex;

    //
    // Create SQL statement string (INSERT INTO samdbmembers ...)
    //
    error = LwAllocateMemory(
                    sizeof(sqlStatementString[0]) * sqlStatementSize,
                    OUT_PPVOID(&sqlStatementString));
    BAIL_ON_SAMDB_ERROR(error);

    error = SamDbPrintfString(
                    &sqlStatementString,
                    &sqlStatementSize,
                    &sqlStatementLength,
                    maxSqlStatementSize,
                    "%s%s = ?1 AND %s = ?2;",
                    SAMDB_SQL_DELETE_MEMBERSHIP,
                    inputAttributes[ATTR_IDX_GROUP_RECORD_ID].DbColumnName,
                    inputAttributes[ATTR_IDX_MEMBER_RECORD_ID].DbColumnName);
    BAIL_ON_SAMDB_ERROR(error);

    error = sqlite3_prepare_v2(
                      DbContext->DbHandle,
                      sqlStatementString,
                      -1,
                      &sqlStatement,
                      NULL);
    if (error)
    {
        SAMDB_LOG_DEBUG("Error when preparing SQL statement:\n%s",
                        sqlStatementString);
    }
    BAIL_ON_SAMDB_SQLITE_ERROR_DB(error, DbContext->DbHandle);

cleanup:
    if (error)
    {
        if (inputAttributes)
        {
            SamDbFreeAttributes(
                     inputAttributes,
                     inputAttributesCount);
            inputAttributes = NULL;
            inputAttributesCount = 0;
        }

        if (sqlStatement)
        {
            sqlite3_finalize(sqlStatement);
            sqlStatement = NULL;
        }
    }

    LW_SAFE_FREE_MEMORY(sqlStatementString);

    *pSqlStatement = sqlStatement;
    *pInputAttributes = inputAttributes;
    *pInputAttributesCount = inputAttributesCount;

    return error;
}


static
BOOLEAN
SamDbIsBuiltinMembership(
    PSID LocalGroupSid,
    PSID LocalDomainSid,
    PSID MemberSid
    )
{
    DWORD membershipIndex = 0;
    BOOLEAN inRwLock = FALSE;
    BOOLEAN isBuiltin = FALSE;

    SAMDB_SHARED_LOCK_RWLOCK(inRwLock, gSamDbGlobals.pRwLock);

    for (membershipIndex = 0;
         membershipIndex < pgSamDbGlobals->BuiltinMembershipsCount;
         membershipIndex++)
    {
        PSAMDB_GROUP_MEMBERSHIP membership =
            &pgSamDbGlobals->BuiltinMemberships[membershipIndex];
        DWORD memberRid =
            MemberSid->SubAuthority[MemberSid->SubAuthorityCount - 1];

        if (memberRid == membership->MemberRid &&
            RtlIsStrictPrefixSid(LocalDomainSid, MemberSid) &&
            RtlEqualSid(LocalGroupSid, membership->LocalGroupSid))
        {
            isBuiltin = TRUE;
            goto cleanup;
        }
    }

cleanup:
    SAMDB_UNLOCK_RWLOCK(inRwLock, gSamDbGlobals.pRwLock);

    return isBuiltin;
}


DWORD
SamDbQueryMemberOf(
    IN PSID Sid,
    OUT PDWORD pMemberOfCount,
    OUT PSID** pMemberOfSids
    )
{
    DWORD dwError = ERROR_SUCCESS;
    dwError = SamDbQueryMembership(
                     Sid,
                     pMemberOfCount,
                     pMemberOfSids,
                     SamDbBuildSelectMembershipsStatement);

    if (dwError == ERROR_FILE_NOT_FOUND)
    {
        dwError = ERROR_SUCCESS;
    }

    return dwError;
}


DWORD
SamDbQueryMembers(
    IN PSID Sid,
    OUT PDWORD pMembersCount,
    OUT PSID** pMembers
    )
{
    DWORD dwError = ERROR_SUCCESS;

    dwError = SamDbQueryMembership(
                     Sid,
                     pMembersCount,
                     pMembers,
                     SamDbBuildSelectMembersStatement);
    if (dwError == ERROR_FILE_NOT_FOUND)
    {
        dwError = ERROR_NO_SUCH_GROUP;
    }

    return dwError;
}


static
DWORD
SamDbQueryMembership(
    PSID Sid,
    PDWORD pCount,
    PSID** pSids,
    PFN_BUILD_QUERY_MEMBERSHIP_STATEMENT BuiltStatementFn
    )
{
    DWORD error = ERROR_SUCCESS;
    NTSTATUS ntStatus = STATUS_SUCCESS;
    DWORD sqliteStatus = 0;
    PSAMDB_CONTEXT dbContext = NULL;
    LONG64 memberRecordId = 0;
    SAMDB_OBJECT_CLASS objectClass = SAMDB_OBJECT_CLASS_UNKNOWN;
    sqlite3_stmt* sqlStatement = NULL;
    PSAMDB_ATTRIBUTE inputAttributes = NULL;
    DWORD inputAttributesCount = 0;
    PSAMDB_ATTRIBUTE outputAttributes = NULL;
    DWORD outputAttributesCount = 0;
    PSID* objectSids = NULL;
    DWORD initialSidsSpace = 32;
    DWORD objectSidsSize = 0;
    DWORD objectSidIndex = 0;
    DWORD objectSidsCount = 0;
    CHAR objectSidAttrName[] = SAMDB_ATTR_OBJECT_SID;
    PSID sid = NULL;
    PSID objectSid = NULL;

    error = SamDbAcquireContext(
                  &dbContext);
    BAIL_ON_SAMDB_ERROR(error);

    error = SamDbFindObjectRecord(
                  dbContext,
                  Sid,
                  &memberRecordId,
                  &objectClass);
    BAIL_ON_SAMDB_ERROR_QUIET(error, error == ERROR_FILE_NOT_FOUND);

    error = BuiltStatementFn(
                  dbContext,
                  memberRecordId,
                  &sqlStatement,
                  &inputAttributes,
                  &inputAttributesCount,
                  &outputAttributes,
                  &outputAttributesCount);
    BAIL_ON_SAMDB_ERROR(error);

    error = SamDbBindAttributeValues(
                  dbContext,
                  sqlStatement,
                  inputAttributes,
                  inputAttributesCount);
    BAIL_ON_SAMDB_ERROR(error);

    error = sqlite3_step(sqlStatement);
    if (error == SQLITE_ROW ||
        error == SQLITE_DONE)
    {
        sqliteStatus = error;
        error = ERROR_SUCCESS;
    }
    else
    {
        SAMDB_LOG_ERROR("Error code %u returned from sqlite3_step() "
                        "while %u or %u was expected",
                        error,
                        SQLITE_ROW,
                        SQLITE_DONE);
        BAIL_ON_SAMDB_SQLITE_ERROR_STMT(error, sqlStatement);
    }

    objectSidsSize = sizeof(*objectSids[0]) * initialSidsSpace;
    error = LwAllocateMemory(
                     objectSidsSize,
                     OUT_PPVOID(&objectSids));
    BAIL_ON_SAMDB_ERROR(error);

    while (sqliteStatus == SQLITE_ROW)
    {
        error = SamDbGetOutputAttributeValues(
                     dbContext,
                     sqlStatement,
                     outputAttributes,
                     outputAttributesCount);
        BAIL_ON_SAMDB_ERROR(error);

        //
        // There's only one attribute we return here so let's just make
        // sure it's the one we're expecting
        //
        if (RtlCStringIsEqual(
                     outputAttributes[0].DbColumnName,
                     objectSidAttrName,
                     TRUE))
        {
            PWSTR* ppwszSid = (PWSTR*)outputAttributes[0].Value;
            DWORD objectSidLength = 0;

            ntStatus = RtlAllocateSidFromWC16String(
                                   &sid,
                                   *ppwszSid);
            BAIL_ON_NT_STATUS(ntStatus);

            outputAttributes[0].FreeFn(outputAttributes[0].Value);
            outputAttributes[0].Value = NULL;
            outputAttributes[0].FreeFn = NULL;

            if ((objectSidIndex + 1) * sizeof(*objectSids) > objectSidsSize)
            {
                PSID* newObjectSids = NULL;

                objectSidsSize *= 2;

                error = LwReallocMemory(
                                   objectSids,
                                   OUT_PPVOID(&newObjectSids),
                                   objectSidsSize);
                BAIL_ON_SAMDB_ERROR(error);

                objectSids = newObjectSids;
                newObjectSids = NULL;
            }

            objectSidLength = RtlLengthSid(sid);
            error = LwAllocateMemory(
                               objectSidLength,
                               OUT_PPVOID(&objectSid));
            BAIL_ON_SAMDB_ERROR(error);

            ntStatus = RtlCopySid(
                               objectSidLength,
                               objectSid,
                               sid);
            BAIL_ON_NT_STATUS(ntStatus);

            objectSids[objectSidIndex++] = objectSid;
            objectSid = NULL;

            RTL_FREE(&sid);
        }

        error = sqlite3_step(sqlStatement);
        if (error == SQLITE_ROW ||
            error == SQLITE_DONE)
        {
            sqliteStatus = error;
            error = ERROR_SUCCESS;
        }
        else
        {
            SAMDB_LOG_ERROR("Error code %u returned from sqlite3_step() "
                            "while %u or %u was expected",
                            error,
                            SQLITE_ROW,
                            SQLITE_DONE);
            BAIL_ON_SAMDB_SQLITE_ERROR_STMT(error, sqlStatement);
        }
    }

    objectSidsCount = objectSidIndex;

cleanup:
error:
    if (error || ntStatus)
    {
        for (objectSidIndex = 0;
             objectSidIndex < objectSidsCount;
             objectSids++)
        {
            LW_SAFE_FREE_MEMORY(objectSids[objectSidIndex]);
        }

        LW_SAFE_FREE_MEMORY(objectSids);
    }

    if (sqlStatement)
    {
        sqlite3_finalize(sqlStatement);
    }

    if (dbContext)
    {
        SamDbReleaseContext(dbContext);
    }

    if (inputAttributes)
    {
        SamDbFreeAttributes(
                    inputAttributes,
                    inputAttributesCount);
    }

    if (outputAttributes)
    {
        SamDbFreeAttributes(
                    outputAttributes,
                    outputAttributesCount);
    }

    RTL_FREE(&sid);

    if (error == ERROR_SUCCESS &&
        ntStatus != STATUS_SUCCESS)
    {
        error = LwNtStatusToWin32Error(ntStatus);
    }

    *pCount = objectSidsCount;
    *pSids = objectSids;

    return error;
}


#define SAMDB_SQL_SELECT_MEMBERSHIP_PREFIX \
    "SELECT "

#define SAMDB_SQL_SELECT_MEMBERSHIP_WHERE \
    " FROM " SAM_DB_MEMBERS_TABLE " INNER JOIN " \
    SAM_DB_OBJECTS_TABLE " ON (" SAM_DB_COL_GROUP_RECORD_ID " = " \
    SAM_DB_COL_RECORD_ID ") WHERE "


static
DWORD
SamDbBuildSelectMembershipsStatement(
    PSAMDB_CONTEXT DbContext,
    LONG64 RecordId,
    sqlite3_stmt** pSqlStatement,
    PSAMDB_ATTRIBUTE* pInputAttributes,
    PDWORD pInputAttributesCount,
    PSAMDB_ATTRIBUTE* pOutputAttributes,
    PDWORD pOutputAttributesCount
    )
{
    DWORD error = ERROR_SUCCESS;
    DWORD inputAttributesCount = 0;
    PSAMDB_ATTRIBUTE inputAttributes = NULL;
    DWORD inputAttributeIndex = 0;
    DWORD outputAttributesCount = 0;
    PSAMDB_ATTRIBUTE outputAttributes = NULL;
    DWORD outputAttributeIndex = 0;
    size_t sqlStatementSize = 512;
    size_t sqlStatementLength = 0;
    size_t maxSqlStatementSize = 8192;
    PSTR sqlStatementString = NULL;
    sqlite3_stmt* sqlStatement = NULL;
    PLONG64 pRecordId = NULL;

    typedef enum _INPUT_ATTRIBUTE_INDEX {
        ATTR_IDX_MEMBER_RECORD_ID = 0,
    } INPUT_ATTRIBUTE_INDEX;

    SAMDB_ATTRIBUTE inputAttrDef[] = {
        {
            SAMDB_ATTR_MEMBER_RECORD_ID,
            SAMDB_ATTR_TYPE_INT64,
            NULL
        },
    };

    SAMDB_ATTRIBUTE outputAttrDef[] = {
        {
            SAMDB_ATTR_OBJECT_SID,
            SAMDB_ATTR_TYPE_TEXT,
            NULL
        },
    };

    error = LwAllocateMemory(
                    sizeof(outputAttrDef),
                    OUT_PPVOID(&outputAttributes));
    BAIL_ON_SAMDB_ERROR(error);

    outputAttributesCount = sizeof(outputAttrDef)/sizeof(outputAttrDef[0]);

    error = LwAllocateMemory(
                    sizeof(*pRecordId),
                    OUT_PPVOID(&pRecordId));
    BAIL_ON_SAMDB_ERROR(error);

    *pRecordId = RecordId;

    error = LwAllocateMemory(
                    sizeof(inputAttrDef),
                    OUT_PPVOID(&inputAttributes));
    BAIL_ON_SAMDB_ERROR(error);

    inputAttributes[inputAttributeIndex] = inputAttrDef[ATTR_IDX_MEMBER_RECORD_ID];

    inputAttributes[inputAttributeIndex].Value = pRecordId;
    inputAttributes[inputAttributeIndex].FreeFn = SamDbAttributeFreePointer;

    //
    // Create SQL statement string (SELECT ... )
    //
    error = LwAllocateMemory(
                    sizeof(sqlStatementString[0]) * sqlStatementSize,
                    OUT_PPVOID(&sqlStatementString));
    BAIL_ON_SAMDB_ERROR(error);

    error = SamDbPrintfString(
                    &sqlStatementString,
                    &sqlStatementSize,
                    &sqlStatementLength,
                    maxSqlStatementSize,
                    "%s",
                    SAMDB_SQL_SELECT_MEMBERSHIP_PREFIX);
    BAIL_ON_SAMDB_ERROR(error);

    for (outputAttributeIndex = 0;
         outputAttributeIndex < outputAttributesCount;
         outputAttributeIndex++)
    {
        //
        // Append SQL column names separated with ","
        //

        outputAttributes[outputAttributeIndex] =
            outputAttrDef[outputAttributeIndex];

        error = SamDbPrintfString(
                      &sqlStatementString,
                      &sqlStatementSize,
                      &sqlStatementLength,
                      maxSqlStatementSize,
                      (outputAttributeIndex < outputAttributesCount - 1) ? "%s," : "%s",
                      outputAttributes[outputAttributeIndex].DbColumnName);
        BAIL_ON_SAMDB_ERROR(error);
    }

    //
    // Append SQL statement (FROM samdbobjects WHERE ...)
    //
    error = SamDbPrintfString(
                    &sqlStatementString,
                    &sqlStatementSize,
                    &sqlStatementLength,
                    maxSqlStatementSize,
                    "%s%s = ?%u;",
                    SAMDB_SQL_SELECT_MEMBERSHIP_WHERE,
                    inputAttributes[inputAttributeIndex].DbColumnName,
                    inputAttributeIndex + 1);
    BAIL_ON_SAMDB_ERROR(error);

    inputAttributeIndex++;
    inputAttributesCount = inputAttributeIndex;

    error = sqlite3_prepare_v2(
                      DbContext->DbHandle,
                      sqlStatementString,
                      -1,
                      &sqlStatement,
                      NULL);
    if (error)
    {
        SAMDB_LOG_DEBUG("Error when preparing SQL statement:\n%s",
                        sqlStatementString);
    }
    BAIL_ON_SAMDB_SQLITE_ERROR_DB(error, DbContext->DbHandle);

cleanup:
    if (error)
    {
        if (inputAttributes)
        {
            SamDbFreeAttributes(
                     inputAttributes,
                     inputAttributesCount);
            inputAttributes = NULL;
            inputAttributesCount = 0;
        }

        if (outputAttributes)
        {
            SamDbFreeAttributes(
                     outputAttributes,
                     outputAttributesCount);
            outputAttributes = NULL;
            outputAttributesCount = 0;
        }

        if (sqlStatement)
        {
            sqlite3_finalize(sqlStatement);
            sqlStatement = NULL;
        }
    }

    LW_SAFE_FREE_MEMORY(sqlStatementString);

    *pSqlStatement = sqlStatement;
    *pInputAttributes = inputAttributes;
    *pInputAttributesCount = inputAttributesCount;
    *pOutputAttributes = outputAttributes;
    *pOutputAttributesCount = outputAttributesCount;

    return error;
}


#define SAMDB_SQL_SELECT_MEMBERS_PREFIX \
    "SELECT "

#define SAMDB_SQL_SELECT_MEMBERS_WHERE \
    " FROM " SAM_DB_MEMBERS_TABLE " INNER JOIN " \
    SAM_DB_OBJECTS_TABLE " ON (" SAM_DB_COL_MEMBER_RECORD_ID " = " \
    SAM_DB_COL_RECORD_ID ") WHERE "


static
DWORD
SamDbBuildSelectMembersStatement(
    PSAMDB_CONTEXT DbContext,
    LONG64 RecordId,
    sqlite3_stmt** pSqlStatement,
    PSAMDB_ATTRIBUTE* pInputAttributes,
    PDWORD pInputAttributesCount,
    PSAMDB_ATTRIBUTE* pOutputAttributes,
    PDWORD pOutputAttributesCount
    )
{
    DWORD error = ERROR_SUCCESS;
    DWORD inputAttributesCount = 0;
    PSAMDB_ATTRIBUTE inputAttributes = NULL;
    DWORD inputAttributeIndex = 0;
    DWORD outputAttributesCount = 0;
    PSAMDB_ATTRIBUTE outputAttributes = NULL;
    DWORD outputAttributeIndex = 0;
    size_t sqlStatementSize = 512;
    size_t sqlStatementLength = 0;
    size_t maxSqlStatementSize = 8192;
    PSTR sqlStatementString = NULL;
    sqlite3_stmt* sqlStatement = NULL;
    PLONG64 pRecordId = NULL;

    typedef enum _INPUT_ATTRIBUTE_INDEX {
        ATTR_IDX_GROUP_RECORD_ID = 0,
    } INPUT_ATTRIBUTE_INDEX;

    SAMDB_ATTRIBUTE inputAttrDef[] = {
        {
            SAMDB_ATTR_GROUP_RECORD_ID,
            SAMDB_ATTR_TYPE_INT64,
            NULL
        },
    };

    SAMDB_ATTRIBUTE outputAttrDef[] = {
        {
            SAMDB_ATTR_OBJECT_SID,
            SAMDB_ATTR_TYPE_TEXT,
            NULL
        },
    };

    error = LwAllocateMemory(
                    sizeof(outputAttrDef),
                    OUT_PPVOID(&outputAttributes));
    BAIL_ON_SAMDB_ERROR(error);

    outputAttributesCount = sizeof(outputAttrDef)/sizeof(outputAttrDef[0]);

    error = LwAllocateMemory(
                    sizeof(*pRecordId),
                    OUT_PPVOID(&pRecordId));
    BAIL_ON_SAMDB_ERROR(error);

    *pRecordId = RecordId;

    error = LwAllocateMemory(
                    sizeof(inputAttrDef),
                    OUT_PPVOID(&inputAttributes));
    BAIL_ON_SAMDB_ERROR(error);

    inputAttributes[inputAttributeIndex] = inputAttrDef[ATTR_IDX_GROUP_RECORD_ID];

    inputAttributes[inputAttributeIndex].Value = pRecordId;
    inputAttributes[inputAttributeIndex].FreeFn = SamDbAttributeFreePointer;

    //
    // Create SQL statement string (SELECT ... )
    //
    error = LwAllocateMemory(
                    sizeof(sqlStatementString[0]) * sqlStatementSize,
                    OUT_PPVOID(&sqlStatementString));
    BAIL_ON_SAMDB_ERROR(error);

    error = SamDbPrintfString(
                    &sqlStatementString,
                    &sqlStatementSize,
                    &sqlStatementLength,
                    maxSqlStatementSize,
                    "%s",
                    SAMDB_SQL_SELECT_MEMBERS_PREFIX);
    BAIL_ON_SAMDB_ERROR(error);

    for (outputAttributeIndex = 0;
         outputAttributeIndex < outputAttributesCount;
         outputAttributeIndex++)
    {
        //
        // Append SQL column names separated with ","
        //

        outputAttributes[outputAttributeIndex] =
            outputAttrDef[outputAttributeIndex];

        error = SamDbPrintfString(
                      &sqlStatementString,
                      &sqlStatementSize,
                      &sqlStatementLength,
                      maxSqlStatementSize,
                      (outputAttributeIndex < outputAttributesCount - 1) ? "%s," : "%s",
                      outputAttributes[outputAttributeIndex].DbColumnName);
        BAIL_ON_SAMDB_ERROR(error);
    }

    //
    // Append SQL statement (FROM samdbobjects WHERE ...)
    //
    error = SamDbPrintfString(
                    &sqlStatementString,
                    &sqlStatementSize,
                    &sqlStatementLength,
                    maxSqlStatementSize,
                    "%s%s = ?%u;",
                    SAMDB_SQL_SELECT_MEMBERS_WHERE,
                    inputAttributes[inputAttributeIndex].DbColumnName,
                    inputAttributeIndex + 1);
    BAIL_ON_SAMDB_ERROR(error);

    inputAttributeIndex++;
    inputAttributesCount = inputAttributeIndex;

    error = sqlite3_prepare_v2(
                      DbContext->DbHandle,
                      sqlStatementString,
                      -1,
                      &sqlStatement,
                      NULL);
    if (error)
    {
        SAMDB_LOG_DEBUG("Error when preparing SQL statement:\n%s",
                        sqlStatementString);
    }
    BAIL_ON_SAMDB_SQLITE_ERROR_DB(error, DbContext->DbHandle);

cleanup:
    if (error)
    {
        if (inputAttributes)
        {
            SamDbFreeAttributes(
                     inputAttributes,
                     inputAttributesCount);
            inputAttributes = NULL;
            inputAttributesCount = 0;
        }

        if (outputAttributes)
        {
            SamDbFreeAttributes(
                     outputAttributes,
                     outputAttributesCount);
            outputAttributes = NULL;
            outputAttributesCount = 0;
        }

        if (sqlStatement)
        {
            sqlite3_finalize(sqlStatement);
            sqlStatement = NULL;
        }
    }

    LW_SAFE_FREE_MEMORY(sqlStatementString);

    *pSqlStatement = sqlStatement;
    *pInputAttributes = inputAttributes;
    *pInputAttributesCount = inputAttributesCount;
    *pOutputAttributes = outputAttributes;
    *pOutputAttributesCount = outputAttributesCount;

    return error;
}
