summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobert Newson <rnewson@apache.org>2013-06-21 11:01:13 +0100
committerRobert Newson <rnewson@apache.org>2013-06-21 22:49:46 +0100
commit8d7ab8b18dd20f8785e69f4420c6f93a2edbfa60 (patch)
treede66b86c2beb72c69b3795b292ae09f70ac62337
parent136b28991fa40b92cde6e544f49c8fd18b9340ab (diff)
downloadcouchdb-8d7ab8b18dd20f8785e69f4420c6f93a2edbfa60.tar.gz
Add a configurable whitelist of public user props
By default no user properties are public and attempts to view a users document other than your own will return a 404. If the public_fields setting of the users_db config section is set to a list of field names, however, you will see that subset of fields for any user. Also, if `public_fields` is set and non-empty, `_users/_all_docs?include_docs=true` will return documents with stripped field. Contributed with code parts from @indutny
-rw-r--r--etc/couchdb/default.ini.tpl.in2
-rw-r--r--share/www/script/test/users_db_security.js44
-rw-r--r--src/couch_mrview/src/couch_mrview_http.erl43
-rw-r--r--src/couchdb/couch_users_db.erl15
4 files changed, 99 insertions, 5 deletions
diff --git a/etc/couchdb/default.ini.tpl.in b/etc/couchdb/default.ini.tpl.in
index 736d9cd07..5eb7ebca7 100644
--- a/etc/couchdb/default.ini.tpl.in
+++ b/etc/couchdb/default.ini.tpl.in
@@ -67,6 +67,8 @@ timeout = 600 ; number of seconds before automatic logout
auth_cache_size = 50 ; size is number of cache entries
allow_persistent_cookies = false ; set to true to allow persistent cookies
iterations = 10 ; iterations for password hashing
+; comma-separated list of public fields, 404 if empty
+; public_fields =
[cors]
credentials = false
diff --git a/share/www/script/test/users_db_security.js b/share/www/script/test/users_db_security.js
index d439fcbfa..cdc3f17a1 100644
--- a/share/www/script/test/users_db_security.js
+++ b/share/www/script/test/users_db_security.js
@@ -256,6 +256,50 @@ couchTests.users_db_security = function(debug) {
// log in one last time so run_on_modified_server can clean up the admin account
TEquals(true, CouchDB.login("jan", "apple").ok);
});
+
+ run_on_modified_server([
+ {
+ section: "couch_httpd_auth",
+ key: "iterations",
+ value: "1"
+ },
+ {
+ section: "couch_httpd_auth",
+ key: "public_fields",
+ value: "name,type"
+ },
+ {
+ section: "admins",
+ key: "jan",
+ value: "apple"
+ }
+ ], function() {
+ var res = usersDb.open("org.couchdb.user:jchris");
+ TEquals("jchris", res.name);
+ TEquals("user", res.type);
+ TEquals(undefined, res.roles);
+ TEquals(undefined, res.salt);
+ TEquals(undefined, res.password_scheme);
+ TEquals(undefined, res.derived_key);
+
+ // log in one last time so run_on_modified_server can clean up the admin account
+ TEquals(true, CouchDB.login("jan", "apple").ok);
+
+ var all = usersDb.allDocs({ include_docs: true });
+ T(all.rows);
+ if (all.rows) {
+ T(all.rows.every(function(row) {
+ T(row.doc);
+ if (row.doc) {
+ return Object.keys(row.doc).every(function(key) {
+ return key === 'name' || key === 'type';
+ });
+ } else {
+ return false;
+ }
+ }));
+ }
+ });
};
usersDb.deleteDb();
diff --git a/src/couch_mrview/src/couch_mrview_http.erl b/src/couch_mrview/src/couch_mrview_http.erl
index 91587f1ff..61db4c008 100644
--- a/src/couch_mrview/src/couch_mrview_http.erl
+++ b/src/couch_mrview/src/couch_mrview_http.erl
@@ -106,8 +106,22 @@ all_docs_req(Req, Db, Keys) ->
ok ->
do_all_docs_req(Req, Db, Keys);
_ ->
- throw({forbidden, <<"Only admins can access _all_docs",
- " of system databases.">>})
+ DbName = ?b2l(Db#db.name),
+ case couch_config:get("couch_httpd_auth",
+ "authentication_db",
+ "_users") of
+ DbName ->
+ case couch_config:get("couch_httpd_auth", "public_fields") of
+ undefined ->
+ throw({forbidden, <<"Only admins can access _all_docs",
+ " of system databases.">>});
+ _ ->
+ do_all_docs_req(Req, Db, Keys)
+ end;
+ _ ->
+ throw({forbidden, <<"Only admins can access _all_docs",
+ " of system databases.">>})
+ end
end;
false ->
do_all_docs_req(Req, Db, Keys)
@@ -126,7 +140,16 @@ do_all_docs_req(Req, Db, Keys) ->
Args = Args0#mrargs{preflight_fun=ETagFun},
{ok, Resp} = couch_httpd:etag_maybe(Req, fun() ->
VAcc0 = #vacc{db=Db, req=Req},
- couch_mrview:query_all_docs(Db, Args, fun view_cb/2, VAcc0)
+ DbName = ?b2l(Db#db.name),
+ Callback = case couch_config:get("couch_httpd_auth",
+ "authentication_db",
+ "_users") of
+ DbName ->
+ fun filtered_view_cb/2;
+ _ ->
+ fun view_cb/2
+ end,
+ couch_mrview:query_all_docs(Db, Args, Callback, VAcc0)
end),
case is_record(Resp, vacc) of
true -> {ok, Resp#vacc.resp};
@@ -154,6 +177,20 @@ design_doc_view(Req, Db, DDoc, ViewName, Keys) ->
end.
+filtered_view_cb({row, Row0}, Acc) ->
+ Row1 = lists:map(fun({doc, null}) ->
+ {doc, null};
+ ({doc, Body}) ->
+ Doc = couch_users_db:strip_non_public_fields(#doc{body=Body}),
+ {doc, Doc#doc.body};
+ (KV) ->
+ KV
+ end, Row0),
+ view_cb({row, Row1}, Acc);
+filtered_view_cb(Obj, Acc) ->
+ view_cb(Obj, Acc).
+
+
view_cb({meta, Meta}, #vacc{resp=undefined}=Acc) ->
Headers = [{"ETag", Acc#vacc.etag}],
{ok, Resp} = couch_httpd:start_json_response(Acc#vacc.req, 200, Headers),
diff --git a/src/couchdb/couch_users_db.erl b/src/couchdb/couch_users_db.erl
index de76142b1..9b875ba56 100644
--- a/src/couchdb/couch_users_db.erl
+++ b/src/couchdb/couch_users_db.erl
@@ -12,7 +12,7 @@
-module(couch_users_db).
--export([before_doc_update/2, after_doc_read/2]).
+-export([before_doc_update/2, after_doc_read/2, strip_non_public_fields/1]).
-include("couch_db.hrl").
@@ -101,10 +101,21 @@ after_doc_read(Doc, #db{user_ctx = UserCtx} = Db) ->
_ when Name =:= DocName ->
Doc;
_ ->
- throw(not_found)
+ Doc1 = strip_non_public_fields(Doc),
+ case Doc1 of
+ #doc{body={[]}} ->
+ throw(not_found);
+ _ ->
+ Doc1
+ end
end.
get_doc_name(#doc{id= <<"org.couchdb.user:", Name/binary>>}) ->
Name;
get_doc_name(_) ->
undefined.
+
+strip_non_public_fields(#doc{body={Props}}=Doc) ->
+ Public = re:split(couch_config:get("couch_httpd_auth", "public_fields", ""),
+ "\\s*,\\s*", [{return, binary}]),
+ Doc#doc{body={[{K, V} || {K, V} <- Props, lists:member(K, Public)]}}.