/* 
   Unix SMB/Netbios implementation.
   Version 2.0

   Winbind daemon for ntdom nss module

   Copyright (C) Tim Potter 2000
   
   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, write to the Free Software
   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

#include "winbindd.h"

/* Fill a grent structure from various other information */

static void winbindd_fill_grent(struct winbindd_gr *gr, char *gr_name,
                                gid_t unix_gid)
{
    /* Fill in uid/gid */

    gr->gr_gid = unix_gid;

    /* Group name and password */

    safe_strcpy(gr->gr_name, gr_name, sizeof(gr->gr_name) - 1);
    safe_strcpy(gr->gr_passwd, "x", sizeof(gr->gr_passwd) - 1);
}

/* Fill in group membership */

struct grent_mem_group {
    uint32 rid;
    enum SID_NAME_USE name_type;
    fstring domain_name;
    struct winbindd_domain *domain;
    struct grent_mem_group *prev, *next;
};

struct grent_mem_list {
    fstring name;
    struct grent_mem_list *prev, *next;
};

/* Name comparison function for qsort() */

static int name_comp(struct grent_mem_list *n1, struct grent_mem_list *n2)
{
    /* Silly cases */

    if (!n1 && !n2) return 0;
    if (!n1) return -1;
    if (!n2) return 1;

    return strcmp(n1->name, n2->name);
}

static struct grent_mem_list *sort_groupmem_list(struct grent_mem_list *list,
                                                 int num_gr_mem)
{
    struct grent_mem_list *groupmem_array, *temp;
    int i;

    /* Allocate and zero an array to hold sorted entries */

    if ((groupmem_array = malloc(num_gr_mem * 
                                 sizeof(struct grent_mem_list))) == NULL) {
        return NULL;
    }

    memset((char *)groupmem_array, 0, num_gr_mem * 
           sizeof(struct grent_mem_list));

    /* Copy list to array */

    for(temp = list, i = 0; temp && i < num_gr_mem; temp = temp->next, i++) {
        fstrcpy(groupmem_array[i].name, temp->name);
    }

    /* Sort array */

    qsort(groupmem_array, num_gr_mem, sizeof(struct grent_mem_list), 
          name_comp);

    /* Fix up resulting array to a linked list and return it */

    for(i = 0; i < num_gr_mem; i++) {

        /* Fix up previous link */

        if (i != 0) {
            groupmem_array[i].prev = &groupmem_array[i - 1];
        }

        /* Fix up next link */

        if (i != (num_gr_mem - 1)) {
            groupmem_array[i].next = &groupmem_array[i + 1];
        }
    }

    return groupmem_array;
}

static BOOL winbindd_fill_grent_mem(struct winbindd_domain *domain,
                                    uint32 group_rid, 
                                    enum SID_NAME_USE group_name_type, 
                                    struct winbindd_response *response)
{
    struct grent_mem_group *done_groups = NULL, *todo_groups = NULL;
    struct grent_mem_group *temp_group;
    struct grent_mem_list *groupmem_list = NULL;
    struct winbindd_gr *gr;

    if (response) {
        gr = &response->data.gr;
    } else {
        return False;
    }
    
    /* Initialise group membership information */

    gr->num_gr_mem = 0;

    /* Add first group to todo_groups list */

    if ((temp_group = 
         (struct grent_mem_group *)malloc(sizeof(*temp_group))) == NULL) {
        return False;
    }

    ZERO_STRUCTP(temp_group);

    temp_group->rid = group_rid;
    temp_group->name_type = group_name_type;
    temp_group->domain = domain;
    fstrcpy(temp_group->domain_name, domain->name);

    DLIST_ADD(todo_groups, temp_group);
            
    /* Iterate over all groups to find members of */

    while(todo_groups != NULL) {
        struct grent_mem_group *current_group = todo_groups;
        uint32 num_names = 0, *rid_mem = NULL;
        enum SID_NAME_USE *name_types = NULL;

        DOM_SID **sids = NULL;
        char **names = NULL;
        BOOL done_group;
        int i;

        /* Check we haven't looked up this group before */

        done_group = 0;

        for (temp_group = done_groups; temp_group != NULL; 
             temp_group = temp_group->next) {

            if ((temp_group->rid == current_group->rid) &&
                (strcmp(temp_group->domain_name, 
                        current_group->domain_name) == 0)) {
                
                done_group = 1;
            }
        }

        if (done_group) goto cleanup;

        /* Lookup group membership for the current group */

        if (current_group->name_type == SID_NAME_DOM_GRP) {

            if (!winbindd_lookup_groupmem(current_group->domain, 
                                          current_group->rid, &num_names, 
                                          &rid_mem, &names, &name_types)) {

                DEBUG(1, ("fill_grent_mem(): could not lookup membership "
                          "for group rid %d in domain %s\n", 
                          current_group->rid,
                          current_group->domain->name));

                /* Exit if we cannot lookup the membership for the group
                   this function was called to look at */

                if (current_group->rid == group_rid) {
                    return False;
                } else {
                    goto cleanup;
                }
            }
        }

        if (current_group->name_type == SID_NAME_ALIAS) {

            if (!winbindd_lookup_aliasmem(current_group->domain, 
                                          current_group->rid, &num_names, 
                                          &sids, &names, &name_types)) {

                DEBUG(1, ("fill_grent_mem(): group rid %d not a local group\n",
                          group_rid));

                /* Exit if we cannot lookup the membership for the group
                   this function was called to look at */

                if (current_group->rid == group_rid) {
                    return False;
                } else {
                    goto cleanup;
                }
            }
        }

        /* Now for each member of the group, add it to the group list if it
           is a user, otherwise push it onto the todo_group list if it is a
           group or an alias. */
    
        for (i = 0; i < num_names; i++) {
            enum SID_NAME_USE name_type;
            fstring name_part1, name_part2;
            char *name_dom, *name_user, *the_name;
            struct winbindd_domain *name_domain;
        
            /* Lookup name */

            ZERO_STRUCT(name_part1);
            ZERO_STRUCT(name_part2);

            the_name = names[i];
	    parse_domain_user(the_name, name_part1, name_part2);

            if (strcmp(name_part1, "") != 0) {
                name_dom = name_part1;
                name_user = name_part2;

                if ((name_domain = find_domain_from_name(name_dom)) == NULL) {
                    DEBUG(0, ("unable to look up domain record for domain "
                              "%s\n", name_dom));
                    continue;
                }

            } else {
                name_dom = current_group->domain->name;
                name_user = name_part2;
                name_domain = current_group->domain;
            }

            if (winbindd_lookup_sid_by_name(name_domain, name_user, NULL, 
                                            &name_type) == WINBINDD_OK) {

                /* Check name type */

                if (name_type == SID_NAME_USER) {
                    struct grent_mem_list *entry;

                    /* Add to group membership list */
                
                    if ((entry = (struct grent_mem_list *)
                         malloc(sizeof(*entry))) != NULL) {

                        /* Create name */
			slprintf(entry->name, sizeof(entry->name),
				 "%s%s%s", name_dom, lp_winbind_separator(), name_user);
                        
                        /* Add to list */

                        DLIST_ADD(groupmem_list, entry);
                        gr->num_gr_mem++;
                    }

                } else {
                    struct grent_mem_group *todo_group;
                    DOM_SID todo_sid;
                    uint32 todo_rid;

                    /* Add group to todo list */

                    if (winbindd_lookup_sid_by_name(name_domain, names[i], 
                                                    &todo_sid, &name_type) 
                        == WINBINDD_OK) {

                        /* Fill in group entry */

                        sid_split_rid(&todo_sid, &todo_rid);

                        if ((todo_group = (struct grent_mem_group *)
                             malloc(sizeof(*todo_group))) != NULL) {
                            
                            ZERO_STRUCTP(todo_group);

                            todo_group->rid = todo_rid;
                            todo_group->name_type = name_type;
                            todo_group->domain = name_domain;

                            fstrcpy(todo_group->domain_name, name_dom);

                            DLIST_ADD(todo_groups, todo_group);
                        }
                    }
                }
            }
        }

    cleanup:
        
        /* Remove group from todo list and add to done_groups list */

        DLIST_REMOVE(todo_groups, current_group);
        DLIST_ADD(done_groups, current_group);

        /* Free memory allocated in winbindd_lookup_{alias,group}mem() */

        safe_free(name_types);
        safe_free(rid_mem);

        free_char_array(num_names, names);
        free_sid_array(num_names, sids);
    }
    
    /* Free done groups list */

    temp_group = done_groups;

    if (temp_group != NULL) {
        while (temp_group != NULL) {
            struct grent_mem_group *next;

            DLIST_REMOVE(done_groups, temp_group);
            next = temp_group->next;

            free(temp_group);
            temp_group = next;
        }
    }

    /* Remove duplicates from group member list. */

    if (gr->num_gr_mem > 0) {
        struct grent_mem_list *sorted_groupmem_list, *temp;
        int extra_data_len = 0;
        fstring prev_name;
        char *head;

        /* Sort list */

        sorted_groupmem_list = sort_groupmem_list(groupmem_list, 
                                                  gr->num_gr_mem);
        /* Remove duplicates by iteration */

        fstrcpy(prev_name, "");

        for(temp = sorted_groupmem_list; temp; temp = temp->next) {
            if (strequal(temp->name, prev_name)) {

                /* Got a duplicate name - delete it.  Don't panic as we're
                   only adjusting the prev and next pointers so memory
                   allocation is not messed up. */

                DLIST_REMOVE(sorted_groupmem_list, temp);
                gr->num_gr_mem--;

            } else {

                /* Got a unique name - count how long it is */

                extra_data_len += strlen(temp->name) + 1;
            }
        }

        extra_data_len++;       /* Don't forget null a terminator */

        /* Convert sorted list into extra data field to send back to ntdom
           client.  Add one to extra_data_len for null termination */

        if ((response->extra_data = malloc(extra_data_len))) {

            /* Initialise extra data */

            memset(response->extra_data, 0, extra_data_len);

            head = response->extra_data;

            /* Fill in extra data */

            for(temp = sorted_groupmem_list; temp; temp = temp->next) {
                int len = strlen(temp->name) + 1;
                
                safe_strcpy(head, temp->name, len);
                head[len - 1] = ',';
                head += len;
            }
            
            *head = '\0';

            /* Update response length */

            response->length = sizeof(struct winbindd_response) +
                extra_data_len;
        }

        /* Free memory for sorted_groupmem_list.  It was allocated as an
           array in sort_groupmem_list() so can be freed in one go. */

        free(sorted_groupmem_list);

        /* Free groupmem_list */

        temp = groupmem_list;

        while (temp != NULL) {
            struct grent_mem_list *next;
            
            DLIST_REMOVE(groupmem_list, temp);
            next = temp->next;
            
            free(temp);
            temp = next;
        }
    }

    return True;
}

/* Return a group structure from a group name */

enum winbindd_result winbindd_getgrnam_from_group(struct winbindd_cli_state *state)
{
    DOM_SID group_sid;
    struct winbindd_domain *domain;
    enum SID_NAME_USE name_type;
    uint32 group_rid;
    fstring name_domain, name_group, name;
    char *tmp;
    gid_t gid;
    int extra_data_len;

    /* Parse domain and groupname */

    memset(name_group, 0, sizeof(fstring));

    tmp = state->request.data.groupname;
    parse_domain_user(tmp, name_domain, name_group);

    /* Reject names that don't have a domain - i.e name_domain contains the
       entire name. */

    if (strequal(name_group, "")) {
        return WINBINDD_ERROR;
    }    

    /* Get info for the domain */

    if ((domain = find_domain_from_name(name_domain)) == NULL) {
        DEBUG(0, ("getgrname_from_group(): could not get domain sid for "
                  "domain %s\n", name_domain));
        return WINBINDD_ERROR;
    }

    /* Check for cached user entry */

    if (winbindd_fetch_group_cache_entry(name_domain, name_group,
					 &state->response.data.gr,
					 &state->response.extra_data,
					 &extra_data_len)) {
            state->response.length += extra_data_len;
            return WINBINDD_OK;
    }

    slprintf(name, sizeof(name), "%s\\%s", name_domain, name_group);

    /* Get rid and name type from name */
        
    if (!winbindd_lookup_sid_by_name(domain, name, &group_sid, 
                                     &name_type)) {
        DEBUG(1, ("group %s in domain %s does not exist\n", name_group,
                  name_domain));
        return WINBINDD_ERROR;
    }

    if ((name_type != SID_NAME_ALIAS) && (name_type != SID_NAME_DOM_GRP)) {
        DEBUG(1, ("from_group: name '%s' is not a local or domain group: %d\n",
                  name_group, name_type));
        return WINBINDD_ERROR;
    }

    /* Fill in group structure */

    sid_split_rid(&group_sid, &group_rid);

    if (!winbindd_idmap_get_gid_from_rid(domain->name, group_rid, &gid)) {
        DEBUG(1, ("error sursing unix gid for sid\n"));
        return WINBINDD_ERROR;
    }

    winbindd_fill_grent(&state->response.data.gr, 
                        state->request.data.groupname, gid);
        
    if (!winbindd_fill_grent_mem(domain, group_rid, name_type,
                                 &state->response)) {
        return WINBINDD_ERROR;
    }

    /* Update cached group info */

    winbindd_fill_group_cache_entry(name_domain, name_group, 
                                    &state->response.data.gr,
                                    state->response.extra_data,
                                    state->response.length - 
                                    sizeof(struct winbindd_response));

    return WINBINDD_OK;
}

/* Return a group structure from a gid number */

enum winbindd_result winbindd_getgrnam_from_gid(struct winbindd_cli_state 
                                                *state)
{
    struct winbindd_domain *domain;
    DOM_SID group_sid;
    enum SID_NAME_USE name_type;
    fstring group_name;
    uint32 group_rid;
    int extra_data_len;

    /* Get rid from gid */
    if (!winbindd_idmap_get_rid_from_gid(state->request.data.gid, &group_rid,
                                         &domain)) {
        DEBUG(1, ("Could not convert gid %d to rid\n", 
                  state->request.data.gid));
        return WINBINDD_ERROR;
    }

    /* try a cached entry */
    if (winbindd_fetch_gid_cache_entry(domain->name, state->request.data.gid,
				       &state->response.data.gr,
				       &state->response.extra_data,
				       &extra_data_len)) {
            state->response.length += extra_data_len;
            return WINBINDD_OK;
    }

    /* Get sid from gid */

    sid_copy(&group_sid, &domain->sid);
    sid_append_rid(&group_sid, group_rid);

    if (!winbindd_lookup_name_by_sid(domain, &group_sid, group_name, 
                                     &name_type)) {
        DEBUG(1, ("Could not lookup sid\n"));
        return WINBINDD_ERROR;
    }

    if (strcmp(lp_winbind_separator(),"\\")) {
	    string_sub(group_name, "\\", lp_winbind_separator(), sizeof(fstring));
    }

    if (!((name_type == SID_NAME_ALIAS) || (name_type == SID_NAME_DOM_GRP))) {
        DEBUG(1, ("from_gid: name '%s' is not a local or domain group: %d\n", 
                  group_name, name_type));
        return WINBINDD_ERROR;
    }

    /* Fill in group structure */

    winbindd_fill_grent(&state->response.data.gr, group_name, 
                        state->request.data.gid);

    if (!winbindd_fill_grent_mem(domain, group_rid, name_type,
                                 &state->response)) {
        return WINBINDD_ERROR;
    }

    /* Update cached group info */
    winbindd_fill_gid_cache_entry(domain->name, state->request.data.gid,
				  &state->response.data.gr,
				  state->response.extra_data,
				  state->response.length - 
				  sizeof(struct winbindd_response));

    return WINBINDD_OK;
}

/*
 * set/get/endgrent functions
 */

/* "Rewind" file pointer for group database enumeration */

enum winbindd_result winbindd_setgrent(struct winbindd_cli_state *state)
{
    struct winbindd_domain *tmp;

    if (state == NULL) return WINBINDD_ERROR;

    /* Free old static data if it exists */

    if (state->getgrent_state != NULL) {
        free_getent_state(state->getgrent_state);
        state->getgrent_state = NULL;
    }

    /* Create sam pipes for each domain we know about */

    for (tmp = domain_list; tmp != NULL; tmp = tmp->next) {
        struct getent_state *domain_state;

        /* Skip domains other than WINBINDD_DOMAIN environment variable */

        if ((strcmp(state->request.domain, "") != 0) &&
            (strcmp(state->request.domain, tmp->name) != 0)) {
                continue;
        }

        /* Create a state record for this domain */

        if ((domain_state = (struct getent_state *)
             malloc(sizeof(struct getent_state))) == NULL) {

            return WINBINDD_ERROR;
        }

        ZERO_STRUCTP(domain_state);

        /* Add to list of open domains */

        domain_state->domain = tmp;
        DLIST_ADD(state->getgrent_state, domain_state);
    }

    return WINBINDD_OK;
}

/* Close file pointer to ntdom group database */

enum winbindd_result winbindd_endgrent(struct winbindd_cli_state *state)
{
    if (state == NULL) return WINBINDD_ERROR;

    free_getent_state(state->getgrent_state);
    state->getgrent_state = NULL;

    return WINBINDD_OK;
}

/* Fetch next group entry from netdom database */

enum winbindd_result winbindd_getgrent(struct winbindd_cli_state *state)
{
    if (state == NULL) return WINBINDD_ERROR;

    /* Process the current head of the getent_state list */

    while(state->getgrent_state != NULL) {
        struct getent_state *ent = state->getgrent_state;

        /* Get list of entries if we haven't already got them */

        if (!ent->got_sam_entries) {
            uint32 status, start_ndx = 0, start_ndx2 = 0;
        
            if (!winbindd_fetch_group_cache(ent->domain->name, 
                                            &ent->sam_entries,
                                            &ent->num_sam_entries)) {

                /* Fetch group entries */

                if (!domain_handles_open(ent->domain)) goto cleanup;

		/* Enumerate domain groups */
                    
		do {
                        status =
				samr_enum_dom_groups(&ent->domain->sam_dom_handle,
						     &start_ndx, 0x100000,
						     &ent->sam_entries,
						     &ent->num_sam_entries);
		} while (status == STATUS_MORE_ENTRIES);
                    
		/* Enumerate domain aliases */
                    
		do {
                        status = 
				samr_enum_dom_aliases(&ent->domain->sam_dom_handle,
						      &start_ndx2, 0x100000,
						      &ent->sam_entries,
						      &ent->num_sam_entries);
		} while (status == STATUS_MORE_ENTRIES);
                
                /* Fill cache with received entries */

                winbindd_fill_group_cache(ent->domain->name, ent->sam_entries,
                                          ent->num_sam_entries);
            }

            ent->got_sam_entries = True;
        }

        /* Send back a group */

        while (ent->sam_entry_index < ent->num_sam_entries) {
            enum winbindd_result result;
            fstring domain_group_name;
            char *group_name = (ent->sam_entries)
                [ent->sam_entry_index].acct_name; 
   
            /* Prepend domain to name */

	    slprintf(domain_group_name, sizeof(domain_group_name),
		     "%s%s%s", ent->domain->name, lp_winbind_separator(), group_name);
   
            /* Get group entry from group name */

            fstrcpy(state->request.data.groupname, domain_group_name);
            result = winbindd_getgrnam_from_group(state);

            ent->sam_entry_index++;
                                                      
            if (result == WINBINDD_OK) {
                return result;
            }

            /* Try next group */

            DEBUG(1, ("could not getgrnam_from_group for group name %s\n",
                      domain_group_name));
        }

        /* We've exhausted all users for this pipe - close it down and
           start on the next one. */
        
    cleanup:

        /* Free mallocated memory for sam entries.  The data stored here
           may have been allocated from the cache. */

        if (ent->sam_entries != NULL) free(ent->sam_entries);
        ent->sam_entries = NULL;

        /* Free state information for this domain */

        {
            struct getent_state *old_ent;

            old_ent = state->getgrent_state;
            DLIST_REMOVE(state->getgrent_state, state->getgrent_state);
            free(old_ent);
        }
    }

    /* Out of pipes so we're done */

    return WINBINDD_ERROR;
}