You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by jc...@apache.org on 2010/02/01 23:51:16 UTC

svn commit: r905436 - in /couchdb/trunk: share/ share/www/ share/www/dialog/ share/www/script/ share/www/script/test/ share/www/style/ src/couchdb/

Author: jchris
Date: Mon Feb  1 22:51:15 2010
New Revision: 905436

URL: http://svn.apache.org/viewvc?rev=905436&view=rev
Log:
Database-level security.

This patch builds on the DB-admins feature to store lists of database admin and reader names and roles, as well as a security object which can be used for configuration in validation functions.

Added:
    couchdb/trunk/share/www/dialog/_database_security.html
    couchdb/trunk/share/www/script/test/reader_acl.js
Modified:
    couchdb/trunk/share/Makefile.am
    couchdb/trunk/share/www/database.html
    couchdb/trunk/share/www/script/couch_tests.js
    couchdb/trunk/share/www/script/futon.browse.js
    couchdb/trunk/share/www/script/jquery.couch.js
    couchdb/trunk/share/www/script/test/security_validation.js
    couchdb/trunk/share/www/style/layout.css
    couchdb/trunk/src/couchdb/couch_db.erl
    couchdb/trunk/src/couchdb/couch_db.hrl
    couchdb/trunk/src/couchdb/couch_db_updater.erl
    couchdb/trunk/src/couchdb/couch_doc.erl
    couchdb/trunk/src/couchdb/couch_httpd_db.erl
    couchdb/trunk/src/couchdb/couch_query_servers.erl

Modified: couchdb/trunk/share/Makefile.am
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/Makefile.am?rev=905436&r1=905435&r2=905436&view=diff
==============================================================================
--- couchdb/trunk/share/Makefile.am (original)
+++ couchdb/trunk/share/Makefile.am Mon Feb  1 22:51:15 2010
@@ -48,6 +48,7 @@
     www/dialog/_create_database.html \
     www/dialog/_delete_database.html \
     www/dialog/_delete_document.html \
+    www/dialog/_database_security.html \
     www/dialog/_save_view_as.html \
     www/dialog/_upload_attachment.html \
     www/document.html \
@@ -134,6 +135,7 @@
     www/script/test/multiple_rows.js \
     www/script/test/oauth.js \
     www/script/test/purge.js \
+    www/script/test/reader_acl.js \
     www/script/test/recreate_doc.js \
     www/script/test/reduce.js \
     www/script/test/reduce_builtin.js \

Modified: couchdb/trunk/share/www/database.html
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/database.html?rev=905436&r1=905435&r2=905436&view=diff
==============================================================================
--- couchdb/trunk/share/www/database.html [utf-8] (original)
+++ couchdb/trunk/share/www/database.html [utf-8] Mon Feb  1 22:51:15 2010
@@ -117,6 +117,7 @@
         $("#toolbar button.add").click(page.newDocument);
         $("#toolbar button.compact").click(page.compactAndCleanup);
         $("#toolbar button.delete").click(page.deleteDatabase);
+        $("#toolbar button.security").click(page.databaseSecurity);
 
         $('#jumpto input').suggest(function(text, callback) {
           page.db.allDocs({
@@ -161,6 +162,7 @@
       </div>
       <ul id="toolbar">
         <li><button class="add">New Document</button></li>
+        <li><button class="security">Security…</button></li>
         <li><button class="compact">Compact &amp; Cleanup…</button></li>
         <li><button class="delete">Delete Database…</button></li>
       </ul>

Added: couchdb/trunk/share/www/dialog/_database_security.html
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/dialog/_database_security.html?rev=905436&view=auto
==============================================================================
--- couchdb/trunk/share/www/dialog/_database_security.html (added)
+++ couchdb/trunk/share/www/dialog/_database_security.html Mon Feb  1 22:51:15 2010
@@ -0,0 +1,50 @@
+<!--
+
+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.
+
+-->
+<form action="" method="post">
+  <h2>Admins and Readers</h2>
+  <p class="help">
+    Each database contains lists of admins and readers. 
+    Admins and readers are each defined by <tt>names</tt> and <tt>roles</tt>, which are lists of strings. For example, if the readers is defined by <tt>names ["jane", "mike"]</tt> and roles <tt>["bbq"]</tt> then anyone with a <tt>"bbq"</tt> role can read the database. Yummy!
+  </p>
+  <fieldset>
+    <h3>Admins</h3>
+    <p class="help">Database admins can update design documents and edit the readers list.</p>
+    <table summary=""><tbody><tr>
+      <th><label>Names:</label></th>
+      <td><input type="text" name="admin_names" size="40"></td>
+    </tr><tr>
+      <th><label>Roles:</label></th>
+      <td><input type="text" name="admin_roles" size="40"></td>
+    </tr>
+    </tbody></table>
+  </fieldset>
+  <fieldset>
+    <h3>Readers</h3>
+    <p class="help">Database readers can access the database. If no readers are defined, the database is public. When readers are defined, only they may read or write to the database.</p>
+    <table summary=""><tbody><tr>
+      <th><label>Names:</label></th>
+      <td><input type="text" name="reader_names" size="40"></td>
+    </tr><tr>
+      <th><label>Roles:</label></th>
+      <td><input type="text" name="reader_roles" size="40"></td>
+    </tr>
+    </tbody></table>
+
+  </fieldset>
+  <div class="buttons">
+    <button type="submit">Update</button>
+    <button type="button" class="cancel">Cancel</button>
+  </div>
+</form>

Modified: couchdb/trunk/share/www/script/couch_tests.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/couch_tests.js?rev=905436&r1=905435&r2=905436&view=diff
==============================================================================
--- couchdb/trunk/share/www/script/couch_tests.js [utf-8] (original)
+++ couchdb/trunk/share/www/script/couch_tests.js [utf-8] Mon Feb  1 22:51:15 2010
@@ -63,6 +63,7 @@
 loadScript("script/sha1.js");
 loadTest("oauth.js");
 loadTest("purge.js");
+loadTest("reader_acl.js");
 loadTest("recreate_doc.js");
 loadTest("reduce.js");
 loadTest("reduce_builtin.js");

Modified: couchdb/trunk/share/www/script/futon.browse.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/futon.browse.js?rev=905436&r1=905435&r2=905436&view=diff
==============================================================================
--- couchdb/trunk/share/www/script/futon.browse.js [utf-8] (original)
+++ couchdb/trunk/share/www/script/futon.browse.js [utf-8] Mon Feb  1 22:51:15 2010
@@ -178,6 +178,32 @@
           }
         });
       }
+      
+      this.databaseSecurity = function() {
+        $.showDialog("dialog/_database_security.html", {
+          load : function(d) {
+            ["admin", "reader"].forEach(function(key) {
+              db.getDbProperty("_"+key+"s", {
+                success : function(r) {
+                  $("input[name="+key+"_names]",d).val(JSON.stringify(r.names||[]));
+                  $("input[name="+key+"_roles]",d).val(JSON.stringify(r.roles||[]));
+                }
+              });
+            });
+          },
+          // maybe this should be 2 forms
+          submit: function(data, callback) {
+            ["admin", "reader"].forEach(function(key) {
+              var new_value = {
+                names : JSON.parse(data[key+"_names"]),
+                roles : JSON.parse(data[key+"_roles"])
+              };
+              db.setDbProperty("_"+key+"s", new_value);
+            });
+            callback();
+          }
+        });
+      }
 
       this.populateViewEditor = function() {
         if (viewName.match(/^_design\//)) {

Modified: couchdb/trunk/share/www/script/jquery.couch.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/jquery.couch.js?rev=905436&r1=905435&r2=905436&view=diff
==============================================================================
--- couchdb/trunk/share/www/script/jquery.couch.js [utf-8] (original)
+++ couchdb/trunk/share/www/script/jquery.couch.js [utf-8] Mon Feb  1 22:51:15 2010
@@ -371,6 +371,25 @@
             },
             options, "An error occurred accessing the view"
           );
+        },
+        getDbProperty: function(propName, options, ajaxOptions) {
+          ajax({url: this.uri + propName + encodeOptions(options)},
+            options,
+            "The property could not be retrieved",
+            ajaxOptions
+          );
+        },
+
+        setDbProperty: function(propName, propValue, options, ajaxOptions) {
+          ajax({
+            type: "PUT", 
+            url: this.uri + propName + encodeOptions(options),
+            data : JSON.stringify(propValue)
+          },
+            options,
+            "The property could not be updated",
+            ajaxOptions
+          );
         }
       };
     },

Added: couchdb/trunk/share/www/script/test/reader_acl.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/test/reader_acl.js?rev=905436&view=auto
==============================================================================
--- couchdb/trunk/share/www/script/test/reader_acl.js (added)
+++ couchdb/trunk/share/www/script/test/reader_acl.js Mon Feb  1 22:51:15 2010
@@ -0,0 +1,95 @@
+// 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.
+
+couchTests.reader_acl = function(debug) {
+  // this tests read access control
+
+  var usersDb = new CouchDB("test_suite_users", {"X-Couch-Full-Commit":"false"});
+  var secretDb = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"});
+  function testFun() {
+    try {
+      usersDb.deleteDb();
+      usersDb.createDb();
+      secretDb.deleteDb();
+      secretDb.createDb();
+
+      // create a user with top-secret-clearance
+      var jchrisUserDoc = CouchDB.prepareUserDoc({
+        name: "jchris@apache.org",
+        roles : ["top-secret"]
+      }, "funnybone");
+      T(usersDb.save(jchrisUserDoc).ok);
+
+      T(CouchDB.session().userCtx.name == null);
+
+      // set secret db to be read controlled
+      T(secretDb.save({_id:"baz",foo:"bar"}).ok);
+      T(secretDb.open("baz").foo == "bar");
+
+      T(secretDb.setDbProperty("_readers", {
+        roles : ["super-secret-club"],
+        names : ["joe","barb"]}).ok);
+      // can't read it as jchris
+      T(CouchDB.login("jchris@apache.org", "funnybone").ok);
+      T(CouchDB.session().userCtx.name == "jchris@apache.org");
+
+      try {
+        secretDb.open("baz");
+        T(false && "can't open a doc from a secret db") ;
+      } catch(e) {
+        T(true)
+      }
+
+      CouchDB.logout();
+
+      // admin now adds the top-secret role to the db's readers
+      T(CouchDB.session().userCtx.roles.indexOf("_admin") != -1);
+
+      T(secretDb.setDbProperty("_readers", {
+        roles : ["super-secret-club", "top-secret"],
+        names : ["joe","barb"]}).ok);
+
+      // now top-secret users can read it
+      T(secretDb.open("baz").foo == "bar");
+      T(CouchDB.login("jchris@apache.org", "funnybone").ok);
+      T(secretDb.open("baz").foo == "bar");
+      
+      CouchDB.logout();
+
+      // can't set non string reader names or roles
+      try {
+        T(!secretDb.setDbProperty("_readers", {
+          roles : ["super-secret-club", {"top-secret":"awesome"}],
+          names : ["joe","barb"]}).ok);      
+        T(false && "only string roles");
+      } catch (e) {}
+
+      try {
+      T(!secretDb.setDbProperty("_readers", {
+        roles : ["super-secret-club", "top-secret"],
+        names : ["joe",22]}).ok);
+        T(false && "only string names");
+        } catch (e) {}
+    } finally {
+      CouchDB.logout();
+    }
+  }
+
+  run_on_modified_server(
+    [{section: "httpd",
+      key: "authentication_handlers",
+      value: "{couch_httpd_auth, cookie_authentication_handler}, {couch_httpd_auth, default_authentication_handler}"},
+     {section: "couch_httpd_auth",
+      key: "authentication_db", value: "test_suite_users"}],
+    testFun
+  );
+}

Modified: couchdb/trunk/share/www/script/test/security_validation.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/test/security_validation.js?rev=905436&r1=905435&r2=905436&view=diff
==============================================================================
--- couchdb/trunk/share/www/script/test/security_validation.js (original)
+++ couchdb/trunk/share/www/script/test/security_validation.js Mon Feb  1 22:51:15 2010
@@ -69,7 +69,13 @@
       var designDoc = {
         _id:"_design/test",
         language: "javascript",
-        validate_doc_update: "(" + (function (newDoc, oldDoc, userCtx) {
+        validate_doc_update: "(" + (function (newDoc, oldDoc, userCtx, secObj) {
+          if (secObj.admin_override) {
+            if (userCtx.roles.indexOf('_admin') != -1) {
+              // user is admin, they can do anything
+              return true;
+            }
+          }
           // docs should have an author field.
           if (!newDoc._deleted && !newDoc.author) {
             throw {forbidden:
@@ -99,11 +105,11 @@
       }
 
       // set user as the admin
-      T(db.setDbProperty("_admins", ["Damien Katz"]).ok);
+      T(db.setDbProperty("_admins", {names : ["Damien Katz"]}).ok);
 
       T(userDb.save(designDoc).ok);
 
-      // test the _whoami endpoint
+      // test the _session API
       var resp = userDb.request("GET", "/_session");
       var user = JSON.parse(resp.responseText).userCtx;
       T(user.name == "Damien Katz");
@@ -158,6 +164,31 @@
         T(e.error == "unauthorized");
         T(userDb.last_req.status == 401);
       }
+      
+      // admin must save with author field unless admin override
+      var resp = db.request("GET", "/_session");
+      var user = JSON.parse(resp.responseText).userCtx;
+      T(user.name == null);
+      // test that we are admin
+      TEquals(user.roles, ["_admin"]);
+      
+      // can't save the doc even though we are admin
+      var doc = db.open("testdoc");
+      doc.foo=3;
+      try {
+        db.save(doc);
+        T(false && "Can't get here. Should have thrown an error 3");
+      } catch (e) {
+        T(e.error == "unauthorized");
+        T(db.last_req.status == 401);
+      }
+
+      // now turn on admin override
+      T(db.setDbProperty("_security", {admin_override : true}).ok);
+      T(db.save(doc).ok);
+
+      // go back to normal
+      T(db.setDbProperty("_security", {admin_override : false}).ok);
 
       // Now delete document
       T(user2Db.deleteDoc(doc).ok);
@@ -188,7 +219,6 @@
       T(db.open("booboo") == null);
       T(db.open("foofoo") == null);
 
-
       // Now test replication
       var AuthHeaders = {"WWW-Authenticate": "X-Couch-Test-Auth Christopher Lenz:dog food"};
       var host = CouchDB.host;

Modified: couchdb/trunk/share/www/style/layout.css
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/style/layout.css?rev=905436&r1=905435&r2=905436&view=diff
==============================================================================
--- couchdb/trunk/share/www/style/layout.css (original)
+++ couchdb/trunk/share/www/style/layout.css Mon Feb  1 22:51:15 2010
@@ -236,6 +236,7 @@
 #toolbar button:hover { background-position: 2px -30px; color: #000; }
 #toolbar button:active { background-position: 2px -62px; color: #000; }
 #toolbar button.add { background-image: url(../image/add.png); }
+#toolbar button.security { background-image: url(../image/compact.png); }
 #toolbar button.compact { background-image: url(../image/compact.png); }
 #toolbar button.delete { background-image: url(../image/delete.png); }
 #toolbar button.load { background-image: url(../image/load.png); }

Modified: couchdb/trunk/src/couchdb/couch_db.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_db.erl?rev=905436&r1=905435&r2=905436&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/couch_db.erl (original)
+++ couchdb/trunk/src/couchdb/couch_db.erl Mon Feb  1 22:51:15 2010
@@ -22,7 +22,8 @@
 -export([enum_docs/4,enum_docs_since/5]).
 -export([enum_docs_since_reduce_to_count/1,enum_docs_reduce_to_count/1]).
 -export([increment_update_seq/1,get_purge_seq/1,purge_docs/2,get_last_purged/1]).
--export([start_link/3,open_doc_int/3,set_admins/2,get_admins/1,ensure_full_commit/1]).
+-export([start_link/3,open_doc_int/3,ensure_full_commit/1]).
+-export([set_readers/2,get_readers/1,set_admins/2,get_admins/1,set_security/2,get_security/1]).
 -export([init/1,terminate/2,handle_call/3,handle_cast/2,code_change/3,handle_info/2]).
 -export([changes_since/5,changes_since/6,read_doc/2,new_revid/1]).
 
@@ -64,7 +65,18 @@
     couch_server:create(DbName, Options).
 
 open(DbName, Options) ->
-    couch_server:open(DbName, Options).
+    case couch_server:open(DbName, Options) of
+        {ok, Db} ->
+            try
+                check_is_reader(Db),
+                {ok, Db}
+            catch
+                throw:Error ->
+                    close(Db),
+                    throw(Error)
+            end;
+        Else -> Else
+    end.
 
 ensure_full_commit(#db{update_pid=UpdatePid,instance_start_time=StartTime}) ->
     ok = gen_server:call(UpdatePid, full_commit, infinity),
@@ -218,22 +230,103 @@
         [], [{start_key, <<"_design/">>}, {end_key_gt, <<"_design0">>}]),
     {ok, Docs}.
 
-check_is_admin(#db{admins=Admins, user_ctx=#user_ctx{name=Name,roles=Roles}}) ->
-    DbAdmins = [<<"_admin">> | Admins],
-    case DbAdmins -- [Name | Roles] of
-    DbAdmins -> % same list, not an admin
-        throw({unauthorized, <<"You are not a db or server admin.">>});
+check_is_admin(#db{user_ctx=#user_ctx{name=Name,roles=Roles}}=Db) ->
+    {Admins} = get_admins(Db),
+    AdminRoles = [<<"_admin">> | proplists:get_value(roles, Admins, [])],
+    AdminNames = proplists:get_value(names, Admins,[]),
+    case AdminRoles -- Roles of
+    AdminRoles -> % same list, not an admin role
+        case AdminNames -- [Name] of
+        AdminNames -> % same names, not an admin
+            throw({unauthorized, <<"You are not a db or server admin.">>});
+        _ ->
+            ok
+        end;
     _ ->
         ok
     end.
 
-get_admins(#db{admins=Admins}) ->
-    Admins.
+check_is_reader(#db{user_ctx=#user_ctx{name=Name,roles=Roles}=UserCtx}=Db) ->
+    % admins are not readers. this is for good reason. 
+    % we don't want to confuse setting admins with making private dbs
+    {Readers} = get_readers(Db),
+    ReaderRoles = proplists:get_value(roles, Readers,[]),
+    WithAdminRoles = [<<"_admin">> | ReaderRoles],
+    ReaderNames = proplists:get_value(names, Readers,[]),
+    case ReaderRoles ++ ReaderNames of 
+    [] -> ok; % no readers == public access
+    _Else ->
+        case WithAdminRoles -- Roles of
+        WithAdminRoles -> % same list, not an reader role
+            case ReaderNames -- [Name] of
+            ReaderNames -> % same names, not a reader
+                ?LOG_DEBUG("Not a reader: UserCtx ~p vs Names ~p Roles ~p",[UserCtx, ReaderNames, WithAdminRoles]),
+                throw({unauthorized, <<"You are not authorized to access this db.">>});
+            _ ->
+                ok
+            end;
+        _ ->
+            ok
+        end
+    end.
+
+get_admins(#db{security=SecProps}) ->
+    proplists:get_value(admins, SecProps, {[]}).
+
+set_admins(#db{security=SecProps,update_pid=Pid}=Db, Admins) ->
+    check_is_admin(Db),
+    SecProps2 = update_sec_field(admins, SecProps, just_names_and_roles(Admins)),
+    gen_server:call(Pid, {set_security, SecProps2}, infinity).
+
+get_readers(#db{security=SecProps}) ->
+    proplists:get_value(readers, SecProps, {[]}).
 
-set_admins(#db{update_pid=Pid}=Db, Admins) when is_list(Admins) ->
+set_readers(#db{security=SecProps,update_pid=Pid}=Db, Readers) ->
     check_is_admin(Db),
-    gen_server:call(Pid, {set_admins, Admins}, infinity).
+    SecProps2 = update_sec_field(readers, SecProps, just_names_and_roles(Readers)),
+    gen_server:call(Pid, {set_security, SecProps2}, infinity).
 
+get_security(#db{security=SecProps}) ->
+    proplists:get_value(sec_obj, SecProps, {[]}).
+
+set_security(#db{security=SecProps, update_pid=Pid}=Db, {SecObjProps}) when is_list(SecObjProps) ->
+    check_is_admin(Db),
+    SecProps2 = update_sec_field(sec_obj, SecProps, {SecObjProps}),
+    gen_server:call(Pid, {set_security, SecProps2}, infinity).
+
+update_sec_field(Field, SecProps, Value) ->
+    Admins = proplists:get_value(admins, SecProps, {[]}),
+    Readers = proplists:get_value(readers, SecProps, {[]}),
+    SecObj = proplists:get_value(sec_obj, SecProps, {[]}),
+    if Field == admins ->
+        [{admins, Value}];
+    true -> [{admins, Admins}] 
+    end ++ if Field == readers ->
+        [{readers, Value}];
+    true -> [{readers, Readers}] 
+    end ++ if Field == sec_obj ->
+        [{sec_obj, Value}];
+    true -> [{sec_obj, SecObj}]
+    end.
+
+% validate user input and convert proplist to atom keys
+just_names_and_roles({Props}) when is_list(Props) ->
+    Names = case proplists:get_value(<<"names">>,Props) of
+    Ns when is_list(Ns) ->
+            [throw("names must be a JSON list of strings") ||N <- Ns, not is_binary(N)],
+            Ns;
+    _ -> []
+    end,
+    Roles = case proplists:get_value(<<"roles">>,Props) of
+    Rs when is_list(Rs) ->
+        [throw("roles must be a JSON list of strings") ||R <- Rs, not is_binary(R)],
+        Rs;
+    _ -> []
+    end,
+    {[
+        {names, Names},
+        {roles, Roles}
+    ]}.
 
 get_revs_limit(#db{revs_limit=Limit}) ->
     Limit.
@@ -282,18 +375,8 @@
        group_alike_docs(Rest, [[Doc]|[Bucket|RestBuckets]])
     end.
 
-
-validate_doc_update(#db{user_ctx=UserCtx, admins=Admins},
-        #doc{id= <<"_design/",_/binary>>}, _GetDiskDocFun) ->
-    UserNames = [UserCtx#user_ctx.name | UserCtx#user_ctx.roles],
-    % if the user is a server admin or db admin, allow the save
-    case length(UserNames -- [<<"_admin">> | Admins]) =:= length(UserNames) of
-    true ->
-        % not an admin
-        {unauthorized, <<"You are not a server or database admin.">>};
-    false ->
-        ok
-    end;
+validate_doc_update(#db{}=Db, #doc{id= <<"_design/",_/binary>>}, _GetDiskDocFun) ->
+    catch check_is_admin(Db);
 validate_doc_update(#db{validate_doc_funs=[]}, _Doc, _GetDiskDocFun) ->
     ok;
 validate_doc_update(_Db, #doc{id= <<"_local/",_/binary>>}, _GetDiskDocFun) ->
@@ -301,7 +384,8 @@
 validate_doc_update(Db, Doc, GetDiskDocFun) ->
     DiskDoc = GetDiskDocFun(),
     JsonCtx = couch_util:json_user_ctx(Db),
-    try [case Fun(Doc, DiskDoc, JsonCtx) of
+    SecObj = get_security(Db),
+    try [case Fun(Doc, DiskDoc, JsonCtx, SecObj) of
             ok -> ok;
             Error -> throw(Error)
         end || Fun <- Db#db.validate_doc_funs],

Modified: couchdb/trunk/src/couchdb/couch_db.hrl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_db.hrl?rev=905436&r1=905435&r2=905436&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/couch_db.hrl (original)
+++ couchdb/trunk/src/couchdb/couch_db.hrl Mon Feb  1 22:51:15 2010
@@ -126,7 +126,7 @@
 % if the disk revision is incremented, then new upgrade logic will need to be
 % added to couch_db_updater:init_db.
 
--define(LATEST_DISK_VERSION, 4).
+-define(LATEST_DISK_VERSION, 5).
 
 -record(db_header,
     {disk_version = ?LATEST_DISK_VERSION,
@@ -137,7 +137,7 @@
      local_docs_btree_state = nil,
      purge_seq = 0,
      purged_docs = nil,
-     admins_ptr = nil,
+     security_ptr = nil,
      revs_limit = 1000
     }).
 
@@ -157,8 +157,8 @@
     name,
     filepath,
     validate_doc_funs = [],
-    admins = [],
-    admins_ptr = nil,
+    security = [],
+    security_ptr = nil,
     user_ctx = #user_ctx{},
     waiting_delayed_commit = nil,
     revs_limit = 1000,

Modified: couchdb/trunk/src/couchdb/couch_db_updater.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_db_updater.erl?rev=905436&r1=905435&r2=905436&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/couch_db_updater.erl (original)
+++ couchdb/trunk/src/couchdb/couch_db_updater.erl Mon Feb  1 22:51:15 2010
@@ -53,9 +53,9 @@
     couch_db_update_notifier:notify({updated, Db#db.name}),
     {reply, {ok, Db2#db.update_seq}, Db2};
 
-handle_call({set_admins, NewAdmins}, _From, Db) ->
-    {ok, Ptr} = couch_file:append_term(Db#db.fd, NewAdmins),
-    Db2 = commit_data(Db#db{admins=NewAdmins, admins_ptr=Ptr,
+handle_call({set_security, NewSec}, _From, Db) ->
+    {ok, Ptr} = couch_file:append_term(Db#db.fd, NewSec),
+    Db2 = commit_data(Db#db{security=NewSec, security_ptr=Ptr,
             update_seq=Db#db.update_seq+1}),
     ok = gen_server:call(Db2#db.main_pid, {db_updated, Db2}),
     {reply, ok, Db2};
@@ -326,7 +326,7 @@
 
 simple_upgrade_record(Old, New) when tuple_size(Old) =:= tuple_size(New) ->
     Old;
-simple_upgrade_record(Old, New) ->
+simple_upgrade_record(Old, New) when tuple_size(Old) < tuple_size(New) ->
     OldSz = tuple_size(Old),
     NewValuesTail =
         lists:sublist(tuple_to_list(New), OldSz + 1, tuple_size(New) - OldSz),
@@ -337,9 +337,10 @@
     Header1 = simple_upgrade_record(Header0, #db_header{}),
     Header =
     case element(2, Header1) of
-    1 -> Header1#db_header{unused = 0}; % 0.9
-    2 -> Header1#db_header{unused = 0}; % post 0.9 and pre 0.10
-    3 -> Header1; % post 0.9 and pre 0.10
+    1 -> Header1#db_header{unused = 0, security_ptr = nil}; % 0.9
+    2 -> Header1#db_header{unused = 0, security_ptr = nil}; % post 0.9 and pre 0.10
+    3 -> Header1#db_header{security_ptr = nil}; % post 0.9 and pre 0.10
+    4 -> Header1#db_header{security_ptr = nil}; % 0.10 and pre 0.11
     ?LATEST_DISK_VERSION -> Header1;
     _ -> throw({database_disk_version_error, "Incorrect disk header version"})
     end,
@@ -362,12 +363,12 @@
             {join, fun(X,Y) -> btree_by_seq_join(X,Y) end},
             {reduce, fun(X,Y) -> btree_by_seq_reduce(X,Y) end}]),
     {ok, LocalDocsBtree} = couch_btree:open(Header#db_header.local_docs_btree_state, Fd),
-    case Header#db_header.admins_ptr of
+    case Header#db_header.security_ptr of
     nil ->
-        Admins = [],
-        AdminsPtr = nil;
-    AdminsPtr ->
-        {ok, Admins} = couch_file:pread_term(Fd, AdminsPtr)
+        Security = [],
+        SecurityPtr = nil;
+    SecurityPtr ->
+        {ok, Security} = couch_file:pread_term(Fd, SecurityPtr)
     end,
     % convert start time tuple to microsecs and store as a binary string
     {MegaSecs, Secs, MicroSecs} = now(),
@@ -386,8 +387,8 @@
         update_seq = Header#db_header.update_seq,
         name = DbName,
         filepath = Filepath,
-        admins = Admins,
-        admins_ptr = AdminsPtr,
+        security = Security,
+        security_ptr = SecurityPtr,
         instance_start_time = StartTime,
         revs_limit = Header#db_header.revs_limit,
         fsync_options = FsyncOptions
@@ -655,7 +656,7 @@
         docinfo_by_seq_btree_state = couch_btree:get_state(Db#db.docinfo_by_seq_btree),
         fulldocinfo_by_id_btree_state = couch_btree:get_state(Db#db.fulldocinfo_by_id_btree),
         local_docs_btree_state = couch_btree:get_state(Db#db.local_docs_btree),
-        admins_ptr = Db#db.admins_ptr,
+        security_ptr = Db#db.security_ptr,
         revs_limit = Db#db.revs_limit}.
 
 commit_data(#db{fd=Fd,header=OldHeader,fsync_options=FsyncOptions}=Db, Delay) ->
@@ -810,9 +811,9 @@
     NewDb3 = copy_docs(Db, NewDb2, lists:reverse(Uncopied), Retry),
 
     % copy misc header values
-    if NewDb3#db.admins /= Db#db.admins ->
-        {ok, Ptr} = couch_file:append_term(NewDb3#db.fd, Db#db.admins),
-        NewDb4 = NewDb3#db{admins=Db#db.admins, admins_ptr=Ptr};
+    if NewDb3#db.security /= Db#db.security ->
+        {ok, Ptr} = couch_file:append_term(NewDb3#db.fd, Db#db.security),
+        NewDb4 = NewDb3#db{security=Db#db.security, security_ptr=Ptr};
     true ->
         NewDb4 = NewDb3
     end,

Modified: couchdb/trunk/src/couchdb/couch_doc.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_doc.erl?rev=905436&r1=905435&r2=905436&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/couch_doc.erl (original)
+++ couchdb/trunk/src/couchdb/couch_doc.erl Mon Feb  1 22:51:15 2010
@@ -307,8 +307,8 @@
     undefined ->
         nil;
     _Else ->
-        fun(EditDoc, DiskDoc, Ctx) ->
-            couch_query_servers:validate_doc_update(DDoc, EditDoc, DiskDoc, Ctx)
+        fun(EditDoc, DiskDoc, Ctx, SecObj) ->
+            couch_query_servers:validate_doc_update(DDoc, EditDoc, DiskDoc, Ctx, SecObj)
         end
     end.
 

Modified: couchdb/trunk/src/couchdb/couch_httpd_db.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_httpd_db.erl?rev=905436&r1=905435&r2=905436&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/couch_httpd_db.erl (original)
+++ couchdb/trunk/src/couchdb/couch_httpd_db.erl Mon Feb  1 22:51:15 2010
@@ -548,8 +548,7 @@
     send_method_not_allowed(Req, "POST");
 
 
-db_req(#httpd{method='PUT',path_parts=[_,<<"_admins">>]}=Req,
-        Db) ->
+db_req(#httpd{method='PUT',path_parts=[_,<<"_admins">>]}=Req, Db) ->
     Admins = couch_httpd:json_body(Req),
     ok = couch_db:set_admins(Db, Admins),
     send_json(Req, {[{<<"ok">>, true}]});
@@ -560,6 +559,28 @@
 db_req(#httpd{path_parts=[_,<<"_admins">>]}=Req, _Db) ->
     send_method_not_allowed(Req, "PUT,GET");
 
+db_req(#httpd{method='PUT',path_parts=[_,<<"_readers">>]}=Req, Db) ->
+    Readers = couch_httpd:json_body(Req),
+    ok = couch_db:set_readers(Db, Readers),
+    send_json(Req, {[{<<"ok">>, true}]});
+
+db_req(#httpd{method='GET',path_parts=[_,<<"_readers">>]}=Req, Db) ->
+    send_json(Req, couch_db:get_readers(Db));
+
+db_req(#httpd{path_parts=[_,<<"_readers">>]}=Req, _Db) ->
+    send_method_not_allowed(Req, "PUT,GET");
+
+db_req(#httpd{method='PUT',path_parts=[_,<<"_security">>]}=Req, Db) ->
+    SecObj = couch_httpd:json_body(Req),
+    ok = couch_db:set_security(Db, SecObj),
+    send_json(Req, {[{<<"ok">>, true}]});
+
+db_req(#httpd{method='GET',path_parts=[_,<<"_security">>]}=Req, Db) ->
+    send_json(Req, couch_db:get_security(Db));
+
+db_req(#httpd{path_parts=[_,<<"_security">>]}=Req, _Db) ->
+    send_method_not_allowed(Req, "PUT,GET");
+
 db_req(#httpd{method='PUT',path_parts=[_,<<"_revs_limit">>]}=Req,
         Db) ->
     Limit = couch_httpd:json_body(Req),

Modified: couchdb/trunk/src/couchdb/couch_query_servers.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_query_servers.erl?rev=905436&r1=905435&r2=905436&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/couch_query_servers.erl (original)
+++ couchdb/trunk/src/couchdb/couch_query_servers.erl Mon Feb  1 22:51:15 2010
@@ -17,7 +17,7 @@
 
 -export([init/1, terminate/2, handle_call/3, handle_cast/2, handle_info/2,code_change/3,stop/0]).
 -export([start_doc_map/2, map_docs/2, stop_doc_map/1]).
--export([reduce/3, rereduce/3,validate_doc_update/4]).
+-export([reduce/3, rereduce/3,validate_doc_update/5]).
 -export([filter_docs/5]).
 
 -export([with_ddoc_proc/2, proc_prompt/2, ddoc_prompt/3, ddoc_proc_prompt/3, json_doc/1]).
@@ -166,10 +166,10 @@
 
 
 % use the function stored in ddoc.validate_doc_update to test an update.
-validate_doc_update(DDoc, EditDoc, DiskDoc, Ctx) ->
+validate_doc_update(DDoc, EditDoc, DiskDoc, Ctx, SecObj) ->
     JsonEditDoc = couch_doc:to_json_obj(EditDoc, [revs]),
     JsonDiskDoc = json_doc(DiskDoc),  
-    case ddoc_prompt(DDoc, [<<"validate_doc_update">>], [JsonEditDoc, JsonDiskDoc, Ctx]) of
+    case ddoc_prompt(DDoc, [<<"validate_doc_update">>], [JsonEditDoc, JsonDiskDoc, Ctx, SecObj]) of
         1 ->
             ok;
         {[{<<"forbidden">>, Message}]} ->