diff options
Diffstat (limited to 'share/www')
-rw-r--r-- | share/www/_sidebar.html | 28 | ||||
-rw-r--r-- | share/www/config.html | 4 | ||||
-rw-r--r-- | share/www/couch_tests.html | 8 | ||||
-rw-r--r-- | share/www/custom_test.html | 4 | ||||
-rw-r--r-- | share/www/database.html | 3 | ||||
-rw-r--r-- | share/www/dialog/_admin_party.html | 33 | ||||
-rw-r--r-- | share/www/dialog/_create_admin.html | 50 | ||||
-rw-r--r-- | share/www/dialog/_login.html | 34 | ||||
-rw-r--r-- | share/www/dialog/_signup.html | 35 | ||||
-rw-r--r-- | share/www/document.html | 9 | ||||
-rw-r--r-- | share/www/index.html | 1 | ||||
-rw-r--r-- | share/www/replicator.html | 2 | ||||
-rw-r--r-- | share/www/script/couch.js | 62 | ||||
-rw-r--r-- | share/www/script/couch_test_runner.js | 64 | ||||
-rw-r--r-- | share/www/script/couch_tests.js | 1 | ||||
-rw-r--r-- | share/www/script/futon.browse.js | 7 | ||||
-rw-r--r-- | share/www/script/futon.js | 128 | ||||
-rw-r--r-- | share/www/script/jquery.couch.js | 68 | ||||
-rw-r--r-- | share/www/script/test/cookie_auth.js | 253 | ||||
-rw-r--r-- | share/www/script/test/oauth.js | 13 | ||||
-rw-r--r-- | share/www/script/test/users_db.js | 66 | ||||
-rw-r--r-- | share/www/style/layout.css | 11 |
22 files changed, 727 insertions, 157 deletions
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..ed67d744 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,62 @@ 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 { + // not a logged in admin. + readyToRun = true; + fun(); + }; + } + }); + } +}; + function updateTestsListing() { for (var name in couchTests) { var testFunction = couchTests[name]; @@ -128,7 +186,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..8328af60 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"); @@ -81,8 +136,10 @@ }, logout: function(options) { options = options || {}; + // TODO this should also login as the logged-out guy using basic auth $.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 +361,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..89e0aaf8 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,14 @@ couchTests.oauth = function(debug) { usersDb.createDb(); // Create a user - T(CouchDB.createUser("jason", "testpassword", "test@somemail.com", ['test'], adminBasicAuthHeaderValue()).ok); + // T(CouchDB.createUser("jason", "testpassword", "test@somemail.com", ['test'], adminBasicAuthHeaderValue()).ok); + // Create a user + var jasonUserDoc = CouchDB.prepareUserDoc({ + username: "jason", + roles: ["test"] + }, "testpassword"); + T(usersDb.save(jasonUserDoc).ok); + var accessor = { consumerSecret: consumerSecret, @@ -227,7 +236,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..9e8024f6 --- /dev/null +++ b/share/www/script/test/users_db.js @@ -0,0 +1,66 @@ +// 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.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; |