You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by fd...@apache.org on 2012/01/23 00:43:14 UTC
[47/50] git commit: Implement "System Database Security"
Implement "System Database Security"
System databases at this point are the _users database and the
_replicator database. For each database we implement two call-
backs: before_doc_update and after_doc_read to modify documents
just before they are written to the database and right after
reading them from the database.
_users database:
The before_doc_update callback has the following workflow:
If the request's userCtx identifies an admin or db-admin
-> save_doc (see below)
If the request's userCtx.name is null:
-> save_doc
// this is an anonymous user registering a new document
// in case a user doc with the same id already exists, the anonymous
// user will get a regular doc update conflict.
If the request's userCtx.name doesn't match the doc's name
-> 404 // Not Found
Else
-> save_doc
When a "password" field is present, save_doc() will hash the password
and write the result to the "password_sha" field, add a "salt" field
and delete the "password" field:
If newDoc.password == null:
->
noop
Else -> // calculate password hash server side
newDoc.password_sha = hash_pw(newDoc.password + salt)
newDoc.salt = salt
newDoc.password = null
The after_doc_read callback has the following workflow:
If the request's userCtx identifies an admin or db-admin
-> return doc
If the doc is a design doc and the userCtx doesn't identify
an admin or db-admin:
-> 403 // Forbidden
If the request's userCtx.name doesn't match the doc's name
-> 404 // Not Found
Else
-> return doc
_replicator database:
after_doc_read callback:
If the request's userCtx identifies an admin
-> return doc
If the request's userCtx.name doesn't match the doc's owner
-> strip/hide sensitive data (passwords, oauth tokens)
Else
-> return doc
before_write callback:
If Couch is in admin party mode
-> save doc (see below)
If the request's userCtx identifies an admin or the _replicator role
-> save_doc (see below)
If the request's userCtx.name doesn't match the doc's owner or doc.owner == null
-> 401
Else
-> save_doc (see below)
save_doc:
If doc.owner == undefined
-> If CouchDB is in Admin Party
-> doc.owner = null
Else
-> doc.owner = req.userCtx.name
Save doc to db.
Feature by Filipe, Benoit and Jan.
Project: http://git-wip-us.apache.org/repos/asf/couchdb/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb/commit/e5503ffe
Tree: http://git-wip-us.apache.org/repos/asf/couchdb/tree/e5503ffe
Diff: http://git-wip-us.apache.org/repos/asf/couchdb/diff/e5503ffe
Branch: refs/heads/COUCHDB-1342
Commit: e5503ffef957dc5e8784c7223e318738ae79b6df
Parents: e2e7e15
Author: Jan Lehnardt <ja...@apache.org>
Authored: Thu Dec 22 17:53:00 2011 +0100
Committer: Jan Lehnardt <ja...@apache.org>
Committed: Tue Jan 3 19:21:23 2012 +0100
----------------------------------------------------------------------
src/couch_mrview/src/couch_mrview_http.erl | 15 ++
.../src/couch_replicator_manager.erl | 68 ++++++++++-
src/couch_replicator/test/03-replication-compact.t | 4 +-
src/couchdb/Makefile.am | 2 +
src/couchdb/couch_compaction_daemon.erl | 2 +-
src/couchdb/couch_db.erl | 31 ++++-
src/couchdb/couch_db.hrl | 4 +-
src/couchdb/couch_db_updater.erl | 9 +-
src/couchdb/couch_httpd_db.erl | 29 ++++-
src/couchdb/couch_js_functions.hrl | 31 ++++-
src/couchdb/couch_server.erl | 29 ++++-
src/couchdb/couch_users_db.erl | 101 +++++++++++++++
src/couchdb/couch_util.erl | 6 +-
13 files changed, 309 insertions(+), 22 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/couchdb/blob/e5503ffe/src/couch_mrview/src/couch_mrview_http.erl
----------------------------------------------------------------------
diff --git a/src/couch_mrview/src/couch_mrview_http.erl b/src/couch_mrview/src/couch_mrview_http.erl
index fffb7dc..91587f1 100644
--- a/src/couch_mrview/src/couch_mrview_http.erl
+++ b/src/couch_mrview/src/couch_mrview_http.erl
@@ -100,6 +100,21 @@ handle_cleanup_req(Req, _Db) ->
all_docs_req(Req, Db, Keys) ->
+ case couch_db:is_system_db(Db) of
+ true ->
+ case (catch couch_db:check_is_admin(Db)) of
+ ok ->
+ do_all_docs_req(Req, Db, Keys);
+ _ ->
+ throw({forbidden, <<"Only admins can access _all_docs",
+ " of system databases.">>})
+ end;
+ false ->
+ do_all_docs_req(Req, Db, Keys)
+ end.
+
+
+do_all_docs_req(Req, Db, Keys) ->
Args0 = parse_qs(Req, Keys),
ETagFun = fun(Sig, Acc0) ->
ETag = couch_httpd:make_etag(Sig),
http://git-wip-us.apache.org/repos/asf/couchdb/blob/e5503ffe/src/couch_replicator/src/couch_replicator_manager.erl
----------------------------------------------------------------------
diff --git a/src/couch_replicator/src/couch_replicator_manager.erl b/src/couch_replicator/src/couch_replicator_manager.erl
index 087c2c7..499e3bf 100644
--- a/src/couch_replicator/src/couch_replicator_manager.erl
+++ b/src/couch_replicator/src/couch_replicator_manager.erl
@@ -16,6 +16,8 @@
% public API
-export([replication_started/1, replication_completed/2, replication_error/2]).
+-export([before_doc_update/2, after_doc_read/2]).
+
% gen_server callbacks
-export([start_link/0, init/1, handle_call/3, handle_info/2, handle_cast/2]).
-export([code_change/3, terminate/2]).
@@ -28,6 +30,9 @@
-define(REP_TO_STATE, couch_rep_id_to_rep_state).
-define(INITIAL_WAIT, 2.5). % seconds
-define(MAX_WAIT, 600). % seconds
+-define(OWNER, <<"owner">>).
+
+-define(replace(L, K, V), lists:keystore(K, 1, L, {K, V})).
-record(rep_state, {
rep,
@@ -237,8 +242,7 @@ changes_feed_loop() ->
#changes_args{
include_docs = true,
feed = "continuous",
- timeout = infinity,
- db_open_options = [sys_db]
+ timeout = infinity
},
{json_req, null},
Db
@@ -628,3 +632,63 @@ state_after_error(#rep_state{retries_left = Left, wait = Wait} = State) ->
_ ->
State#rep_state{retries_left = Left - 1, wait = Wait2}
end.
+
+
+before_doc_update(#doc{id = <<?DESIGN_DOC_PREFIX, _/binary>>} = Doc, _Db) ->
+ Doc;
+before_doc_update(#doc{body = {Body}} = Doc, #db{user_ctx=UserCtx} = Db) ->
+ #user_ctx{roles = Roles, name = Name} = UserCtx,
+ case lists:member(<<"_replicator">>, Roles) of
+ true ->
+ Doc;
+ false ->
+ case couch_util:get_value(?OWNER, Body) of
+ undefined ->
+ Doc#doc{body = {?replace(Body, ?OWNER, Name)}};
+ Name ->
+ Doc;
+ Other ->
+ case (catch couch_db:check_is_admin(Db)) of
+ ok when Other =:= null ->
+ Doc#doc{body = {?replace(Body, ?OWNER, Name)}};
+ ok ->
+ Doc;
+ _ ->
+ throw({forbidden, <<"Can't update replication documents",
+ " from other users.">>})
+ end
+ end
+ end.
+
+
+after_doc_read(#doc{id = <<?DESIGN_DOC_PREFIX, _/binary>>} = Doc, _Db) ->
+ Doc;
+after_doc_read(#doc{body = {Body}} = Doc, #db{user_ctx=UserCtx} = Db) ->
+ #user_ctx{name = Name} = UserCtx,
+ case (catch couch_db:check_is_admin(Db)) of
+ ok ->
+ Doc;
+ _ ->
+ case couch_util:get_value(?OWNER, Body) of
+ Name ->
+ Doc;
+ _Other ->
+ Source = strip_credentials(couch_util:get_value(<<"source">>, Body)),
+ Target = strip_credentials(couch_util:get_value(<<"target">>, Body)),
+ NewBody0 = ?replace(Body, <<"source">>, Source),
+ NewBody = ?replace(NewBody0, <<"target">>, Target),
+ #doc{revs = {Pos, [_ | Revs]}} = Doc,
+ NewDoc = Doc#doc{body = {NewBody}, revs = {Pos - 1, Revs}},
+ NewRevId = couch_db:new_revid(NewDoc),
+ NewDoc#doc{revs = {Pos, [NewRevId | Revs]}}
+ end
+ end.
+
+
+strip_credentials(Url) when is_binary(Url) ->
+ re:replace(Url,
+ "http(s)?://(?:[^:]+):[^@]+@(.*)$",
+ "http\\1://\\2",
+ [{return, binary}]);
+strip_credentials({Props}) ->
+ {lists:keydelete(<<"oauth">>, 1, Props)}.
http://git-wip-us.apache.org/repos/asf/couchdb/blob/e5503ffe/src/couch_replicator/test/03-replication-compact.t
----------------------------------------------------------------------
diff --git a/src/couch_replicator/test/03-replication-compact.t b/src/couch_replicator/test/03-replication-compact.t
index c8b265e..7c4d38c 100755
--- a/src/couch_replicator/test/03-replication-compact.t
+++ b/src/couch_replicator/test/03-replication-compact.t
@@ -48,7 +48,9 @@
revs_limit = 1000,
fsync_options = [],
options = [],
- compression
+ compression,
+ before_doc_update,
+ after_doc_read
}).
-record(rep, {
http://git-wip-us.apache.org/repos/asf/couchdb/blob/e5503ffe/src/couchdb/Makefile.am
----------------------------------------------------------------------
diff --git a/src/couchdb/Makefile.am b/src/couchdb/Makefile.am
index f208693..8efb1c0 100644
--- a/src/couchdb/Makefile.am
+++ b/src/couchdb/Makefile.am
@@ -71,6 +71,7 @@ source_files = \
couch_stats_collector.erl \
couch_stream.erl \
couch_task_status.erl \
+ couch_users_db.erl \
couch_util.erl \
couch_uuids.erl \
couch_db_updater.erl \
@@ -125,6 +126,7 @@ compiled_files = \
couch_stats_collector.beam \
couch_stream.beam \
couch_task_status.beam \
+ couch_users_db.beam \
couch_util.beam \
couch_uuids.beam \
couch_db_updater.beam \
http://git-wip-us.apache.org/repos/asf/couchdb/blob/e5503ffe/src/couchdb/couch_compaction_daemon.erl
----------------------------------------------------------------------
diff --git a/src/couchdb/couch_compaction_daemon.erl b/src/couchdb/couch_compaction_daemon.erl
index 2e9a6b1..bc8cfea 100644
--- a/src/couchdb/couch_compaction_daemon.erl
+++ b/src/couchdb/couch_compaction_daemon.erl
@@ -130,7 +130,7 @@ compact_loop(Parent) ->
maybe_compact_db(DbName, Config) ->
- case (catch couch_db:open_int(DbName, [])) of
+ case (catch couch_db:open_int(DbName, [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}])) of
{ok, Db} ->
DDocNames = db_ddoc_names(Db),
case can_db_compact(Config, Db) of
http://git-wip-us.apache.org/repos/asf/couchdb/blob/e5503ffe/src/couchdb/couch_db.erl
----------------------------------------------------------------------
diff --git a/src/couchdb/couch_db.erl b/src/couchdb/couch_db.erl
index 2d7c45e..ae21bfa 100644
--- a/src/couchdb/couch_db.erl
+++ b/src/couchdb/couch_db.erl
@@ -29,7 +29,7 @@
-export([init/1,terminate/2,handle_call/3,handle_cast/2,code_change/3,handle_info/2]).
-export([changes_since/4,changes_since/5,read_doc/2,new_revid/1]).
-export([check_is_admin/1, check_is_member/1]).
--export([reopen/1]).
+-export([reopen/1, is_system_db/1]).
-include("couch_db.hrl").
@@ -101,6 +101,9 @@ reopen(#db{main_pid = Pid, fd_ref_counter = OldRefCntr, user_ctx = UserCtx}) ->
end,
{ok, NewDb#db{user_ctx = UserCtx}}.
+is_system_db(#db{options = Options}) ->
+ lists:member(sys_db, Options).
+
ensure_full_commit(#db{update_pid=UpdatePid,instance_start_time=StartTime}) ->
ok = gen_server:call(UpdatePid, full_commit, infinity),
{ok, StartTime}.
@@ -683,7 +686,7 @@ update_docs(Db, Docs, Options, replicated_changes) ->
increment_stat(Db, {couchdb, database_writes}),
% associate reference with each doc in order to track duplicates
Docs2 = lists:map(fun(Doc) -> {Doc, make_ref()} end, Docs),
- DocBuckets = group_alike_docs(Docs2),
+ DocBuckets = before_docs_update(Db, group_alike_docs(Docs2)),
case (Db#db.validate_doc_funs /= []) orelse
lists:any(
fun({#doc{id= <<?DESIGN_DOC_PREFIX, _/binary>>}, _Ref}) -> true;
@@ -724,7 +727,7 @@ update_docs(Db, Docs, Options, interactive_edit) ->
end
end, {[], []}, Docs2),
- DocBuckets = group_alike_docs(Docs3),
+ DocBuckets = before_docs_update(Db, group_alike_docs(Docs3)),
case (Db#db.validate_doc_funs /= []) orelse
lists:any(
@@ -873,6 +876,17 @@ prepare_doc_summaries(Db, BucketList) ->
Bucket) || Bucket <- BucketList].
+before_docs_update(#db{before_doc_update = nil}, BucketList) ->
+ BucketList;
+before_docs_update(#db{before_doc_update = Fun} = Db, BucketList) ->
+ [lists:map(
+ fun({Doc, Ref}) ->
+ NewDoc = Fun(couch_doc:with_ejson_body(Doc), Db),
+ {NewDoc, Ref}
+ end,
+ Bucket) || Bucket <- BucketList].
+
+
set_new_att_revpos(#doc{revs={RevPos,_Revs},atts=Atts}=Doc) ->
Doc#doc{atts= lists:map(fun(#att{data={_Fd,_Sp}}=Att) ->
% already commited to disk, do not set new rev
@@ -1292,13 +1306,20 @@ make_doc(#db{updater_fd = Fd} = Db, Id, Deleted, Bp, RevisionPath) ->
data={Fd,Sp}}
end, Atts0)}
end,
- #doc{
+ Doc = #doc{
id = Id,
revs = RevisionPath,
body = BodyData,
atts = Atts,
deleted = Deleted
- }.
+ },
+ after_doc_read(Db, Doc).
+
+
+after_doc_read(#db{after_doc_read = nil}, Doc) ->
+ Doc;
+after_doc_read(#db{after_doc_read = Fun} = Db, Doc) ->
+ Fun(couch_doc:with_ejson_body(Doc), Db).
increment_stat(#db{options = Options}, Stat) ->
http://git-wip-us.apache.org/repos/asf/couchdb/blob/e5503ffe/src/couchdb/couch_db.hrl
----------------------------------------------------------------------
diff --git a/src/couchdb/couch_db.hrl b/src/couchdb/couch_db.hrl
index 14556f8..65eb7f0 100644
--- a/src/couchdb/couch_db.hrl
+++ b/src/couchdb/couch_db.hrl
@@ -188,7 +188,9 @@
revs_limit = 1000,
fsync_options = [],
options = [],
- compression
+ compression,
+ before_doc_update = nil, % nil | fun(Doc, Db) -> NewDoc
+ after_doc_read = nil % nil | fun(Doc, Db) -> NewDoc
}).
http://git-wip-us.apache.org/repos/asf/couchdb/blob/e5503ffe/src/couchdb/couch_db_updater.erl
----------------------------------------------------------------------
diff --git a/src/couchdb/couch_db_updater.erl b/src/couchdb/couch_db_updater.erl
index 2b6635c..e57f05b 100644
--- a/src/couchdb/couch_db_updater.erl
+++ b/src/couchdb/couch_db_updater.erl
@@ -481,7 +481,9 @@ init_db(DbName, Filepath, Fd, ReaderFd, Header0, Options) ->
revs_limit = Header#db_header.revs_limit,
fsync_options = FsyncOptions,
options = Options,
- compression = Compression
+ compression = Compression,
+ before_doc_update = couch_util:get_value(before_doc_update, Options, nil),
+ after_doc_read = couch_util:get_value(after_doc_read, Options, nil)
}.
open_reader_fd(Filepath, Options) ->
@@ -498,7 +500,8 @@ close_db(#db{fd_ref_counter = RefCntr}) ->
couch_ref_counter:drop(RefCntr).
-refresh_validate_doc_funs(Db) ->
+refresh_validate_doc_funs(Db0) ->
+ Db = Db0#db{user_ctx = #user_ctx{roles=[<<"_admin">>]}},
DesignDocs = couch_db:get_design_docs(Db),
ProcessDocFuns = lists:flatmap(
fun(DesignDocInfo) ->
@@ -509,7 +512,7 @@ refresh_validate_doc_funs(Db) ->
Fun -> [Fun]
end
end, DesignDocs),
- Db#db{validate_doc_funs=ProcessDocFuns}.
+ Db0#db{validate_doc_funs=ProcessDocFuns}.
% rev tree functions
http://git-wip-us.apache.org/repos/asf/couchdb/blob/e5503ffe/src/couchdb/couch_httpd_db.erl
----------------------------------------------------------------------
diff --git a/src/couchdb/couch_httpd_db.erl b/src/couchdb/couch_httpd_db.erl
index 90baba8..1bcfeff 100644
--- a/src/couchdb/couch_httpd_db.erl
+++ b/src/couchdb/couch_httpd_db.erl
@@ -148,12 +148,23 @@ handle_design_req(#httpd{
path_parts=[_DbName, _Design, DesignName, <<"_",_/binary>> = Action | _Rest],
design_url_handlers = DesignUrlHandlers
}=Req, Db) ->
+ case couch_db:is_system_db(Db) of
+ true ->
+ case (catch couch_db:check_is_admin(Db)) of
+ ok -> ok;
+ _ ->
+ throw({forbidden, <<"Only admins can access design document",
+ " actions for system databases.">>})
+ end;
+ false -> ok
+ end,
+
% load ddoc
DesignId = <<"_design/", DesignName/binary>>,
DDoc = couch_httpd_db:couch_doc_open(Db, DesignId, nil, [ejson_body]),
Handler = couch_util:dict_find(Action, DesignUrlHandlers, fun(_, _, _) ->
- throw({not_found, <<"missing handler: ", Action/binary>>})
- end),
+ throw({not_found, <<"missing handler: ", Action/binary>>})
+ end),
Handler(Req, Db, DDoc);
handle_design_req(Req, Db) ->
@@ -448,6 +459,20 @@ db_req(#httpd{path_parts=[_, DocId | FileNameParts]}=Req, Db) ->
db_attachment_req(Req, Db, DocId, FileNameParts).
all_docs_view(Req, Db, Keys) ->
+ case couch_db:is_system_db(Db) of
+ true ->
+ case (catch couch_db:check_is_admin(Db)) of
+ ok ->
+ do_all_docs_view(Req, Db, Keys);
+ _ ->
+ throw({forbidden, <<"Only admins can access _all_docs",
+ " of system databases.">>})
+ end;
+ false ->
+ do_all_docs_view(Req, Db, Keys)
+ end.
+
+do_all_docs_view(Req, Db, Keys) ->
RawCollator = fun(A, B) -> A < B end,
#view_query_args{
start_key = StartKey,
http://git-wip-us.apache.org/repos/asf/couchdb/blob/e5503ffe/src/couchdb/couch_js_functions.hrl
----------------------------------------------------------------------
diff --git a/src/couchdb/couch_js_functions.hrl b/src/couchdb/couch_js_functions.hrl
index 1c1dee1..1c2f167 100644
--- a/src/couchdb/couch_js_functions.hrl
+++ b/src/couchdb/couch_js_functions.hrl
@@ -11,7 +11,7 @@
% the License.
-define(AUTH_DB_DOC_VALIDATE_FUNCTION, <<"
- function(newDoc, oldDoc, userCtx) {
+ function(newDoc, oldDoc, userCtx, secObj) {
if (newDoc._deleted === true) {
// allow deletes by admins and matching users
// without checking the other fields
@@ -54,7 +54,34 @@
});
}
- if (userCtx.roles.indexOf('_admin') === -1) {
+ var is_server_or_database_admin = function(userCtx, secObj) {
+ // see if the user is a server admin
+ if(userCtx.roles.indexOf('_admin') !== -1) {
+ return true; // a server admin
+ }
+
+ // see if the user a database admin specified by name
+ if(secObj && secObj.admins && secObj.admins.names) {
+ if(secObj.admins.names.indexOf(userCtx.name) !== -1) {
+ return true; // database admin
+ }
+ }
+
+ // see if the user a database admin specified by role
+ if(secObj && secObj.admins && secObj.admins.roles) {
+ var db_roles = secObj.admins.roles;
+ for(var idx = 0; idx < userCtx.roles.length; idx++) {
+ var user_role = userCtx.roles[idx];
+ if(db_roles.indexOf(user_role) !== -1) {
+ return true; // role matches!
+ }
+ }
+ }
+
+ return false; // default to no admin
+ }
+
+ if (!is_server_or_database_admin(userCtx, secObj)) {
if (oldDoc) { // validate non-admin updates
if (userCtx.name !== newDoc.name) {
throw({
http://git-wip-us.apache.org/repos/asf/couchdb/blob/e5503ffe/src/couchdb/couch_server.erl
----------------------------------------------------------------------
diff --git a/src/couchdb/couch_server.erl b/src/couchdb/couch_server.erl
index 7418e23..332b44f 100644
--- a/src/couchdb/couch_server.erl
+++ b/src/couchdb/couch_server.erl
@@ -51,7 +51,8 @@ get_stats() ->
sup_start_link() ->
gen_server:start_link({local, couch_server}, couch_server, [], []).
-open(DbName, Options) ->
+open(DbName, Options0) ->
+ Options = maybe_add_sys_db_callbacks(DbName, Options0),
case gen_server:call(couch_server, {open, DbName, Options}, infinity) of
{ok, Db} ->
Ctx = couch_util:get_value(user_ctx, Options, #user_ctx{}),
@@ -60,7 +61,8 @@ open(DbName, Options) ->
Error
end.
-create(DbName, Options) ->
+create(DbName, Options0) ->
+ Options = maybe_add_sys_db_callbacks(DbName, Options0),
case gen_server:call(couch_server, {create, DbName, Options}, infinity) of
{ok, Db} ->
Ctx = couch_util:get_value(user_ctx, Options, #user_ctx{}),
@@ -72,6 +74,29 @@ create(DbName, Options) ->
delete(DbName, Options) ->
gen_server:call(couch_server, {delete, DbName, Options}, infinity).
+maybe_add_sys_db_callbacks(DbName, Options) when is_binary(DbName) ->
+ maybe_add_sys_db_callbacks(?b2l(DbName), Options);
+maybe_add_sys_db_callbacks(DbName, Options) ->
+ case couch_config:get("replicator", "db", "_replicator") of
+ DbName ->
+ [
+ {before_doc_update, fun couch_replicator_manager:before_doc_update/2},
+ {after_doc_read, fun couch_replicator_manager:after_doc_read/2},
+ sys_db | Options
+ ];
+ _ ->
+ case couch_config:get("couch_httpd_auth", "authentication_db", "_users") of
+ DbName ->
+ [
+ {before_doc_update, fun couch_users_db:before_doc_update/2},
+ {after_doc_read, fun couch_users_db:after_doc_read/2},
+ sys_db | Options
+ ];
+ _ ->
+ Options
+ end
+ end.
+
check_dbname(#server{dbname_regexp=RegExp}, DbName) ->
case re:run(DbName, RegExp, [{capture, none}]) of
nomatch ->
http://git-wip-us.apache.org/repos/asf/couchdb/blob/e5503ffe/src/couchdb/couch_users_db.erl
----------------------------------------------------------------------
diff --git a/src/couchdb/couch_users_db.erl b/src/couchdb/couch_users_db.erl
new file mode 100644
index 0000000..d6e522e
--- /dev/null
+++ b/src/couchdb/couch_users_db.erl
@@ -0,0 +1,101 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_users_db).
+
+-export([before_doc_update/2, after_doc_read/2]).
+
+-include("couch_db.hrl").
+
+-define(NAME, <<"name">>).
+-define(PASSWORD, <<"password">>).
+-define(PASSWORD_SHA, <<"password_sha">>).
+-define(SALT, <<"salt">>).
+-define(replace(L, K, V), lists:keystore(K, 1, L, {K, V})).
+
+% If the request's userCtx identifies an admin
+% -> save_doc (see below)
+%
+% If the request's userCtx.name is null:
+% -> save_doc
+% // this is an anonymous user registering a new document
+% // in case a user doc with the same id already exists, the anonymous
+% // user will get a regular doc update conflict.
+% If the request's userCtx.name doesn't match the doc's name
+% -> 404 // Not Found
+% Else
+% -> save_doc
+before_doc_update(Doc, #db{user_ctx = UserCtx} = Db) ->
+ #user_ctx{name=Name} = UserCtx,
+ DocName = get_doc_name(Doc),
+ case (catch couch_db:check_is_admin(Db)) of
+ ok ->
+ save_doc(Doc);
+ _ when Name =:= DocName orelse Name =:= null ->
+ save_doc(Doc);
+ _ ->
+ throw(not_found)
+ end.
+
+% If newDoc.password == null:
+% ->
+% noop
+% Else -> // calculate password hash server side
+% newDoc.password_sha = hash_pw(newDoc.password + salt)
+% newDoc.salt = salt
+% newDoc.password = null
+save_doc(#doc{body={Body}} = Doc) ->
+ case couch_util:get_value(?PASSWORD, Body) of
+ undefined ->
+ Doc;
+ ClearPassword ->
+ Salt = ?b2l(couch_uuids:random()),
+ PasswordSha = couch_util:to_hex(crypto:sha(?b2l(ClearPassword) ++ Salt)),
+ Body1 = ?replace(Body, ?PASSWORD_SHA, ?l2b(PasswordSha)),
+ Body2 = ?replace(Body1, ?SALT, ?l2b(Salt)),
+ Body3 = proplists:delete(?PASSWORD, Body2),
+ Doc#doc{body={Body3}}
+ end.
+
+% If the doc is a design doc
+% If the request's userCtx identifies an admin
+% -> return doc
+% Else
+% -> 403 // Forbidden
+% If the request's userCtx identifies an admin
+% -> return doc
+% If the request's userCtx.name doesn't match the doc's name
+% -> 404 // Not Found
+% Else
+% -> return doc
+after_doc_read(#doc{id = <<?DESIGN_DOC_PREFIX, _/binary>>} = Doc, Db) ->
+ case (catch couch_db:check_is_admin(Db)) of
+ ok ->
+ Doc;
+ _ ->
+ throw({forbidden,
+ <<"Only administrators can view design docs in the users database.">>})
+ end;
+after_doc_read(Doc, #db{user_ctx = UserCtx} = Db) ->
+ #user_ctx{name=Name} = UserCtx,
+ DocName = get_doc_name(Doc),
+ case (catch couch_db:check_is_admin(Db)) of
+ ok ->
+ Doc;
+ _ when Name =:= DocName ->
+ Doc;
+ _ ->
+ throw(not_found)
+ end.
+
+get_doc_name(#doc{body={Body}}) ->
+ couch_util:get_value(?NAME, Body).
http://git-wip-us.apache.org/repos/asf/couchdb/blob/e5503ffe/src/couchdb/couch_util.erl
----------------------------------------------------------------------
diff --git a/src/couchdb/couch_util.erl b/src/couchdb/couch_util.erl
index 2667cc3..d023bb6 100644
--- a/src/couchdb/couch_util.erl
+++ b/src/couchdb/couch_util.erl
@@ -80,7 +80,7 @@ shutdown_sync(Pid) ->
after
erlang:demonitor(MRef, [flush])
end.
-
+
simple_call(Pid, Message) ->
MRef = erlang:monitor(process, Pid),
@@ -182,7 +182,7 @@ json_user_ctx(#db{name=DbName, user_ctx=Ctx}) ->
{[{<<"db">>, DbName},
{<<"name">>,Ctx#user_ctx.name},
{<<"roles">>,Ctx#user_ctx.roles}]}.
-
+
% returns a random integer
rand32() ->
@@ -435,7 +435,7 @@ encode_doc_id(Id) ->
with_db(Db, Fun) when is_record(Db, db) ->
Fun(Db);
with_db(DbName, Fun) ->
- case couch_db:open_int(DbName, []) of
+ case couch_db:open_int(DbName, [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of
{ok, Db} ->
try
Fun(Db)