[MEDIUM] stats: add an admin level

The stats web interface must be read-only by default to prevent security
holes. As it is now allowed to enable/disable servers, a new keyword
"stats admin" is introduced to activate this admin level, conditioned by ACLs.
(cherry picked from commit 5334bab92ca7debe36df69983c19c21b6dc63f78)
diff --git a/include/common/uri_auth.h b/include/common/uri_auth.h
index bffd694..906cb2c 100644
--- a/include/common/uri_auth.h
+++ b/include/common/uri_auth.h
@@ -43,6 +43,7 @@
 	struct stat_scope *scope;	/* linked list of authorized proxies */
 	struct userlist *userlist;	/* private userlist to emulate legacy "stats auth user:password" */
 	struct list req_acl; 		/* http stats ACL: allow/deny/auth */
+	struct list admin_rules;	/* 'stats admin' rules (chained) */
 	struct uri_auth *next;		/* Used at deinit() to build a list of unique elements */
 };
 
@@ -61,6 +62,12 @@
 #endif
 
 
+struct stats_admin_rule {
+	struct list list;	/* list linked to from the proxy */
+	struct acl_cond *cond;	/* acl condition to meet */
+};
+
+
 /* Various functions used to set the fields during the configuration parsing.
  * Please that all those function can initialize the root entry in order not to
  * force the user to respect a certain order in the configuration file.
diff --git a/include/proto/dumpstats.h b/include/proto/dumpstats.h
index 7038f46..9cf5eec 100644
--- a/include/proto/dumpstats.h
+++ b/include/proto/dumpstats.h
@@ -33,6 +33,7 @@
 #define STAT_SHOW_INFO  0x00000004	/* dump the info part */
 #define STAT_HIDE_DOWN  0x00000008	/* hide 'down' servers in the stats page */
 #define STAT_NO_REFRESH 0x00000010	/* do not automatically refresh the stats page */
+#define STAT_ADMIN      0x00000020	/* indicate a stats admin level */
 #define STAT_BOUND      0x00800000	/* bound statistics to selected proxies/types/services */
 
 #define STATS_TYPE_FE  0
@@ -58,6 +59,7 @@
 #define STAT_STATUS_DONE "DONE"	/* the action is successful */
 #define STAT_STATUS_NONE "NONE"	/* nothing happened (no action chosen or servers state didn't change) */
 #define STAT_STATUS_EXCD "EXCD"	/* an error occured becayse the buffer couldn't store all data */
+#define STAT_STATUS_DENY "DENY"	/* action denied */
 
 
 int stats_accept(struct session *s);
diff --git a/src/cfgparse.c b/src/cfgparse.c
index ef2d9ca..d1ffa37 100644
--- a/src/cfgparse.c
+++ b/src/cfgparse.c
@@ -2533,6 +2533,40 @@
 
 		if (!*args[1]) {
 			goto stats_error_parsing;
+		} else if (!strcmp(args[1], "admin")) {
+			struct stats_admin_rule *rule;
+
+			if (curproxy == &defproxy) {
+				Alert("parsing [%s:%d]: '%s %s' not allowed in 'defaults' section.\n", file, linenum, args[0], args[1]);
+				err_code |= ERR_ALERT | ERR_FATAL;
+				goto out;
+			}
+
+			if (!stats_check_init_uri_auth(&curproxy->uri_auth)) {
+				Alert("parsing [%s:%d]: out of memory.\n", file, linenum);
+				err_code |= ERR_ALERT | ERR_ABORT;
+				goto out;
+			}
+
+			if (strcmp(args[2], "if") != 0 && strcmp(args[2], "unless") != 0) {
+				Alert("parsing [%s:%d] : '%s %s' requires either 'if' or 'unless' followed by a condition.\n",
+				file, linenum, args[0], args[1]);
+				err_code |= ERR_ALERT | ERR_FATAL;
+				goto out;
+			}
+			if ((cond = build_acl_cond(file, linenum, curproxy, (const char **)args + 2)) == NULL) {
+				Alert("parsing [%s:%d] : error detected while parsing a '%s %s' rule.\n",
+				file, linenum, args[0], args[1]);
+				err_code |= ERR_ALERT | ERR_FATAL;
+				goto out;
+			}
+
+			err_code |= warnif_cond_requires_resp(cond, file, linenum);
+
+			rule = (struct stats_admin_rule *)calloc(1, sizeof(*rule));
+			rule->cond = cond;
+			LIST_INIT(&rule->list);
+			LIST_ADDQ(&curproxy->uri_auth->admin_rules, &rule->list);
 		} else if (!strcmp(args[1], "uri")) {
 			if (*(args[2]) == 0) {
 				Alert("parsing [%s:%d] : 'uri' needs an URI prefix.\n", file, linenum);
@@ -2695,7 +2729,7 @@
 			}
 		} else {
 stats_error_parsing:
-			Alert("parsing [%s:%d]: %s '%s', expects 'uri', 'realm', 'auth', 'scope', 'enable', 'hide-version', 'show-node', 'show-desc' or 'show-legends'.\n",
+			Alert("parsing [%s:%d]: %s '%s', expects 'admin', 'uri', 'realm', 'auth', 'scope', 'enable', 'hide-version', 'show-node', 'show-desc' or 'show-legends'.\n",
 			      file, linenum, *args[1]?"unknown stats parameter":"missing keyword in", args[*args[1]?1:0]);
 			err_code |= ERR_ALERT | ERR_FATAL;
 			goto out;
diff --git a/src/dumpstats.c b/src/dumpstats.c
index 5195d8c..f0d6c83 100644
--- a/src/dumpstats.c
+++ b/src/dumpstats.c
@@ -1514,6 +1514,13 @@
 						     "You should retry with less servers at a time.</b>"
 						     "</div>\n", uri->uri_prefix);
 				}
+				else if (strcmp(s->data_ctx.stats.st_code, STAT_STATUS_DENY) == 0) {
+					chunk_printf(&msg,
+						     "<p><div class=active0>"
+						     "<a class=lfsb href=\"%s\" title=\"Remove this message\">[X]</a> "
+						     "<b>Action denied.</b>"
+						     "</div>\n", uri->uri_prefix);
+				}
 				else {
 					chunk_printf(&msg,
 						     "<p><div class=active6>"
@@ -1624,7 +1631,7 @@
 
 	case DATA_ST_PX_TH:
 		if (!(s->data_ctx.stats.flags & STAT_FMT_CSV)) {
-			if (px->cap & PR_CAP_BE && px->srv) {
+			if (px->cap & PR_CAP_BE && px->srv && (s->data_ctx.stats.flags & STAT_ADMIN)) {
 				/* A form to enable/disable this proxy servers */
 				chunk_printf(&msg,
 					"<form action=\"%s\" method=\"post\">",
@@ -1659,7 +1666,7 @@
 				     (uri->flags & ST_SHLGNDS)?"</u>":"",
 				     px->desc ? "desc" : "empty", px->desc ? px->desc : "");
 
-			if (px->cap & PR_CAP_BE && px->srv) {
+			if (px->cap & PR_CAP_BE && px->srv && (s->data_ctx.stats.flags & STAT_ADMIN)) {
 				 /* Column heading for Enable or Disable server */
 				chunk_printf(&msg, "<th rowspan=2 width=1></th>");
 			}
@@ -1699,7 +1706,7 @@
 				     /* name, queue */
 				     "<tr class=\"frontend\">");
 
-				if (px->cap & PR_CAP_BE && px->srv) {
+				if (px->cap & PR_CAP_BE && px->srv && (s->data_ctx.stats.flags & STAT_ADMIN)) {
 					/* Column sub-heading for Enable or Disable server */
 					chunk_printf(&msg, "<td></td>");
 				}
@@ -1867,7 +1874,7 @@
 
 			if (!(s->data_ctx.stats.flags & STAT_FMT_CSV)) {
 				chunk_printf(&msg, "<tr class=socket>");
-				if (px->cap & PR_CAP_BE && px->srv) {
+				if (px->cap & PR_CAP_BE && px->srv && (s->data_ctx.stats.flags & STAT_ADMIN)) {
 					 /* Column sub-heading for Enable or Disable server */
 					chunk_printf(&msg, "<td></td>");
 				}
@@ -2055,10 +2062,13 @@
 					    (sv->state & SRV_BACKUP) ? "backup" : "active", sv_state);
 				}
 
-				chunk_printf(&msg,
-					     "<td><input type=\"checkbox\" name=\"s\" value=\"%s\"></td>"
-					     "<td class=ac",
-					     sv->id);
+				if (px->cap & PR_CAP_BE && px->srv && (s->data_ctx.stats.flags & STAT_ADMIN)) {
+					chunk_printf(&msg,
+						"<td><input type=\"checkbox\" name=\"s\" value=\"%s\"></td>",
+						sv->id);
+				}
+
+				chunk_printf(&msg, "<td class=ac");
 
 				if (uri->flags&ST_SHLGNDS) {
 					char str[INET6_ADDRSTRLEN];
@@ -2391,7 +2401,7 @@
 		    (!(s->data_ctx.stats.flags & STAT_BOUND) || (s->data_ctx.stats.type & (1 << STATS_TYPE_BE)))) {
 			if (!(s->data_ctx.stats.flags & STAT_FMT_CSV)) {
 				chunk_printf(&msg, "<tr class=\"backend\">");
-				if (px->cap & PR_CAP_BE && px->srv) {
+				if (px->cap & PR_CAP_BE && px->srv && (s->data_ctx.stats.flags & STAT_ADMIN)) {
 					/* Column sub-heading for Enable or Disable server */
 					chunk_printf(&msg, "<td></td>");
 				}
@@ -2584,7 +2594,7 @@
 		if (!(s->data_ctx.stats.flags & STAT_FMT_CSV)) {
 			chunk_printf(&msg, "</table>");
 
-			if (px->cap & PR_CAP_BE && px->srv) {
+			if (px->cap & PR_CAP_BE && px->srv && (s->data_ctx.stats.flags & STAT_ADMIN)) {
 				/* close the form used to enable/disable this proxy servers */
 				chunk_printf(&msg,
 					"Choose the action to perform on the checked servers : "
diff --git a/src/proto_http.c b/src/proto_http.c
index 3d408e1..0cbfef2 100644
--- a/src/proto_http.c
+++ b/src/proto_http.c
@@ -3152,14 +3152,38 @@
 	}
 
 	if (do_stats) {
-		/* We need to provied stats for this request.
+		struct stats_admin_rule *stats_admin_rule;
+
+		/* We need to provide stats for this request.
 		 * FIXME!!! that one is rather dangerous, we want to
 		 * make it follow standard rules (eg: clear req->analysers).
 		 */
 
+		/* now check whether we have some admin rules for this request */
+		list_for_each_entry(stats_admin_rule, &s->be->uri_auth->admin_rules, list) {
+			int ret = 1;
+
+			if (stats_admin_rule->cond) {
+				ret = acl_exec_cond(stats_admin_rule->cond, s->be, s, &s->txn, ACL_DIR_REQ);
+				ret = acl_pass(ret);
+				if (stats_admin_rule->cond->pol == ACL_COND_UNLESS)
+					ret = !ret;
+			}
+
+			if (ret) {
+				/* no rule, or the rule matches */
+				s->data_ctx.stats.flags |= STAT_ADMIN;
+				break;
+			}
+		}
+
 		/* Was the status page requested with a POST ? */
 		if (txn->meth == HTTP_METH_POST) {
-			http_process_req_stat_post(s, req);
+			if (s->data_ctx.stats.flags & STAT_ADMIN) {
+				http_process_req_stat_post(s, req);
+			} else {
+				s->data_ctx.stats.st_code = STAT_STATUS_DENY;
+			}
 		}
 
 		s->logs.tv_request = now;
@@ -7124,6 +7148,8 @@
 				t->data_ctx.stats.st_code = STAT_STATUS_NONE;
 			else if (memcmp(h, STAT_STATUS_EXCD, 4) == 0)
 				t->data_ctx.stats.st_code = STAT_STATUS_EXCD;
+			else if (memcmp(h, STAT_STATUS_DENY, 4) == 0)
+				t->data_ctx.stats.st_code = STAT_STATUS_DENY;
 			else
 				t->data_ctx.stats.st_code = STAT_STATUS_UNKN;
 			break;
diff --git a/src/uri_auth.c b/src/uri_auth.c
index 6b2ca2a..fdbcef0 100644
--- a/src/uri_auth.c
+++ b/src/uri_auth.c
@@ -32,6 +32,7 @@
 			goto out_u;
 
 		LIST_INIT(&u->req_acl);
+		LIST_INIT(&u->admin_rules);
 	} else
 		u = *root;