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

/*
 * When configured, a trusted agent is allowed to request DACS credentials on
 * behalf of a specified user without direct authentication of that user.
 *
 * There are two modes of operation:
 *  1) The user for whom credentials are requested is "known" to the
 *     jurisdiction (possibly capable of authenticating directly) and so
 *     may have roles ("Mode 1")
 *
 *  2) The user for whom credentials are requested is not known to the
 *     jurisdiction (with no roles is the usual sense, therefore).  His
 *     "alien" (extra-jurisdictional) identity needs to be "imported" to this
 *     federation, yielding credentials, username, roles, etc. that
 *     can be used directly within this federation.
 *     The resulting username syntax should reflect this importation,
 *     credentials should be specially marked, and a predicate should be
 *     available to test for importation. ("Mode 2")
 *
 * For either mode, it should be possible to configure accept/reject,
 * dynamic role assignment, and so on.
 *
 * Applications:
 *  o superuser type capability to assume another identity (like su(1))
 *  o testing and debugging
 *  o used by trusted middleware that does its own authentication to acquire
 *    DACS credentials or to map an arbitrary identity (whether
 *    non-DACS or belonging to a different DACS federations) into a DACS
 *    identity
 */

#ifndef lint
static const char copyright[] =
"Copyright (c) 2003-2012\n\
Distributed Systems Software.  All rights reserved.";
static const char revid[] =
  "$Id: auth_agent.c 2594 2012-10-19 17:28:49Z brachman $";
#endif

#include "dacs.h"

static const char *log_module_name = "dacs_auth_agent";

static int
is_valid_external_name(char *name)
{
  char *p;

  for (p = name; *p != '\0'; p++) {
	if (!isprint((int) *p))
	  return(0);
  }

  if (p == name)
	return(0);

  return(1);
}

/*
 * 
 */
static char *
escape_external_name(char *name)
{
  int need_to_escape;
  char *p;

  need_to_escape = 0;
  for (p = name; *p != '\0'; p++) {
	if (!isprint((int) *p))
	  return(NULL);
	if (*p == '%' || *p == ':')
	  need_to_escape = 1;
  }

  if (p == name)
	return(NULL);

  if (need_to_escape)
	return(percent_encode_chars(name, "%:", 0));

  return(strdup(name));
}

/*
 * Return 1 if local USERNAME is recognized or if this feature has not
 * been enabled, 0 if it is enabled but USERNAME is not recognized,
 * and -1 upon error.
 * If enabled, USERNAME must exist in kwv auth_agent_local.
 */
static int
check_local_username(Kwv *kwv_args, char *ext_username,
					 char **replacement_username)
{
  int ns;
  Acs_expr_ns_arg ns_args[10];
  Kwv *x;
  Var_ns *env_ns;
  Vfs_directive *vd;

  if (!is_valid_external_name(ext_username)) {
	log_msg((LOG_ERROR_LEVEL, "Invalid external name"));
	return(-1);
  }

  if ((vd = vfs_lookup_item_type(ITEM_TYPE_AUTH_AGENT_LOCAL)) == NULL) {
	log_msg((LOG_TRACE_LEVEL,
			 "item_type %s is not configured - ok",
			 ITEM_TYPE_AUTH_AGENT_LOCAL));
	return(1);
  }
  if (vd->uri_str != NULL)
	log_msg((LOG_TRACE_LEVEL, "auth_agent_local maps to VFS \"%s\"",
			 vd->uri_str));

  ns = 0;
  x = kwv_init(10);
  ns_args[ns].name = "DACS";
  ns_args[ns++].kwv = x;

  x = kwv_init(10);
  ns_args[ns].name = "Expr";
  kwv_add(x, "_", ext_username);
  ns_args[ns++].kwv = x;

  if (dacs_conf != NULL && dacs_conf->conf_var_ns != NULL) {
	ns_args[ns].name = dacs_conf->conf_var_ns->ns;
	ns_args[ns++].kwv = dacs_conf->conf_var_ns->kwv;
  }

  if ((env_ns = var_ns_from_env("Env")) != NULL) {
	ns_args[ns].name = "Env";
	ns_args[ns++].kwv = env_ns->kwv;
  }

  ns_args[ns].name = NULL;
  ns_args[ns++].kwv = NULL;

  if (acs_expr_list(NULL, "auth_agent_local", ns_args,
					replacement_username) == ACS_EXPR_TRUE) {
	if (is_valid_username(*replacement_username))
	  return(1);
	log_msg((LOG_ERROR_LEVEL, "Invalid mapped name: \"%s\"",
			 *replacement_username));
  }

  log_msg((LOG_ERROR_LEVEL, "Unable to map name: \"%s\"", ext_username));
  return(-1);
}

#ifdef NOTDEF
static Acs_expr_result
eval(char *expr, Kwv *kwv, char **result_str)
{
  char *p;
  Acs_expr_result st;
  Acs_expr_ns_arg ns_args[4];
  Kwv *x;

  x = kwv_init(10);
  if ((p = getenv("REMOTE_USER")) != NULL)
	kwv_add(x, "REMOTE_USER", p);

  ns_args[0].name = "Args";
  ns_args[0].kwv = kwv;
  ns_args[1].name = "DACS";
  ns_args[1].kwv = x;
  ns_args[2].name = dacs_conf->conf_var_ns->ns;
  ns_args[2].kwv = dacs_conf->conf_var_ns->kwv;
  ns_args[3].name = NULL;
  ns_args[3].kwv = NULL;
  st = acs_expr_string(expr, ns_args, result_str);

  if (st == ACS_EXPR_TRUE && result_str != NULL && *result_str != NULL)
	log_msg((LOG_TRACE_LEVEL, "Eval result: %s", *result_str));

  return(st);
}
#endif

/*
 * 
 */
static char *
map_alien_username(char *alien_federation, char *alien_username)
{
  char *fedname, *fed_item_type, *lookup_name, *username;
  Vfs_handle *h;

  if (!is_valid_external_name(alien_federation)) {
	log_msg((LOG_ERROR_LEVEL, "Invalid ALIEN_FEDERATION"));
    return(NULL);
  }

  if (!is_valid_external_name(alien_username)) {
	log_msg((LOG_ERROR_LEVEL, "Invalid ALIEN_USERNAME"));
    return(NULL);
  }

  /*
   * Verify that this alien federation is acceptable and optionally
   * get a replacement name to use for it.
   */
  if ((h = vfs_open_item_type(ITEM_TYPE_AUTH_AGENT_FEDERATIONS)) == NULL) {
	log_msg((LOG_TRACE_LEVEL,
			 "item_type %s is not configured",
			 ITEM_TYPE_AUTH_AGENT_FEDERATIONS));
	return(NULL);
  }

  if ((lookup_name = escape_external_name(alien_federation)) == NULL)
	return(NULL);

  fedname = NULL;
  if (vfs_get(h, lookup_name, (void **) &fedname, NULL) == -1) {
    log_msg((LOG_DEBUG_LEVEL,
             "Lookup of alien federation \"%s\" in auth_agent_federations failed",
             alien_federation));
	return(NULL);
  }

  vfs_close(h);

  if (fedname == NULL || *fedname == '\0')
	fedname = alien_federation;

  if (!is_valid_username(fedname)) {
	log_msg((LOG_ERROR_LEVEL, "Mapped federation name \"%s\" is invalid",
			 fedname));
    return(NULL);
  }

  log_msg((LOG_TRACE_LEVEL, "Alien federation \"%s\" maps to \"%s\"",
		   alien_federation, fedname));

  fed_item_type = ds_xprintf("%s%s",
							 ITEM_TYPE_AUTH_AGENT_FEDERATION_PREFIX, fedname);
  log_msg((LOG_TRACE_LEVEL, "Using item_type \"%s\"", fed_item_type));

  if ((h = vfs_open_item_type(fed_item_type)) == NULL) {
	log_msg((LOG_TRACE_LEVEL, "item_type \"%s\" is not configured",
			 fed_item_type));
	return(NULL);
  }

  if ((lookup_name = escape_external_name(alien_username)) == NULL)
	return(NULL);

  username = NULL;
  if (vfs_get(h, lookup_name, (void **) &username, NULL) == -1) {
    log_msg((LOG_DEBUG_LEVEL,
             "Lookup of alien username \"%s\" in item type \"%s\" failed",
             alien_username, fed_item_type));
	return(NULL);
  }

  vfs_close(h);

  if (username == NULL || *username == '\0')
	username = alien_username;

  if (!is_valid_username(username)) {
	log_msg((LOG_ERROR_LEVEL, "Mapped username \"%s\" is invalid", username));
    return(NULL);
  }

  return(ds_xprintf("[%s]%s", fedname, username));
}

int
main(int argc, char **argv)
{
  int browser, i, is_local_user, rc, selected;
  char *alien_federation, *alien_username;
  char *bp, *errmsg, *jurisdiction, *remote_addr, *sp, *username;
  char *default_lifetime, *it, *lifetime, *replacement_username;
  Auth_failure_reason failure_reason;
  Auth_style auth_style;
  Common_status common_status;
  Credentials *cr, *credentials;
  DACS_app_type app_type;
  Ds *ds_roles;
  Kwv *kwv, *kwv_auth;

  errmsg = "internal";
  username = jurisdiction = NULL;
  failure_reason = AUTH_FAILURE_REASON_UNKNOWN;
  common_status.message = NULL;
  selected = 0;
  browser = 0;
  is_local_user = 0;
  alien_federation = NULL;
  alien_username = NULL;

  if ((remote_addr = getenv("REMOTE_ADDR")) != NULL)
    app_type = DACS_WEB_SERVICE;
  else
    app_type = DACS_UTILITY;

  if (dacs_init(app_type, &argc, &argv, &kwv, &errmsg) == -1) {
  fail:
	if (test_emit_xml_format()) {
	  emit_xml_header(stdout, "dacs_auth_agent");
	  printf("<%s>\n", make_xml_root_element("dacs_auth_agent"));
	  init_common_status(&common_status, NULL, NULL, errmsg);
	  printf("%s", make_xml_common_status(&common_status));
	  printf("</dacs_auth_agent>\n");
	  emit_xml_trailer(stdout);
	}
	else if (test_emit_format(EMIT_FORMAT_JSON)) {
	  emit_json_header(stdout, "dacs_auth_agent");
	  init_common_status(&common_status, NULL, NULL, errmsg);
	  printf("%s", make_json_common_status(&common_status));
	  emit_json_trailer(stdout);
	}
	else if (test_emit_format(EMIT_FORMAT_HTML)) {
	  emit_html_header(stdout, NULL);
	  printf("%s\n", errmsg);
	  emit_html_trailer(stdout);
	}
	else {
	  emit_plain_header(stdout);
	  printf("%s\n", errmsg);
	  emit_plain_trailer(stdout);
	}

	log_msg((LOG_NOTICE_LEVEL, "Authentication failed - exiting"));
	exit(1);
  }

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

  for (i = 0; i < kwv->nused; i++) {
	if (streq(kwv->pairs[i]->name, "USERNAME"))
	  username = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "ALIEN_FEDERATION"))
	  alien_federation = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "ALIEN_USERNAME"))
	  alien_username = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "DACS_VERSION"))
	  ;
	else if (streq(kwv->pairs[i]->name, "DACS_JURISDICTION"))
	  jurisdiction = kwv->pairs[i]->val;
	else if (streq(kwv->pairs[i]->name, "DACS_BROWSER"))
	  browser = (kwv->pairs[i]->val != NULL
				 && streq(kwv->pairs[i]->val, "1"));
	else if (streq(kwv->pairs[i]->name, "OPERATION")) {
	  /* XXX Might handle ADD operation too... */
	  if (strcaseeq(kwv->pairs[i]->val, "SELECT"))
		selected = 1;
	  else {
		log_msg((LOG_WARN_LEVEL, "Unrecognized OPERATION parameter"));
		failure_reason = AUTH_FAILURE_REASON_INVALID_INFO;
		goto fail;
	  }
	}
	else if (streq(kwv->pairs[i]->name, "FORMAT"))
	  ;
	else if (streq(kwv->pairs[i]->name, "COOKIE_SYNTAX")) {
	  if (configure_cookie_syntax(kwv->pairs[i]->val, &errmsg) == -1)
		goto fail;
	}
	else {
	  errmsg = ds_xprintf("Unrecognized parameter: \"%s\"",
						  kwv->pairs[i]->name);
	  failure_reason = AUTH_FAILURE_REASON_USER_ERROR;
	  goto fail;
	}
  }

  if (remote_addr == NULL) {
	log_msg((LOG_WARN_LEVEL, "Can't determine REMOTE_ADDR"));
	goto fail;
  }

  if (jurisdiction == NULL
	  || !name_eq(jurisdiction, conf_val(CONF_JURISDICTION_NAME),
				  DACS_NAME_CMP_CONFIG)) {
	if (jurisdiction == NULL)
	  errmsg = "Missing DACS_JURISDICTION argument";
	else {
	  errmsg = "Authentication request was received by the wrong jurisdiction";
	  log_msg((LOG_ERROR_LEVEL,
			   "Request for jurisdiction %s received by jurisdiction %s",
			   jurisdiction, conf_val(CONF_JURISDICTION_NAME)));
	}
	failure_reason = AUTH_FAILURE_REASON_USER_ERROR;
	goto fail;
  }

  if (alien_federation != NULL || alien_username != NULL) {
	/* The request is import alien (foreign) credentials. */
	if (alien_username == NULL) {
	  errmsg = "ALIEN_USERNAME argument is required";
	  goto fail;
	}
	if (alien_federation == NULL) {
	  errmsg = "ALIEN_FEDERATION argument is required";
	  goto fail;
	}
	/*
	 * We must map the pair (ALIEN_FEDERATION, ALIEN_USERNAME) to a username,
	 * which will be the name for the identity relative to this
	 * jurisdiction.
	 * Note that ALIEN_FEDERATION and/or ALIEN_USERNAME might contain
	 * characters that are invalid within a DACS username.
	 *
	 * We would like such imported identities to have:
	 *  o a common naming syntax for the username that distinguishes imported
	 *    identities from native ones
	 *  o an "imported" flag in the credentials
	 *  o optional roles
	 *
	 * Method:
	 *  o map ALIEN_FEDERATION to a locally-configured, legal short name
	 *  o map ALIEN_USERNAME to its legal, "imported" syntax
	 *  o map the resulting names to a legal DACS username
	 *  o optionally find roles for the resulting DACS username
	 * Revise credentials-to-text code
	 */

	if ((username = map_alien_username(alien_federation, alien_username))
		== NULL) {
	  log_msg((LOG_DEBUG_LEVEL,
			   "Alien username \"%s\" from \"%s\" is disallowed",
			   alien_username, alien_federation));
	  errmsg = "Authentication of alien identity has been denied";
	  goto fail;
	}
	auth_style = AUTH_STYLE_IMPORTED | AUTH_STYLE_ALIEN;
  }
  else {
	/*
	 * The request says that this user is known to this jurisdiction.
	 * XXX We might provide an optional lookup to restrict such names
	 */
	is_local_user = 1;
	if (username == NULL) {
	  errmsg = "Additional authentication arguments are required";
	  goto fail;
	}

	replacement_username = NULL;
	if (check_local_username(kwv, username, &replacement_username) != 1) {
	  log_msg((LOG_DEBUG_LEVEL, "Local username \"%s\" is disallowed",
			   username));
	  errmsg = "Authentication of local identity has been denied";
	  goto fail;
	}
	if (replacement_username != NULL)
	  username = replacement_username;
	auth_style = AUTH_STYLE_IMPORTED;
  }

  if (!is_valid_auth_username(username)) {
	log_msg((LOG_DEBUG_LEVEL, "Invalid local username \"%s\" is disallowed",
			 username));
	errmsg = "Cannot assign invalid username";
	goto fail;
  }

  if (is_dacs_admin_name(username)) {
	log_msg((LOG_DEBUG_LEVEL, "Reserved username \"%s\" is disallowed",
			 username));
	errmsg = "Cannot assign reserved username";
	goto fail;
  }

  default_lifetime = conf_val(CONF_AUTH_CREDENTIALS_DEFAULT_LIFETIME_SECS);
  lifetime = NULL;

  /* XXX should the user agent string for ua_hash be a web service argument? */
  credentials = make_credentials(NULL, NULL, username, remote_addr, "",
								 (lifetime != NULL)
								 ? lifetime : default_lifetime, auth_style,
								 AUTH_VALID_FOR_ACS, NULL, NULL);

  /* Allow an admin identity to be assumed only when enabled. */
  if (is_dacs_admin(credentials)
	  && !conf_val_eq(CONF_AUTH_AGENT_ALLOW_ADMIN_IDENTITY, "yes")) {
	log_msg((LOG_DEBUG_LEVEL, "ADMIN_IDENTITY \"%s\" is disallowed",
			 username));
	errmsg = "Assignment of a DACS admin identity is not permitted";
	goto fail;
  }

  if (vfs_lookup_item_type(ITEM_TYPE_REVOCATIONS) != NULL)
	it = "revocations";
  else
	it = NULL;

  if (it != NULL)
	rc = check_revocation(credentials, kwv, it, 0);
  else
	rc = 0;

  if (rc == 1
	  || (rc == 0 && streq(credentials->valid_for, AUTH_VALID_FOR_NOTHING))) {
	log_msg((LOG_ALERT_LEVEL | LOG_AUDIT_FLAG,
			 "Access has been revoked for '%s'",
			 auth_identity(NULL, conf_val(CONF_JURISDICTION_NAME), username,
						   NULL)));
	errmsg = "Authentication failed";
	/* Don't give any extra detail. */
	failure_reason = AUTH_FAILURE_REASON_INVALID_INFO;
	goto fail;
  }
  else if (rc == -1) {
	log_msg((LOG_ALERT_LEVEL, "Couldn't process revocation list"));
	errmsg = "Revocation testing error";
	goto fail;
  }

  credentials->role_str = "";
  if (is_local_user) {
	ds_roles = ds_init(NULL);
	kwv_auth = kwv_init(10);
	if (collect_roles(username, jurisdiction, kwv, kwv_auth,
					  &ds_roles, &common_status, &failure_reason) == -1) {
	  log_msg((LOG_ERROR_LEVEL, "Couldn't collect roles for \"%s\"",
			   username));
	}
	else
	  credentials->role_str = ds_buf(ds_roles);

	/* One last check */
	if (!is_valid_role_str(credentials->role_str)) {
	  log_msg((LOG_ERROR_LEVEL,
			   "Final role string is invalid, ignoring roles"));
	  credentials->role_str = "";
	}
  }

  if (make_set_auth_cookie_header(credentials, NULL, 0, &bp) == -1) {
	init_common_status(&common_status, NULL, NULL, "Couldn't create cookie");
	goto fail;
  }

  if (selected)
	make_set_scredentials_cookie_header(credentials, &sp);
  else
	sp = NULL;

  if (browser)
	printf("%s%s", bp, (sp != NULL) ? sp : "");

  log_msg((LOG_DEBUG_LEVEL | LOG_AUDIT_FLAG,
		   "Authentication succeeded for %s",
		   auth_identity(NULL, credentials->home_jurisdiction,
						 credentials->username, auth_tracker(credentials))));
  if (test_emit_xml_format()) {
	emit_xml_header(stdout, "dacs_auth_agent");
	printf("<%s>\n", make_xml_root_element("dacs_auth_agent"));
	printf("%s", make_xml_dacs_current_credentials(credentials, NULL, 0));
	printf("</dacs_auth_agent>\n");
	emit_xml_trailer(stdout);
  }
  else if (test_emit_format(EMIT_FORMAT_JSON)) {
	emit_json_header(stdout, "dacs_auth_agent");
	printf("%s", make_json_dacs_current_credentials(credentials, NULL, 0));
	printf(" }\n");
	emit_json_trailer(stdout);
  }
  else if (test_emit_format(EMIT_FORMAT_HTML)) {
	emit_html_header(stdout, NULL);
	cr = credentials;
	printf("New credentials were issued for <B>%s</B>\n",
		   auth_identity(NULL, cr->home_jurisdiction, cr->username, NULL));
	if (cr->role_str != NULL && *cr->role_str != '\0')
	  printf(" with roles <TT><B>%s</B></TT>\n", cr->role_str);
	emit_html_trailer(stdout);
  }
  else {
	emit_plain_header(stdout);
	cr = credentials;
	printf("New credentials were issued for \"%s\"\n",
		   auth_identity(NULL, cr->home_jurisdiction, cr->username, NULL));
	if (cr->role_str != NULL && *cr->role_str != '\0')
	  printf("With roles \"%s\"\n", cr->role_str);
	emit_plain_trailer(stdout);
  }

  exit(0);
}
