summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Christopher Anderson <jchris@apache.org>2010-01-07 20:02:46 +0000
committerJohn Christopher Anderson <jchris@apache.org>2010-01-07 20:02:46 +0000
commitcd0e9c9b6384e4c9200d10088a13164ce4229ea6 (patch)
tree0ac40098a49b2dd62b0099f742323a7811399489
parentdd15c8ed5bf5873aec08a99a0687849f1d29f4c3 (diff)
merge account branch to trunk
git-svn-id: https://svn.apache.org/repos/asf/couchdb/trunk@896989 13f79535-47bb-0310-9956-ffa450edef68
-rw-r--r--etc/couchdb/default.ini.tpl.in5
-rw-r--r--etc/couchdb/local.ini6
-rw-r--r--share/Makefile.am5
-rw-r--r--share/www/_sidebar.html28
-rw-r--r--share/www/config.html4
-rw-r--r--share/www/couch_tests.html8
-rw-r--r--share/www/custom_test.html4
-rw-r--r--share/www/database.html3
-rw-r--r--share/www/dialog/_admin_party.html33
-rw-r--r--share/www/dialog/_create_admin.html50
-rw-r--r--share/www/dialog/_login.html34
-rw-r--r--share/www/dialog/_signup.html35
-rw-r--r--share/www/document.html9
-rw-r--r--share/www/index.html1
-rw-r--r--share/www/replicator.html2
-rw-r--r--share/www/script/couch.js62
-rw-r--r--share/www/script/couch_test_runner.js67
-rw-r--r--share/www/script/couch_tests.js1
-rw-r--r--share/www/script/futon.browse.js7
-rw-r--r--share/www/script/futon.js128
-rw-r--r--share/www/script/jquery.couch.js67
-rw-r--r--share/www/script/test/cookie_auth.js253
-rw-r--r--share/www/script/test/oauth.js11
-rw-r--r--share/www/script/test/users_db.js67
-rw-r--r--share/www/style/layout.css11
-rw-r--r--src/couchdb/couch_db.hrl7
-rw-r--r--src/couchdb/couch_httpd.erl28
-rw-r--r--src/couchdb/couch_httpd_auth.erl439
-rw-r--r--src/couchdb/couch_httpd_db.erl35
-rw-r--r--src/couchdb/couch_httpd_oauth.erl3
30 files changed, 962 insertions, 451 deletions
diff --git a/etc/couchdb/default.ini.tpl.in b/etc/couchdb/default.ini.tpl.in
index 71656d26..409bfe98 100644
--- a/etc/couchdb/default.ini.tpl.in
+++ b/etc/couchdb/default.ini.tpl.in
@@ -17,9 +17,8 @@ batch_save_interval = 1000 ; milliseconds after which to save batches
[httpd]
port = 5984
bind_address = 127.0.0.1
-authentication_handlers = {couch_httpd_oauth, oauth_authentication_handler}, {couch_httpd_auth, default_authentication_handler}
+authentication_handlers = {couch_httpd_oauth, oauth_authentication_handler}, {couch_httpd_auth, cookie_authentication_handler}, {couch_httpd_auth, default_authentication_handler}
default_handler = {couch_httpd_db, handle_request}
-WWW-Authenticate = Basic realm="administrator"
[log]
file = %localstatelogdir%/couch.log
@@ -27,7 +26,6 @@ level = info
[couch_httpd_auth]
authentication_db = users
-secret = replace this with a real secret in your local.ini file
require_valid_user = false
[query_servers]
@@ -70,7 +68,6 @@ _log = {couch_httpd_misc_handlers, handle_log_req}
_sleep = {couch_httpd_misc_handlers, handle_sleep_req}
_session = {couch_httpd_auth, handle_session_req}
_oauth = {couch_httpd_oauth, handle_oauth_req}
-_user = {couch_httpd_auth, handle_user_req}
[httpd_db_handlers]
_view_cleanup = {couch_httpd_db, handle_view_cleanup_req}
diff --git a/etc/couchdb/local.ini b/etc/couchdb/local.ini
index 0a118770..96fcdc76 100644
--- a/etc/couchdb/local.ini
+++ b/etc/couchdb/local.ini
@@ -10,14 +10,12 @@
[httpd]
;port = 5984
;bind_address = 127.0.0.1
+; Uncomment next line to trigger basic-auth popup on unauthorized requests.
+;WWW-Authenticate = Basic realm="administrator"
[log]
;level = debug
-[couch_httpd_auth]
-;secret = replace this with a real secret
-
-
[update_notification]
;unique notifier name=/full/path/to/exe -with "cmd line arg"
diff --git a/share/Makefile.am b/share/Makefile.am
index 694b89b7..dafbc786 100644
--- a/share/Makefile.am
+++ b/share/Makefile.am
@@ -39,8 +39,13 @@ nobase_dist_localdata_DATA = \
www/couch_tests.html \
www/custom_test.html \
www/database.html \
+ www/dialog/_admin_party.html \
www/dialog/_compact_database.html \
+ www/dialog/_compact_view.html \
www/dialog/_view_cleanup.html \
+ www/dialog/_create_admin.html \
+ www/dialog/_login.html \
+ www/dialog/_signup.html \
www/dialog/_create_database.html \
www/dialog/_delete_database.html \
www/dialog/_delete_document.html \
diff --git a/share/www/_sidebar.html b/share/www/_sidebar.html
index c83b100c..6c7abc99 100644
--- a/share/www/_sidebar.html
+++ b/share/www/_sidebar.html
@@ -30,7 +30,31 @@ specific language governing permissions and limitations under the License.
</li>
</ul>
<div id="footer">
- Futon on <a href="http://couchdb.apache.org/">Apache CouchDB</a>
- <span id="version">?</span>
+ <span id="userCtx">
+ <span class="loggedout">
+ <a href="#" class="signup">Signup</a> or <a href="#" class="login">Login</a>
+ </span>
+ <span class="loggedin">
+ Welcome <a class="username">?</a>!
+ <br/>
+ <a href="#" class="logout">Logout</a>
+ </span>
+ <span class="loggedinadmin">
+ Welcome <a class="username">?</a>!
+ <br/>
+ <a href="#" class="createadmin">Setup more admins</a> or
+ <a href="#" class="logout">Logout</a>
+ </span>
+ <span class="adminparty">
+ Welcome to Admin Party!
+ <br/>
+ Everyone is admin. <a href="#" class="createadmin">Fix this</a>
+ </span>
+ </span>
+ <hr/>
+ <span class="couch">
+ Futon on <a href="http://couchdb.apache.org/">Apache CouchDB</a>
+ <span id="version">?</span>
+ </span>
</div>
</div>
diff --git a/share/www/config.html b/share/www/config.html
index f324c923..8f788041 100644
--- a/share/www/config.html
+++ b/share/www/config.html
@@ -19,10 +19,12 @@ specific language governing permissions and limitations under the License.
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<link rel="stylesheet" href="style/layout.css?0.11.0" type="text/css">
<script src="script/json2.js"></script>
+ <script src="script/sha1.js"></script>
<script src="script/jquery.js?1.3.2"></script>
<script src="script/jquery.couch.js?0.11.0"></script>
- <script src="script/jquery.editinline.js?0.11.0"></script>
+ <script src="script/jquery.dialog.js?0.11.0"></script>
<script src="script/futon.js?0.11.0"></script>
+ <script src="script/jquery.editinline.js?0.11.0"></script>
<script>
$(function() {
$.couch.config({
diff --git a/share/www/couch_tests.html b/share/www/couch_tests.html
index d34e3f8c..fa347764 100644
--- a/share/www/couch_tests.html
+++ b/share/www/couch_tests.html
@@ -19,15 +19,19 @@ specific language governing permissions and limitations under the License.
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<link rel="stylesheet" href="style/layout.css?0.11.0" type="text/css">
<script src="script/json2.js"></script>
+ <script src="script/sha1.js"></script>
<script src="script/jquery.js?1.3.2"></script>
<script src="script/jquery.couch.js?0.11.0"></script>
- <script src="script/couch.js?0.11.0"></script>
+ <script src="script/jquery.dialog.js?0.11.0"></script>
<script src="script/futon.js?0.11.0"></script>
+ <script src="script/couch.js?0.11.0"></script>
<script src="script/couch_test_runner.js?0.11.0"></script>
<script>
$(function() {
updateTestsListing();
- $("#toolbar button.run").click(runAllTests);
+ $("#toolbar button.run").click(function() {
+ setupAdminParty(runAllTests) ;
+ });
$("#toolbar button.load").click(function() {
location.reload(true);
});
diff --git a/share/www/custom_test.html b/share/www/custom_test.html
index 21d33ea5..6106c879 100644
--- a/share/www/custom_test.html
+++ b/share/www/custom_test.html
@@ -19,11 +19,13 @@ specific language governing permissions and limitations under the License.
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<link rel="stylesheet" href="style/layout.css?0.11.0" type="text/css">
<script src="script/json2.js"></script>
+ <script src="script/sha1.js"></script>
<script src="script/jquery.js?1.3.2"></script>
<script src="script/jquery.couch.js?0.11.0"></script>
+ <script src="script/jquery.dialog.js?0.11.0"></script>
+ <script src="script/futon.js?0.11.0"></script>
<script src="script/jquery.resizer.js?0.11.0"></script>
<script src="script/couch.js?0.11.0"></script>
- <script src="script/futon.js?0.11.0"></script>
<script>
function T(arg, desc) {
if(!arg) {
diff --git a/share/www/database.html b/share/www/database.html
index 65177eb7..bafac223 100644
--- a/share/www/database.html
+++ b/share/www/database.html
@@ -19,12 +19,13 @@ specific language governing permissions and limitations under the License.
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<link rel="stylesheet" href="style/layout.css?0.11.0" type="text/css">
<script src="script/json2.js"></script>
+ <script src="script/sha1.js"></script>
<script src="script/jquery.js?1.3.2"></script>
<script src="script/jquery.couch.js?0.11.0"></script>
<script src="script/jquery.dialog.js?0.11.0"></script>
+ <script src="script/futon.js?0.11.0"></script>
<script src="script/jquery.resizer.js?0.11.0"></script>
<script src="script/jquery.suggest.js?0.11.0"></script>
- <script src="script/futon.js?0.11.0"></script>
<script src="script/futon.browse.js?0.11.0"></script>
<script src="script/futon.format.js?0.11.0"></script>
<script>
diff --git a/share/www/dialog/_admin_party.html b/share/www/dialog/_admin_party.html
new file mode 100644
index 00000000..f52099a6
--- /dev/null
+++ b/share/www/dialog/_admin_party.html
@@ -0,0 +1,33 @@
+<!--
+
+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>Admin Party!</h2>
+ <fieldset>
+ <p class="help">
+ The test suite requires CouchDB to be in <em>Admin Party</em> mode. This
+ mode give all users admin capabilities. This is the least secure mode of
+ operation. Do not run the tests on production servers, as you'll impact
+ both performance and security.
+ </p>
+ <p class="help">
+ Clicking start will remove all admins from the configuration. You will
+ have to recreate any admins by hand after the tests have finished.
+ </p>
+ </fieldset>
+ <div class="buttons">
+ <button type="submit">Remove Admins</button>
+ <button type="button" class="cancel">Cancel</button>
+ </div>
+</form>
diff --git a/share/www/dialog/_create_admin.html b/share/www/dialog/_create_admin.html
new file mode 100644
index 00000000..e4141e1d
--- /dev/null
+++ b/share/www/dialog/_create_admin.html
@@ -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>Create Server Admin</h2>
+ <fieldset>
+ <p class="help">
+ Before a server admin is configured, all clients have admin privileges.
+ This is fine when HTTP access is restricted
+ to trusted users. <strong>If end-users will be accessing this CouchDB, you must
+ create an admin account to prevent accidental (or malicious) data loss.</strong>
+ </p>
+ <p class="help">Server admins can create and destroy databases, install
+ and update _design documents, run the test suite, and edit all aspects of CouchDB
+ configuration.
+ </p>
+ <table summary=""><tbody><tr>
+ <th><label>Username:</label></th>
+ <td><input type="text" name="username" size="24"></td>
+ </tr><tr>
+ <th><label>Password:</label></th>
+ <td><input type="password" name="password" size="24"></td>
+ </tr>
+ </tbody></table>
+ <p class="help">Non-admin users have read and write access to all databases, which
+ are controlled by validation functions. CouchDB can be configured to block all
+ access to anonymous users.
+ </p>
+ <h3>About Authentication</h3>
+ <p class="help">
+ Couch has a pluggable authentication mechanism. Futon exposes a user friendly cookie-auth which handles login and logout, so app developers can relax. Just use <tt>$.couch.session()</tt> to load the current user's info.
+ </p>
+
+ </fieldset>
+ <div class="buttons">
+ <button type="submit">Create</button>
+ <button type="button" class="cancel">Cancel</button>
+ </div>
+</form>
diff --git a/share/www/dialog/_login.html b/share/www/dialog/_login.html
new file mode 100644
index 00000000..959f7233
--- /dev/null
+++ b/share/www/dialog/_login.html
@@ -0,0 +1,34 @@
+<!--
+
+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>Login</h2>
+ <fieldset>
+ <p class="help">
+ Login to CouchDB with your username and password.
+ </p>
+ <table summary=""><tbody><tr>
+ <th><label>Username:</label></th>
+ <td><input type="text" name="username" size="24"></td>
+ </tr><tr>
+ <th><label>Password:</label></th>
+ <td><input type="password" name="password" size="24"></td>
+ </tr>
+ </tbody></table>
+ </fieldset>
+ <div class="buttons">
+ <button type="submit">Login</button>
+ <button type="button" class="cancel">Cancel</button>
+ </div>
+</form>
diff --git a/share/www/dialog/_signup.html b/share/www/dialog/_signup.html
new file mode 100644
index 00000000..884b4be2
--- /dev/null
+++ b/share/www/dialog/_signup.html
@@ -0,0 +1,35 @@
+<!--
+
+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>Create User Account</h2>
+ <fieldset>
+ <p class="help">
+ Create a user document on this CouchDB. You will be logged in as this
+ user after the document is created.
+ </p>
+ <table summary=""><tbody><tr>
+ <th><label>Username:</label></th>
+ <td><input type="text" name="username" size="24"></td>
+ </tr><tr>
+ <th><label>Password:</label></th>
+ <td><input type="password" name="password" size="24"></td>
+ </tr>
+ </tbody></table>
+ </fieldset>
+ <div class="buttons">
+ <button type="submit">Create</button>
+ <button type="button" class="cancel">Cancel</button>
+ </div>
+</form>
diff --git a/share/www/document.html b/share/www/document.html
index 2202f170..772876aa 100644
--- a/share/www/document.html
+++ b/share/www/document.html
@@ -19,15 +19,16 @@ specific language governing permissions and limitations under the License.
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<link rel="stylesheet" href="style/layout.css?0.11.0" type="text/css">
<script src="script/json2.js"></script>
+ <script src="script/sha1.js"></script>
<script src="script/jquery.js?1.3.2"></script>
<script src="script/jquery.couch.js?0.11.0"></script>
<script src="script/jquery.dialog.js?0.11.0"></script>
- <script src="script/jquery.editinline.js?0.11.0"></script>
- <script src="script/jquery.form.js?0.11.0"></script>
- <script src="script/jquery.resizer.js?0.11.0"></script>
<script src="script/futon.js?0.11.0"></script>
+ <script src="script/jquery.resizer.js?0.11.0"></script>
<script src="script/futon.browse.js?0.11.0"></script>
- <script src="script/futon.format.js?0.11.0"></script>
+ <script src="script/futon.format.js?0.11.0"></script>
+ <script src="script/jquery.editinline.js?0.11.0"></script>
+ <script src="script/jquery.form.js?0.11.0"></script>
<script>
var page = new $.futon.CouchDocumentPage();
diff --git a/share/www/index.html b/share/www/index.html
index 8115bc4d..2cf04c87 100644
--- a/share/www/index.html
+++ b/share/www/index.html
@@ -19,6 +19,7 @@ specific language governing permissions and limitations under the License.
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<link rel="stylesheet" href="style/layout.css?0.11.0" type="text/css">
<script src="script/json2.js"></script>
+ <script src="script/sha1.js"></script>
<script src="script/jquery.js?1.3.2"></script>
<script src="script/jquery.couch.js?0.11.0"></script>
<script src="script/jquery.dialog.js?0.11.0"></script>
diff --git a/share/www/replicator.html b/share/www/replicator.html
index 3516128d..5a09ca16 100644
--- a/share/www/replicator.html
+++ b/share/www/replicator.html
@@ -19,8 +19,10 @@ specific language governing permissions and limitations under the License.
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<link rel="stylesheet" href="style/layout.css?0.11.0" type="text/css">
<script src="script/json2.js"></script>
+ <script src="script/sha1.js"></script>
<script src="script/jquery.js?1.3.2"></script>
<script src="script/jquery.couch.js?0.11.0"></script>
+ <script src="script/jquery.dialog.js?0.11.0"></script>
<script src="script/futon.js?0.11.0"></script>
<script>
$(document).ready(function() {
diff --git a/share/www/script/couch.js b/share/www/script/couch.js
index 19b98edb..21ea39b3 100644
--- a/share/www/script/couch.js
+++ b/share/www/script/couch.js
@@ -340,60 +340,34 @@ CouchDB.logout = function() {
return JSON.parse(CouchDB.last_req.responseText);
}
-CouchDB.createUser = function(username, password, email, roles, basicAuth) {
- var roles_str = ""
- if (roles) {
- for (var i=0; i< roles.length; i++) {
- roles_str += "&roles=" + encodeURIComponent(roles[i]);
- }
- }
- var headers = {"Content-Type": "application/x-www-form-urlencoded"};
- if (basicAuth) {
- headers['Authorization'] = basicAuth
- } else {
- headers['X-CouchDB-WWW-Authenticate'] = 'Cookie';
- }
-
- CouchDB.last_req = CouchDB.request("POST", "/_user/", {
- headers: headers,
- body: "username=" + encodeURIComponent(username) + "&password="
- + encodeURIComponent(password) + "&email="
- + encodeURIComponent(email) + roles_str
- });
+CouchDB.session = function(options) {
+ options = options || {};
+ CouchDB.last_req = CouchDB.request("GET", "/_session", options);
+ CouchDB.maybeThrowError(CouchDB.last_req);
return JSON.parse(CouchDB.last_req.responseText);
-}
-
-CouchDB.updateUser = function(username, email, roles, password, old_password) {
- var roles_str = ""
- if (roles) {
- for (var i=0; i< roles.length; i++) {
- roles_str += "&roles=" + encodeURIComponent(roles[i]);
- }
- }
+};
- var body = "email="+ encodeURIComponent(email)+ roles_str;
+CouchDB.user_prefix = "org.couchdb.user:";
- if (typeof(password) != "undefined" && password) {
- body += "&password=" + password;
+CouchDB.prepareUserDoc = function(user_doc, new_password) {
+ user_doc._id = user_doc._id || CouchDB.user_prefix + user_doc.username;
+ if (new_password) {
+ // handle the password crypto
+ user_doc.salt = CouchDB.newUuids(1)[0];
+ user_doc.password_sha = hex_sha1(new_password + user_doc.salt);
}
-
- if (typeof(old_password) != "undefined" && old_password) {
- body += "&old_password=" + old_password;
+ user_doc.type = "user";
+ if (!user_doc.roles) {
+ user_doc.roles = []
}
-
- CouchDB.last_req = CouchDB.request("PUT", "/_user/"+encodeURIComponent(username), {
- headers: {"Content-Type": "application/x-www-form-urlencoded",
- "X-CouchDB-WWW-Authenticate": "Cookie"},
- body: body
- });
- return JSON.parse(CouchDB.last_req.responseText);
-}
+ return user_doc;
+};
CouchDB.allDbs = function() {
CouchDB.last_req = CouchDB.request("GET", "/_all_dbs");
CouchDB.maybeThrowError(CouchDB.last_req);
return JSON.parse(CouchDB.last_req.responseText);
-}
+};
CouchDB.allDesignDocs = function() {
var ddocs = {}, dbs = CouchDB.allDbs();
diff --git a/share/www/script/couch_test_runner.js b/share/www/script/couch_test_runner.js
index a5ece9cd..237f9312 100644
--- a/share/www/script/couch_test_runner.js
+++ b/share/www/script/couch_test_runner.js
@@ -63,6 +63,8 @@ var numFailures = 0;
var currentRow = null;
function runTest(button, callback, debug, noSave) {
+
+ // offer to save admins
if (currentRow != null) {
alert("Can not run multiple tests simultaneously.");
return;
@@ -116,6 +118,65 @@ function showSource(cell) {
win.document.location = "script/test/" + name + ".js";
}
+var readyToRun;
+function setupAdminParty(fun) {
+ if (readyToRun) {
+ fun();
+ } else {
+ function removeAdmins(confs, doneFun) {
+ // iterate through the config and remove current user last
+ // current user is at front of list
+ var remove = confs.pop();
+ if (remove) {
+ $.couch.config({
+ success : function() {
+ removeAdmins(confs, doneFun);
+ }
+ }, "admins", remove[0], null);
+ } else {
+ doneFun();
+ }
+ };
+ $.couch.session({
+ success : function(userCtx) {
+ if (userCtx.name && userCtx.roles.indexOf("_admin") != -1) {
+ // admin but not admin party. dialog offering to make admin party
+ $.showDialog("dialog/_admin_party.html", {
+ submit: function(data, callback) {
+ $.couch.config({
+ success : function(conf) {
+ var meAdmin, adminConfs = [];
+ for (var name in conf) {
+ if (name == userCtx.name) {
+ meAdmin = [name, conf[name]];
+ } else {
+ adminConfs.push([name, conf[name]]);
+ }
+ }
+ adminConfs.unshift(meAdmin);
+ removeAdmins(adminConfs, function() {
+ callback();
+ $.futon.session.sidebar();
+ readyToRun = true;
+ setTimeout(fun, 500);
+ });
+ }
+ }, "admins");
+ }
+ });
+ } else if (userCtx.roles.indexOf("_admin") != -1) {
+ // admin party!
+ readyToRun = true;
+ fun();
+ } else {
+ // not an admin
+ alert("Error: You need to be an admin to run the tests.");
+ };
+ }
+ });
+ }
+};
+
function updateTestsListing() {
for (var name in couchTests) {
var testFunction = couchTests[name];
@@ -128,7 +189,11 @@ function updateTestsListing() {
.find("td:nth(2)").addClass("details").end();
$("<button type='button' class='run' title='Run test'></button>").click(function() {
this.blur();
- runTest(this);
+ var self = this;
+ // check for admin party
+ setupAdminParty(function() {
+ runTest(self);
+ });
return false;
}).prependTo(row.find("th"));
row.attr("id", name).appendTo("#tests tbody.content");
diff --git a/share/www/script/couch_tests.js b/share/www/script/couch_tests.js
index 817bfa5e..5ae4f1d9 100644
--- a/share/www/script/couch_tests.js
+++ b/share/www/script/couch_tests.js
@@ -73,6 +73,7 @@ loadTest("security_validation.js");
loadTest("show_documents.js");
loadTest("stats.js");
loadTest("update_documents.js");
+loadTest("users_db.js");
loadTest("utf8.js");
loadTest("uuids.js");
loadTest("view_collation.js");
diff --git a/share/www/script/futon.browse.js b/share/www/script/futon.browse.js
index 437c30c2..4d06d283 100644
--- a/share/www/script/futon.browse.js
+++ b/share/www/script/futon.browse.js
@@ -294,7 +294,8 @@
fill_language();
}
}, "native_query_servers");
- }
+ },
+ error : function() {}
}, "query_servers");
}
@@ -727,7 +728,7 @@
},
- // Page class for browse/database.html
+ // Page class for browse/document.html
CouchDocumentPage: function() {
var urlParts = location.search.substr(1).split("/");
var dbName = decodeURIComponent(urlParts.shift());
@@ -1169,7 +1170,7 @@
return false;
}).prependTo($("a", li));
}
- }
+ },
});
diff --git a/share/www/script/futon.js b/share/www/script/futon.js
index 1f51bdee..33c72318 100644
--- a/share/www/script/futon.js
+++ b/share/www/script/futon.js
@@ -12,6 +12,130 @@
(function($) {
+ function Session() {
+
+ function doLogin(username, password, callback) {
+ $.couch.login({
+ username : username,
+ password : password,
+ success : function() {
+ $.futon.session.sidebar();
+ callback();
+ },
+ error : function(code, error, reason) {
+ $.futon.session.sidebar();
+ callback({username : "Error logging in: "+reason});
+ }
+ });
+ };
+
+ function doSignup(username, password, callback, runLogin) {
+ $.couch.signup({
+ username : username
+ }, password, {
+ success : function() {
+ if (runLogin) {
+ doLogin(username, password, callback);
+ } else {
+ callback();
+ }
+ },
+ error : function(status, error, reason) {
+ $.futon.session.sidebar();
+ if (error = "conflict") {
+ callback({username : "Name '"+username+"' is taken"});
+ } else {
+ callback({username : "Signup error: "+reason});
+ }
+ }
+ });
+ };
+
+ function validateUsernameAndPassword(data, callback) {
+ if (!data.username || data.username.length == 0) {
+ callback({username: "Please enter a username."});
+ return false;
+ };
+ if (!data.password || data.password.length == 0) {
+ callback({password: "Please enter a password."});
+ return false;
+ };
+ return true;
+ };
+
+ function createAdmin() {
+ $.showDialog("dialog/_create_admin.html", {
+ submit: function(data, callback) {
+ if (!validateUsernameAndPassword(data, callback)) return;
+ $.couch.config({
+ success : function() {
+ callback();
+ doLogin(data.username, data.password, callback);
+ doSignup(data.username, null, callback, false);
+ }
+ }, "admins", data.username, data.password);
+ }
+ });
+ return false;
+ };
+
+ function login() {
+ $.showDialog("dialog/_login.html", {
+ submit: function(data, callback) {
+ if (!validateUsernameAndPassword(data, callback)) return;
+ doLogin(data.username, data.password, callback);
+ }
+ });
+ return false;
+ };
+
+ function logout() {
+ $.couch.logout({
+ success : function(resp) {
+ $.futon.session.sidebar();
+ }
+ })
+ };
+
+ function signup() {
+ $.showDialog("dialog/_signup.html", {
+ submit: function(data, callback) {
+ if (!validateUsernameAndPassword(data, callback)) return;
+ doSignup(data.username, data.password, callback, true);
+ }
+ });
+ return false;
+ };
+
+ this.setupSidebar = function() {
+ $("#userCtx .login").click(login);
+ $("#userCtx .logout").click(logout);
+ $("#userCtx .signup").click(signup);
+ $("#userCtx .createadmin").click(createAdmin);
+ };
+
+ this.sidebar = function() {
+ // get users db info?
+ $("#userCtx span").hide();
+ $.couch.session({
+ success : function(userCtx) {
+ if (userCtx.name) {
+ $("#userCtx .username").text(userCtx.name).attr({href : "/_utils/document.html?users/org.couchdb.user%3A"+userCtx.name});
+ if (userCtx.roles.indexOf("_admin") != -1) {
+ $("#userCtx .loggedinadmin").show();
+ } else {
+ $("#userCtx .loggedin").show();
+ }
+ } else if (userCtx.roles.indexOf("_admin") != -1) {
+ $("#userCtx .adminparty").show();
+ } else {
+ $("#userCtx .loggedout").show();
+ };
+ }
+ })
+ };
+ };
+
function Navigation() {
var nav = this;
this.loaded = false;
@@ -233,6 +357,7 @@
$.futon = $.futon || {};
$.extend($.futon, {
navigation: new Navigation(),
+ session : new Session(),
storage: new Storage()
});
@@ -309,12 +434,15 @@
$.futon.navigation.updateDatabases();
$.futon.navigation.updateSelection();
$.futon.navigation.ready();
+ $.futon.session.setupSidebar();
+ $.futon.session.sidebar();
$.couch.info({
success: function(info, status) {
$("#version").text(info.version);
}
});
+
});
});
diff --git a/share/www/script/jquery.couch.js b/share/www/script/jquery.couch.js
index 6812ed9a..7e8a0236 100644
--- a/share/www/script/jquery.couch.js
+++ b/share/www/script/jquery.couch.js
@@ -20,7 +20,26 @@
return "_design/" + encodeURIComponent(parts.join('/'));
}
return encodeURIComponent(docID);
- }
+ };
+
+ function prepareUserDoc(user_doc, new_password) {
+ if (typeof hex_sha1 == "undefined") {
+ alert("creating a user doc requires sha1.js to be loaded in the page");
+ return;
+ }
+ var user_prefix = "org.couchdb.user:";
+ user_doc._id = user_doc._id || user_prefix + user_doc.username;
+ if (new_password) {
+ // handle the password crypto
+ user_doc.salt = $.couch.newUUID();
+ user_doc.password_sha = hex_sha1(new_password + user_doc.salt);
+ }
+ user_doc.type = "user";
+ if (!user_doc.roles) {
+ user_doc.roles = []
+ }
+ return user_doc;
+ };
uuidCache = [];
@@ -49,7 +68,9 @@
req.url += encodeURIComponent(option);
}
}
- if (value !== undefined) {
+ if (value === null) {
+ req.type = "DELETE";
+ } else if (value !== undefined) {
req.type = "PUT";
req.data = toJSON(value);
req.contentType = "application/json";
@@ -60,12 +81,46 @@
"An error occurred retrieving/updating the server configuration"
);
},
+
+ session: function(options) {
+ options = options || {};
+ $.ajax({
+ type: "GET", url: "/_session",
+ complete: function(req) {
+ var resp = $.httpData(req, "json");
+ if (req.status == 200) {
+ if (options.success) options.success(resp);
+ } else if (options.error) {
+ options.error(req.status, resp.error, resp.reason);
+ } else {
+ alert("An error occurred getting session info: " + resp.reason);
+ }
+ }
+ });
+ },
- // TODO make login/logout and db.login/db.logout DRY
+ userDb : function(callback) {
+ $.couch.session({
+ success : function(resp) {
+ var userDb = $.couch.db(resp.info.user_db);
+ callback(userDb);
+ }
+ });
+ },
+
+ signup: function(user_doc, password, options) {
+ options = options || {};
+ // prepare user doc based on name and password
+ user_doc = prepareUserDoc(user_doc, password);
+ $.couch.userDb(function(db) {
+ db.saveDoc(user_doc, options);
+ })
+ },
+
login: function(options) {
options = options || {};
$.ajax({
- type: "POST", url: "/_login", dataType: "json",
+ type: "POST", url: "/_session", dataType: "json",
data: {username: options.username, password: options.password},
complete: function(req) {
var resp = $.httpData(req, "json");
@@ -82,7 +137,8 @@
logout: function(options) {
options = options || {};
$.ajax({
- type: "POST", url: "/_logout", dataType: "json",
+ type: "DELETE", url: "/_session", dataType: "json",
+ username : "_", password : "_",
complete: function(req) {
var resp = $.httpData(req, "json");
if (req.status == 200) {
@@ -304,7 +360,6 @@
var keys = options["keys"];
delete options["keys"];
data = toJSON({ "keys": keys });
- console.log(data);
}
ajax({
type: type,
diff --git a/share/www/script/test/cookie_auth.js b/share/www/script/test/cookie_auth.js
index 0a42b4a9..9eadfee0 100644
--- a/share/www/script/test/cookie_auth.js
+++ b/share/www/script/test/cookie_auth.js
@@ -36,117 +36,192 @@ couchTests.cookie_auth = function(debug) {
usersDb.deleteDb();
usersDb.createDb();
+ // test that the users db is born with the auth ddoc
+ var ddoc = usersDb.open("_design/_auth");
+ T(ddoc.validate_doc_update);
+
+ // TODO test that changing the config so an existing db becomes the users db installs the ddoc also
+
var password = "3.141592653589";
// Create a user
- T(usersDb.save({
- _id: "a1",
- salt: "123",
- password_sha: hex_sha1(password + "123"),
+ var jasonUserDoc = CouchDB.prepareUserDoc({
username: "Jason Davies",
- author: "Jason Davies",
- type: "user",
- roles: ["_admin"]
- }).ok);
-
- var validationDoc = {
- _id : "_design/validate",
- validate_doc_update: "(" + (function (newDoc, oldDoc, userCtx) {
- // docs should have an author field.
- if (!newDoc._deleted && !newDoc.author) {
- throw {forbidden:
- "Documents must have an author field"};
- }
- if (oldDoc && oldDoc.author != userCtx.name) {
- throw {unauthorized:
- "You are not the author of this document. You jerk."+userCtx.name};
- }
- }).toString() + ")"
- };
-
- T(db.save(validationDoc).ok);
+ roles: ["dev"]
+ }, password);
+ T(usersDb.save(jasonUserDoc).ok);
+
+ var checkDoc = usersDb.open(jasonUserDoc._id);
+ T(checkDoc.username == "Jason Davies");
+
+ var jchrisUserDoc = CouchDB.prepareUserDoc({
+ username: "jchris@apache.org"
+ }, "funnybone");
+ T(usersDb.save(jchrisUserDoc).ok);
+
+ // make sure we cant create duplicate users
+ var duplicateJchrisDoc = CouchDB.prepareUserDoc({
+ username: "jchris@apache.org"
+ }, "eh, Boo-Boo?");
+ try {
+ usersDb.save(duplicateJchrisDoc)
+ T(false && "Can't create duplicate user names. Should have thrown an error.");
+ } catch (e) {
+ T(e.error == "conflict");
+ T(usersDb.last_req.status == 409);
+ }
+ // we can't create _usernames
+ var underscoreUserDoc = CouchDB.prepareUserDoc({
+ username: "_why"
+ }, "copperfield");
- T(CouchDB.login('Jason Davies', password).ok);
- // update the credentials document
- var doc = usersDb.open("a1");
- doc.foo=2;
- T(usersDb.save(doc).ok);
+ try {
+ usersDb.save(underscoreUserDoc)
+ T(false && "Can't create underscore user names. Should have thrown an error.");
+ } catch (e) {
+ T(e.error == "forbidden");
+ T(usersDb.last_req.status == 403);
+ }
+
+ // we can't create docs with malformed ids
+ var badIdDoc = CouchDB.prepareUserDoc({
+ username: "foo"
+ }, "bar");
+
+ badIdDoc._id = "org.apache.couchdb:w00x";
- // Save a document that's missing an author field.
try {
- // db has a validation function
- db.save({foo:1});
- T(false && "Can't get here. Should have thrown an error 2");
+ usersDb.save(badIdDoc)
+ T(false && "Can't create malformed docids. Should have thrown an error.");
} catch (e) {
T(e.error == "forbidden");
- T(db.last_req.status == 403);
+ T(usersDb.last_req.status == 403);
}
- // TODO should login() throw an exception here?
- T(!CouchDB.login('Jason Davies', "2.71828").ok);
- T(!CouchDB.login('Robert Allen Zimmerman', 'd00d').ok);
-
- // test redirect
- xhr = CouchDB.request("POST", "/_session?next=/", {
- headers: {"Content-Type": "application/x-www-form-urlencoded"},
- body: "username=Jason%20Davies&password="+encodeURIComponent(password)
- });
- // should this be a redirect code instead of 200?
- // The cURL adapter is returning the expected 302 here.
- // I imagine this has to do with whether the client is willing
- // to follow the redirect, ie, the browser follows and does a
- // GET on the returned Location
- T(xhr.status == 200 || xhr.status == 302);
-
- usersDb.deleteDb();
- // test user creation
- T(CouchDB.createUser("test", "testpassword", "test@somemail.com", ['read', 'write']).ok);
+ try {
+ usersDb.save(underscoreUserDoc)
+ T(false && "Can't create underscore user names. Should have thrown an error.");
+ } catch (e) {
+ T(e.error == "forbidden");
+ T(usersDb.last_req.status == 403);
+ }
- // make sure we create a unique user
- T(!CouchDB.createUser("test", "testpassword2", "test2@somemail.com", ['read', 'write']).ok);
+ // login works
+ T(CouchDB.login('Jason Davies', password).ok);
+ T(CouchDB.session().name == 'Jason Davies');
- // test login
- T(CouchDB.login("test", "testpassword").ok);
- T(!CouchDB.login('test', "testpassword2").ok);
+ // update one's own credentials document
+ jasonUserDoc.foo=2;
+ T(usersDb.save(jasonUserDoc).ok);
+
+ // TODO should login() throw an exception here?
+ T(!CouchDB.login('Jason Davies', "2.71828").ok);
+ T(!CouchDB.login('Robert Allen Zimmerman', 'd00d').ok);
+
+ // a failed login attempt should log you out
+ T(CouchDB.session().name != 'Jason Davies');
+
+ // test redirect
+ xhr = CouchDB.request("POST", "/_session?next=/", {
+ headers: {"Content-Type": "application/x-www-form-urlencoded"},
+ body: "username=Jason%20Davies&password="+encodeURIComponent(password)
+ });
+ // should this be a redirect code instead of 200?
+ // The cURL adapter is returning the expected 302 here.
+ // I imagine this has to do with whether the client is willing
+ // to follow the redirect, ie, the browser follows and does a
+ // GET on the returned Location
+ if (xhr.status == 200) {
+ T(/Welcome/.test(xhr.responseText))
+ } else {
+ T(xhr.status == 302)
+ T(xhr.getResponseHeader("Location"))
+ }
+
+ // test users db validations
+ //
+ // test that you can't update docs unless you are logged in as the user (or are admin)
+ T(CouchDB.login("jchris@apache.org", "funnybone").ok);
+ T(CouchDB.session().name == "jchris@apache.org");
+ T(CouchDB.session().roles.length == 0);
- // test update user without changing password
- T(CouchDB.updateUser("test", "test2@somemail.com").ok);
- result = usersDb.view("_auth/users", {key: "test"});
- T(result.rows[0].value['email'] == "test2@somemail.com");
-
-
- // test changing password
- result = usersDb.view("_auth/users", {key: "test"});
- T(CouchDB.updateUser("test", "test2@somemail.com", [], "testpassword2", "testpassword").ok);
- result1 = usersDb.view("_auth/users", {key: "test"});
- T(result.rows[0].value['password_sha'] != result1.rows[0].value['password_sha']);
+ jasonUserDoc.foo=3;
+
+ try {
+ usersDb.save(jasonUserDoc)
+ T(false && "Can't update someone else's user doc. Should have thrown an error.");
+ } catch (e) {
+ T(e.error == "forbidden");
+ T(usersDb.last_req.status == 403);
+ }
+
+ // test that you can't edit roles unless you are admin
+ jchrisUserDoc.roles = ["foo"];
+ try {
+ usersDb.save(jchrisUserDoc)
+ T(false && "Can't set roles unless you are admin. Should have thrown an error.");
+ } catch (e) {
+ T(e.error == "forbidden");
+ T(usersDb.last_req.status == 403);
+ }
- // test changing password with passing old password
- T(!CouchDB.updateUser("test", "test2@somemail.com", [], "testpassword2").ok);
+ T(CouchDB.logout().ok);
+ T(CouchDB.session().roles[0] == "_admin");
+
+ jchrisUserDoc.foo = ["foo"];
+ T(usersDb.save(jchrisUserDoc).ok);
+
+ // test that you can't save system (underscore) roles even if you are admin
+ jchrisUserDoc.roles = ["_bar"];
- // test changing password whith bad old password
- T(!CouchDB.updateUser("test", "test2@somemail.com", [], "testpassword2", "badpasswword").ok);
+ try {
+ usersDb.save(jchrisUserDoc)
+ T(false && "Can't add system roles to user's db. Should have thrown an error.");
+ } catch (e) {
+ T(e.error == "forbidden");
+ T(usersDb.last_req.status == 403);
+ }
- // Only admins can change roles
- T(!CouchDB.updateUser("test", "test2@somemail.com", ['read', 'write']).ok);
+ // make sure the foo role has been applied
+ T(CouchDB.login("jchris@apache.org", "funnybone").ok);
+ T(CouchDB.session().name == "jchris@apache.org");
+ T(CouchDB.session().roles.indexOf("_admin") == -1);
+ T(CouchDB.session().roles.indexOf("foo") != -1);
+ // now let's make jchris a server admin
T(CouchDB.logout().ok);
+ T(CouchDB.session().roles[0] == "_admin");
+ T(CouchDB.session().name == null);
- T(CouchDB.updateUser("test", "test2@somemail.com").ok);
- result = usersDb.view("_auth/users", {key: "test"});
- T(result.rows[0].value['email'] == "test2@somemail.com");
-
- // test changing password, we don't need to set old password when we are admin
- result = usersDb.view("_auth/users", {key: "test"});
- T(CouchDB.updateUser("test", "test2@somemail.com", [], "testpassword3").ok);
- result1 = usersDb.view("_auth/users", {key: "test"});
- T(result.rows[0].value['password_sha'] != result1.rows[0].value['password_sha']);
-
- // Only admins can change roles
- T(CouchDB.updateUser("test", "test2@somemail.com", ['read']).ok);
+ // set the -hashed- password so the salt matches
+ // todo ask on the ML about this
+ run_on_modified_server([{section: "admins",
+ key: "jchris@apache.org", value: "funnybone"}], function() {
+ T(CouchDB.login("jchris@apache.org", "funnybone").ok);
+ T(CouchDB.session().name == "jchris@apache.org");
+ T(CouchDB.session().roles.indexOf("_admin") != -1);
+ // test that jchris still has the foo role
+ T(CouchDB.session().roles.indexOf("foo") != -1);
+
+ // should work even when user doc has no password
+ jchrisUserDoc = usersDb.open(jchrisUserDoc._id);
+ delete jchrisUserDoc.salt;
+ delete jchrisUserDoc.password_sha;
+ T(usersDb.save(jchrisUserDoc).ok);
+ T(CouchDB.logout().ok);
+ T(CouchDB.login("jchris@apache.org", "funnybone").ok);
+ var s = CouchDB.session();
+ T(s.name == "jchris@apache.org");
+ T(s.roles.indexOf("_admin") != -1);
+ // test session info
+ T(s.info.authenticated == "{couch_httpd_auth, cookie_authentication_handler}");
+ T(s.info.user_db == "test_suite_users");
+ // test that jchris still has the foo role
+ T(CouchDB.session().roles.indexOf("foo") != -1);
+ });
} finally {
// Make sure we erase any auth cookies so we don't affect other tests
@@ -157,7 +232,7 @@ couchTests.cookie_auth = function(debug) {
run_on_modified_server(
[{section: "httpd",
key: "authentication_handlers",
- value: "{couch_httpd_auth, cookie_authentication_handler}"},
+ value: "{couch_httpd_auth, cookie_authentication_handler}, {couch_httpd_auth, default_authentication_handler}"},
{section: "couch_httpd_auth",
key: "secret", value: generateSecret(64)},
{section: "couch_httpd_auth",
diff --git a/share/www/script/test/oauth.js b/share/www/script/test/oauth.js
index 5c6c0083..d55d13e8 100644
--- a/share/www/script/test/oauth.js
+++ b/share/www/script/test/oauth.js
@@ -97,6 +97,8 @@ couchTests.oauth = function(debug) {
CouchDB.request("GET", "/_sleep?time=50");
+ CouchDB.newUuids(2); // so we have one to make the salt
+
CouchDB.request("PUT", "http://" + host + "/_config/couch_httpd_auth/require_valid_user", {
headers: {
"X-Couch-Persist": "false",
@@ -113,7 +115,12 @@ couchTests.oauth = function(debug) {
usersDb.createDb();
// Create a user
- T(CouchDB.createUser("jason", "testpassword", "test@somemail.com", ['test'], adminBasicAuthHeaderValue()).ok);
+ var jasonUserDoc = CouchDB.prepareUserDoc({
+ username: "jason",
+ roles: ["test"]
+ }, "testpassword");
+ T(usersDb.save(jasonUserDoc).ok);
+
var accessor = {
consumerSecret: consumerSecret,
@@ -227,7 +234,7 @@ couchTests.oauth = function(debug) {
run_on_modified_server(
[
{section: "httpd",
- key: "WWW-Authenticate", value: 'Basic realm="administrator",OAuth'},
+ key: "WWW-Authenticate", value: 'OAuth'},
{section: "couch_httpd_auth",
key: "secret", value: generateSecret(64)},
{section: "couch_httpd_auth",
diff --git a/share/www/script/test/users_db.js b/share/www/script/test/users_db.js
new file mode 100644
index 00000000..2cf63fcf
--- /dev/null
+++ b/share/www/script/test/users_db.js
@@ -0,0 +1,67 @@
+// 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.users_db = function(debug) {
+ // This tests the users db, especially validations
+ // this should also test that you can log into the couch
+
+ var usersDb = new CouchDB("test_suite_users", {"X-Couch-Full-Commit":"false"});
+
+ // test that you can treat "_user" as a db-name
+ // this can complicate people who try to secure the users db with
+ // an http proxy and fail to get both the actual db and the _user path
+ // maybe it's not the right approach...
+ // hard to know what else to do, as we don't let non-admins inspect the config
+ // to determine the actual users db name.
+
+ function testFun() {
+ usersDb.deleteDb();
+
+ // test that the validation function is installed
+ var ddoc = usersDb.open("_design/_auth");
+ T(ddoc.validate_doc_update);
+
+ // test that you can login as a user using basic auth
+ var jchrisUserDoc = CouchDB.prepareUserDoc({
+ username: "jchris@apache.org"
+ }, "funnybone");
+ T(usersDb.save(jchrisUserDoc).ok);
+
+ T(CouchDB.session().name == null);
+ var s = CouchDB.session({
+ headers : {
+ "Authorization" : "Basic amNocmlzQGFwYWNoZS5vcmc6ZnVubnlib25l"
+ }
+ });
+ T(s.name == "jchris@apache.org");
+ T(s.user_doc._id == "org.couchdb.user:jchris@apache.org")
+ T(s.info.authenticated == "{couch_httpd_auth, default_authentication_handler}");
+ T(s.info.user_db == "test_suite_users");
+ TEquals(["{couch_httpd_oauth, oauth_authentication_handler}",
+ "{couch_httpd_auth, cookie_authentication_handler}",
+ "{couch_httpd_auth, default_authentication_handler}"], s.info.handlers);
+ var s = CouchDB.session({
+ headers : {
+ "Authorization" : "Basic Xzpf" // username and pass of _:_
+ }
+ });
+ T(s.name == null);
+ T(s.info.authenticated == "{couch_httpd_auth, default_authentication_handler}");
+ };
+
+ run_on_modified_server(
+ [{section: "couch_httpd_auth",
+ key: "authentication_db", value: "test_suite_users"}],
+ testFun
+ );
+
+} \ No newline at end of file
diff --git a/share/www/style/layout.css b/share/www/style/layout.css
index 94e59746..b07a59e5 100644
--- a/share/www/style/layout.css
+++ b/share/www/style/layout.css
@@ -211,7 +211,9 @@ body.fullwidth #sidebar { border-bottom: 1px solid #333; right: 0;
font-size: 80%; opacity: .7; padding: 5px 10px; position: absolute; right: 0;
bottom: 0; min-height: 1.3em; width: 190px; text-align: right;
}
-#footer :link, #footer :visited { color: #000; }
+#footer .couch :link, #footer .couch :visited { color: #000; }
+
+#userCtx span { display:none; }
#wrap { background: #fff url(../image/bg.png) 100% 0 repeat-y;
height: 100%; margin-right: 210px; position: relative;
@@ -258,12 +260,19 @@ body.fullwidth #wrap { margin-right: 0; }
-webkit-box-shadow: 4px 4px 6px #333;
}
*html #dialog { width: 33em; }
+body.loading #dialog h2 {
+ background: url(../image/spinner.gif) center no-repeat;
+}
#dialog.loading { width: 220px; height: 80px; }
#dialog.loaded { background-image: none; }
#dialog h2 { background: #666; border-top: 1px solid #555;
border-bottom: 1px solid #777; color: #ccc; font-size: 110%;
font-weight: bold; margin: 0 -2em; padding: .35em 2em;
}
+#dialog h3 {
+ color: #ccc; font-size: 110%;
+ font-weight: bold; margin: 0 -2em; padding: .35em 2em;
+}
#dialog fieldset { background: #222; border-top: 1px solid #111;
margin: 0 0 1em; padding: .5em 1em 1em;
-moz-border-radius-bottomleft: 7px; -moz-border-radius-bottomright: 7px;
diff --git a/src/couchdb/couch_db.hrl b/src/couchdb/couch_db.hrl
index f2ccb453..99ef8997 100644
--- a/src/couchdb/couch_db.hrl
+++ b/src/couchdb/couch_db.hrl
@@ -107,8 +107,11 @@
-record(user_ctx,
- {name=null,
- roles=[]
+ {
+ name=null,
+ roles=[],
+ handler,
+ user_doc
}).
% This should be updated anytime a header change happens that requires more
diff --git a/src/couchdb/couch_httpd.erl b/src/couchdb/couch_httpd.erl
index baa22d8f..252ecdb7 100644
--- a/src/couchdb/couch_httpd.erl
+++ b/src/couchdb/couch_httpd.erl
@@ -17,6 +17,7 @@
-export([header_value/2,header_value/3,qs_value/2,qs_value/3,qs/1,path/1,absolute_uri/2,body_length/1]).
-export([verify_is_server_admin/1,unquote/1,quote/1,recv/2,recv_chunked/4,error_info/1]).
+-export([make_fun_spec_strs/1]).
-export([parse_form/1,json_body/1,json_body_obj/1,body/1,doc_etag/1, make_etag/1, etag_respond/3]).
-export([primary_header_value/2,partition/1,serve_file/3,serve_file/4, server_header/0]).
-export([start_chunked_response/3,send_chunk/2,log_request/2]).
@@ -119,8 +120,8 @@ make_arity_3_fun(SpecStr) ->
end.
% SpecStr is "{my_module, my_fun}, {my_module2, my_fun2}"
-make_arity_1_fun_list(SpecStr) ->
- [make_arity_1_fun(FunSpecStr) || FunSpecStr <- re:split(SpecStr, "(?<=})\\s*,\\s*(?={)", [{return, list}])].
+make_fun_spec_strs(SpecStr) ->
+ [FunSpecStr || FunSpecStr <- re:split(SpecStr, "(?<=})\\s*,\\s*(?={)", [{return, list}])].
stop() ->
mochiweb_http:stop(?MODULE).
@@ -129,7 +130,7 @@ stop() ->
handle_request(MochiReq, DefaultFun,
UrlHandlers, DbUrlHandlers, DesignUrlHandlers) ->
Begin = now(),
- AuthenticationFuns = make_arity_1_fun_list(
+ AuthenticationSrcs = make_fun_spec_strs(
couch_config:get("httpd", "authentication_handlers")),
% for the path, use the raw path with the query string and fragment
% removed, but URL quoting left intact
@@ -180,7 +181,7 @@ handle_request(MochiReq, DefaultFun,
{ok, Resp} =
try
- case authenticate_request(HttpReq, AuthenticationFuns) of
+ case authenticate_request(HttpReq, AuthenticationSrcs) of
#httpd{} = Req ->
HandlerFun(Req);
Response ->
@@ -216,8 +217,10 @@ handle_request(MochiReq, DefaultFun,
couch_stats_collector:increment({httpd, requests}),
{ok, Resp}.
-% Try authentication handlers in order until one returns a result
-authenticate_request(#httpd{user_ctx=#user_ctx{}} = Req, _AuthFuns) ->
+% Try authentication handlers in order until one sets a user_ctx
+% the auth funs also have the option of returning a response
+% move this to couch_httpd_auth?
+authenticate_request(#httpd{user_ctx=#user_ctx{}} = Req, _AuthSrcs) ->
Req;
authenticate_request(#httpd{} = Req, []) ->
case couch_config:get("couch_httpd_auth", "require_valid_user", "false") of
@@ -226,9 +229,15 @@ authenticate_request(#httpd{} = Req, []) ->
"false" ->
Req#httpd{user_ctx=#user_ctx{}}
end;
-authenticate_request(#httpd{} = Req, [AuthFun|Rest]) ->
- authenticate_request(AuthFun(Req), Rest);
-authenticate_request(Response, _AuthFuns) ->
+authenticate_request(#httpd{} = Req, [AuthSrc|Rest]) ->
+ AuthFun = make_arity_1_fun(AuthSrc),
+ R = case AuthFun(Req) of
+ #httpd{user_ctx=#user_ctx{}=UserCtx}=Req2 ->
+ Req2#httpd{user_ctx=UserCtx#user_ctx{handler=?l2b(AuthSrc)}};
+ Else -> Else
+ end,
+ authenticate_request(R, Rest);
+authenticate_request(Response, _AuthSrcs) ->
Response.
increment_method_stats(Method) ->
@@ -586,6 +595,7 @@ send_error(_Req, {already_sent, Resp, _Error}) ->
send_error(#httpd{mochi_req=MochiReq}=Req, Error) ->
{Code, ErrorStr, ReasonStr} = error_info(Error),
Headers = if Code == 401 ->
+ % this is where the basic auth popup is triggered
case MochiReq:get_header_value("X-CouchDB-WWW-Authenticate") of
undefined ->
case couch_config:get("httpd", "WWW-Authenticate", nil) of
diff --git a/src/couchdb/couch_httpd_auth.erl b/src/couchdb/couch_httpd_auth.erl
index b244e16e..554886ca 100644
--- a/src/couchdb/couch_httpd_auth.erl
+++ b/src/couchdb/couch_httpd_auth.erl
@@ -18,7 +18,6 @@
-export([null_authentication_handler/1]).
-export([cookie_auth_header/2]).
-export([handle_session_req/1]).
--export([handle_user_req/1]).
-export([ensure_users_db_exists/1, get_user/1]).
-import(couch_httpd, [header_value/2, send_json/2,send_json/4, send_method_not_allowed/2]).
@@ -49,6 +48,9 @@ basic_username_pw(Req) ->
case AuthorizationHeader of
"Basic " ++ Base64Value ->
case string:tokens(?b2l(couch_util:decodeBase64(Base64Value)),":") of
+ ["_", "_"] ->
+ % special name and pass to be logged out
+ nil;
[User, Pass] ->
{User, Pass};
[User] ->
@@ -63,11 +65,22 @@ basic_username_pw(Req) ->
default_authentication_handler(Req) ->
case basic_username_pw(Req) of
{User, Pass} ->
- case couch_server:is_admin(User, Pass) of
- true ->
- Req#httpd{user_ctx=#user_ctx{name=?l2b(User), roles=[<<"_admin">>]}};
- false ->
- throw({unauthorized, <<"Name or password is incorrect.">>})
+ case get_user(?l2b(User)) of
+ nil ->
+ throw({unauthorized, <<"Name or password is incorrect.">>});
+ UserProps ->
+ UserSalt = proplists:get_value(<<"salt">>, UserProps, <<>>),
+ PasswordHash = hash_password(?l2b(Pass), UserSalt),
+ case proplists:get_value(<<"password_sha">>, UserProps, nil) of
+ ExpectedHash when ExpectedHash == PasswordHash ->
+ Req#httpd{user_ctx=#user_ctx{
+ name=?l2b(User),
+ roles=proplists:get_value(<<"roles">>, UserProps, []),
+ user_doc={UserProps}
+ }};
+ _Else ->
+ throw({unauthorized, <<"Name or password is incorrect.">>})
+ end
end;
nil ->
case couch_server:has_admins() of
@@ -86,138 +99,161 @@ default_authentication_handler(Req) ->
null_authentication_handler(Req) ->
Req#httpd{user_ctx=#user_ctx{roles=[<<"_admin">>]}}.
-% Cookie auth handler using per-node user db
-cookie_authentication_handler(Req) ->
- case cookie_auth_user(Req) of
- % Fall back to default authentication handler
- nil -> default_authentication_handler(Req);
- Req2 -> Req2
- end.
-
-% Cookie auth handler using per-db user db
-% cookie_authentication_handler(#httpd{path_parts=Path}=Req) ->
-% case Path of
-% [DbName|_] ->
-% case cookie_auth_user(Req, DbName) of
-% nil -> default_authentication_handler(Req);
-% Req2 -> Req2
-% end;
-% _Else ->
-% % Fall back to default authentication handler
-% default_authentication_handler(Req)
-% end.
-
% maybe we can use hovercraft to simplify running this view query
+% rename to get_user_from_users_db
get_user(UserName) ->
- % In the future this will be pluggable. For now we check the .ini first,
- % then fall back to querying the db.
case couch_config:get("admins", ?b2l(UserName)) of
"-hashed-" ++ HashedPwdAndSalt ->
+ % the username is an admin, now check to see if there is a user doc
+ % which has a matching username, salt, and password_sha
[HashedPwd, Salt] = string:tokens(HashedPwdAndSalt, ","),
- [{<<"roles">>, [<<"_admin">>]},
- {<<"salt">>, ?l2b(Salt)},
- {<<"password_sha">>, ?l2b(HashedPwd)}];
- _ ->
- DesignId = <<"_design/_auth">>,
- ViewName = <<"users">>,
- % if the design doc or the view doesn't exist, then make it
- DbName = couch_config:get("couch_httpd_auth", "authentication_db"),
- {ok, Db} = ensure_users_db_exists(?l2b(DbName)),
-
- ensure_users_view_exists(Db, DesignId, ViewName),
+ case get_user_props_from_db(UserName) of
+ nil ->
+ [{<<"roles">>, [<<"_admin">>]},
+ {<<"salt">>, ?l2b(Salt)},
+ {<<"password_sha">>, ?l2b(HashedPwd)}];
+ UserProps when is_list(UserProps) ->
+ DocRoles = proplists:get_value(<<"roles">>, UserProps),
+ [{<<"roles">>, [<<"_admin">> | DocRoles]},
+ {<<"salt">>, ?l2b(Salt)},
+ {<<"password_sha">>, ?l2b(HashedPwd)},
+ {<<"user_doc">>, {UserProps}}]
+ end;
+ Else ->
+ get_user_props_from_db(UserName)
+ end.
- case (catch couch_view:get_map_view(Db, DesignId, ViewName, nil)) of
- {ok, View, _Group} ->
- FoldFun = fun({_, Value}, _, {_}) -> {stop, Value} end,
- {ok, _, {Result}} = couch_view:fold(View, FoldFun, {nil},
- [{start_key, {UserName, ?MIN_STR}},{end_key, {UserName, ?MAX_STR}}]),
- Result;
- {not_found, _Reason} ->
- nil
- % case (catch couch_view:get_reduce_view(Db, DesignId, ViewName, nil)) of
- % {ok, _ReduceView, _Group} ->
- % not_implemented;
- % {not_found, _Reason} ->
- % nil
- % end
- end
+get_user_props_from_db(UserName) ->
+ DbName = couch_config:get("couch_httpd_auth", "authentication_db"),
+ {ok, Db} = ensure_users_db_exists(?l2b(DbName)),
+ DocId = <<"org.couchdb.user:", UserName/binary>>,
+ try couch_httpd_db:couch_doc_open(Db, DocId, nil, []) of
+ #doc{}=Doc ->
+ {DocProps} = couch_query_servers:json_doc(Doc),
+ DocProps
+ catch
+ throw:Throw ->
+ nil
end.
-
+
+% this should handle creating the ddoc
ensure_users_db_exists(DbName) ->
case couch_db:open(DbName, [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of
{ok, Db} ->
+ ensure_auth_ddoc_exists(Db, <<"_design/_auth">>),
{ok, Db};
_Error ->
- ?LOG_ERROR("Create the db ~p", [DbName]),
{ok, Db} = couch_db:create(DbName, [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]),
- ?LOG_ERROR("Created the db ~p", [DbName]),
+ ensure_auth_ddoc_exists(Db, <<"_design/_auth">>),
{ok, Db}
end.
-ensure_users_view_exists(Db, DDocId, VName) ->
+ensure_auth_ddoc_exists(Db, DDocId) ->
try couch_httpd_db:couch_doc_open(Db, DDocId, nil, []) of
_Foo -> ok
catch
_:Error ->
- ?LOG_ERROR("create the design document ~p : ~p", [DDocId, Error]),
% create the design document
- {ok, AuthDesign} = auth_design_doc(DDocId, VName),
+ {ok, AuthDesign} = auth_design_doc(DDocId),
{ok, _Rev} = couch_db:update_doc(Db, AuthDesign, []),
- ?LOG_ERROR("created the design document", []),
ok
end.
-auth_design_doc(DocId, VName) ->
+% add the validation function here
+auth_design_doc(DocId) ->
DocProps = [
{<<"_id">>, DocId},
{<<"language">>,<<"javascript">>},
{<<"views">>,
- {[{VName,
+ {[{<<"users">>,
{[{<<"map">>,
<<"function (doc) {\n if (doc.type == \"user\") {\n emit(doc.username, doc);\n}\n}">>
}]}
}]}
+ },
+ {
+ <<"validate_doc_update">>,
+ <<"function(newDoc, oldDoc, userCtx) {
+ if (newDoc.type != 'user') {
+ return;
+ } // we only validate user docs for now
+ if (!newDoc.username) {
+ throw({forbidden : 'doc.username is required'});
+ }
+ if (!(newDoc.roles && (typeof newDoc.roles.length != 'undefined') )) {
+ throw({forbidden : 'doc.roles must be an array'});
+ }
+ if (newDoc._id != 'org.couchdb.user:'+newDoc.username) {
+ throw({forbidden : 'Docid must be of the form org.couchdb.user:username'});
+ }
+ if (oldDoc) { // validate all updates
+ if (oldDoc.username != newDoc.username) {
+ throw({forbidden : 'Usernames may not be changed.'});
+ }
+ }
+ if (newDoc.password_sha && !newDoc.salt) {
+ throw({forbidden : 'Users with password_sha must have a salt. See /_utils/script/couch.js for example code.'});
+ }
+ if (userCtx.roles.indexOf('_admin') == -1) { // not an admin
+ if (oldDoc) { // validate non-admin updates
+ if (userCtx.name != newDoc.username) {
+ throw({forbidden : 'You may only update your own user document.'});
+ }
+ // validate role updates
+ var oldRoles = oldDoc.roles.sort();
+ var newRoles = newDoc.roles.sort();
+ if (oldRoles.length != newRoles.length) {
+ throw({forbidden : 'Only _admin may edit roles'});
+ }
+ for (var i=0; i < oldRoles.length; i++) {
+ if (oldRoles[i] != newRoles[i]) {
+ throw({forbidden : 'Only _admin may edit roles'});
+ }
+ };
+ } else if (newDoc.roles.length > 0) {
+ throw({forbidden : 'Only _admin may set roles'});
+ }
+ }
+ // no system roles in users db
+ for (var i=0; i < newDoc.roles.length; i++) {
+ if (newDoc.roles[i][0] == '_') {
+ throw({forbidden : 'No system roles (starting with underscore) in users db.'});
+ }
+ };
+ // no system names as usernames
+ if (newDoc.username[0] == '_') {
+ throw({forbidden : 'Username may not start with underscore.'});
+ }
+ }">>
}],
{ok, couch_doc:from_json_obj({DocProps})}.
-
-
-user_doc(DocId, Username, UserSalt, PasswordHash, Email, Active, Roles) ->
- user_doc(DocId, Username, UserSalt, PasswordHash, Email, Active, Roles, nil).
-user_doc(DocId, Username, UserSalt, PasswordHash, Email, Active, Roles, Rev) ->
- DocProps = [
- {<<"_id">>, DocId},
- {<<"type">>, <<"user">>},
- {<<"username">>, Username},
- {<<"password_sha">>, PasswordHash},
- {<<"salt">>, UserSalt},
- {<<"email">>, Email},
- {<<"active">>, Active},
- {<<"roles">>, Roles}],
- DocProps1 = case Rev of
- nil -> DocProps;
- _Rev ->
- [{<<"_rev">>, Rev}] ++ DocProps
- end,
- {ok, couch_doc:from_json_obj({DocProps1})}.
-cookie_auth_user(#httpd{mochi_req=MochiReq}=Req) ->
+cookie_authentication_handler(#httpd{mochi_req=MochiReq}=Req) ->
case MochiReq:get_cookie_value("AuthSession") of
- undefined -> nil;
- [] -> nil;
+ undefined -> Req;
+ [] -> Req;
Cookie ->
- AuthSession = couch_util:decodeBase64Url(Cookie),
- [User, TimeStr | HashParts] = string:tokens(?b2l(AuthSession), ":"),
+ [User, TimeStr | HashParts] = try
+ AuthSession = couch_util:decodeBase64Url(Cookie),
+ [A, B | Cs] = string:tokens(?b2l(AuthSession), ":")
+ catch
+ _:Error ->
+ Reason = <<"Malformed AuthSession cookie. Please clear your cookies.">>,
+ throw({bad_request, Reason})
+ end,
% Verify expiry and hash
{NowMS, NowS, _} = erlang:now(),
CurrentTime = NowMS * 1000000 + NowS,
case couch_config:get("couch_httpd_auth", "secret", nil) of
- nil -> nil;
+ nil ->
+ ?LOG_ERROR("cookie auth secret is not set",[]),
+ Req;
SecretStr ->
Secret = ?l2b(SecretStr),
case get_user(?l2b(User)) of
- nil -> nil;
- Result ->
- UserSalt = proplists:get_value(<<"salt">>, Result, <<"">>),
+ nil -> Req;
+ UserProps ->
+ UserSalt = proplists:get_value(<<"salt">>, UserProps, <<"">>),
FullSecret = <<Secret/binary, UserSalt/binary>>,
ExpectedHash = crypto:sha_mac(FullSecret, User ++ ":" ++ TimeStr),
Hash = ?l2b(string:join(HashParts, ":")),
@@ -230,10 +266,11 @@ cookie_auth_user(#httpd{mochi_req=MochiReq}=Req) ->
?LOG_DEBUG("Successful cookie auth as: ~p", [User]),
Req#httpd{user_ctx=#user_ctx{
name=?l2b(User),
- roles=proplists:get_value(<<"roles">>, Result, [])
+ roles=proplists:get_value(<<"roles">>, UserProps, []),
+ user_doc=proplists:get_value(<<"user_doc">>, UserProps, null)
}, auth={FullSecret, TimeLeft < Timeout*0.9}};
_Else ->
- nil
+ Req
end
end
end
@@ -270,8 +307,19 @@ cookie_auth_cookie(User, Secret, TimeStamp) ->
hash_password(Password, Salt) ->
?l2b(couch_util:to_hex(crypto:sha(<<Password/binary, Salt/binary>>))).
+ensure_cookie_auth_secret() ->
+ case couch_config:get("couch_httpd_auth", "secret", nil) of
+ nil ->
+ NewSecret = ?b2l(couch_uuids:random()),
+ couch_config:set("couch_httpd_auth", "secret", NewSecret),
+ NewSecret;
+ Secret -> Secret
+ end.
+
+% session handlers
% Login handler with user db
-handle_login_req(#httpd{method='POST', mochi_req=MochiReq}=Req) ->
+% TODO this should also allow a JSON POST
+handle_session_req(#httpd{method='POST', mochi_req=MochiReq}=Req) ->
ReqBody = MochiReq:recv_body(),
Form = case MochiReq:get_primary_header_value("content-type") of
"application/x-www-form-urlencoded" ++ _ ->
@@ -281,6 +329,7 @@ handle_login_req(#httpd{method='POST', mochi_req=MochiReq}=Req) ->
end,
UserName = ?l2b(proplists:get_value("username", Form, "")),
Password = ?l2b(proplists:get_value("password", Form, "")),
+ ?LOG_DEBUG("Attempt Login: ~s",[UserName]),
User = case get_user(UserName) of
nil -> [];
Result -> Result
@@ -289,10 +338,12 @@ handle_login_req(#httpd{method='POST', mochi_req=MochiReq}=Req) ->
PasswordHash = hash_password(Password, UserSalt),
case proplists:get_value(<<"password_sha">>, User, nil) of
ExpectedHash when ExpectedHash == PasswordHash ->
- Secret = ?l2b(couch_config:get("couch_httpd_auth", "secret", nil)),
+ % setup the session cookie
+ Secret = ?l2b(ensure_cookie_auth_secret()),
{NowMS, NowS, _} = erlang:now(),
CurrentTime = NowMS * 1000000 + NowS,
Cookie = cookie_auth_cookie(?b2l(UserName), <<Secret/binary, UserSalt/binary>>, CurrentTime),
+ % TODO document the "next" feature in Futon
{Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of
nil ->
{200, [Cookie]};
@@ -300,32 +351,38 @@ handle_login_req(#httpd{method='POST', mochi_req=MochiReq}=Req) ->
{302, [Cookie, {"Location", couch_httpd:absolute_uri(Req, Redirect)}]}
end,
send_json(Req#httpd{req_body=ReqBody}, Code, Headers,
- {[{ok, true}]});
+ {[
+ {ok, true},
+ {name, proplists:get_value(<<"username">>, User, null)},
+ {roles, proplists:get_value(<<"roles">>, User, [])},
+ {user_doc, proplists:get_value(<<"user_doc">>, User, null)}
+ ]});
_Else ->
- throw({unauthorized, <<"Name or password is incorrect.">>})
- end.
-
-% Session Handler
-
-handle_session_req(#httpd{method='POST'}=Req) ->
- handle_login_req(Req);
+ % clear the session
+ Cookie = mochiweb_cookies:cookie("AuthSession", "", [{path, "/"}, {http_only, true}]),
+ send_json(Req, 401, [Cookie], {[{error, <<"unauthorized">>},{reason, <<"Name or password is incorrect.">>}]})
+ end;
+% get user info
handle_session_req(#httpd{method='GET', user_ctx=UserCtx}=Req) ->
- % whoami
Name = UserCtx#user_ctx.name,
- Roles = UserCtx#user_ctx.roles,
ForceLogin = couch_httpd:qs_value(Req, "basic", "false"),
case {Name, ForceLogin} of
{null, "true"} ->
throw({unauthorized, <<"Please login.">>});
- _False -> ok
- end,
- send_json(Req, {[
- {ok, true},
- {name, Name},
- {roles, Roles}
- ]});
+ {Name, _} ->
+ send_json(Req, {[
+ {ok, true},
+ {name, Name},
+ {roles, UserCtx#user_ctx.roles},
+ {info, {[
+ {user_db, ?l2b(couch_config:get("couch_httpd_auth", "authentication_db"))},
+ {handlers, [?l2b(H) || H <- couch_httpd:make_fun_spec_strs(
+ couch_config:get("httpd", "authentication_handlers"))]}
+ ] ++ maybe_value(authenticated, UserCtx#user_ctx.handler)}}
+ ] ++ maybe_value(user_doc, UserCtx#user_ctx.user_doc)})
+ end;
+% logout by deleting the session
handle_session_req(#httpd{method='DELETE'}=Req) ->
- % logout
Cookie = mochiweb_cookies:cookie("AuthSession", "", [{path, "/"}, {http_only, true}]),
{Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of
nil ->
@@ -336,135 +393,9 @@ handle_session_req(#httpd{method='DELETE'}=Req) ->
send_json(Req, Code, Headers, {[{ok, true}]});
handle_session_req(Req) ->
send_method_not_allowed(Req, "GET,HEAD,POST,DELETE").
-
-create_user_req(#httpd{method='POST', mochi_req=MochiReq}=Req, Db) ->
- ReqBody = MochiReq:recv_body(),
- Form = case MochiReq:get_primary_header_value("content-type") of
- "application/x-www-form-urlencoded" ++ _ ->
- ?LOG_INFO("body parsed ~p", [mochiweb_util:parse_qs(ReqBody)]),
- mochiweb_util:parse_qs(ReqBody);
- _ ->
- []
- end,
- Roles = proplists:get_all_values("roles", Form),
- UserName = ?l2b(proplists:get_value("username", Form, "")),
- Password = ?l2b(proplists:get_value("password", Form, "")),
- Email = ?l2b(proplists:get_value("email", Form, "")),
- Active = couch_httpd_view:parse_bool_param(proplists:get_value("active", Form, "true")),
- case get_user(UserName) of
- nil ->
- Roles1 = case Roles of
- [] -> Roles;
- _ ->
- ok = couch_httpd:verify_is_server_admin(Req),
- [?l2b(R) || R <- Roles]
- end,
-
- UserSalt = couch_uuids:random(),
- PasswordHash = hash_password(Password, UserSalt),
- DocId = couch_uuids:random(),
- {ok, UserDoc} = user_doc(DocId, UserName, UserSalt, PasswordHash, Email, Active, Roles1),
- {ok, _Rev} = couch_db:update_doc(Db, UserDoc, []),
- ?LOG_DEBUG("User ~s (~s) with password, ~s created.", [?b2l(UserName), ?b2l(DocId), ?b2l(Password)]),
- {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of
- nil ->
- {200, []};
- Redirect ->
- {302, [{"Location", couch_httpd:absolute_uri(Req, Redirect)}]}
- end,
- send_json(Req, Code, Headers, {[{ok, true}]});
- _Result ->
- ?LOG_DEBUG("Can't create ~s: already exists", [?b2l(UserName)]),
- throw({forbidden, <<"User already exists.">>})
- end.
-
-update_user_req(#httpd{method='PUT', mochi_req=MochiReq, user_ctx=UserCtx}=Req, Db, UserName) ->
- Name = UserCtx#user_ctx.name,
- UserRoles = UserCtx#user_ctx.roles,
- case User = get_user(UserName) of
- nil ->
- throw({not_found, <<"User doesn't exist">>});
- _Result ->
- ReqBody = MochiReq:recv_body(),
- Form = case MochiReq:get_primary_header_value("content-type") of
- "application/x-www-form-urlencoded" ++ _ ->
- mochiweb_util:parse_qs(ReqBody);
- _ ->
- []
- end,
- Roles = proplists:get_all_values("roles", Form),
- Password = ?l2b(proplists:get_value("password", Form, "")),
- Email = ?l2b(proplists:get_value("email", Form, "")),
- Active = couch_httpd_view:parse_bool_param(proplists:get_value("active", Form, "true")),
- OldPassword = proplists:get_value("old_password", Form, ""),
- OldPassword1 = ?l2b(OldPassword),
- UserSalt = proplists:get_value(<<"salt">>, User, <<>>),
- OldRev = proplists:get_value(<<"_rev">>, User, <<>>),
- DocId = proplists:get_value(<<"_id">>, User, <<>>),
- CurrentPasswordHash = proplists:get_value(<<"password_sha">>, User, nil),
-
-
- Roles1 = case Roles of
- [] -> Roles;
- _ ->
- ok = couch_httpd:verify_is_server_admin(Req),
- [?l2b(R) || R <- Roles]
- end,
-
- PasswordHash = case lists:member(<<"_admin">>, UserRoles) of
- true ->
- case Password of
- <<>> -> CurrentPasswordHash;
- _Else ->
- hash_password(Password, UserSalt)
- end;
- false when Name =:= UserName ->
- %% for user we test old password before allowing change
- case Password of
- <<>> ->
- CurrentPasswordHash;
- _P when OldPassword =:= [] ->
- throw({forbidden, <<"Old password is incorrect.">>});
- _Else ->
- OldPasswordHash = hash_password(OldPassword1, UserSalt),
- ?LOG_DEBUG("~p == ~p", [CurrentPasswordHash, OldPasswordHash]),
- case CurrentPasswordHash of
- ExpectedHash when ExpectedHash =:= OldPasswordHash ->
- hash_password(Password, UserSalt);
- _ ->
- throw({forbidden, <<"Old password is incorrect.">>})
- end
- end;
- _ ->
- throw({forbidden, <<"You aren't allowed to change this password.">>})
- end,
- {ok, UserDoc} = user_doc(DocId, UserName, UserSalt, PasswordHash, Email, Active, Roles1, OldRev),
- {ok, _Rev} = couch_db:update_doc(Db, UserDoc, []),
- ?LOG_DEBUG("User ~s (~s)updated.", [?b2l(UserName), ?b2l(DocId)]),
- {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of
- nil -> {200, []};
- Redirect ->
- {302, [{"Location", couch_httpd:absolute_uri(Req, Redirect)}]}
- end,
- send_json(Req, Code, Headers, {[{ok, true}]})
- end.
-handle_user_req(#httpd{method='POST'}=Req) ->
- DbName = couch_config:get("couch_httpd_auth", "authentication_db"),
- ensure_users_db_exists(?l2b(DbName)),
- {ok, Db} = couch_db:open(?l2b(DbName),
- [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]),
- create_user_req(Req, Db);
-handle_user_req(#httpd{method='PUT', path_parts=[_]}=_Req) ->
- throw({bad_request, <<"Username is missing">>});
-handle_user_req(#httpd{method='PUT', path_parts=[_, UserName]}=Req) ->
- DbName = couch_config:get("couch_httpd_auth", "authentication_db"),
- ensure_users_db_exists(?l2b(DbName)),
- {ok, Db} = couch_db:open(?l2b(DbName),
- [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]),
- update_user_req(Req, Db, UserName);
-handle_user_req(Req) ->
- couch_httpd:send_method_not_allowed(Req, "POST,PUT").
+maybe_value(Key, undefined) -> [];
+maybe_value(Key, Else) -> [{Key, Else}].
to_int(Value) when is_binary(Value) ->
to_int(?b2l(Value));
@@ -472,25 +403,3 @@ to_int(Value) when is_list(Value) ->
list_to_integer(Value);
to_int(Value) when is_integer(Value) ->
Value.
-
-% % Login handler
-% handle_login_req(#httpd{method='POST'}=Req) ->
-% DbName = couch_config:get("couch_httpd_auth", "authentication_db"),
-% case couch_db:open(?l2b(DbName), [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of
-% {ok, Db} -> handle_login_req(Req, Db)
-% end;
-% handle_login_req(Req) ->
-% send_method_not_allowed(Req, "POST").
-%
-% % Logout handler
-% handle_logout_req(#httpd{method='POST'}=Req) ->
-% Cookie = mochiweb_cookies:cookie("AuthSession", "", [{path, "/"}, {http_only, true}]),
-% {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of
-% nil ->
-% {200, [Cookie]};
-% Redirect ->
-% {302, [Cookie, {"Location", couch_httpd:absolute_uri(Req, Redirect)}]}
-% end,
-% send_json(Req, Code, Headers, {[{ok, true}]});
-% handle_logout_req(Req) ->
-% send_method_not_allowed(Req, "POST").
diff --git a/src/couchdb/couch_httpd_db.erl b/src/couchdb/couch_httpd_db.erl
index 35f32514..9233e953 100644
--- a/src/couchdb/couch_httpd_db.erl
+++ b/src/couchdb/couch_httpd_db.erl
@@ -298,13 +298,23 @@ handle_design_info_req(Req, _Db, _DDoc) ->
create_db_req(#httpd{user_ctx=UserCtx}=Req, DbName) ->
ok = couch_httpd:verify_is_server_admin(Req),
- case couch_server:create(DbName, [{user_ctx, UserCtx}]) of
- {ok, Db} ->
- couch_db:close(Db),
- DocUrl = absolute_uri(Req, "/" ++ couch_util:url_encode(DbName)),
- send_json(Req, 201, [{"Location", DocUrl}], {[{ok, true}]});
- Error ->
- throw(Error)
+ LDbName = ?b2l(DbName),
+ case couch_config:get("couch_httpd_auth", "authentication_db") of
+ LDbName ->
+ % make sure user's db always has the auth ddoc
+ {ok, Db} = couch_httpd_auth:ensure_users_db_exists(DbName),
+ couch_db:close(Db),
+ DbUrl = absolute_uri(Req, "/" ++ couch_util:url_encode(DbName)),
+ send_json(Req, 201, [{"Location", DbUrl}], {[{ok, true}]});
+ _Else ->
+ case couch_server:create(DbName, [{user_ctx, UserCtx}]) of
+ {ok, Db} ->
+ couch_db:close(Db),
+ DbUrl = absolute_uri(Req, "/" ++ couch_util:url_encode(DbName)),
+ send_json(Req, 201, [{"Location", DbUrl}], {[{ok, true}]});
+ Error ->
+ throw(Error)
+ end
end.
delete_db_req(#httpd{user_ctx=UserCtx}=Req, DbName) ->
@@ -317,6 +327,15 @@ delete_db_req(#httpd{user_ctx=UserCtx}=Req, DbName) ->
end.
do_db_req(#httpd{user_ctx=UserCtx,path_parts=[DbName|_]}=Req, Fun) ->
+ LDbName = ?b2l(DbName),
+ % I hope this lookup is cheap.
+ case couch_config:get("couch_httpd_auth", "authentication_db") of
+ LDbName ->
+ % make sure user's db always has the auth ddoc
+ {ok, ADb} = couch_httpd_auth:ensure_users_db_exists(DbName),
+ couch_db:close(ADb);
+ _Else -> ok
+ end,
case couch_db:open(DbName, [{user_ctx, UserCtx}]) of
{ok, Db} ->
try
@@ -553,7 +572,7 @@ db_req(#httpd{path_parts=[_,<<"_revs_limit">>]}=Req, _Db) ->
% as slashes in document IDs must otherwise be URL encoded.
db_req(#httpd{method='GET',mochi_req=MochiReq, path_parts=[DbName,<<"_design/",_/binary>>|_]}=Req, _Db) ->
PathFront = "/" ++ couch_httpd:quote(binary_to_list(DbName)) ++ "/",
- [PathFront|PathTail] = re:split(MochiReq:get(raw_path), "_design%2F",
+ [_|PathTail] = re:split(MochiReq:get(raw_path), "_design%2F",
[{return, list}]),
couch_httpd:send_redirect(Req, PathFront ++ "_design/" ++
mochiweb_util:join(PathTail, "_design%2F"));
diff --git a/src/couchdb/couch_httpd_oauth.erl b/src/couchdb/couch_httpd_oauth.erl
index e46ea5bc..ddf84008 100644
--- a/src/couchdb/couch_httpd_oauth.erl
+++ b/src/couchdb/couch_httpd_oauth.erl
@@ -36,8 +36,7 @@ oauth_authentication_handler(#httpd{mochi_req=MochiReq}=Req) ->
% Look up the consumer key and get the roles to give the consumer
set_user_ctx(Req, AccessToken) ->
- DbName = couch_config:get("couch_httpd_auth", "authentication_db"),
- {ok, _Db} = couch_httpd_auth:ensure_users_db_exists(?l2b(DbName)),
+ % TODO move to db storage
Name = case couch_config:get("oauth_token_users", AccessToken) of
undefined -> throw({bad_request, unknown_oauth_token});
Value -> ?l2b(Value)