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

/*
 * local_ldap_auth - LDAP/Microsoft Active Directory authentication module
 * local_ldap_roles - LDAP/Microsoft Active Directory roles module
 *
 * Each entry in an LDAP directory has a unique distinguished name (DN).
 * A user is authenticated if we can take the given username, form a DN,
 * and use the given password to bind to that DN.
 *
 * If authentication succeeds, then optionally do a search operation to
 * return attributes required to construct roles information.
 *
 * In addition to the username provided during login, we need configuration
 * information to allow us to construct the DN of the entry for the user.
 * In some cases, we can directly produce a DN given the username by
 * making a template-based substitution.

 * In other cases this isn't possible; we may need to first bind to a
 * specified DN (possibly using a specified password), do a search for the
 * (presumably unique) attribute having the given username as its value
 * (guided by a DN at which to base the search and a filter produced by
 * a template-based substitution.
 * The result of a successful search provides the DN for the user's entry.
 * For example, a directory schema might associate an email address with
 * each user's entry but email addresses might not be used to name entries.
 * We might bind to the directory as the administrator,
 * perform a search for the entry containing the email address, then
 * bind to the DN of that entry.
 *
 * Here are the two methods:
 * 1) If you know how to map the USERNAME into the corresponding DN, create the
 *    DN and bind to it using PASSWORD.  If the bind works, the user is
 *    authenticated.
 *
 * 2) If you don't know how to map the USERNAME directly into the corresponding
 *    DN, bind to the directory as an administrator (we're given the DN and
 *    password to use) and do a search for USERNAME (with some
 *    configuration guidance).  If the search returns a single result, we
 *    use its DN and the PASSWORD to try to bind to the directory.
 *
 * In either situation, the DACS_USERNAME could optionally be obtained from
 * an attribute value or by mapping USERNAME.
 * The directive
 *    AUTH_LDAP_MAP_USERNAME="map [-r string regex ] attribute-name [flag]*"
 * causes DACS_USERNAME to be set to the value of the specified attribute
 * name, if it's present in the entry.
 *
 * If we follow mod_auth_ldap's approach (AuthLDAPGroupAttribute Directive),
 * we'd simply concatenate the values of the selected attributes.
 *
 * For ideas, see:
 * http://httpd.apache.org/docs-2.0/mod/mod_auth_ldap.html
 * http://www.rfc-editor.org/rfc/rfc2254.txt
 * http://www.rfc-editor.org/rfc/rfc2255.txt
 */

#ifndef lint
static const char copyright[] =
"Copyright (c) 2003-2012\n\
Distributed Systems Software.  All rights reserved.";
static const char revid[] =
  "$Id: local_ldap_auth.c 2577 2012-03-07 20:53:07Z brachman $";
#endif

#include "dacs.h"

#include <ldap.h>
#include <dacs_ldif.h>

enum {
  LDAP_DEFAULT_PORT     = 389,
  LDAP_DEFAULT_SSL_PORT = 636
};

/* An attribute-value assertion */
typedef struct Ava {
  char *attr_name;
  char *attr_value;
} Ava;

static char *log_module_name = "local_ldap_auth";

static int local_ldap_roles = 0;

extern int ldap_bind_s(LDAP *, LDAP_CONST char *, LDAP_CONST char *, int);
extern LDAP *ldap_init(LDAP_CONST char *, int);
extern int ldap_unbind(LDAP *);

static int
do_bind(LDAP *ld, char *binddn, char *password)
{

  if (password == NULL || *password == '\0')
    log_msg((LOG_DEBUG_LEVEL, "Note: password is NULL or empty"));

  if (ldap_bind_s(ld, binddn, password, LDAP_AUTH_SIMPLE) != LDAP_SUCCESS) {
	log_msg((LOG_ERROR_LEVEL, "ldap_bind_s as \"%s\" failed", binddn));
	return(-1);
  }
  log_msg((LOG_DEBUG_LEVEL, "ldap_bind_s as \"%s\" succeeded", binddn));

  return(0);
}

static char *
make_ldif(int type, char *name, char *value, ber_len_t vallen)
{
  char *ldif;

  if ((ldif = ldif_put(type, name, value, vallen)) == NULL)
	return(NULL);

  return(ldif);
}

static int
print_entry(LDAP *ld, char *dn, LDAPMessage *entry, int attrsonly)
{
  int i, rc;
  char *a;
  BerElement *ber = NULL;
  struct berval **bvals;
  LDAPControl **ctrls = NULL;

  log_msg((LOG_TRACE_LEVEL, "Entry: \"%s\"", dn));

  rc = ldap_get_entry_controls(ld, entry, &ctrls);

  if (rc != LDAP_SUCCESS) {
	log_msg((LOG_ERROR_LEVEL, "ldap_get_entry_controls: error=\"%s\", rc = %d",
			 ldap_err2string(rc), rc));
	return(-1);
  }

  for (a = ldap_first_attribute(ld, entry, &ber); a != NULL;
	   a = ldap_next_attribute(ld, entry, ber)) {
	if (attrsonly)
	  log_msg((LOG_TRACE_LEVEL, "%s",
			   strtrim(make_ldif(LDIF_PUT_NOVALUE, a, NULL, 0), "\n", 0)));
	else if ((bvals = ldap_get_values_len(ld, entry, a)) != NULL) {
	  for (i = 0; bvals[i] != NULL; i++) {
		log_msg((LOG_TRACE_LEVEL, "%s",
				 strtrim(make_ldif(LDIF_PUT_VALUE, a,
								   bvals[i]->bv_val, bvals[i]->bv_len),
						 "\n", 0)));
	  }
	}
  }

  return(0);
}

static int
print_result(LDAP *ld, LDAPMessage *result, int search)
{
  int err, rc;
  char *matched_dn, **refs, *text;
  LDAPControl **ctrls;

  matched_dn = NULL;
  text = NULL;
  refs = NULL;
  ctrls = NULL;

  rc = ldap_parse_result(ld, result,
						 &err, &matched_dn, &text, &refs, &ctrls, 0);

  if (rc != LDAP_SUCCESS) {
	log_msg((LOG_ERROR_LEVEL, "ldap_parse_result: error=\"%s\", rc=%d",
			 ldap_err2string(rc), rc));
	return(-1);
  }

  log_msg((LOG_TRACE_LEVEL, "ldap_parse_result: result=%d %s",
		   err, ldap_err2string(err)));

  if (matched_dn && matched_dn[0] != '\0')
	log_msg((LOG_TRACE_LEVEL, "Matched DN: %s", matched_dn));

  if (text && text[0] != '\0')
	log_msg((LOG_TRACE_LEVEL, "Additional information: %s", text));

  if (refs) {
	int i;

	for (i = 0; refs[i] != NULL; i++)
	  log_msg((LOG_TRACE_LEVEL, "Referral: %s", refs[i]));
  }

  return(err);
}

#ifdef NOTDEF
/*
 * Count the number of attribute/value pairs in the entry.
 * Return that number or -1 on error.
 */
static int
count_avas(LDAP *ld, LDAPMessage *entry)
{
  int i, n;
  char *a;
  BerElement *ber = NULL;
  struct berval **bvals;

  n = 0;
  for (a = ldap_first_attribute(ld, entry, &ber); a != NULL;
	   a = ldap_next_attribute(ld, entry, ber)) {
	if ((bvals = ldap_get_values_len(ld, entry, a)) != NULL) {
	  for (i = 0; bvals[i] != NULL; i++)
		;
	  n += i;
	}
  }

  return(n);
}
#endif

/*
 * Extract all of the attribute-value pairs in ENTRY and convert them
 * into a vector of AVA structures.
 * Return the number of AVA structures or -1 on error.
 * An extra AVA structure is added at the end and has NULL fields.
 */
static int
get_entry_avas(LDAP *ld, LDAPMessage *entry, Dsvec *avas)
{
  int anum, i;
  char *attr, *aname, *avalue, *ava, *p;
  BerElement *ber = NULL;
  Ava *a;
  struct berval **bvals;

#ifdef NOTDEF
  n = count_avas(ld, entry);
  if (n <= 0)
	return(n);
#endif

  anum = 0;
  for (attr = ldap_first_attribute(ld, entry, &ber); attr != NULL;
	   attr = ldap_next_attribute(ld, entry, ber)) {
	if ((bvals = ldap_get_values_len(ld, entry, attr)) != NULL) {
	  for (i = 0; bvals[i] != NULL; i++) {
		/*
		 * Unfortunately, it doesn't appear as though we're provided
		 * with a nice library function to get at the attribute value
		 * as a simple character string.
		 * So we whip something up by using a library function and
		 * then breaking it into pieces; it gives us the attribute
		 * name, followed by a colon and a space, and then the
		 * attribute value.
		 */
		ava = make_ldif(LDIF_PUT_VALUE, attr,
						bvals[i]->bv_val, bvals[i]->bv_len);
		aname = ava;
		if ((p = strchr(ava, ':')) == NULL)
		  return(-1);
		/* Multiple colons may appear... */
		while (*(p + 1) == ':')
		  p++;
		*p++ = '\0';
		if (*p++ != ' ')
		  return(-1);
		avalue = p;
		if ((p = strchr(avalue, '\n')) == NULL)
		  return(-1);
		*p = '\0';

		log_msg((LOG_TRACE_LEVEL,
				 "attr_name=\"%s\", attr_value=\"%s\"", aname, avalue));

		a = ALLOC(Ava);
		a->attr_name = aname;
		a->attr_value = avalue;
		dsvec_add_ptr(avas, a);
		anum++;
	  }
	}
  }

  return(anum);
}

/*
 * Use a Search operation to locate a particular entry.
 */
static int
fetch_entry(LDAP *ld, char *base, int scope, char *filtpatt, char *value,
			char **attrs, int attrsonly, LDAPControl **sctrls,
			LDAPControl **cctrls, struct timeval *timeout,
			int sizelimit, char **dnp, Dsvec *avas)
{
  int nentries, navas, rc;
  char *dn, *filter;
  LDAPMessage *res, *msg;
  ber_int_t msgid;

  if (filtpatt != NULL)
	filter = ds_xprintf(filtpatt, value);
  else
	filter = value;

  log_msg((LOG_TRACE_LEVEL, "Searching: base=\"%s\", filter=\"%s\"",
		   base, filter));

  rc = ldap_search_ext(ld, base, scope, filter, attrs, attrsonly,
					   sctrls, cctrls, timeout, sizelimit, &msgid);

  if (rc != LDAP_SUCCESS) {
	log_msg((LOG_ERROR_LEVEL,
			 "ldap_search_ext failed: %s (%d)", ldap_err2string(rc), rc));
	return(-1);
  }

  nentries = 0;
  navas = 0;
  res = NULL;

  while ((rc = ldap_result(ld, LDAP_RES_ANY, LDAP_MSG_ONE, NULL, &res)) > 0) {
	for (msg = ldap_first_message(ld, res); msg != NULL;
		 msg = ldap_next_message(ld, msg)) {
	  dn = ldap_get_dn(ld, msg);

	  switch (ldap_msgtype(msg)) {
	  case LDAP_RES_SEARCH_ENTRY:
		if (nentries++ == 0) {
		  if ((navas = get_entry_avas(ld, msg, avas)) == -1)
			log_msg((LOG_ERROR_LEVEL, "get_entry_avas failed"));
		  log_msg((LOG_TRACE_LEVEL, "Got %d avas", navas));
		  if (dnp != NULL)
			*dnp = dn;
		}
		log_msg((LOG_TRACE_LEVEL, "LDAP_RES_SEARCH_ENTRY:%s", dn));
		print_entry(ld, dn, msg, attrsonly);
		break;

	  case LDAP_RES_SEARCH_RESULT:
		log_msg((LOG_TRACE_LEVEL, "LDAP_RES_SEARCH_RESULT:%s", dn));
		print_result(ld, msg, 1);
		goto done;

	  default:
	  case LDAP_RES_SEARCH_REFERENCE:
	  case LDAP_RES_EXTENDED:
#ifdef LDAP_RES_EXTENDED_PARTIAL
	  case LDAP_RES_EXTENDED_PARTIAL:
#endif
#ifdef LDAP_RES_INTERMEDIATE
	  case LDAP_RES_INTERMEDIATE:
#endif
		goto done;
	  }
	}

	ldap_msgfree(res);
  }

  if (rc == -1)
	log_msg((LOG_ERROR_LEVEL, "ldap_result: %s", ldap_err2string(rc)));

 done:

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

  return(nentries);
}

static LDAP *
do_init(char *host, int port)
{
  int debug, protocol;
  LDAP *ld;

  if ((ld = ldap_init(host, port)) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "ldap_init to %s:%d failed", host, port));
	return(NULL);
  }

  if (ldap_set_option(ld, LDAP_OPT_REFERRALS, LDAP_OPT_OFF)
	  != LDAP_OPT_SUCCESS) {
	log_msg((LOG_ERROR_LEVEL, "Could not set LDAP_OPT_REFERRALS off"));
	return(NULL);
  }

  /* LDAPv3 only */
  protocol = LDAP_VERSION3;
  if (ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &protocol)
	  != LDAP_OPT_SUCCESS) {
	log_msg((LOG_ERROR_LEVEL, "Could not set LDAP_OPT_PROTOCOL_VERSION %d",
			 protocol));
	return(NULL);
  }

  debug = (trace_level > 1);
  if (ldap_set_option(NULL, LDAP_OPT_DEBUG_LEVEL, &debug) != LDAP_OPT_SUCCESS)
	log_msg((LOG_DEBUG_LEVEL, "Could not set LDAP_OPT_DEBUG_LEVEL %d", debug));

  return(ld);
}

/*
 * Bind to URL using PASSWORD.
 * Return 1 if successful, 0 if unsuccessful but another bind method can
 * be attempted, -1 on error.
 */
int
local_ldap_auth_by_dn(Uri *url, char *username, char *password,
					  struct timeval *timeout, Dsvec *avas)
{
  int attrsonly, n, scope;
  char *user_dn;
  char *attrs[100];
  LDAP *ld;

  if ((ld = do_init(url->host, url->port)) == NULL)
	return(-1);

  if (*url->path == '/')
	user_dn = url->path + 1;
  else
	user_dn = url->path;

  log_msg((LOG_TRACE_LEVEL, "Authenticating by binding to \"%s\"", user_dn));

  if (do_bind(ld, user_dn, password) == -1) {
	log_msg((LOG_ERROR_LEVEL, "Bind to \"%s\" failed", user_dn));
	ldap_unbind(ld);
	return(0);
  }
  log_msg((LOG_DEBUG_LEVEL, "Bind to \"%s\" succeeded", user_dn));

  if (avas != NULL) {
	scope = LDAP_SCOPE_BASE;
	attrsonly = 0;
	attrs[0] = "*";		/* Get all attributes */
	attrs[1] = NULL;
	n = fetch_entry(ld, user_dn, scope, NULL, NULL,
					attrs, attrsonly, NULL, NULL, timeout, -1, NULL, avas);
	if (n == -1) {
	  log_msg((LOG_ERROR_LEVEL, "Can't read entry for DN %s", user_dn));
	  ldap_unbind(ld);
	  return(-1);
	}
	else if (n > 1) {
	  log_msg((LOG_WARN_LEVEL, "Too many results for DN %s", user_dn));
	  ldap_unbind(ld);
	  return(-1);
	}
	log_msg((LOG_DEBUG_LEVEL, "Read entry for DN %s", user_dn));
  }

  ldap_unbind(ld);

  return(1);
}

/*
 * Bind as ADMIN_URL using ADMIN_PASSWORD (if provided) and search
 * for a (unique) entry rooted at ROOT_DN, applying FILTER and TIMEOUT to
 * the search.
 * If the search is successful, attempt to bind to that entry using PASSWORD
 * and return that entry's AVAs.
 * Return 1 if successful, 0 if unsuccessful but another bind method can
 * be attempted, -1 on error.
 */
int
local_ldap_auth_by_attr(Uri *admin_url, char *admin_password,
						char *root_dn, char *filter, char *password,
						struct timeval *timeout, Dsvec *avas)
{
  int attrsonly, n, scope;
  char *admin_bind_dn, *user_dn;
  char *attrs[1];
  LDAP *ld;

  if ((ld = do_init(admin_url->host, admin_url->port)) == NULL)
	return(-1);

  if (*admin_url->path == '/')
	admin_bind_dn = admin_url->path + 1;
  else
	admin_bind_dn = admin_url->path;

  if (do_bind(ld, admin_bind_dn, admin_password) == -1) {
	log_msg((LOG_ERROR_LEVEL, "Admin bind to %s failed", admin_bind_dn));
	ldap_unbind(ld);
	return(-1);
  }

  log_msg((LOG_DEBUG_LEVEL, "Bind to \"%s\" succeeded", admin_bind_dn));

  scope = LDAP_SCOPE_SUBTREE;
  attrsonly = 0;
  attrs[0] = NULL;

  log_msg((LOG_TRACE_LEVEL,
		   "Authenticating by searching under \"%s\" for \"%s\"",
		   root_dn, filter));

  n = fetch_entry(ld, root_dn, scope, NULL, filter,
				  attrs, attrsonly, NULL, NULL, timeout, -1, &user_dn, avas);
  if (n < 1) {
	log_msg((LOG_ERROR_LEVEL, "Search at %s for %s failed", root_dn, filter));
	ldap_unbind(ld);
	return(0);
  }
  else if (n > 1) {
	log_msg((LOG_ERROR_LEVEL, "Too many search results at %s for %s",
			 root_dn, filter));
	ldap_unbind(ld);
	return(-1);
  }

  log_msg((LOG_TRACE_LEVEL, "Search found DN \"%s\"", user_dn));

  ldap_unbind(ld);

  if ((ld = do_init(admin_url->host, admin_url->port)) == NULL)
	return(-1);

  log_msg((LOG_DEBUG_LEVEL, "Authenticating by binding to DN: %s", user_dn));

  if (!local_ldap_roles) {
	if (do_bind(ld, user_dn, password) == -1) {
	  log_msg((LOG_ERROR_LEVEL, "User bind to %s failed", user_dn));
	  ldap_unbind(ld);
	  return(-1);
	}
  }

  ldap_unbind(ld);

  return(1);
}

static char *
make_roles(Kwv_pair *roles_selectors, Kwv *attrs)
{
  int n;
  Ds ds;
  Kwv_pair *k, *rs;

  ds_init(&ds);

  /*
   * Evaluate each LDAP_ROLES_SELECTOR* directive, iterating through each
   * attribute of the entry (ATTRS) and passing the variable ${LDAP::attrname}
   * to the name of the attribute and ${LDAP::attrvalue} to its value.
   */
  n = 0;
  for (rs = roles_selectors; rs != NULL; rs = rs->next) {
	Kwv_iter *iter;

	log_msg((LOG_TRACE_LEVEL, "Trying roles selector: %s", rs->val));
	if ((iter = kwv_iter_begin(attrs, NULL)) != NULL) {
	  for (k = kwv_iter_first(iter); k != NULL; k = kwv_iter_next(iter)) {
		char *p;
		Acs_expr_ns_arg ns_args[3];

		if (streq(k->name, "attrname") || streq(k->name, "attrvalue"))
		  continue;
		if (k->val == NULL)
		  continue;

		kwv_replace(attrs, "attrname", k->name);
		kwv_replace(attrs, "attrvalue", k->val);
		log_msg((LOG_TRACE_LEVEL, "Trying attr: %s=\"%s\"", k->name, k->val));
		ns_args[0].name = "LDAP";
		ns_args[0].kwv = attrs;
		ns_args[1].name = dacs_conf->conf_var_ns->ns;
		ns_args[1].kwv = dacs_conf->conf_var_ns->kwv;
		ns_args[2].name = NULL;
		ns_args[2].kwv = NULL;
		acs_expr_string(rs->val, ns_args, &p);
		if (p == NULL || *p == '\0')
		  continue;
		if (is_valid_role_str(p)) {
		  ds_asprintf(&ds, "%s%s", n ? "," : "", p);
		  n++;
		  log_msg((LOG_TRACE_LEVEL, "Selected role attr value: %s", p));
		}
	  }
	  kwv_iter_end(iter);
	  kwv_delete(attrs, "attrname");
	  kwv_delete(attrs, "attrvalue");
	}
  }

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

  return(ds_buf(&ds));
}

int
main(int argc, char **argv)
{
  int direct_bind, indirect_bind, emitted_dtd, i, st, success;
  char *bind_method, *errmsg, *jurisdiction, *p, *username, *password, *aux;
  char *mapped_username, *role_str, *timeout_secs;
  char *auth_ldap_username_url;
  char *auth_ldap_username_expr;
  char *auth_ldap_admin_url;
  char *auth_ldap_admin_password;
  char *auth_ldap_search_root_dn, *auth_ldap_search_filter;
  struct timeval timeval, *tv;
  Auth_reply_ok ok;
  Dsvec *avas;
  Kwv *kwv, *kwv_attrs;
  Kwv_pair *v;
  Uri *url;

  errmsg = "Internal error";
  emitted_dtd = 0;
  username = password = aux = jurisdiction = NULL;

  /*
   * Depending on what name is used to invoke the program, function as
   * either local_ldap_authenticate or local_ldap_roles.
   * This seems sensible because they share almost all of the code.
   */
  if ((p = current_uri_prog()) == NULL
	  || streq(p, "local_ldap_authenticate")) {
	log_module_name = "local_ldap_auth";
	local_ldap_roles = 0;
  }
  else if (streq(p, "local_ldap_roles")) {
	log_module_name = "local_ldap_roles";
	local_ldap_roles = 1;
  }
  else {
	errmsg = "An unrecognized URI was used for this service";
	goto fail;
  }

  if (dacs_init(DACS_LOCAL_SERVICE, &argc, &argv, &kwv, &errmsg) == -1) {
	/* If we fail here, we may not have a DTD with which to reply... */
  fail:
	if (password != NULL)
	  strzap(password);
	if (aux != NULL)
	  strzap(aux);

	if (local_ldap_roles)
	  emit_roles_reply_failed(stdout, errmsg);
	else {
	  if (emitted_dtd) {
		printf("%s\n", make_xml_auth_reply_failed(NULL, NULL));
		emit_xml_trailer(stdout);
	  }
	}

	if (errmsg != NULL)
	  log_msg((LOG_ERROR_LEVEL, "Failed: reason=%s", errmsg));

	exit(1);
  }

  if (!local_ldap_roles) {
	/* This must go after initialization. */
	emitted_dtd = emit_xml_header(stdout, "auth_reply");
  }

  if (argc > 1) {
	errmsg = "Usage: unrecognized parameter";
	goto fail;
  }

  for (i = 0; i < kwv->nused; i++) {
	if (streq(kwv->pairs[i]->name, "USERNAME") && username == NULL)
	  username = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "PASSWORD") && password == NULL)
	  password = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "AUXILIARY") && aux == NULL)
	  aux = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "DACS_JURISDICTION")
			 && jurisdiction == NULL)
	  jurisdiction = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "DACS_VERSION"))
	  ;
	else
	  log_msg((LOG_TRACE_LEVEL, "Parameter: '%s'", kwv->pairs[i]->name));
  }

  /* Verify that we're truly responsible for DACS_JURISDICTION. */
  if (dacs_verify_jurisdiction(jurisdiction) == -1) {
	errmsg = "Missing or incorrect DACS_JURISDICTION";
	goto fail;
  }

  direct_bind = 0;
  indirect_bind = 0;
  if ((bind_method = kwv_lookup_value(kwv, "LDAP_BIND_METHOD")) == NULL) {
	errmsg = "No LDAP_BIND_METHOD";
	goto fail;
  }
  if (strcaseeq(bind_method, "direct"))
	direct_bind = 1;
  else if (strcaseeq(bind_method, "indirect"))
	indirect_bind = 1;
  else if (strcaseeq(bind_method, "both"))
	direct_bind = indirect_bind = 1;
  else {
	errmsg = ds_xprintf("Unrecognized LDAP_BIND_METHOD type: \"%s\"",
						bind_method);;
	goto fail;
  }

  avas = dsvec_init(NULL, sizeof(Ava));

  if ((timeout_secs = kwv_lookup_value(kwv, "LDAP_TIMEOUT_SECS")) != NULL) {
	unsigned long ul;

	if (strnum(timeout_secs, STRNUM_UL, &ul) == -1) {
	  ul = 0;
	  log_msg((LOG_WARN_LEVEL,
			   "Invalid LDAP_TIMEOUT_SECS, ignored: \"%s\"", timeout_secs));
	}
	timeval.tv_sec = ul;
	timeval.tv_usec = 0;
	tv = &timeval;
  }
  else
	tv = NULL;

  success = 0;
  if (direct_bind) {
	char *p_e;
	Acs_expr_ns_arg ns_args[3];

	log_msg((LOG_TRACE_LEVEL, "Trying direct bind method"));
	p_e = kwv_lookup_value(kwv, "LDAP_USERNAME_URL*");
	p = kwv_lookup_value(kwv, "LDAP_USERNAME_URL");
	if (p == NULL && p_e == NULL) {
	  errmsg = "No LDAP_USERNAME_URL or LDAP_USERNAME_URL* available";
	  goto fail;
	}
	if (p != NULL && p_e != NULL) {
	  errmsg = "Either LDAP_USERNAME_URL or LDAP_USERNAME_URL* is required";
	  goto fail;
	}

	if (p_e != NULL) {
	  ns_args[0].name = "Args";
	  ns_args[0].kwv = kwv;
	  ns_args[1].name = dacs_conf->conf_var_ns->ns;
	  ns_args[1].kwv = dacs_conf->conf_var_ns->kwv;
	  ns_args[2].name = NULL;
	  ns_args[2].kwv = NULL;
	  acs_expr_string(p_e, ns_args, &auth_ldap_username_url);
	}
	else
	  auth_ldap_username_url = p;

	if ((url = uri_parse(auth_ldap_username_url)) == NULL) {
	  errmsg = ds_xprintf("Can't parse LDAP_USERNAME_URL: \"%s\"",
						  auth_ldap_username_url);
	  goto fail;
	}
	if (url->scheme != NULL && strcaseeq(url->scheme, "ldap")) {
	  if (url->port_given == NULL)
		url->port = LDAP_DEFAULT_PORT;
	}
	else if (url->scheme != NULL && strcaseeq(url->scheme, "ldaps")) {
	  if (url->port_given == NULL)
		url->port = LDAP_DEFAULT_SSL_PORT;
	}
	else {
	  errmsg = "Unrecognized scheme in LDAP_USERNAME_URL";
	  goto fail;
	}

	if (strcaseeq(url->scheme, "ldaps")) {
	  errmsg = "ldaps scheme is unsupported in LDAP_USERNAME_URL";
	  goto fail;
	}

	log_msg((LOG_TRACE_LEVEL, "Direct bind: %s", url->uri));
	st = local_ldap_auth_by_dn(url, username, password, tv, avas);
	if (st == -1) {
	  errmsg = "Username/Password/Aux incorrect";
	  goto fail;
	}
	if (st == 1) {
	  log_msg((LOG_DEBUG_LEVEL, "Authentication was successful"));
	  success = 1;
    }
  }

  if (!success && indirect_bind) {
	char *p_e;
	Acs_expr_ns_arg ns_args[3];

	log_msg((LOG_TRACE_LEVEL, "Trying indirect bind method"));
	if ((p = kwv_lookup_value(kwv, "LDAP_ADMIN_URL")) == NULL) {
	  errmsg = "No LDAP_ADMIN_URL available";
	  goto fail;
	}
	auth_ldap_admin_url = p;

	if ((p = kwv_lookup_value(kwv, "LDAP_ADMIN_PASSWORD")) == NULL) {
	  p = "";
	  log_msg((LOG_DEBUG_LEVEL, "No LDAP_ADMIN_PASSWORD provided"));
	}
	auth_ldap_admin_password = p;

	if ((p = kwv_lookup_value(kwv, "LDAP_SEARCH_ROOT_DN")) == NULL) {
	  p = "";
	  log_msg((LOG_DEBUG_LEVEL, "No LDAP_SEARCH_ROOT_DN provided"));
	}
	auth_ldap_search_root_dn = p;

	p_e = kwv_lookup_value(kwv, "LDAP_SEARCH_FILTER*");
	p = kwv_lookup_value(kwv, "LDAP_SEARCH_FILTER");
	if (p == NULL && p_e == NULL) {
	  errmsg = "No LDAP_SEARCH_FILTER or LDAP_SEARCH_FILTER* available";
	  goto fail;
	}
	if (p != NULL && p_e != NULL) {
	  errmsg = "Either LDAP_SEARCH_FILTER or LDAP_SEARCH_FILTER* is required";
	  goto fail;
	}

	if (p_e != NULL) {
	  ns_args[0].name = "Args";
	  ns_args[0].kwv = kwv;
	  ns_args[1].name = dacs_conf->conf_var_ns->ns;
	  ns_args[1].kwv = dacs_conf->conf_var_ns->kwv;
	  ns_args[2].name = NULL;
	  ns_args[2].kwv = NULL;
	  acs_expr_string(p_e, ns_args, &auth_ldap_search_filter);
	  if (auth_ldap_search_filter == NULL) {
		errmsg = "Could not form search filter";
		goto fail;
	  }
	}
	else
	  auth_ldap_search_filter = p;

	if ((url = uri_parse(auth_ldap_admin_url)) == NULL) {
	  errmsg = "Can't parse LDAP_ADMIN_URL";
	  goto fail;
	}
	if (url->scheme != NULL && strcaseeq(url->scheme, "ldap")) {
	  if (url->port_given == NULL)
		url->port = LDAP_DEFAULT_PORT;
	}
	else if (url->scheme != NULL && strcaseeq(url->scheme, "ldaps")) {
	  if (url->port_given == NULL)
		url->port = LDAP_DEFAULT_SSL_PORT;
	}
	else {
	  errmsg = "Unrecognized scheme in LDAP_ADMIN_URL";
	  goto fail;
	}
	log_msg((LOG_DEBUG_LEVEL,
			 "auth_ldap_admin_url=\"%s\", auth_ldap_search_root_dn=\"%s\"",
			 auth_ldap_admin_url, auth_ldap_search_root_dn));

	if (strcaseeq(url->scheme, "ldaps")) {
	  errmsg = "ldaps scheme is unsupported in LDAP_ADMIN_URL";
	  goto fail;
	}

	log_msg((LOG_TRACE_LEVEL, "Indirect bind: %s", url->uri));
	st = local_ldap_auth_by_attr(url, auth_ldap_admin_password,
								 auth_ldap_search_root_dn,
								 auth_ldap_search_filter, password, tv, avas);
	if (st == -1) {
	  errmsg = "Username/Password/Aux incorrect";
	  goto fail;
	}
	if (st == 1) {
	  log_msg((LOG_DEBUG_LEVEL, "Authentication was successful"));
	  success = 1;
    }
  }

  if (!success) {
	if (username != NULL)
	  errmsg = ds_xprintf("Could not bind to directory as user \"%s\"",
						  username);
	else
	  errmsg = "Could not bind to directory";
	goto fail;
  }

  if (password != NULL)
	strzap(password);
  if (aux != NULL)
	strzap(aux);

  if (!local_ldap_roles) {
	ok.username = NULL;
	/* If this wasn't specified, dacs_authenticate will use the default. */
	ok.lifetime = kwv_lookup_value(kwv, "CREDENTIALS_LIFETIME_SECS");
	ok.roles_reply = NULL;
  }

  kwv_attrs = kwv_init(40);
  kwv_attrs->dup_mode = KWV_ALLOW_DUPS;
  for (i = 0; i < dsvec_len(avas); i++) {
	Ava *a;

	a = dsvec_ptr(avas, i, Ava *);
	log_msg((LOG_TRACE_LEVEL, "attr=\"%s\" value=\"%s\"",
			 a->attr_name, a->attr_value));
	kwv_add(kwv_attrs, a->attr_name, a->attr_value);
  }

  if ((p = kwv_lookup_value(kwv, "LDAP_USERNAME_EXPR*")) == NULL) {
	log_msg((LOG_DEBUG_LEVEL, "No LDAP_USERNAME_EXPR* directive"));
	mapped_username = username;
  }
  else {
	Acs_expr_ns_arg ns_args[3];

	auth_ldap_username_expr = p;
	ns_args[0].name = "LDAP";
	ns_args[0].kwv = kwv_attrs;
	ns_args[1].name = dacs_conf->conf_var_ns->ns;
	ns_args[1].kwv = dacs_conf->conf_var_ns->kwv;
	ns_args[2].name = NULL;
	ns_args[2].kwv = NULL;
	acs_expr_string(auth_ldap_username_expr, ns_args, &p);
	if (p == NULL) {
	  errmsg = "username string replacement failed";
	  goto fail;
	}
	mapped_username = p;
  }

  role_str = "";
  if ((v = kwv_lookup(kwv, "LDAP_ROLES_SELECTOR*")) != NULL) {
	if (dsvec_len(avas) > 0) {
	  kwv_add(kwv_attrs, "USERNAME", mapped_username);
	  if ((role_str = make_roles(v, kwv_attrs)) != NULL) {
		if (!local_ldap_roles) {
		  ok.roles_reply = ALLOC(Roles_reply);
		  ok.roles_reply->ok = ALLOC(Roles_reply_ok);
		  ok.roles_reply->ok->roles = role_str;
		  ok.roles_reply->failed = NULL;
		  ok.roles_reply->status = NULL;
		}

		log_msg((LOG_TRACE_LEVEL, "Returning role string: \"%s\"", role_str));
	  }
	}
	else
	  log_msg((LOG_TRACE_LEVEL, "No LDAP attributes for roles selection"));
  }
  else
	log_msg((LOG_TRACE_LEVEL, "No LDAP_ROLES_SELECTOR* provided"));

  if (local_ldap_roles)
	emit_roles_reply_ok(stdout, role_str);
  else {
	ok.username = mapped_username;
	printf("%s\n", make_xml_auth_reply_ok(&ok));
	emit_xml_trailer(stdout);
  }

  exit(0);
}
