You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ja...@apache.org on 2010/07/26 13:20:18 UTC

svn commit: r979242 - in /couchdb/trunk: etc/couchdb/local.ini share/www/script/test/config.js src/couchdb/couch_httpd_misc_handlers.erl

Author: jan
Date: Mon Jul 26 11:20:18 2010
New Revision: 979242

URL: http://svn.apache.org/viewvc?rev=979242&view=rev
Log:
Multi-part patch to enable white-listing of _config API values:

 1. Refactor read-only config handlers to be near each other

 2. Refactor PUT and DELETE config handlers to a wrapper

 3. Support a whitelist for modifying the config via HTTP, itself stored in the config

 4. Document the whitelist process

Patch(es) by Jason Smith.

Closes COUCHDB-835.

Modified:
    couchdb/trunk/etc/couchdb/local.ini
    couchdb/trunk/share/www/script/test/config.js
    couchdb/trunk/src/couchdb/couch_httpd_misc_handlers.erl

Modified: couchdb/trunk/etc/couchdb/local.ini
URL: http://svn.apache.org/viewvc/couchdb/trunk/etc/couchdb/local.ini?rev=979242&r1=979241&r2=979242&view=diff
==============================================================================
--- couchdb/trunk/etc/couchdb/local.ini (original)
+++ couchdb/trunk/etc/couchdb/local.ini Mon Jul 26 11:20:18 2010
@@ -13,6 +13,13 @@
 ; Uncomment next line to trigger basic-auth popup on unauthorized requests.
 ;WWW-Authenticate = Basic realm="administrator"
 
+; Uncomment next line to set the configuration modification whitelist. Only
+; whitelisted values may be changed via the /_config URLs. To allow the admin
+; to change this value over HTTP, remember to include {httpd,config_whitelist}
+; itself. Excluding it from the list would require editing this file to update
+; the whitelist.
+;config_whitelist = [{httpd,config_whitelist}, {log,level}, {etc,etc}]
+
 [couch_httpd_auth]
 ; If you set this to true, you should also uncomment the WWW-Authenticate line
 ; above. If you don't configure a WWW-Authenticate header, CouchDB will send

Modified: couchdb/trunk/share/www/script/test/config.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/test/config.js?rev=979242&r1=979241&r2=979242&view=diff
==============================================================================
--- couchdb/trunk/share/www/script/test/config.js (original)
+++ couchdb/trunk/share/www/script/test/config.js Mon Jul 26 11:20:18 2010
@@ -50,7 +50,8 @@ couchTests.config = function(debug) {
   T(config.log.level);
   T(config.query_servers.javascript);
 
-  // test that settings can be altered
+  // test that settings can be altered, and that an undefined whitelist allows any change
+  TEquals(undefined, config.httpd.config_whitelist, "Default whitelist is empty");
   xhr = CouchDB.request("PUT", "/_config/test/foo",{
     body : JSON.stringify("bar"),
     headers: {"X-Couch-Persist": "false"}
@@ -64,4 +65,93 @@ couchTests.config = function(debug) {
   xhr = CouchDB.request("GET", "/_config/test/foo");
   config = JSON.parse(xhr.responseText);
   T(config == "bar");
+
+  // Non-term whitelist values allow further modification of the whitelist.
+  xhr = CouchDB.request("PUT", "/_config/httpd/config_whitelist",{
+    body : JSON.stringify("!This is an invalid Erlang term!"),
+    headers: {"X-Couch-Persist": "false"}
+  });
+  TEquals(200, xhr.status, "Set config whitelist to an invalid Erlang term");
+  xhr = CouchDB.request("DELETE", "/_config/httpd/config_whitelist",{
+    headers: {"X-Couch-Persist": "false"}
+  });
+  TEquals(200, xhr.status, "Modify whitelist despite it being invalid syntax");
+
+  // Non-list whitelist values allow further modification of the whitelist.
+  xhr = CouchDB.request("PUT", "/_config/httpd/config_whitelist",{
+    body : JSON.stringify("{[yes, a_valid_erlang_term, but_unfortunately, not_a_list]}"),
+    headers: {"X-Couch-Persist": "false"}
+  });
+  TEquals(200, xhr.status, "Set config whitelist to an non-list term");
+  xhr = CouchDB.request("DELETE", "/_config/httpd/config_whitelist",{
+    headers: {"X-Couch-Persist": "false"}
+  });
+  TEquals(200, xhr.status, "Modify whitelist despite it not being a list");
+
+  // Keys not in the whitelist may not be modified.
+  xhr = CouchDB.request("PUT", "/_config/httpd/config_whitelist",{
+    body : JSON.stringify("[{httpd,config_whitelist}, {test,foo}]"),
+    headers: {"X-Couch-Persist": "false"}
+  });
+  TEquals(200, xhr.status, "Set config whitelist to something valid");
+
+  ["PUT", "DELETE"].forEach(function(method) {
+    ["test/not_foo", "not_test/foo", "neither_test/nor_foo"].forEach(function(pair) {
+      var path = "/_config/" + pair;
+      var test_name = method + " to " + path + " disallowed: not whitelisted";
+
+      xhr = CouchDB.request(method, path, {
+        body : JSON.stringify("Bummer! " + test_name),
+        headers: {"X-Couch-Persist": "false"}
+      });
+      TEquals(400, xhr.status, test_name);
+    });
+  });
+
+  // Keys in the whitelist may be modified.
+  ["PUT", "DELETE"].forEach(function(method) {
+    xhr = CouchDB.request(method, "/_config/test/foo",{
+      body : JSON.stringify(method + " to whitelisted config variable"),
+      headers: {"X-Couch-Persist": "false"}
+    });
+    TEquals(200, xhr.status, "Keys in the whitelist may be modified");
+  });
+
+  // Non-2-tuples in the whitelist are ignored
+  xhr = CouchDB.request("PUT", "/_config/httpd/config_whitelist",{
+    body : JSON.stringify("[{httpd,config_whitelist}, these, {are}, {nOt, 2, tuples}," +
+                          " [so], [they, will], [all, become, noops], {test,foo}]"),
+    headers: {"X-Couch-Persist": "false"}
+  });
+  TEquals(200, xhr.status, "Set config whitelist with some inert values");
+  ["PUT", "DELETE"].forEach(function(method) {
+    xhr = CouchDB.request(method, "/_config/test/foo",{
+      body : JSON.stringify(method + " to whitelisted config variable"),
+      headers: {"X-Couch-Persist": "false"}
+    });
+    TEquals(200, xhr.status, "Update whitelisted variable despite invalid entries");
+  });
+
+  // Atoms, binaries, and strings suffice as whitelist sections and keys.
+  ["{test,foo}", '{"test","foo"}', '{<<"test">>,<<"foo">>}'].forEach(function(pair) {
+    xhr = CouchDB.request("PUT", "/_config/httpd/config_whitelist",{
+      body : JSON.stringify("[{httpd,config_whitelist}, " + pair + "]"),
+      headers: {"X-Couch-Persist": "false"}
+    });
+    TEquals(200, xhr.status, "Set config whitelist to include " + pair);
+
+    var pair_format = {"t":"tuple", '"':"string", "<":"binary"}[pair[1]];
+    ["PUT", "DELETE"].forEach(function(method) {
+      xhr = CouchDB.request(method, "/_config/test/foo",{
+        body : JSON.stringify(method + " with " + pair_format),
+        headers: {"X-Couch-Persist": "false"}
+      });
+      TEquals(200, xhr.status, "Whitelist works with " + pair_format);
+    });
+  });
+
+  xhr = CouchDB.request("DELETE", "/_config/httpd/config_whitelist",{
+    headers: {"X-Couch-Persist": "false"}
+  });
+  TEquals(200, xhr.status, "Reset config whitelist to undefined");
 };

Modified: couchdb/trunk/src/couchdb/couch_httpd_misc_handlers.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_httpd_misc_handlers.erl?rev=979242&r1=979241&r2=979242&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/couch_httpd_misc_handlers.erl (original)
+++ couchdb/trunk/src/couchdb/couch_httpd_misc_handlers.erl Mon Jul 26 11:20:18 2010
@@ -155,15 +155,6 @@ handle_config_req(#httpd{method='GET', p
     KVs = [{list_to_binary(Key), list_to_binary(Value)}
             || {Key, Value} <- couch_config:get(Section)],
     send_json(Req, 200, {KVs});
-% PUT /_config/Section/Key
-% "value"
-handle_config_req(#httpd{method='PUT', path_parts=[_, Section, Key]}=Req) ->
-    ok = couch_httpd:verify_is_server_admin(Req),
-    Value = couch_httpd:json_body(Req),
-    Persist = couch_httpd:header_value(Req, "X-Couch-Persist") /= "false",
-    OldValue = couch_config:get(Section, Key, ""),
-    ok = couch_config:set(Section, Key, ?b2l(Value), Persist),
-    send_json(Req, 200, list_to_binary(OldValue));
 % GET /_config/Section/Key
 handle_config_req(#httpd{method='GET', path_parts=[_, Section, Key]}=Req) ->
     ok = couch_httpd:verify_is_server_admin(Req),
@@ -173,19 +164,82 @@ handle_config_req(#httpd{method='GET', p
     Value ->
         send_json(Req, 200, list_to_binary(Value))
     end;
-% DELETE /_config/Section/Key
-handle_config_req(#httpd{method='DELETE',path_parts=[_,Section,Key]}=Req) ->
+% PUT or DELETE /_config/Section/Key
+handle_config_req(#httpd{method=Method, path_parts=[_, Section, Key]}=Req)
+      when (Method == 'PUT') or (Method == 'DELETE') ->
     ok = couch_httpd:verify_is_server_admin(Req),
     Persist = couch_httpd:header_value(Req, "X-Couch-Persist") /= "false",
+    case couch_config:get(<<"httpd">>, <<"config_whitelist">>, null) of
+        null ->
+            % No whitelist; allow all changes.
+            handle_approved_config_req(Req, Persist);
+        WhitelistValue ->
+            % Provide a failsafe to protect against inadvertently locking
+            % onesself out of the config by supplying a syntactically-incorrect
+            % Erlang term. To intentionally lock down the whitelist, supply a
+            % well-formed list which does not include the whitelist config
+            % variable itself.
+            FallbackWhitelist = [{<<"httpd">>, <<"config_whitelist">>}],
+
+            Whitelist = case couch_util:parse_term(WhitelistValue) of
+                {ok, Value} when is_list(Value) ->
+                    Value;
+                {ok, _NonListValue} ->
+                    FallbackWhitelist;
+                {error, _} ->
+                    [{WhitelistSection, WhitelistKey}] = FallbackWhitelist,
+                    ?LOG_ERROR("Only whitelisting ~s/~s due to error parsing: ~p",
+                               [WhitelistSection, WhitelistKey, WhitelistValue]),
+                    FallbackWhitelist
+            end,
+
+            IsRequestedKeyVal = fun(Element) ->
+                case Element of
+                    {A, B} ->
+                        % For readability, tuples may be used instead of binaries
+                        % in the whitelist.
+                        case {couch_util:to_binary(A), couch_util:to_binary(B)} of
+                            {Section, Key} ->
+                                true;
+                            {Section, <<"*">>} ->
+                                true;
+                            _Else ->
+                                false
+                        end;
+                    _Else ->
+                        false
+                end
+            end,
+
+            case lists:any(IsRequestedKeyVal, Whitelist) of
+                true ->
+                    % Allow modifying this whitelisted variable.
+                    handle_approved_config_req(Req, Persist);
+                _NotWhitelisted ->
+                    % Disallow modifying this non-whitelisted variable.
+                    send_error(Req, 400, <<"modification_not_allowed">>,
+                               ?l2b("This config variable is read-only"))
+            end
+    end;
+handle_config_req(Req) ->
+    send_method_not_allowed(Req, "GET,PUT,DELETE").
+
+% PUT /_config/Section/Key
+% "value"
+handle_approved_config_req(#httpd{method='PUT', path_parts=[_, Section, Key]}=Req, Persist) ->
+    Value = couch_httpd:json_body(Req),
+    OldValue = couch_config:get(Section, Key, ""),
+    ok = couch_config:set(Section, Key, ?b2l(Value), Persist),
+    send_json(Req, 200, list_to_binary(OldValue));
+% DELETE /_config/Section/Key
+handle_approved_config_req(#httpd{method='DELETE',path_parts=[_,Section,Key]}=Req, Persist) ->
     case couch_config:get(Section, Key, null) of
     null ->
         throw({not_found, unknown_config_value});
     OldValue ->
         couch_config:delete(Section, Key, Persist),
         send_json(Req, 200, list_to_binary(OldValue))
-    end;
-handle_config_req(Req) ->
-    send_method_not_allowed(Req, "GET,PUT,DELETE").
+    end.
 
 
 % httpd db handlers