diff options
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) |