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

/*
 * Create and check access tokens
 */

#ifndef lint
static const char copyright[] =
"Copyright (c) 2003-2012\n\
Distributed Systems Software.  All rights reserved.";
static const char revid[] =
  "$Id: acs_token.c 2586 2012-03-15 16:21:40Z brachman $";
#endif

#include "auth.h"
#include "acs.h"

static char *log_module_name = "acs_token";

static char *
make_token_cookie_name(Acs_token *token)
{
  char *name;

  name = ds_xprintf("DACS:%s::TOKEN-%s", token->issued_by, token->unique);

  return(name);
}

static MAYBE_UNUSED int
make_set_void_token_cookie_header(Acs_token *token, char **buf)
{
  char *name;

  name = make_token_cookie_name(token);

  return(make_set_void_cookie_header(name, 1, buf));
}

static int
make_token_value(char *str, char **value)
{
  Crypt_keys *ck;
  Hmac_handle *conf;
  unsigned char outbuf[256];

  ck = crypt_keys_from_vfs(ITEM_TYPE_JURISDICTION_KEYS);
  conf = crypto_hmac_open("SHA1", ck->hmac_key, CRYPTO_HMAC_KEY_LENGTH);
  crypt_keys_free(ck);
  crypto_hmac_hash(conf, (unsigned char *) str, strlen(str));
  crypto_hmac_close(conf, outbuf, NULL);
  strba64(outbuf, CRYPTO_HMAC_BYTE_LENGTH, value);

  return(0);
}

/*
 * Create a Set-Cookie directive for an access token.
 * The name of the token includes the federation name and the jurisdiction
 * name for which it is valid, as well as a (hopefully) unique component
 * since many access tokens might be issued from the same jurisdiction.
 * The unique component is used as a key to lookup the token.
 * The value of the token is a secure keyed hash of the unique component,
 * using a jurisdiction-specific key.
 */
static int
make_set_token_cookie_header(Acs_token *token, char **cookie_buf)
{
  char *cookie, *name, *value;

  make_token_value(token->unique, &value);
  name = make_token_cookie_name(token);
  cookie = ds_xprintf("%s=%s", name, value);

  make_set_cookie_header(cookie, NULL, 1, 0, cookie_buf);

  return(1);
}

static void
flatten_token(Acs_token *token, Kwv *token_kwv, char **value,
			  unsigned int *valuelen)
{
  char *p;
  Credentials *cr;
  Kwv *kwv;

  if (token_kwv == NULL) {
	kwv = kwv_init(10);
	kwv->dup_mode = KWV_ALLOW_DUPS;

	kwv_add(kwv, "ISSUED_BY", token->issued_by);
	kwv_add(kwv, "URL_PATH", token->url_path);
	if (token->identity != NULL)
	  kwv_add(kwv, "IDENTITY", token->identity);
	for (cr = token->credentials; cr != NULL; cr = cr->next)
	  kwv_add(kwv, "CREDENTIALS", cr->unique);
	kwv_add(kwv, "EXPIRES", ds_xprintf("%lu", token->expires));
	kwv_add(kwv, "LIMIT", ds_xprintf("%ld", token->limit));
	if (token->attrs != NULL) {
	  if (token->attrs->constraint != NULL)
		kwv_add(kwv, "ATTR_CONSTRAINT", token->attrs->constraint);
	  if (token->attrs->permit_chaining != NULL)
		kwv_add(kwv, "ATTR_PERMIT_CHAINING", token->attrs->permit_chaining);
	  if (token->attrs->pass_credentials != NULL)
		kwv_add(kwv, "ATTR_PASS_CREDENTIALS", token->attrs->pass_credentials);
	  if (token->attrs->pass_http_cookie != NULL)
		kwv_add(kwv, "ATTR_PASS_HTTP_COOKIE", token->attrs->pass_http_cookie);
	}
	kwv_add(kwv, "UNIQUE", token->unique);
  }
  else
	kwv = token_kwv;

  p = kwv_str(kwv);
  *valuelen = strba64((unsigned char *) p, strlen(p) + 1, value);
}

/*
 * The default will be to replace an existing item.
 * The same DB can be used for multiple federations/jurisdictions,
 * provided the keys are "unique enough".
 */
static int
set_token(Acs_token *token, Kwv *kwv)
{
  unsigned int valuelen;
  char *key, *value;
  Vfs_handle *h;

  if (token != NULL)
	key = token->unique;
  else
	key = kwv_lookup_value(kwv, "UNIQUE");

  flatten_token(token, kwv, &value, &valuelen);

  if ((h = vfs_open_item_type("tokens")) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Cannot open item type \"tokens\""));
	return(-1);
  }

  /* Null-terminate the value. */
  if (vfs_put(h, key, value, valuelen) == -1) {
	if (h->error_msg != NULL)
	  log_msg((LOG_ERROR_LEVEL, "%s",
			   ds_xprintf("vfs_put with key=\"%s\" failed: %s",
						  key, h->error_msg)));
	else
	  log_msg((LOG_ERROR_LEVEL,
			   ds_xprintf("vfs_put with key=\"%s\" failed", key)));
	return(-1);
  }

  if ((vfs_close(h)) == -1) {
	log_msg((LOG_ERROR_LEVEL, "Error close/put access token: vfs_close"));
	return(-1);
  }

  return(0);
}

Kwv *
access_token_get(char *key)
{
  char *enc_t, *t;
  Kwv *kwv;
  Vfs_handle *h;
  Kwv_conf kwv_conf = {
	"=", NULL, " ", KWV_CONF_DEFAULT, NULL, 10, NULL, NULL
  };

  if ((h = vfs_open_item_type("tokens")) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Cannot open item type \"tokens\""));
	return(NULL);
  }

  if (vfs_get(h, key, (void *) &enc_t, NULL) == -1) {
	if (h->error_msg != NULL)
	  log_msg((LOG_ERROR_LEVEL, "%s",
			   ds_xprintf("vfs_get with key=\"%s\" failed: %s",
						  key, h->error_msg)));
	else
	  log_msg((LOG_ERROR_LEVEL,
			   ds_xprintf("vfs_get with key=\"%s\" failed", key)));
	return(NULL);
  }

  log_msg((LOG_TRACE_LEVEL, "access_token_get read: %s", enc_t));

  if (stra64b(enc_t, (unsigned char **) &t, NULL) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Access token decoding error"));
	return(NULL);
  }
  log_msg((LOG_TRACE_LEVEL, "access_token_get found: %s", t));

  if ((kwv = kwv_make_new(t, &kwv_conf)) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Access token parsing error"));
	return(NULL);
  }

  if ((vfs_close(h)) == -1) {
	log_msg((LOG_ERROR_LEVEL, "Error close/get access token: vfs_close"));
	return(NULL);
  }

  return(kwv);
}

static int
list_add(char *naming_context, char *name, void ***names)
{
  Dsvec *dsv;

  dsv = (Dsvec *) names;
  dsvec_add_ptr(dsv, strdup(name));

  return(1);
}

Dsvec *
access_token_list(void)
{
  int i, n;
  char **keys;
  Dsvec *dsv, *dsv_kwv;
  Vfs_handle *h;

  if ((h = vfs_open_item_type("tokens")) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Cannot open item type \"tokens\""));
	return(NULL);
  }

  dsv = dsvec_init(NULL, sizeof(char *));
  if ((n = vfs_list(h, NULL, NULL, list_add, (void ***) dsv)) == -1) {
	if (h->error_msg != NULL)
	  log_msg((LOG_ERROR_LEVEL, "%s",
			   ds_xprintf("vfs_list failed: %s", h->error_msg)));
	else
	  log_msg((LOG_ERROR_LEVEL, "vfs_list failed"));
	return(NULL);
  }

  keys = (char **) dsvec_base(dsv);
  dsv_kwv = dsvec_init(NULL, sizeof(Kwv *));
  for (i = 0; i < n; i++) {
	Kwv *kwv;

	if ((kwv = access_token_get(keys[i])) == NULL) {
	  log_msg((LOG_ERROR_LEVEL,
			   ds_xprintf("Could not get key \"%s\"", keys[i])));
	  continue;
	}
	dsvec_add_ptr(dsv_kwv, kwv);
  }
  
  if (vfs_close(h) == -1)
	log_msg((LOG_ERROR_LEVEL, "vfs_close failed"));	

  return(dsv_kwv);
}

static int
do_delete(Vfs_handle *h, char *key)
{

  if (vfs_delete(h, key) == -1) {
	if (h->error_msg != NULL)
	  log_msg((LOG_ERROR_LEVEL, "%s",
			   ds_xprintf("vfs_delete with key=\"%s\" failed: %s",
						  key, h->error_msg)));
	else
	  log_msg((LOG_ERROR_LEVEL,
			   ds_xprintf("vfs_delete with key=\"%s\" failed", key)));
	return(-1);
  }

  return(0);
}

int
access_token_delete(char *key)
{
  Vfs_handle *h;

  if ((h = vfs_open_item_type("tokens")) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Cannot open item type \"tokens\""));
	return(-1);
  }

  if (do_delete(h, key) == -1) {
	if (h->error_msg != NULL)
	  log_msg((LOG_ERROR_LEVEL, "%s",
			   ds_xprintf("vfs_delete with key=\"%s\" failed: %s",
						  key, h->error_msg)));
	else
	  log_msg((LOG_ERROR_LEVEL,
			   ds_xprintf("vfs_delete with key=\"%s\" failed", key)));
	return(-1);
  }

  if ((vfs_close(h)) == -1) {
	log_msg((LOG_ERROR_LEVEL, "Error close/delete access token: vfs_close"));
	return(-1);
  }

  return(0);
}

/*
 * Create an access token for this jurisdiction.  This involves storing
 * an entry locally (via item type 'tokens') and returning a cookie to the
 * user that points to the entry.
 * Return 1 if successful, 0 if no token can be issued, -1 on error.
 */
int
acs_token_create(Credentials *selected, Acs_result *result, char **cookie_buf)
{
  unsigned int lifetime_secs, lifetime_limit;
  char *p;
  Acs_token token;

  if (!conf_val_eq(CONF_ACS_ACCESS_TOKEN_ENABLE, "yes"))
	return(0);

#ifdef ENABLE_ACCESS_TOKENS
  log_msg((LOG_WARN_LEVEL, "Experimental access tokens are being created"));
#else
  log_msg((LOG_NOTICE_LEVEL, "DACS has not been built to use access tokens"));
  log_msg((LOG_NOTICE_LEVEL, "See dacs.install(7)"));
  return(-1);
#endif

  /*
   * Check that the rule is suitable for an access token.
   * For example, assume the request is for /a/b/c.cgi
   * If the matching effective url_pattern is a wildcard, then
   * a token cannot be issued if there is a rule in the
   * jurisdiction having a url_pattern that falls under it.
   * This is because that other rule may have different access control
   * restrictions than the rule for /a/b/c.cgi; we don't want a subsequent
   * request for, say, /a/b/d.cgi to automatically be granted by the token
   * based on "/a/ *" rather than for the specific rule for /a/b/d.cgi.
   */
  if (!result->m.exact && result->m.subordinate != NULL) {
	log_msg((LOG_INFO_LEVEL, "Can't issue token, subordinate exists"));
	return(0);
  }

  token.issued_by = dacs_current_jurisdiction();

  /*
   * Note: only the path of the matching Service is significant.
   * This may be more restrictive than necessary.
   */
  token.url_path = result->m.s->url_path;

  if (result->cr != NULL)
	token.identity = result->cr->unique;
  else
	token.identity = NULL;
  token.credentials = selected;

  if ((p = conf_val(CONF_ACS_ACCESS_TOKEN_LIFETIME_SECS)) != NULL) {
	if (strnum(p, STRNUM_UI, &lifetime_secs) == -1 || lifetime_secs == 0) {
	  log_msg((LOG_WARN_LEVEL,
			   "invalid ACS_ACCESS_TOKEN_LIFETIME_SECS value"));
	  log_msg((LOG_WARN_LEVEL, "Access tokens are disabled"));
	  return(-1);
	}
	token.expires = time(NULL) + lifetime_secs;
  }
  else
	token.expires = 0;

  if ((p = conf_val(CONF_ACS_ACCESS_TOKEN_LIFETIME_LIMIT)) != NULL) {
	if (strnum(p, STRNUM_UI, &lifetime_limit) == -1 || lifetime_limit == 0) {
	  log_msg((LOG_WARN_LEVEL,
			   "Invalid ACS_ACCESS_TOKEN_LIFETIME_LIMIT value"));
	  log_msg((LOG_WARN_LEVEL, "Access tokens are disabled"));
	  return(-1);
	}
	token.limit = lifetime_limit;
  }
  else
	token.limit = -1;

  if (token.expires == 0 && token.limit == -1) {
	log_msg((LOG_WARN_LEVEL,
			 "At least one of ACS_ACCESS_TOKEN_LIFETIME_SECS and ACS_ACCESS_TOKEN_LIFETIME_LIMIT must be configured"));
	log_msg((LOG_WARN_LEVEL, "Access tokens are disabled"));
	return(-1);
  }

  token.attrs = &result->attrs;
  token.unique = crypto_make_random_a64("", 16);

  if (set_token(&token, NULL) == -1) {
	log_msg((LOG_ERROR_LEVEL, "Couldn't store access token"));
	return(-1);
  }

  make_set_token_cookie_header(&token, cookie_buf);

  log_msg((LOG_INFO_LEVEL, "Access token created"));

  return(1);
}

/*
 * At startup time, every Nth startup, or using a random distribution,
 * we should iterate through all stored tokens and delete the
 * invalid/expired ones.
 * At sign off time this could be called to delete entries that are
 * no longer valid.
 */
int
acs_token_cleanup(void)
{
  int i, n;
  char **keys, *v;
  long limit;
  time_t expires, now;
  Dsvec *dsv;
  Kwv *token_kwv;
  Kwv_pair *pair;
  Vfs_handle *h;

  if (!conf_val_eq(CONF_ACS_ACCESS_TOKEN_ENABLE, "yes"))
	return(0);

  if ((h = vfs_open_item_type("tokens")) == NULL) {
	log_msg((LOG_ERROR_LEVEL, "Cannot open item type \"tokens\""));
	return(0);
  }

  dsv = dsvec_init(NULL, sizeof(char *));
  if ((n = vfs_list(h, NULL, NULL, list_add, (void ***) dsv)) == -1) {
	if (h->error_msg != NULL)
	  log_msg((LOG_ERROR_LEVEL, "%s",
			   ds_xprintf("vfs_list failed: %s", h->error_msg)));
	else
	  log_msg((LOG_ERROR_LEVEL, "vfs_list failed"));
	return(-1);
  }

  keys = (char **) dsvec_base(dsv);
  for (i = 0; i < n; i++) {
	printf("%s\n", keys[i]);
	if ((token_kwv = access_token_get(keys[i])) == NULL) {
	  log_msg((LOG_WARN_LEVEL, "acs_token_cleanup: access_token_get() failed?"));
	  do_delete(h, keys[i]);
	  continue;
	}
	if (kwv_lookup_value(token_kwv, "ISSUED_BY") == NULL) {
	  do_delete(h, keys[i]);
	  continue;
	}
	if ((v = kwv_lookup_value(token_kwv, "UNIQUE")) == NULL
		|| !streq(v, keys[i])) {
	  do_delete(h, keys[i]);
	  continue;
	}

	time(&now);
	if ((v = kwv_lookup_value(token_kwv, "EXPIRES")) == NULL || *v == '\0') {
	  do_delete(h, keys[i]);
	  continue;
	}
	if (strnum(v, STRNUM_TIME_T, &expires) == -1) {
	  do_delete(h, keys[i]);
	  continue;
	}
	if (expires != 0 && now >= expires) {
	  do_delete(h, keys[i]);
	  continue;
	}

	if ((pair = kwv_lookup(token_kwv, "LIMIT")) == NULL
		  || pair->val[0] == '\0') {
	  do_delete(h, keys[i]);
	  continue;
	}
	v = pair->val;
	if (strnum(v, STRNUM_L, &limit) == -1) {
	  do_delete(h, keys[i]);
	  continue;
	}
	if (limit == 0) {
	  do_delete(h, keys[i]);
	  continue;
	}
  }

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

  return(0);
}

/*
 * Look for an access token that grants access to URI.
 * Return 1 if one is found, 0 if not, -1 on error.
 */
int
acs_token_grants(Credentials *selected, char *uri, Cookie *cookies,
				 Acs_result *result)
{
  int exact, token_grants;
  char *key, *identity, *value, *v;
  long limit;
  time_t expires, now;
  Cookie *c;
  Credentials *cr;
  Dsvec *dsv;
  Kwv *token_kwv;
  Kwv_pair *pair;

  if (!conf_val_eq(CONF_ACS_ACCESS_TOKEN_ENABLE, "yes"))
	return(0);

#ifdef ENABLE_ACCESS_TOKENS
  log_msg((LOG_WARN_LEVEL, "Experimental access tokens may be used"));
#else
  log_msg((LOG_NOTICE_LEVEL, "DACS has not been built to use access tokens"));
  log_msg((LOG_NOTICE_LEVEL, "See dacs.install(7)"));
  return(-1);
#endif

  token_grants = 0;
  for (c = cookies; c != NULL; c = c->next) {
	if (c->parsed_name->special != NULL
		&& strneq(c->parsed_name->special, "TOKEN-", 6)) {
	  key = c->parsed_name->special + 6;
	  /*
	   * Validate the key, then look it up and compare the stored information
	   * to the request.  If ok, initialize RESULT.
	   */
	  make_token_value(key, &value);
	  if (!streq(c->value, value)) {
		log_msg((LOG_WARN_LEVEL, "Invalid token found... ignoring"));
		c->set_void = 1;
		continue;
	  }
	  log_msg((LOG_WARN_LEVEL, "Token appears to be valid"));

	  if ((token_kwv = access_token_get(key)) == NULL) {
		log_msg((LOG_WARN_LEVEL, "Token lookup failed... ignoring"));
		c->set_void = 1;
		continue;
	  }

	  /*
	   * XXX Should we check that the token is coming from the same IP address
	   * as when it was issued?  This is a problematic test...
	   */
	  if ((v = kwv_lookup_value(token_kwv, "ISSUED_BY")) == NULL
		  || !streq(v, dacs_current_jurisdiction()))
		continue;
	  if ((v = kwv_lookup_value(token_kwv, "UNIQUE")) == NULL
		  || !streq(v, key))
		continue;

	  time(&now);
	  if ((v = kwv_lookup_value(token_kwv, "EXPIRES")) == NULL || *v == '\0') {
		c->set_void = 1;
		access_token_delete(key);
		continue;
	  }
	  if (strnum(v, STRNUM_TIME_T, &expires) == -1) {
		c->set_void = 1;
		access_token_delete(key);
		continue;
	  }
	  if (expires != 0 && now >= expires) {
		c->set_void = 1;
		access_token_delete(key);
		log_msg((LOG_INFO_LEVEL, "Expired token deleted"));
		continue;
	  }

	  if ((pair = kwv_lookup(token_kwv, "LIMIT")) == NULL
		  || pair->val[0] == '\0') {
		c->set_void = 1;
		access_token_delete(key);
		continue;
	  }
	  v = pair->val;
	  if (strnum(v, STRNUM_L, &limit) == -1) {
		c->set_void = 1;
		access_token_delete(key);
		continue;
	  }
	  if (limit == 0) {
		c->set_void = 1;
		access_token_delete(key);
		log_msg((LOG_INFO_LEVEL, "Limited token deleted"));
		continue;
	  }
	  else if (limit > 0) {
		limit--;
		pair->val = ds_xprintf("%ld", limit);
		value = kwv_str(token_kwv);
		/* Update the stored access token */
		if (set_token(NULL, token_kwv) == -1) {
		  log_msg((LOG_ERROR_LEVEL, "Token replacement failed!"));
		  access_token_delete(key);
		  c->set_void = 1;
		}
	  }

	  /*
	   * Verify that all credentials in effect at the time the token was
	   * issued are still in effect now.  This means that a bad guy must
	   * not only capture an access token cookie, but also all of credentials
	   * that the user had obtained.
	   * We also locate the credentials, if any, that were associated with
	   * the original granting of access because that identity will also be
	   * associated with access token grants.
	   */
	  identity = kwv_lookup_value(token_kwv, "IDENTITY");
	  for (pair = kwv_lookup(token_kwv, "CREDENTIALS"); pair != NULL;
		   pair = pair->next) {
		for (cr = selected; cr != NULL; cr = cr->next) {
		  if (identity != NULL && streq(cr->unique, identity))
			result->cr = cr;
		  if (streq(cr->unique, pair->val))
			break;
		}
		if (cr == NULL)
		  continue;
	  }

	  /*
	   * We don't need to search for the URL matching most closely
	   * because an access token is issued only if there is no better match.
	   * Do this last because we're weeding out invalid tokens as we
	   * do this and we want to get all of them.
	   */
	  if ((v = kwv_lookup_value(token_kwv, "URL_PATH")) == NULL)
		continue;

	  if ((dsv = uri_path_parse(uri)) == NULL) {
		log_msg((LOG_ERROR_LEVEL, "URL decode failed: %s", uri));
		continue;
	  }
	  if (!acs_match_url_segs(v, dsv, &exact))
		continue;

	  if ((v = kwv_lookup_value(token_kwv, "ATTR_CONSTRAINT")) != NULL)
		result->attrs.constraint = v;
	  if ((v = kwv_lookup_value(token_kwv, "ATTR_PERMIT_CHAINING")) != NULL)
		result->chaining = result->attrs.permit_chaining = v;
	  if ((v = kwv_lookup_value(token_kwv, "ATTR_PASS_CREDENTIALS")) != NULL)
		result->pass = result->attrs.pass_credentials = v;
	  if ((v = kwv_lookup_value(token_kwv, "ATTR_PASS_HTTP_COOKIE")) != NULL)
		result->pass_http_cookie = result->attrs.pass_http_cookie = v;

	  /*
	   * We will continue to validate the remaining cookies even though
	   * we've found a match.  This isn't necessary but will keep the
	   * client's cookie cache less cluttered.
	   * XXX Maybe reconsider whether this should be done here...
	   */
	  token_grants = 1;
	}
  }

  if (!token_grants && trace_level > 1)
	log_msg((LOG_INFO_LEVEL, "No access token found"));

  return(token_grants);
}
