/*
 * Copyright (c) 2003-2015
 * Distributed Systems Software.  All rights reserved.
 * See the file LICENSE for redistribution information.
 */

/*****************************************************************************
 * COPYRIGHT AND PERMISSION NOTICE
 * 
 * Copyright (c) 2001-2003 The Queen in Right of Canada
 * 
 * All rights reserved.
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, and/or sell
 * copies of the Software, and to permit persons to whom the Software is 
 * furnished to do so, provided that the above copyright notice(s) and this
 * permission notice appear in all copies of the Software and that both the
 * above copyright notice(s) and this permission notice appear in supporting
 * documentation.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS.
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE 
 * BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
 * OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
 * WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 
 * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS 
 * SOFTWARE.
 * 
 * Except as contained in this notice, the name of a copyright holder shall not
 * be used in advertising or otherwise to promote the sale, use or other
 * dealings in this Software without prior written authorization of the
 * copyright holder.
 ***************************************************************************/

/*
 * Parsing and support for group definitions
 */

#ifndef lint
static const char copyright[] =
"Copyright (c) 2003-2015\n\
Distributed Systems Software.  All rights reserved.";
static const char revid[] =
  "$Id: grouplib.c 2861 2015-12-28 22:17:08Z brachman $";
#endif

#include "group.h"

#include <stdarg.h>

static const char *log_module_name = "grouplib";

char *group_errmsg = NULL;

/*************************************************************************/

static Parse_xml_groups_state *
parse_xml_make_groups_state(Parse_xml_groups_state_code code, void *object)
{
  Parse_xml_groups_state *s;

  s = ALLOC(Parse_xml_groups_state);
  s->code = code;

  switch (code) {
  case GROUPS_PARSE_GROUPS:
	s->object.groups = (Groups *) object;
	break;
  case GROUPS_PARSE_GROUP_DEFINITION:
	s->object.group_definition = (Group_definition *) object;
	break;
  case GROUPS_PARSE_GROUP_MEMBER:
	s->object.group_member = (Group_member *) object;
	break;
  case GROUPS_PARSE_GROUP_NAME:
	s->object.group_name = (Group_name *) object;
	break;
  default:
	/* XXX ??? */
	return(NULL);
  }

  return(s);
}

static Parse_xml_group_deltas_state *
parse_xml_make_group_deltas_state(Parse_xml_group_deltas_state_code code,
								  void *object)
{
  Parse_xml_group_deltas_state *s;

  s = ALLOC(Parse_xml_group_deltas_state);
  s->code = code;

  switch (code) {
  case GROUPS_PARSE_GROUP_DELTAS:
	s->object.group_deltas = (Group_deltas *) object;
	break;
  default:
	/* XXX ??? */
	return(NULL);
  }

  return(s);
}

static int
is_valid_parse_symbol(char **stab, const char *str)
{
  int i;

  for (i = 0; stab[i] != NULL; i++) {
	if (streq(stab[i], str))
	  return(1);
  }
  return(0);
}

/*************************************************************************/

static Dsvec *dacs_group_files = NULL;

Jurisdiction *jurisdictions = NULL;
int njurisdictions = 0;

int
is_valid_jurisdiction_name(char *jurisdiction)
{
  char *p;

  if (jurisdiction == NULL || !isalpha((int) jurisdiction[0]))
	return(0);

  for (p = jurisdiction + 1; *p != '\0'; p++) {
	if (!isalnum((int) *p) && *p != '-' && *p != '_')
	  return(0);
  }

  return(1);
}

int
is_valid_federation_name(char *federation)
{

  return(is_valid_jurisdiction_name(federation));
}

/*
 * This governs the validity of the "name" part of a group name
 * and some other simple names.
 */
int
is_valid_name(const char *name)
{
  const char *p;

  if (name == NULL || !isalpha((int) name[0]))
	return(0);

  for (p = name + 1; *p != '\0'; p++) {
	if (!isalnum((int) *p) && *p != '-' && *p != '_')
	  return(0);
  }

  return(1);
}

int
is_valid_group_name(Group_name *gn)
{

  return(is_valid_jurisdiction_name(gn->jurisdiction) &&
		 is_valid_name((const char *) gn->username));
}

int
rename_group_file(char *jurisdiction, char *oldname, char *newname)
{
  int st;
  char *newpath, *oldpath;
  Vfs_handle *h;

  st = 0;
  if ((h = vfs_open_item_type(ITEM_TYPE_GROUPS)) == NULL)
	return(-1);
  oldpath = make_group_itemname(jurisdiction, oldname);
  newpath = make_group_itemname(jurisdiction, newname);
  if ((st = vfs_rename(h, oldpath, newpath)) == -1)
	log_msg((LOG_ERROR_LEVEL, "vfs_rename '%s' to '%s' failed",
			 oldpath, newpath));
  vfs_close(h);

  return(st);
}

int
load_jurisdictions(void)
{
  int i;
  char *buf;
  Groups *groups;
  Group_member *gm;

  if (load_group("DACS", "jurisdictions", &buf) == -1)
	return(-1);

  if (parse_xml_groups(buf, &groups) == -1)
	return(-1);

  njurisdictions = 0;
  for (gm = groups->group_definition_head->group_member_head; gm != NULL;
	   gm = gm->next)
	njurisdictions++;

  jurisdictions = ALLOC_N(Jurisdiction, njurisdictions + 1);
  i = 0;
  for (gm = groups->group_definition_head->group_member_head; gm != NULL;
	   gm = gm->next) {
	jurisdictions[i].jname = strdup(gm->jurisdiction);
	jurisdictions[i].name = strdup(gm->name);
	jurisdictions[i].alt_name = strdup(gm->alt_name);
	jurisdictions[i].dacs_url = strdup(gm->dacs_url);
	jurisdictions[i].authenticates = strdup(gm->authenticates);
	jurisdictions[i].prompts = strdup(gm->prompts);
	jurisdictions[i].auxiliary = (gm->auxiliary == NULL
								  ? NULL : strdup(gm->auxiliary));
	if (gm->public_key_str == NULL) {
	  jurisdictions[i].public_key_str = NULL;
	  jurisdictions[i].public_key = NULL;
	  jurisdictions[i].public_key_pem = NULL;
	}
	else {
	  jurisdictions[i].public_key_str = strdup(gm->public_key_str);
	  /* XXX does this need to be copied? */
	  jurisdictions[i].public_key = gm->public_key;
	  jurisdictions[i].public_key_pem = strdup(gm->public_key_pem);
	}
	i++;
  }
  jurisdictions[i].jname = jurisdictions[i].dacs_url = NULL;
  jurisdictions[i].name = NULL;
  jurisdictions[i].alt_name = NULL;
  jurisdictions[i].authenticates = NULL;
  jurisdictions[i].prompts = NULL;
  jurisdictions[i].auxiliary = NULL;
  jurisdictions[i].public_key_str = NULL;
  jurisdictions[i].public_key = NULL;
  jurisdictions[i].public_key_pem = NULL;

  return(njurisdictions);
}

void
add_group_to_groups(Groups *groups, Group_definition *gd)
{
  Group_definition **gdp;

  for (gdp = &groups->group_definition_head; *gdp != NULL; gdp = &(*gdp)->next)
	;
  *gdp = gd;
}

int
delete_group_member_from_group(Group_definition *gd, char *jurisdiction,
							   char *name)
{
  Group_member *gm, *gm_prev;

  gm_prev = NULL;
  for (gm = gd->group_member_head; gm != NULL; gm = gm->next) {
	if (name_eq(gm->jurisdiction, jurisdiction, DACS_NAME_CMP_CONFIG)
		&& name_eq(gm->name, name, DACS_NAME_CMP_CONFIG))
	  break;
	gm_prev = gm;
  }

  if (gm == NULL)
	return(-1);

  if (gm_prev == NULL)
	gd->group_member_head = gm->next;
  else
	gm_prev->next = gm->next;

  return(0);
}

void
add_group_member_to_group(Group_definition *gd, Group_member *gm)
{
  Group_member **gmp;

  delete_group_member_from_group(gd, gm->jurisdiction, gm->name);

  for (gmp = &gd->group_member_head; *gmp != NULL; gmp = &(*gmp)->next)
	;
  *gmp = gm;
}

Group_name *
make_group_name_from_str(Group_name *gn, const char *str)
{
  char *p, *gn_start;
  const char *j_start;

  j_start = str;
  if ((p = strchr(j_start, JURISDICTION_NAME_SEP_CHAR)) == NULL)
	return(NULL);
  gn_start = p + 1;

  gn->jurisdiction = strdup(j_start);
  gn->jurisdiction[gn_start - j_start - 1] = '\0';
  /* The name may be an empty string */
  gn->username = &gn->jurisdiction[gn_start - j_start];

  return(gn);
}

int
group_member_name_from_str(const char *str, char **jurisdiction, char **name)
{
  char *p, *j, *n, *gn_start;
  const char *j_start;

  j_start = str;
  if ((p = strchr(j_start, JURISDICTION_NAME_SEP_CHAR)) == NULL)
	return(-1);
  gn_start = p + 1;

  j = strdup(j_start);
  j[gn_start - j_start - 1] = '\0';
  if (!is_valid_jurisdiction_name(j))
	return(-1);
  n = &j[gn_start - j_start];
  if (!is_valid_auth_username((const char *) n))
	return(-1);

  *jurisdiction = j;
  *name = n;
  return(0);
}

Group_name **
make_group_names_from_role_str(char *jurisdiction, char *role_str)
{
  int n;
  char *p, *prefix, *q;
  Group_name **group_names;

  for (n = 1, p = role_str; *p != '\0'; p++) {
	if (*p == '/' || *p == ',')
	  n++;
  }

  group_names = ALLOC_N(Group_name *, n + 1);

  p = q = role_str;
  prefix = NULL;
  n = 0;

  while (1) {
	while (*q != '/' && *q != ',' && *q != '\0')
	  q++;
	group_names[n] = ALLOC(Group_name);
	group_names[n]->jurisdiction = strdup(jurisdiction);
	if (prefix == NULL)
	  group_names[n]->username = ds_xprintf("%.*s", (int) (q - p), p);
	else
	  group_names[n]->username = ds_xprintf("%s/%.*s", prefix, (int) (q - p), p);

	n++;
	if (*q == '/')
	  prefix = group_names[n - 1]->username;
	else if (*q == ',')
	  prefix = NULL;
	else
	  break;

	p = ++q;
  }

  group_names[n] = NULL;

  return(group_names);
}

#ifdef NOTDEF
static Group_name *
make_group_name(Group_name *gn, char *jurisdiction, char *group)
{

  gn->jurisdiction = strdup(jurisdiction);
  gn->name = strdup(group);
  return(gn);
}

#endif

static Parse_attr_tab group_member_attr_tab[] = {
  { "jurisdiction",    NULL, ATTR_REQUIRED, NULL, 0 },
  { "name",            NULL, ATTR_REQUIRED, NULL, 0 },
  { "type",            NULL, ATTR_REQUIRED, NULL, 0 },
  { "dacs_url",        NULL, ATTR_IMPLIED,  NULL, 0 },
  { "alt_name",        NULL, ATTR_IMPLIED,  NULL, 0 },
  { "authenticates",   NULL, ATTR_IMPLIED,  ATTR_BINARY, 0 },
  { "prompts",         NULL, ATTR_IMPLIED,  ATTR_BINARY, 0 },
  { "auxiliary",       NULL, ATTR_IMPLIED,  NULL, 0 },
  { "public_key",      NULL, ATTR_IMPLIED,  NULL, 0 },
  { NULL,              NULL, ATTR_END,      NULL, 0 }
};

static int
parse_xml_group_member(Group_member **group_member, const char **attr)
{
  int n;
  char *errmsg;
  Group_member *gm;
  Parse_xml_groups_state *state;

  gm = ALLOC(Group_member);
  parse_xml_push((void *)
				 parse_xml_make_groups_state(GROUPS_PARSE_GROUP_MEMBER,
											 (void *) gm));
  gm->jurisdiction = NULL;
  gm->name = NULL;
  gm->type = NULL;
  gm->alt_name = NULL;
  gm->dacs_url = NULL;
  gm->authenticates = NULL;
  gm->prompts = NULL;
  gm->auxiliary = NULL;
  gm->public_key_str = NULL;
  gm->public_key = NULL;
  gm->public_key_pem = NULL;
  gm->next = NULL;

  group_member_attr_tab[0].value = &gm->jurisdiction;
  group_member_attr_tab[1].value = &gm->name;
  group_member_attr_tab[2].value = &gm->type;
  group_member_attr_tab[3].value = &gm->dacs_url;
  group_member_attr_tab[4].value = &gm->alt_name;
  group_member_attr_tab[5].value = &gm->authenticates;
  group_member_attr_tab[6].value = &gm->prompts;
  group_member_attr_tab[7].value = &gm->auxiliary;
  group_member_attr_tab[8].value = &gm->public_key_str;
  if ((n = parse_xml_attr(group_member_attr_tab, attr, &errmsg)) == -1) {
	parse_xml_set_error(errmsg);
	return(-1);
  }

  if (gm->public_key_str != NULL) {
	unsigned int nbytes;
	unsigned char *public_key;
#if (OPENSSL_VERSION_NUMBER >= 0x0090800fL)
	const unsigned char *next;
#else
	unsigned char *next;
#endif

	if (stra64b(gm->public_key_str, &public_key, &nbytes) == NULL) {
	  log_msg((LOG_ERROR_LEVEL, "Unpacking error for public_key"));
	  dacs_fatal("parse_xml_group_member_element_start");
	  /*NOTREACHED*/
	}

	next = public_key;
	if ((gm->public_key = d2i_PublicKey(EVP_PKEY_RSA, NULL, &next,
										(long) nbytes)) == NULL) {
	  log_msg((LOG_ERROR_LEVEL,
			   "Error decoding public_key, OpenSSL messages follow:"));
	  crypto_log_error();
	  dacs_fatal("parse_xml_group_member_element_start");
	  /*NOTREACHED*/
	}

	gm->public_key_pem = pem_from_evp_pub_pkey(gm->public_key);
	memzap(public_key, nbytes);
  }

  if (streq(gm->type, "role")) {
	if (n != 3) {
	  parse_xml_set_error("Invalid attribute name");
	  return(-1);
	}
  }
  else if (streq(gm->type, "meta")) {
	if (gm->alt_name == NULL
		|| gm->dacs_url == NULL || gm->authenticates == NULL
		|| gm->prompts == NULL) {
	  parse_xml_set_error("One or more attributes are missing");
	  return(-1);
	}
	if (!streq(gm->authenticates, "yes") && !streq(gm->authenticates, "no")) {
	  parse_xml_set_error("Invalid attribute value");
	  return(-1);
	}
	if (!streq(gm->prompts, "yes") && !streq(gm->prompts, "no")) {
	  parse_xml_set_error("Invalid attribute value");
	  return(-1);
	}
  }
  else if (streq(gm->type, "username")) {
	if (n != 3) {
	  parse_xml_set_error("Invalid attribute name");
	  return(-1);
	}
  }
  else if (streq(gm->type, "dacs")) {
	if (n != 3) {
	  parse_xml_set_error("Invalid attribute name");
	  return(-1);
	}
  }
  else {
	parse_xml_set_error(ds_xprintf("Unknown type: %s", gm->type));
	return(-1);
  }

  *group_member = gm;
  parse_xml_pop((void **) &state);
  free(state);

  return(0);
}

static Parse_attr_tab group_definition_attr_tab[] = {
  { "jurisdiction", NULL, ATTR_REQUIRED, NULL, 0 },
  { "name",         NULL, ATTR_REQUIRED, NULL, 0 },
  { "mod_date",     NULL, ATTR_REQUIRED, NULL, 0 },
  { "type",         NULL, ATTR_REQUIRED, NULL, 0 },
  { NULL,           NULL, ATTR_END,      NULL, 0 }
};

static void
parse_xml_groups_element_start(void *data, const char *element,
							   const char **attr)
{
  char *el, *errmsg;
  Groups **gp;

  if (parse_xmlns_name(element, NULL, &el) == -1) {
	parse_xml_set_error(ds_xprintf("Invalid namespace: %s", element));
	return;
  }

  gp = (Groups **) data;

  if (parse_xml_is_error(NULL))
	return;

  if (streq(el, "groups")) {
	Groups *groups;

	if (parse_xml_is_not_empty()) {
	  parse_xml_set_error("Unexpected \"groups\" element");
	  return;
	}
	groups = ALLOC(Groups);
	parse_xml_push(parse_xml_make_groups_state(GROUPS_PARSE_GROUPS,
											   (void *) groups));
	groups->group_definition_head = NULL;
	if (parse_xml_attr(NULL, attr, &errmsg) == -1) {
	  parse_xml_set_error(errmsg);
	  return;
	}
  }
  else if (streq(el, "group_definition")) {
	Groups *groups;
	Group_definition *gd, **gp;
	Parse_xml_groups_state *state;

	if (parse_xml_top((void **) &state) != PARSE_XML_OK
		|| state->code != GROUPS_PARSE_GROUPS) {
	  parse_xml_set_error("Unexpected \"group_definition\" element");
	  return;
	}
	groups = state->object.groups;
	gd = ALLOC(Group_definition);
	gd->group_member_head = NULL;
	gd->next = NULL;

	parse_xml_push((void *)
				   parse_xml_make_groups_state(GROUPS_PARSE_GROUP_DEFINITION,
											   (void *) gd));
	group_definition_attr_tab[0].value = &gd->jurisdiction;
	group_definition_attr_tab[1].value = &gd->name;
	group_definition_attr_tab[2].value = &gd->mod_date;
	group_definition_attr_tab[3].value = &gd->type;
	if (parse_xml_attr(group_definition_attr_tab, attr, &errmsg) == -1) {
	  parse_xml_set_error(errmsg);
	  return;
	}

	if (!is_valid_parse_symbol(Group_definition_type_symbols, gd->type)) {
	  parse_xml_set_error("group_definition: invalid symbol");
	  return;
	}

	for (gp = &groups->group_definition_head; *gp != NULL; gp = &(*gp)->next)
	  ;
	*gp = gd;
  }
  else if (streq(el, "group_member")) {
	Group_definition *gd;
	Group_member *gm, **gp;
	Parse_xml_groups_state *state;

	if (parse_xml_top((void **) &state) != PARSE_XML_OK
		|| state->code != GROUPS_PARSE_GROUP_DEFINITION) {
	  parse_xml_set_error("Unexpected \"group_member\" element");
	  return;
	}
	gd = state->object.group_definition;

	if (parse_xml_group_member(&gm, attr) == -1)
	  return;

	for (gp = &gd->group_member_head; *gp != NULL; gp = &(*gp)->next)
	  ;
	*gp = gm;

  }
  else {
	parse_xml_set_error(ds_xprintf("Unknown element: %s", el));
	return;
  }
}

static void
parse_xml_groups_element_end(void *data, const char *element)
{
  char *el;
  Groups **gp;
  Parse_xml_groups_state *state;

  if (parse_xmlns_name(element, NULL, &el) == -1) {
	parse_xml_set_error(ds_xprintf("Invalid namespace: %s", element));
	return;
  }

  gp = (Groups **) data;

  if (parse_xml_is_error(NULL))
	return;

  if (streq(el, "groups")) {
	if (parse_xml_pop((void **) &state) == PARSE_XML_ERROR)
	  goto err;
	if (state->code != GROUPS_PARSE_GROUPS || parse_xml_is_not_empty()) {
	  parse_xml_set_error("Unexpected \"groups\" terminating element");
	  goto err;
	}
	*gp = state->object.groups;
  }
  else if (streq(el, "group_definition")) {
	if (parse_xml_pop((void **) &state) == PARSE_XML_ERROR)
	  goto err;
	if (state->code != GROUPS_PARSE_GROUP_DEFINITION) {
	  parse_xml_set_error("Unexpected \"group_definition\" terminating element");
	  goto err;
	}
	free(state);
  }
  else if (streq(el, "group_member")) {
	/* Do nothing */
  }
  else {
	parse_xml_set_error(ds_xprintf("Unknown element: %s", el));
	goto err;
  }

  return;

 err:
  parse_xml_set_error("");
}

static void
parse_xml_groups_cdata_start(void *userData)
{

  log_msg((LOG_ERROR_LEVEL, "CDATA start"));
}

static void
parse_xml_groups_cdata_end(void *userData)
{

  log_msg((LOG_ERROR_LEVEL, "CDATA end"));
}

int
parse_xml_groups(char *str, Groups **groups)
{
  int st;
  Parse_xml_error err;
  XML_Parser p = XML_ParserCreateNS(NULL, XMLNS_SEP_CHAR);

  if (p == NULL)
	return(-1);

  parse_xml_init("Groups", p);

  XML_SetElementHandler(p, parse_xml_groups_element_start,
						parse_xml_groups_element_end);
  XML_SetCommentHandler(p, parse_xml_comment_handler);
  XML_SetDefaultHandler(p, parse_xml_default_handler);
  XML_SetCdataSectionHandler(p, parse_xml_groups_cdata_start,
							 parse_xml_groups_cdata_end);
  XML_SetUserData(p, (void *) groups);

  st = XML_Parse(p, str, strlen(str), 1);

  if (parse_xml_is_error(&err) || st == 0) {
	if (err.mesg == NULL) {
	  parse_xml_set_error(NULL);
	  parse_xml_is_error(&err);
	}
	log_msg((LOG_ERROR_LEVEL, "parse_xml_groups: line %d, pos %d",
			 err.line, err.pos));
	if (err.mesg != NULL)
	  log_msg((LOG_ERROR_LEVEL, "parse_xml_groups: %s", err.mesg));
	parse_xml_end();
	return(-1);
  }

  parse_xml_end();

  if (parse_xml_is_not_empty())
	return(-1);

  return(0);
}

void
free_xml_groups(Groups *groups)
{
  /* XXX */
}

static Parse_attr_tab group_deltas_attr_tab[] = {
  { "jurisdiction", NULL, ATTR_REQUIRED, NULL, 0 },
  { "name",         NULL, ATTR_REQUIRED, NULL, 0 },
  { "type",         NULL, ATTR_REQUIRED, NULL, 0 },
  { "timestamp",    NULL, ATTR_REQUIRED, NULL, 0 },
  { NULL,           NULL, ATTR_END,      NULL, 0 }
};

static Parse_attr_tab group_meta_update_attr_tab[] = {
  { "newname", NULL, ATTR_REQUIRED, NULL, 0 },
  { NULL,      NULL, ATTR_END,      NULL, 0 }
};

static Parse_attr_tab group_delete_member_attr_tab[] = {
  { "jurisdiction", NULL, ATTR_REQUIRED, NULL, 0 },
  { "name",         NULL, ATTR_REQUIRED, NULL, 0 },
  { NULL,           NULL, ATTR_END,      NULL, 0 }
};

static void
parse_xml_group_deltas_element_start(void *data, const char *element,
									 const char **attr)
{
  char *el, *errmsg;
  Group_deltas **gdp;

  if (parse_xmlns_name(element, NULL, &el) == -1) {
	parse_xml_set_error(ds_xprintf("Invalid namespace: %s", element));
	return;
  }

  gdp = (Group_deltas **) data;

  if (parse_xml_is_error(NULL))
	return;

  if (streq(el, "group_deltas")) {
	Group_deltas *gd;

	if (parse_xml_is_not_empty())
	  return;
	gd = ALLOC(Group_deltas);
	gd->group_meta_update = NULL;
	gd->group_add_delete = NULL;
	parse_xml_push(parse_xml_make_group_deltas_state(GROUPS_PARSE_GROUP_DELTAS,
													 (void *) gd));

	group_deltas_attr_tab[0].value = &gd->jurisdiction;
	group_deltas_attr_tab[1].value = &gd->name;
	group_deltas_attr_tab[2].value = &gd->type;
	group_deltas_attr_tab[3].value = &gd->timestamp;
	if (parse_xml_attr(group_deltas_attr_tab, attr, &errmsg) == -1) {
	  parse_xml_set_error(errmsg);
	  return;
	}

	if (!is_valid_parse_symbol(Group_deltas_type_symbols, gd->type)) {
	  parse_xml_set_error("Invalid attribute value");
	  return;
	}
  }
  else if (streq(el, "group_meta_update")) {
	Group_deltas *gd;
	Group_meta_update *gmu;
	Parse_xml_group_deltas_state *state;

	if (parse_xml_top((void **) &state) != PARSE_XML_OK
		|| state->code != GROUPS_PARSE_GROUP_DELTAS)
	  return;
	gd = state->object.group_deltas;
	if (gd->group_meta_update != NULL) {
	  parse_xml_set_error("multiple group_meta_update elements");
	  return;
	}
	gmu = ALLOC(Group_meta_update);
	gmu->newname = NULL;

	group_meta_update_attr_tab[0].value = &gmu->newname;
	if (parse_xml_attr(group_meta_update_attr_tab, attr, &errmsg) == -1) {
	  parse_xml_set_error(errmsg);
	  return;
	}

	if (!is_valid_name(gmu->newname)) {
	  parse_xml_set_error("invalid group name");
	  return;
	}
	gd->group_meta_update = gmu;
  }
  else if (streq(el, "group_member")) {
	Group_deltas *gd;
	Group_add_delete **g, *gad;
	Parse_xml_group_deltas_state *state;

	if (parse_xml_top((void **) &state) != PARSE_XML_OK
		|| state->code != GROUPS_PARSE_GROUP_DELTAS)
	  return;
	gd = state->object.group_deltas;

	gad = ALLOC(Group_add_delete);
	gad->group_member = NULL;
	gad->group_delete_member = NULL;
	gad->next = NULL;

	parse_xml_group_member(&gad->group_member, attr);

	for (g = &gd->group_add_delete; *g != NULL; g = &(*g)->next)
	  ;
	*g = gad;
  }
  else if (streq(el, "group_delete_member")) {
	Group_deltas *gd;
	Group_add_delete **g, *gad;
	Group_delete_member *gdm;
	Parse_xml_group_deltas_state *state;

	if (parse_xml_top((void **) &state) != PARSE_XML_OK
		|| state->code != GROUPS_PARSE_GROUP_DELTAS)
	  return;
	gd = state->object.group_deltas;
	if (!streq(gd->type, "add_delete")) {
	  parse_xml_set_error("can't delete member in replace mode");
	  return;
	}

	gad = ALLOC(Group_add_delete);
	gad->group_member = NULL;
	gad->group_delete_member = NULL;
	gad->next = NULL;

	gdm = ALLOC(Group_delete_member);
	gdm->jurisdiction = NULL;
	gdm->name = NULL;

	group_delete_member_attr_tab[0].value = &gdm->jurisdiction;
	group_delete_member_attr_tab[1].value = &gdm->name;
	if (parse_xml_attr(group_delete_member_attr_tab, attr, &errmsg) == -1) {
	  parse_xml_set_error(errmsg);
	  return;
	}

	if (!is_valid_jurisdiction_name(gdm->jurisdiction)) {
	  parse_xml_set_error("invalid group_delete_member jurisdiction name");
	  return;
	}

	gad->group_delete_member = gdm;
	for (g = &gd->group_add_delete; *g != NULL; g = &(*g)->next)
	  ;
	*g = gad;
  }
  else {
	parse_xml_set_error(ds_xprintf("Unknown element: %s", el));
	return;
  }
}

static void
parse_xml_group_deltas_element_end(void *data, const char *element)
{
  char *el;
  Group_deltas **gd;
  Parse_xml_group_deltas_state *state;

  if (parse_xmlns_name(element, NULL, &el) == -1) {
	parse_xml_set_error(ds_xprintf("Invalid namespace: %s", element));
	return;
  }

  gd = (Group_deltas **) data;

  if (parse_xml_is_error(NULL))
	return;

  if (streq(el, "group_deltas")) {
	if (parse_xml_pop((void **) &state) == PARSE_XML_ERROR)
	  goto err;
	if (state->code != GROUPS_PARSE_GROUP_DELTAS || parse_xml_is_not_empty())
	  goto err;
	*gd = state->object.group_deltas;
  }
  else if (streq(el, "group_meta_update")) {
	/* Do nothing */
  }
  else if (streq(el, "group_member")) {
	/* Do nothing */
  }
  else if (streq(el, "group_delete_member")) {
	/* Do nothing */
  }
  else {
	parse_xml_set_error(ds_xprintf("Unknown element: %s", el));
	return;
  }

  return;

 err:
  parse_xml_set_error("");
}

int
parse_xml_group_deltas(char *str, Group_deltas **group_deltas)
{
  int st;
  Parse_xml_error err;
  XML_Parser p = XML_ParserCreateNS(NULL, XMLNS_SEP_CHAR);

  if (p == NULL)
	return(-1);

  parse_xml_init("group_deltas", p);

  XML_SetElementHandler(p, parse_xml_group_deltas_element_start,
						parse_xml_group_deltas_element_end);
  XML_SetCommentHandler(p, parse_xml_comment_handler);
  XML_SetDefaultHandler(p, parse_xml_default_handler);
  XML_SetUserData(p, (void *) group_deltas);

  st = XML_Parse(p, str, strlen(str), 1);

  if (parse_xml_is_error(&err) || st == 0) {
	if (err.mesg == NULL) {
	  parse_xml_set_error(NULL);
	  parse_xml_is_error(&err);
	}
	log_msg((LOG_ERROR_LEVEL, "parse_xml_deltas: line %d, pos %d",
			 err.line, err.pos));
	if (err.mesg != NULL)
	  log_msg((LOG_ERROR_LEVEL, "parse_xml_deltas: %s", err.mesg));
	parse_xml_end();
	return(-1);
  }

  parse_xml_end();

  if (parse_xml_is_not_empty())
	return(-1);

  return(0);
}

/*
 * Find the definition of the group with the given NAME in JURISDICTION.
 */
static Group_definition *
group_lookup(Groups *groups, char *jurisdiction, char *name)
{
  Group_definition *gd;

  for (gd = groups->group_definition_head; gd != NULL; gd = gd->next) {
	if (name_eq(gd->jurisdiction, jurisdiction, DACS_NAME_CMP_CONFIG)
		&& name_eq(gd->name, name, DACS_NAME_CMP_CONFIG))
	  return(gd);
  }

  return(NULL);
}

int
set_mod_date(Group_definition *gd)
{
  time_t now;
  struct tm *tm;

  now = time(NULL);
  tm = gmtime(&now);

  gd->mod_date = strdup(make_utc_date_string(tm)); 

  return(0);
}

int
load_group(char *jurisdiction, char *name, char **buf)
{
  char *path;
  Vfs_handle *h;

  if ((h = vfs_open_item_type(ITEM_TYPE_GROUPS)) == NULL)
	return(-1);

  if ((path = make_group_itemname(jurisdiction, name)) == NULL)
	return(-1);

  if (vfs_get(h, path, (void **) buf, NULL) == -1) {
	vfs_close(h);
	free(path);
	return(-1);
  }

  if (vfs_close(h) == -1)
	log_msg((LOG_ERROR_LEVEL, "load_group: vfs_close() failed"));

  free(path);
  return(0);
}

int
vfs_group_file(Groups *groups, char *jurisdiction, char *name)
{
  char *path;
  Vfs_handle *h;
  char *ptr;

  if ((ptr = groups_xml_text_to_buf(groups)) == NULL)
	return(-1);

  if ((path = make_group_itemname(jurisdiction, name)) == NULL)
	return(-1);

  if ((h = vfs_open_item_type(ITEM_TYPE_GROUPS)) == NULL)
	return(-1);

  if (vfs_put(h, path, (void *) ptr, strlen(ptr)) == -1) {
	vfs_close(h);
	return(-1);
  }

  if (vfs_close(h) == -1)
	log_msg((LOG_ERROR_LEVEL, "vfs_group_file: vfs_close() failed"));

  free(path);
  return(0);
}

/***************************************************************************/

static int
exists_group_filename(char *jurisdiction, char *name)
{
  int st;
  char *path;
  Vfs_handle *h;

  if ((h = vfs_open_item_type(ITEM_TYPE_GROUPS)) == NULL)
	return(-1);

  if ((path = make_group_itemname(jurisdiction, name)) == NULL)
	return(-1);

  if (vfs_exists(h, path) == -1) {
	vfs_close(h);
	return(-1);
  }

  if (vfs_close(h) == -1)
	log_msg((LOG_ERROR_LEVEL, "exists_group_filename: vfs_close() failed"));
  st = 0;

  return((st == -1) ? -1 : 0);
}

int
exists_group(char *jurisdiction, char *name)
{
  int st;

  st = exists_group_filename(jurisdiction, name);

  return((st == -1) ? -1 : 0);
}

char *
make_group_itemname(char *jurisdiction, char *name)
{
  char *path;
  Group_name gn;

  gn.jurisdiction = jurisdiction;
  gn.username = name;
  if (!is_valid_group_name(&gn))
	return(NULL);

  path = ds_xprintf("%s/%s%s", jurisdiction, name, GROUP_FILE_SUFFIX);

  return(path);
}

/*
 * Map a group name to the full pathname of the file containing the group
 * definition.
 * Given the names of a jurisdiction and a group defined by that jurisdiction,
 * return the full pathname of the file containing that group.
 * That file may not actually exist at the moment.
 */
char *
make_group_pathname(char *jurisdiction, char *name)
{
  char *path;
  Group_name gn;

  gn.jurisdiction = jurisdiction;
  gn.username = name;
  if (!is_valid_group_name(&gn))
	return(NULL);

  path = ds_xprintf("%s/%s%s", jurisdiction, name, GROUP_FILE_SUFFIX);
  return(path);
}

/*
 * Map a filename to a group name.
 * Given the full or relative (to the base directory) pathname of a group
 * file, return the name of the group associated with that file (or NULL).
 */
static Group_name *
make_group_name_from_path(Group_name *gn, const char *str)
{
  char *j, *n, *p, *s;

  s = strdup(str);

  if ((n = strrchr(s, '/')) == NULL || *(n + 1) == '\0') {
	free(s);
	return(NULL);
  }

  if (n == s) {
	free(s);
	return(NULL);
  }

  *n++ = '\0';

  if ((j = strrchr(s, '/')) == NULL) {
	free(s);
	return(NULL);
  }

  if ((p = strsuffix(n, strlen(n), GROUP_FILE_SUFFIX)) == NULL) {
	free(s);
	return(NULL);
  }

  *p = '\0';
  gn->jurisdiction = ++j;
  gn->username = n;

  if (!is_valid_group_name(gn)) {
	free(s);
	return(NULL);
  }

  return(gn);
}

static Group_file *
group_file_lookup(char *jurisdiction, char *name)
{
  int i;
  Group_file *gf;

  for (i = 0; i < dsvec_len(dacs_group_files); i++) {
	gf = (Group_file *) dsvec_ptr_index(dacs_group_files, i);

	if (name_eq(name, gf->group_name.username, DACS_NAME_CMP_CONFIG)
		&& name_eq(jurisdiction, gf->group_name.jurisdiction,
				   DACS_NAME_CMP_CONFIG))
	  return(gf);
  }

  return(NULL);
}

static int
add_to_group_list(char *naming_context, char *name, void ***names)
{
  Dsvec *dsv;
  Group_file *g;
  Group_name gn;
  struct stat sb;

  if (make_group_name_from_path(&gn, name) == NULL)
	return(0);

  if (stat(name, &sb) == -1) {
	log_err((LOG_ERROR_LEVEL, "add_to_group_files: Couldn't stat \"%s\"",
			 name));
	return(0);
  }

  dsv = (Dsvec *) names;

  g = ALLOC(Group_file);
  g->path = strdup(name);
  g->group_name = gn;
  g->mod_date = sb.st_mtime;
  g->type = (is_private_group_mode(sb.st_mode)
			 ? GROUP_DEFINITION_TYPE_PRIVATE : GROUP_DEFINITION_TYPE_PUBLIC);

  dsvec_add_ptr(dsv, g);

  return(1);
}

/*
 * Get a list of all locally known groups.
 */
int
get_group_list(Dsvec **gf)
{
  int n;
  Dsvec *dsv;
  Vfs_handle *h;

  if (dacs_group_files == NULL) {
	if ((h = vfs_open_item_type(ITEM_TYPE_GROUPS)) == NULL) {
	fail:
	  log_msg((LOG_ERROR_LEVEL, "get_group_list: failed"));
	  if (h != NULL)
		vfs_close(h);
	  return(-1);
	}
	/* XXX Remove when st_mtime/st_mode available from other store methods */
	if (!streq(h->sd->store_name, "fs")
		&& !streq(h->sd->store_name, "file")
		&& !streq(h->sd->store_name, "dacs-fs")) {
	  log_msg((LOG_ERROR_LEVEL,
			   "get_group_list: only supports store method \"file\""));
	  goto fail;
	}

	dsv = dsvec_init(NULL, sizeof(Group_file *));
    n = vfs_list(h, NULL, NULL, add_to_group_list, (void ***) dsv);
    if (n != -1)
	  dacs_group_files = dsv;

	if (vfs_close(h) == -1)
	  log_msg((LOG_ERROR_LEVEL, "get_group_list: vfs_close() failed"));
  }

  if (gf != NULL)
	*gf = dacs_group_files;

  return(dsvec_len(dacs_group_files));
}

int
refresh_group_list(Dsvec **gf)
{

  if (dacs_group_files != NULL) {
	/* XXX Free old list */
	dacs_group_files = NULL;
  }

  return(get_group_list(gf));
}

/***************************************************************************/

void
groups_xml_html(FILE *fp, Groups *groups)
{
  Group_definition *gd;
  Group_member *gm;

  printf("<TT>&lt;");
  element_hl(fp, "groups");
  printf("&gt;<BR>");
  for (gd = groups->group_definition_head; gd != NULL; gd = gd->next) {
	printf("&nbsp;&nbsp;&lt;");
	element_hl(fp, "group_definition");
	attribute_hl(fp, NULL, "jurisdiction", gd->jurisdiction);
	attribute_hl(fp, NULL, "name", gd->name);
	attribute_hl(fp, NULL, "mod_date", gd->mod_date);
	attribute_hl(fp, NULL, "type", gd->type);
	printf("&gt;<BR>\n");
	for (gm = gd->group_member_head; gm != NULL; gm = gm->next) {
	  printf("&nbsp;&nbsp;&nbsp;&nbsp;");
	  printf("&lt;");
	  element_hl(fp, "group_member");
	  attribute_hl(fp, NULL, "jurisdiction", gm->jurisdiction);
	  attribute_hl(fp, NULL, "name", gm->name);
	  attribute_hl(fp, NULL, "type", gm->type);
	  if (gm->dacs_url != NULL)
		attribute_hl(fp, NULL, "dacs_url", gm->dacs_url);
	  printf("/&gt;<BR>\n");
	}
	printf("&nbsp;&nbsp;&lt;/");
	element_hl(fp, "group_definition");
	printf("&gt;<BR>\n");
  }
  printf("&lt;/");
  element_hl(fp, "groups");
  printf("&gt;<BR></TT>\n");
}

struct outf {
  int use_stream;
  FILE *stream;
  Ds ds;
};

static int
outfunc(struct outf *x, char *fmt, ...)
{
  int len;
  va_list ap;

  va_start(ap, fmt);
  if (x->use_stream)
	len = vfprintf(x->stream, fmt, ap);
  else
	len = ds_vasprintf(&x->ds, fmt, ap);

  va_end(ap);
  return(len);
}

static int
groups_xml_text_internal(struct outf *x, Groups *groups)
{
  int len;
  Group_definition *gd;
  Group_member *gm;

  len = 0;
  len += outfunc(x, "<groups>\n");
  for (gd = groups->group_definition_head; gd != NULL; gd = gd->next) {
	len += outfunc(x, "<group_definition");
	len += outfunc(x, " jurisdiction=\"%s\"", gd->jurisdiction);
	len += outfunc(x, " name=\"%s\"", gd->name);
	len += outfunc(x, " mod_date=\"%s\"", gd->mod_date);
	len += outfunc(x, " type=\"%s\">\n", gd->type);
	for (gm = gd->group_member_head; gm != NULL; gm = gm->next) {
	  len += outfunc(x, "<group_member");
	  len += outfunc(x, " jurisdiction=\"%s\"", gm->jurisdiction);
	  len += outfunc(x, " name=\"%s\"", gm->name);
	  len += outfunc(x, " type=\"%s\"", gm->type);
	  if (gm->dacs_url != NULL)
		len += outfunc(x, " dacs_url=\"%s\"", gm->dacs_url);
	  len += outfunc(x, "/>\n");
	}
	len += outfunc(x, "</group_definition>\n");
  }
  len += outfunc(x, "</groups>\n");

  return(len);
}

int
groups_xml_text(FILE *fp, Groups *groups)
{
  int len;
  struct outf x;

  x.use_stream = 1;
  x.stream = fp;

  len = groups_xml_text_internal(&x, groups);

  return(len);
}

char *
groups_xml_text_to_buf(Groups *groups)
{
  int len;
  struct outf x;

  x.use_stream = 0;
  x.stream = NULL;
  ds_init(&x.ds);
  len = groups_xml_text_internal(&x, groups);

  return(ds_buf(&x.ds));
}

/*
 * GM is a DACS group.  If it exists, load it, otherwise report an error.
 */
static Group_definition *
group_resolve_dacs_membership(Groups *groups,  Group_member *gm)
{
  int st;
  char *buf;
  Group_file *gf;
  Groups *g;

  /* Is the group defined? */
  if ((gf = group_file_lookup(gm->jurisdiction, gm->name)) == NULL)
	return(NULL);

  if (load_group(gm->jurisdiction, gm->name, &buf) == -1)
	return(NULL);

  st = parse_xml_groups(buf, &g);
  free(buf);
  if (st == -1)
	return(NULL);

  add_group_to_groups(groups, g->group_definition_head);

  return(g->group_definition_head);
}

/*
 * Recursive function to fully expand the membership of a group.
 */
static int
group_resolve(Groups *groups, Group_definition *gd, int depth)
{
  Group_definition *gd_new;
  Group_member *gm;

#ifdef NOTDEF
  if (depth >= max_depth)
	return(-1);
#endif

  for (gm = gd->group_member_head; gm != NULL; gm = gm->next) {
	if (streq(gm->type, "dacs")) {
	  /* Has the group been already included? */
	  if (group_lookup(groups, gm->jurisdiction, gm->name) == NULL) {
		if ((gd_new = group_resolve_dacs_membership(groups, gm)) == NULL) {
		  log_msg((LOG_ERROR_LEVEL, "Can't resolve group %s",
				   auth_identity(NULL, gm->jurisdiction, gm->name, NULL)));
		  /* Leave the unresolved group in the output list. */
		  continue;
		}
		else if (group_resolve(groups, gd_new, depth + 1) == -1)
		  return(-1);
	  }
	  if (delete_group_member_from_group(gd, gm->jurisdiction, gm->name) == -1)
		return(-1);
	}
  }

  return(0);
}

/*
 * Scan through the given GROUP to try to fully resolve its members into
 * type GROUP_MEMBER_TYPE_USERNAME.
 * If the type is GROUP_MEMBER_TYPE_DACS, we must recursively resolve the
 * membership of that group.
 *
 * Detect cycles of inclusion, eliminate duplicates, and enforce the maximum
 * recursion depth.
 */
static int
group_resolve_membership(char *jurisdiction, char *name, Groups **groups)
{
  int st;
  char *buf;

  get_group_list(NULL);

  if (load_group(jurisdiction, name, &buf) == -1)
	return(-1);

  st = parse_xml_groups(buf, groups);
  free(buf);

  if (st == -1)
	return(-1);

  return(group_resolve(*groups, (*groups)->group_definition_head, 0));
}

static int
has_role(Group_name **roles, char *jurisdiction, char *name)
{
  int i;

  for (i = 0; roles != NULL && roles[i] != NULL; i++) {
	if (name_eq(roles[i]->jurisdiction, jurisdiction, DACS_NAME_CMP_CONFIG)
		&& name_eq(roles[i]->username, name, DACS_NAME_CMP_CONFIG))
	  return(1);
  }

  return(0);
}

int
has_unauth_role(char *role_str, char *jurisdiction, char *name)
{
  char *j;
  Group_name **roles;

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

  if ((j = conf_val(CONF_JURISDICTION_NAME)) == NULL)
	return(0);

  roles = make_group_names_from_role_str(j, role_str);
  if (has_role(roles, jurisdiction, name) == 1)
	return(1);

  return(0);
}

/*
 * Helper function for is_group_member().
 * We don't need to expand the group's full membership - we can stop
 * if the requested group name is found.
 */
static int
is_group_member_subr(Groups *groups, Group_definition *gd,
					 char *jname, char *uname, Group_name **roles)
{
  int i, st;
  Group_definition *gd_new;
  Group_member *gm;

  /*
   * Check if there are any ordinary usernames in the list of
   * members and whether this person is named.
   * Also check if any roles are listed as members of the group and whether
   * this person is associated with any of those roles.
   */
  for (gm = gd->group_member_head; gm != NULL; gm = gm->next) {
	if (streq(gm->type, "username")
		&& name_eq(jname, gm->jurisdiction, DACS_NAME_CMP_CONFIG)
		&& name_eq(uname, gm->name, DACS_NAME_CMP_CONFIG))
	  return(1);
	else if (streq(gm->type, "role") && roles != NULL) {
	  for (i = 0; roles[i] != NULL; i++) {
		if (name_eq(roles[i]->jurisdiction, gm->jurisdiction,
					DACS_NAME_CMP_CONFIG)
			&& name_eq(roles[i]->username, gm->name, DACS_NAME_CMP_CONFIG))
		  return(1);
	  }
	}
  }

  /*
   * Scan through again, recursively checking if this person belongs to any
   * groups that are members of the group we are currently inspecting.
   */
  for (gm = gd->group_member_head; gm != NULL; gm = gm->next) {
	if (streq(gm->type, "dacs")) {
	  /* Has the group been already included? */
	  if (group_lookup(groups, gm->jurisdiction, gm->name) == NULL) {
		if ((gd_new = group_resolve_dacs_membership(groups, gm)) == NULL)
		  continue;
		st = is_group_member_subr(groups, gd_new, jname, uname, roles);
		if (st != 0)
		  return(st);
	  }
	  if (delete_group_member_from_group(gd, gm->jurisdiction, gm->name) == -1)
		return(-1);
	}
  }

  return(0);
}

/*
 * Determine if the user JNAME:UNAME with roles ROLES is a member of
 * group NAME in JURISDICTION.
 * Return 1 if true, 0 if false, and -1 on error.
 */
int
is_group_member(char *jname, char *uname, Group_name **roles,
				char *jurisdiction, char *name)
{
  int st;
  char *buf;
  Groups *groups;

  if (has_role(roles, jurisdiction, name) == 1)
	return(1);

  get_group_list(NULL);

  /*
   * XXX We should cache group membership resolution so that subsequent
   * requests by the process are much faster.
   */
 if (load_group(jurisdiction, name, &buf) == -1)
	return(0);

  st = parse_xml_groups(buf, &groups);
  free(buf);
  if (st == -1)
	return(-1);

  return(is_group_member_subr(groups, groups->group_definition_head,
							  jname, uname, roles));
}

/*
 * Get the metadata for JURISDICTION; if NULL, assume the current jurisdiction.
 * It is the local copy of federation's metadata that is returned.
 */
int
get_jurisdiction_meta(char *jurisdiction, Jurisdiction **jp)
{
  int i;
  char *jname;

  if (load_jurisdictions() == -1)
	return(-1);

  if ((jname = jurisdiction) == NULL)
	jname = conf_val(CONF_JURISDICTION_NAME);

  for (i = 0; i < njurisdictions; i++) {
	if (name_eq(jname, jurisdictions[i].jname, DACS_NAME_CMP_CONFIG)) {
	  *jp = &jurisdictions[i];
	  return(0);
	}
  }

  return(-1);
}

/*************************************************************************/

int
dacs_group_get_membership(char *jurisdiction, char *gname, Groups **groups)
{

  if (group_resolve_membership(jurisdiction, gname, groups) == -1)
	return(-1);

  return(0);
}

int
dacs_create_group(char *jurisdiction, char *gname, char *gtype)
{
  int st;
  time_t now;
  struct tm *tm;
  Groups groups;
  Group_definition gd;

  now = time(NULL);
  tm = gmtime(&now);

  groups.group_definition_head = &gd;
  gd.jurisdiction = jurisdiction;
  gd.name = gname;
  gd.mod_date = make_utc_date_string(tm);
  gd.type = gtype;
  gd.group_member_head = NULL;
  gd.next = NULL;

  st = vfs_group_file(&groups, jurisdiction, gname);

  return(st);
}

/*
 * Remove the file containing a group definition.
 */
int
dacs_delete_group(char *jurisdiction, char *name)
{
  int st;
  char *path;

  st = 0;

  {
	Vfs_handle *h;

	if ((h = vfs_open_item_type(ITEM_TYPE_GROUPS)) == NULL)
	  return(-1);

	path = make_group_itemname(jurisdiction, name);
    if (vfs_delete(h, path) == -1)
	  st = -1;

	if (vfs_close(h) == -1)
	  log_msg((LOG_ERROR_LEVEL, "dacs_delete_group: vfs_close() failed"));
  }

  free(path);
  return(st);
}

Dsvec *
dacs_list_groups(char *jurisdiction, Dsvec **gfp)
{
  int i, n;
  Dsvec *dsv;
  Group_file *gf;

  if ((n = get_group_list(&dsv)) == -1)
	return(NULL);

  *gfp = dsvec_init(NULL, sizeof(Group_file *));
  for (i = 0; i < n; i++) {
	gf = (Group_file *) dsvec_ptr_index(dsv, i);
	if (jurisdiction == NULL
		|| name_eq(jurisdiction, gf->group_name.jurisdiction,
				   DACS_NAME_CMP_CONFIG))
	  dsvec_add_ptr(*gfp, gf);
  }

  return(*gfp);
}

int
dacs_add_group_member(char *jurisdiction, char *gname, char *gmname,
					  char *mtype, char *dacs)
{
  char *buf, *mjurisdiction, *mname;
  Groups *groups;
  Group_definition *gd;
  Group_member *gm;

  if (!is_valid_name(gname))
	return(-1);
  if (group_member_name_from_str(gmname, &mjurisdiction, &mname) == -1)
	return(-1);
  if (load_group(jurisdiction, gname, &buf) == -1)
	return(-1);
  if (parse_xml_groups(buf, &groups) == -1)
	return(-1);

  for (gd = groups->group_definition_head; gd != NULL; gd = gd->next) {
	if (name_eq(gd->jurisdiction, jurisdiction, DACS_NAME_CMP_CONFIG)
		&& name_eq(gd->name, gname, DACS_NAME_CMP_CONFIG))
	  break;
  }

  if (gd == NULL)
	return(-1);

  gm = ALLOC(Group_member);
  gm->jurisdiction = strdup(mjurisdiction);
  gm->name = strdup(mname);
  gm->type = strdup(mtype);
  gm->next = NULL;

  if (dacs != NULL && dacs[0] != '\0') {
	if (!streq(gm->type, "meta"))
	  return(-1);
	if (!streq(gm->name, "DACS"))
	  return(-1);
	gm->dacs_url = strdup(dacs);
  }
  else
	gm->dacs_url = NULL;

  add_group_member_to_group(gd, gm);
  if (set_mod_date(gd) == -1)
	return(-1);

  if (vfs_group_file(groups, jurisdiction, gname) == -1)
	return(-1);

  return(0);
}

int
dacs_delete_group_member(char *gname, char *jurisdiction, char *mname)
{
  char *buf;
  Groups *groups;
  Group_definition *gd;

  if (load_group(conf_val(CONF_JURISDICTION_NAME), gname, &buf) == -1)
	return(-1);
  if (parse_xml_groups(buf, &groups) == -1)
	return(-1);

  for (gd = groups->group_definition_head; gd != NULL; gd = gd->next) {
	if (name_eq(gd->jurisdiction, conf_val(CONF_JURISDICTION_NAME),
				DACS_NAME_CMP_CONFIG)
		&& name_eq(gd->name, gname, DACS_NAME_CMP_CONFIG))
	  break;
  }

  if (gd == NULL)
	return(-1);

  if (delete_group_member_from_group(gd, jurisdiction, mname) == -1)
	return(-1);

  if (set_mod_date(gd) == -1)
	return(-1);

  if (vfs_group_file(groups, conf_val(CONF_JURISDICTION_NAME), gname) == -1)
	return(-1);

  return(0);
}

int
dacs_get_group_definition(char *jurisdiction, char *gname, char **def)
{

  if (load_group(jurisdiction, gname, def) == -1)
	return(-1);

  return(0);
}

int
dacs_receive_group_definition(char *jurisdiction, char *groups_str)
{
  char *j;
  Groups g, *groups;
  Group_definition *gd, *gd_next;

  if (parse_xml_groups(groups_str, &groups) == -1) {
	group_errmsg = "Groups parse failed";
	return(-1);
  }

  for (gd = groups->group_definition_head; gd != NULL; gd = gd_next) {
	j = gd->jurisdiction;
	if (name_eq(j, jurisdiction, DACS_NAME_CMP_CONFIG)) {
	  group_errmsg = "Attempt to replace master copy";
	  return(-1);
	}

	gd_next = gd->next;
	gd->next = NULL;
	g.group_definition_head = gd;

	if (vfs_group_file(&g, j, gd->name) == -1) {
	  group_errmsg = "vfs_group_file failed";
	  return(-1);
	}
  }

  return(0);
}

typedef struct Group_name_list {
  Group_name gn;
  struct Group_name_list *next;
} Group_name_list;

static Group_name_list *
make_group_name_list(char *gnl_str)
{
  char *p, *q, *str;
  Group_name_list *gnl, *gnl_head;

  gnl_head = NULL;
  str = strdup(gnl_str);

  while ((p = strsep(&str, ",")) != NULL) {
	gnl = ALLOC(Group_name_list);
	if (streq(p, "*")) {
	  gnl->gn.jurisdiction = "*";
	  gnl->gn.username = NULL;
	  gnl->next = NULL;
	  return(gnl);
	}
	if ((q = strchr(p, JURISDICTION_NAME_SEP_CHAR)) == NULL)
	  return(NULL);
	if (q == p)
	  return(NULL);
	*q++ = '\0';

	gnl->gn.jurisdiction = p;
	gnl->gn.username = q;
	if (!is_valid_group_name(&gnl->gn))
	  return(NULL);

	gnl->next = gnl_head;
	gnl_head = gnl;
  }

  return(gnl_head);
}

int
dacs_send_group_definition(char *group_name_list, Groups **gp)
{
  int i, n;
  char *buf;
  Dsvec *dsv;
  Groups *g, *groups;
  Group_file *gf;
  Group_name_list *gnl, *gnl_head;

  if ((n = get_group_list(&dsv)) == -1) {
	log_msg((LOG_ERROR_LEVEL, "dacs_send_group_definition failed"));
	return(-1);
  }

  *gp = NULL;
  if (n == 0)
	return(0);

  if ((gnl_head = make_group_name_list(group_name_list)) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "make_group_name_list failed"));
	return(-1);
  }

  g = ALLOC(Groups);
  g->group_definition_head = NULL;
  for (i = 0; i < n; i++) {
	gf = (Group_file *) dsvec_ptr_index(dsv, i);

	for (gnl = gnl_head; gnl != NULL; gnl = gnl->next) {
	  if (streq(gnl->gn.jurisdiction, "*")
		  || (name_eq(gnl->gn.jurisdiction, gf->group_name.jurisdiction,
					  DACS_NAME_CMP_CONFIG)
			  && (gnl->gn.username[0] == '\0'
				  || name_eq(gnl->gn.username, gf->group_name.username,
							 DACS_NAME_CMP_CONFIG)))) {
		if (load_group(gf->group_name.jurisdiction,
					   gf->group_name.username, &buf) == -1) {
		  log_msg((LOG_ERROR_LEVEL, "load_group failed"));
		  return(-1);
		}
		if (parse_xml_groups(buf, &groups) == -1) {
		  log_msg((LOG_ERROR_LEVEL, "parse_xml_groups failed"));
		  return(-1);
		}
		free(buf);
		groups->group_definition_head->next = g->group_definition_head;
		g->group_definition_head = groups->group_definition_head;
		break;
	  }
	}
  }

  if (g->group_definition_head != NULL)
	*gp = g;

  return(0);
}

int
dacs_change_group_definition(char *jurisdiction, char *gname, char *ngname)
{
  char *buf;
  Groups *groups;
  Group_definition *gd;

  if (!is_valid_name(gname))
	return(-1);
  if (is_valid_name(ngname))
	return(-1);

  if (load_group(jurisdiction, gname, &buf) == -1)
	return(-1);
  if (parse_xml_groups(buf, &groups) == -1)
	return(-1);

  for (gd = groups->group_definition_head; gd != NULL; gd = gd->next) {
	if (name_eq(gd->jurisdiction, jurisdiction, DACS_NAME_CMP_CONFIG)
		&& name_eq(gd->name, gname, DACS_NAME_CMP_CONFIG))
	  break;
  }

  if (gd == NULL)
	return(-1);

  if (ngname != NULL) {
	if (rename_group_file(jurisdiction, gd->name, ngname) == -1)
	  return(-1);
	gd->name = strdup(ngname);
  }

  if (set_mod_date(gd) == -1)
	return(-1);

  if (vfs_group_file(groups, jurisdiction, gd->name) == -1)
	return(-1);

  return(0);
}

int
dacs_get_roles(Credentials *credentials, Roles_list **roles_list)
{
  Group_name **group_names;
  Credentials *cr;
  Roles_list *rl, *rl_prev;

  *roles_list = rl_prev = NULL;

  for (cr = credentials; cr != NULL; cr = cr->next) {
	group_names
	  = make_group_names_from_role_str(cr->home_jurisdiction, cr->role_str);

	rl = ALLOC(Roles_list);
	rl->cr = cr;
	rl->group_names = group_names;
	rl->next = NULL;

	if (rl_prev == NULL)
	  *roles_list = rl;
	else
	  rl_prev->next = rl;
	rl_prev = rl;
  }

  return(0);
}

int
dacs_list_group_membership(char *gmname, char *match_jurisdiction,
						   char *match_name, Dsvec **gfp)
{
  int errcode, i, n, st;
  char *jname, *uname;
  char errbuf[100];
  regex_t prog;
  Dsvec *dsv;
  Group_file *gf;

  if (group_member_name_from_str(gmname, &jname, &uname) == -1) {
	group_errmsg = "Can't construct credentials";
	return(-1);
  }

  if ((n = get_group_list(&dsv)) == -1) {
	group_errmsg = "Can't obtain group list";
	return(-1);
  }

  if (match_name != NULL) {
	if ((errcode = regcomp(&prog, match_name, REG_EXTENDED)) != 0) {
	regex_fail:
	  regerror(errcode, &prog, errbuf, sizeof(errbuf));
	  log_msg((LOG_ERROR_LEVEL, "Invalid regex: %s", errbuf));
	  group_errmsg = "MATCH_GROUP_NAME: invalid regular expression";
	  return(-1);
	}
  }

  *gfp = dsvec_init(NULL, sizeof(Group_file *));

  for (i = 0; i < n; i++) {
	gf = (Group_file *) dsvec_ptr_index(dsv, i);

	if (match_jurisdiction != NULL
		&& !name_eq(match_jurisdiction, gf->group_name.jurisdiction,
					DACS_NAME_CMP_CONFIG))
	  continue;

	if (match_name != NULL) {
	  if ((errcode
		   = regexec(&prog, gf->group_name.username, 0, NULL, 0)) != 0) {
		if (errcode == REG_NOMATCH)
		  continue;
		else
		  goto regex_fail;
	  }
	}

	st = is_group_member(jname, uname, NULL, gf->group_name.jurisdiction,
						 gf->group_name.username);
	if (st == -1) {
	  group_errmsg = "Group membership test failed";
	  return(-1);
	}

	if (st == 1)
	  dsvec_add_ptr(*gfp, gf);
  }

  return(0);
}

int
dacs_test_group_membership(Credentials *credentials, char *gname)
{
  int st;
  Credentials *cr;
  Group_name gn;

  if (make_group_name_from_str(&gn, gname) == NULL
	  || !is_valid_group_name(&gn)) {
	group_errmsg = "Invalid group name";
	return(-1);
  }

  for (cr = credentials; cr != NULL; cr = cr->next) {
	if (cr->role_str != NULL)
	  cr->roles = make_group_names_from_role_str(cr->home_jurisdiction,
												 cr->role_str);
	st = is_group_member(cr->home_jurisdiction, cr->username, cr->roles,
						 gn.jurisdiction, gn.username);
	if (st == -1) {
	  group_errmsg = "Internal error: is_group_member failed";
	  return(-1);
	}
	if (st == 1)
	  return(1);
  }

  return(0);
}

/*
 * Apply changes to an existing group.  Either:
 * a) replace the entire membership with a set of group_member elements or
 * b) perform a sequence of add/delete members.
 * Also, optionally change the group's name.
 * Always set the modification date of the group.
 */
int
dacs_apply_deltas(char *jurisdiction)
{
  char *buf;
  Groups *groups;
  Group_definition *gdef;
  Group_deltas *gd;
  Group_member *gm, *gm_prev;
  Group_add_delete *gad;
  Group_delete_member *gdm;
  time_t oldt, newt;
  Vfs_handle *handle;

  if ((handle = vfs_open_item_type(ITEM_TYPE_DELTAS)) == NULL)
	return(-1);

  if (vfs_get(handle, "deltas", (void **) &buf, NULL) == -1) {
	vfs_close(handle);
	return(-1);
  }

  if (vfs_close(handle) == -1)
	log_msg((LOG_ERROR_LEVEL, "dacs_apply_deltas: vfs_close() failed"));

  if (parse_xml_group_deltas(buf, &gd) == -1)
	return(-1);
  free(buf);

  if (!name_eq(gd->jurisdiction, jurisdiction, DACS_NAME_CMP_CONFIG))
	return(-1);

  if (load_group(gd->jurisdiction, gd->name, &buf) == -1) {
	group_errmsg = "Group not found";
	return(-1);
  }

  if (parse_xml_groups(buf, &groups) == -1)
	return(-1);

  gdef = groups->group_definition_head;
  if (utc_date_string_to_secs(&oldt, gdef->mod_date) == -1)
	return(-1);
  if (utc_date_string_to_secs(&newt, gd->timestamp) == -1)
	return(-1);
  if (newt <= oldt) {
	group_errmsg = "Timestamp is not more recent than current version";
	return(-1);
  }

  if (streq(gd->type, "replace")) {
	/* Delete existing membership, add new set of members. */
	gdef->group_member_head = NULL;
	gm_prev = NULL;
	for (gad = gd->group_add_delete; gad != NULL; gad = gad->next) {
	  gm = gad->group_member;
	  gm->next = NULL;
	  if (gm_prev == NULL)
		gdef->group_member_head = gm;
	  else
		gm_prev->next = gm;
	  gm_prev = gm;
	}
  }
  else if (gd->group_add_delete != NULL) {
	/* Perform a sequence of adds/deletes, in order. */
	for (gad = gd->group_add_delete; gad != NULL; gad = gad->next) {
	  if (gad->group_member != NULL)
		add_group_member_to_group(gdef, gad->group_member);
	  else if (gad->group_delete_member != NULL) {
		gdm = gad->group_delete_member;
		if (delete_group_member_from_group(gdef, gdm->jurisdiction,
										   gdm->name) == -1)
		  return(-1);
	  }
	  else
		return(-1);
	}
  }

  gdef->mod_date = gd->timestamp;

  if (gd->group_meta_update != NULL
	  && gd->group_meta_update->newname != NULL) {
	char *oldname, *newname;

	oldname = gdef->name;
	newname = gd->group_meta_update->newname;
	if (exists_group(jurisdiction, newname) == 0)
	  return(-1);

	gdef->name = strdup(newname);
	if (vfs_group_file(groups, jurisdiction, newname) == -1)
	  return(-1);
	if (dacs_delete_group(gdef->jurisdiction, oldname) == -1)
	  return(-1);
  }
  else {
	if (vfs_group_file(groups, jurisdiction, gdef->name) == -1)
	  return(-1);
  }

  return(0);
}
