couchdb-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From j..@apache.org
Subject [11/11] git commit: Implement "System Database Security"
Date Tue, 03 Jan 2012 19:33:21 GMT
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/master
Commit: e5503ffef957dc5e8784c7223e318738ae79b6df
Parents: e2e7e15
Author: Jan Lehnardt <jan@apache.org>
Authored: Thu Dec 22 17:53:00 2011 +0100
Committer: Jan Lehnardt <jan@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)


Mime
View raw message