/*
   Unix SMB/CIFS implementation.
   async lookupgroupmembers
   Copyright (C) Volker Lendecke 2009

   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 3 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/>.
*/

#include "includes.h"
#include "winbindd.h"
#include "librpc/gen_ndr/cli_wbint.h"

/*
 * We have 3 sets of routines here:
 *
 * wb_lookupgroupmem is the low-level one-group routine
 *
 * wb_groups_members walks a list of groups
 *
 * wb_group_members finally is the high-level routine expanding groups
 * recursively
 */

/*
 * TODO: fill_grent_mem_domusers must be re-added
 */

/*
 * Look up members of a single group. Essentially a wrapper around the
 * lookup_groupmem winbindd_methods routine.
 */

struct wb_lookupgroupmem_state {
	struct dom_sid sid;
	struct wbint_Principals members;
};

static void wb_lookupgroupmem_done(struct tevent_req *subreq);

static struct tevent_req *wb_lookupgroupmem_send(TALLOC_CTX *mem_ctx,
						 struct tevent_context *ev,
						 const struct dom_sid *group_sid,
						 enum lsa_SidType type)
{
	struct tevent_req *req, *subreq;
	struct wb_lookupgroupmem_state *state;
	struct winbindd_domain *domain;

	req = tevent_req_create(mem_ctx, &state,
				struct wb_lookupgroupmem_state);
	if (req == NULL) {
		return NULL;
	}
	sid_copy(&state->sid, group_sid);

	domain = find_domain_from_sid_noinit(group_sid);
	if (domain == NULL) {
		tevent_req_nterror(req, NT_STATUS_NO_SUCH_GROUP);
		return tevent_req_post(req, ev);
	}

	subreq = rpccli_wbint_LookupGroupMembers_send(
		state, ev, domain->child.rpccli, &state->sid, type,
		&state->members);
	if (tevent_req_nomem(subreq, req)) {
		return tevent_req_post(req, ev);
	}
	tevent_req_set_callback(subreq, wb_lookupgroupmem_done, req);
	return req;
}

static void wb_lookupgroupmem_done(struct tevent_req *subreq)
{
	struct tevent_req *req = tevent_req_callback_data(
		subreq, struct tevent_req);
	struct wb_lookupgroupmem_state *state = tevent_req_data(
		req, struct wb_lookupgroupmem_state);
	NTSTATUS status, result;

	status = rpccli_wbint_LookupGroupMembers_recv(subreq, state, &result);
	TALLOC_FREE(subreq);
	if (!NT_STATUS_IS_OK(status)) {
		tevent_req_nterror(req, status);
		return;
	}
	if (!NT_STATUS_IS_OK(result)) {
		tevent_req_nterror(req, result);
		return;
	}
	tevent_req_done(req);
}

static NTSTATUS wb_lookupgroupmem_recv(struct tevent_req *req,
					   TALLOC_CTX *mem_ctx,
					   int *num_members,
					   struct wbint_Principal **members)
{
	struct wb_lookupgroupmem_state *state = tevent_req_data(
		req, struct wb_lookupgroupmem_state);
	NTSTATUS status;

	if (tevent_req_is_nterror(req, &status)) {
		return status;
	}

	*num_members = state->members.num_principals;
	*members = talloc_move(mem_ctx, &state->members.principals);
	return NT_STATUS_OK;
}

/*
 * Same as wb_lookupgroupmem for a list of groups
 */

struct wb_groups_members_state {
	struct tevent_context *ev;
	struct wbint_Principal *groups;
	int num_groups;
	int next_group;
	struct wbint_Principal *all_members;
};

static NTSTATUS wb_groups_members_next_subreq(
	struct wb_groups_members_state *state,
	TALLOC_CTX *mem_ctx, struct tevent_req **psubreq);
static void wb_groups_members_done(struct tevent_req *subreq);

static struct tevent_req *wb_groups_members_send(TALLOC_CTX *mem_ctx,
						 struct tevent_context *ev,
						 int num_groups,
						 struct wbint_Principal *groups)
{
	struct tevent_req *req, *subreq;
	struct wb_groups_members_state *state;
	NTSTATUS status;

	req = tevent_req_create(mem_ctx, &state,
				struct wb_groups_members_state);
	if (req == NULL) {
		return NULL;
	}
	state->ev = ev;
	state->groups = groups;
	state->num_groups = num_groups;
	state->next_group = 0;
	state->all_members = NULL;

	status = wb_groups_members_next_subreq(state, state, &subreq);
	if (!NT_STATUS_IS_OK(status)) {
		tevent_req_nterror(req, status);
		return tevent_req_post(req, ev);
	}
	if (subreq == NULL) {
		tevent_req_done(req);
		return tevent_req_post(req, ev);
	}
	tevent_req_set_callback(subreq, wb_groups_members_done, req);
	return req;
}

static NTSTATUS wb_groups_members_next_subreq(
	struct wb_groups_members_state *state,
	TALLOC_CTX *mem_ctx, struct tevent_req **psubreq)
{
	struct tevent_req *subreq;
	struct wbint_Principal *g;

	if (state->next_group >= state->num_groups) {
		*psubreq = NULL;
		return NT_STATUS_OK;
	}

	g = &state->groups[state->next_group];
	state->next_group += 1;

	subreq = wb_lookupgroupmem_send(mem_ctx, state->ev, &g->sid, g->type);
	if (subreq == NULL) {
		return NT_STATUS_NO_MEMORY;
	}
	*psubreq = subreq;
	return NT_STATUS_OK;
}

static void wb_groups_members_done(struct tevent_req *subreq)
{
	struct tevent_req *req = tevent_req_callback_data(
		subreq, struct tevent_req);
	struct wb_groups_members_state *state = tevent_req_data(
		req, struct wb_groups_members_state);
	int i, num_all_members;
	int num_members = 0;
	struct wbint_Principal *members = NULL;
	NTSTATUS status;

	status = wb_lookupgroupmem_recv(subreq, state, &num_members,
					    &members);
	TALLOC_FREE(subreq);

	/*
	 * In this error handling here we might have to be a bit more generous
	 * and just continue if an error occured.
	 */

	if (!NT_STATUS_IS_OK(status)) {
		tevent_req_nterror(req, status);
		return;
	}

	num_all_members = talloc_array_length(state->all_members);

	state->all_members = talloc_realloc(
		state, state->all_members, struct wbint_Principal,
		num_all_members + num_members);
	if ((num_all_members + num_members != 0)
	    && tevent_req_nomem(state->all_members, req)) {
		return;
	}
	for (i=0; i<num_members; i++) {
		struct wbint_Principal *src, *dst;
		src = &members[i];
		dst = &state->all_members[num_all_members + i];
		sid_copy(&dst->sid, &src->sid);
		dst->name = talloc_move(state->all_members, &src->name);
		dst->type = src->type;
	}
	TALLOC_FREE(members);

	status = wb_groups_members_next_subreq(state, state, &subreq);
	if (!NT_STATUS_IS_OK(status)) {
		tevent_req_nterror(req, status);
		return;
	}
	if (subreq == NULL) {
		tevent_req_done(req);
		return;
	}
	tevent_req_set_callback(subreq, wb_groups_members_done, req);
}

static NTSTATUS wb_groups_members_recv(struct tevent_req *req,
				       TALLOC_CTX *mem_ctx,
				       int *num_members,
				       struct wbint_Principal **members)
{
	struct wb_groups_members_state *state = tevent_req_data(
		req, struct wb_groups_members_state);
	NTSTATUS status;

	if (tevent_req_is_nterror(req, &status)) {
		return status;
	}
	*num_members = talloc_array_length(state->all_members);
	*members = talloc_move(mem_ctx, &state->all_members);
	return NT_STATUS_OK;
}


/*
 * This is the routine expanding a list of groups up to a certain level. We
 * collect the users in a talloc_dict: We have to add them without duplicates,
 * and and talloc_dict is an indexed (here indexed by SID) data structure.
 */

struct wb_group_members_state {
	struct tevent_context *ev;
	int depth;
	struct talloc_dict *users;
	struct wbint_Principal *groups;
};

static NTSTATUS wb_group_members_next_subreq(
	struct wb_group_members_state *state,
	TALLOC_CTX *mem_ctx, struct tevent_req **psubreq);
static void wb_group_members_done(struct tevent_req *subreq);

struct tevent_req *wb_group_members_send(TALLOC_CTX *mem_ctx,
					 struct tevent_context *ev,
					 const struct dom_sid *sid,
					 enum lsa_SidType type,
					 int max_depth)
{
	struct tevent_req *req, *subreq;
	struct wb_group_members_state *state;
	NTSTATUS status;

	req = tevent_req_create(mem_ctx, &state,
				struct wb_group_members_state);
	if (req == NULL) {
		return NULL;
	}
	state->ev = ev;
	state->depth = max_depth;
	state->users = talloc_dict_init(state);
	if (tevent_req_nomem(state->users, req)) {
		return tevent_req_post(req, ev);
	}

	state->groups = talloc(state, struct wbint_Principal);
	if (tevent_req_nomem(state->groups, req)) {
		return tevent_req_post(req, ev);
	}
	state->groups->name = NULL;
	sid_copy(&state->groups->sid, sid);
	state->groups->type = type;

	status = wb_group_members_next_subreq(state, state, &subreq);
	if (!NT_STATUS_IS_OK(status)) {
		tevent_req_nterror(req, status);
		return tevent_req_post(req, ev);
	}
	if (subreq == NULL) {
		tevent_req_done(req);
		return tevent_req_post(req, ev);
	}
	tevent_req_set_callback(subreq, wb_group_members_done, req);
	return req;
}

static NTSTATUS wb_group_members_next_subreq(
	struct wb_group_members_state *state,
	TALLOC_CTX *mem_ctx, struct tevent_req **psubreq)
{
	struct tevent_req *subreq;

	if ((talloc_array_length(state->groups) == 0)
	    || (state->depth <= 0)) {
		*psubreq = NULL;
		return NT_STATUS_OK;
	}
	state->depth -= 1;

	subreq = wb_groups_members_send(
		mem_ctx, state->ev, talloc_array_length(state->groups),
		state->groups);
	if (subreq == NULL) {
		return NT_STATUS_NO_MEMORY;
	}
	*psubreq = subreq;
	return NT_STATUS_OK;
}

static void wb_group_members_done(struct tevent_req *subreq)
{
	struct tevent_req *req = tevent_req_callback_data(
		subreq, struct tevent_req);
	struct wb_group_members_state *state = tevent_req_data(
		req, struct wb_group_members_state);
	int i, num_groups, new_users, new_groups;
	int num_members = 0;
	struct wbint_Principal *members = NULL;
	NTSTATUS status;

	status = wb_groups_members_recv(subreq, state, &num_members, &members);
	TALLOC_FREE(subreq);
	if (!NT_STATUS_IS_OK(status)) {
		tevent_req_nterror(req, status);
		return;
	}

	new_users = new_groups = 0;
	for (i=0; i<num_members; i++) {
		switch (members[i].type) {
		case SID_NAME_DOM_GRP:
		case SID_NAME_ALIAS:
		case SID_NAME_WKN_GRP:
			new_groups += 1;
			break;
		default:
			/* Ignore everything else */
			break;
		}
	}

	num_groups = 0;
	TALLOC_FREE(state->groups);
	state->groups = talloc_array(state, struct wbint_Principal,
				     new_groups);

	/*
	 * Collect the users into state->users and the groups into
	 * state->groups for the next iteration.
	 */

	for (i=0; i<num_members; i++) {
		switch (members[i].type) {
		case SID_NAME_USER:
		case SID_NAME_COMPUTER: {
			/*
			 * Add a copy of members[i] to state->users
			 */
			struct wbint_Principal *m;
			struct dom_sid *sid;
			DATA_BLOB key;

			m = talloc(talloc_tos(), struct wbint_Principal);
			if (tevent_req_nomem(m, req)) {
				return;
			}
			sid_copy(&m->sid, &members[i].sid);
			m->name = talloc_move(m, &members[i].name);
			m->type = members[i].type;

			sid = &members[i].sid;
			key = data_blob_const(
				sid, ndr_size_dom_sid(sid, NULL, 0));

			if (!talloc_dict_set(state->users, key, &m)) {
				tevent_req_nterror(req, NT_STATUS_NO_MEMORY);
				return;
			}
			break;
		}
		case SID_NAME_DOM_GRP:
		case SID_NAME_ALIAS:
		case SID_NAME_WKN_GRP: {
			struct wbint_Principal *g;
			/*
			 * Save members[i] for the next round
			 */
			g = &state->groups[num_groups];
			sid_copy(&g->sid, &members[i].sid);
			g->name = talloc_move(state->groups, &members[i].name);
			g->type = members[i].type;
			num_groups += 1;
			break;
		}
		default:
			/* Ignore everything else */
			break;
		}
	}

	status = wb_group_members_next_subreq(state, state, &subreq);
	if (!NT_STATUS_IS_OK(status)) {
		tevent_req_nterror(req, status);
		return;
	}
	if (subreq == NULL) {
		tevent_req_done(req);
		return;
	}
	tevent_req_set_callback(subreq, wb_group_members_done, req);
}

NTSTATUS wb_group_members_recv(struct tevent_req *req, TALLOC_CTX *mem_ctx,
			       struct talloc_dict **members)
{
	struct wb_group_members_state *state = tevent_req_data(
		req, struct wb_group_members_state);
	NTSTATUS status;

	if (tevent_req_is_nterror(req, &status)) {
		return status;
	}
	*members = talloc_move(mem_ctx, &state->users);
	return NT_STATUS_OK;
}