diff --git a/boot/boot.js b/boot/boot.js index 7396d7bff..b4bdc00f2 100644 --- a/boot/boot.js +++ b/boot/boot.js @@ -336,7 +336,7 @@ Get the browser location.hash. We don't use location.hash because of the way tha */ $tw.utils.getLocationHash = function() { var href = window.location.href; - var idx = href.indexOf("#"); + var idx = href.indexOf('#'); if(idx === -1) { return "#"; } else if(href.substr(idx + 1,1) === "#" || href.substr(idx + 1,3) === "%23") { @@ -605,7 +605,7 @@ var globalCheck =[ " delete Object.prototype.__temp__;", " }", " delete Object.prototype.__temp__;", -].join("\n"); +].join('\n'); /* Run code globally with specified context variables in scope @@ -1997,7 +1997,7 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) { value = path.relative(rootPath, filename).split(path.sep).slice(0, -1); break; case "filepath": - value = path.relative(rootPath, filename).split(path.sep).join("/"); + value = path.relative(rootPath, filename).split(path.sep).join('/'); break; case "filename": value = path.basename(filename); @@ -2623,7 +2623,7 @@ $tw.boot.executeNextStartupTask = function(callback) { } taskIndex++; } - if(typeof callback === "function") { + if(typeof callback === 'function') { callback(); } return false; diff --git a/editions/multiwikiserver/tiddlywiki.info b/editions/multiwikiserver/tiddlywiki.info index d18bc5744..abc3e460d 100644 --- a/editions/multiwikiserver/tiddlywiki.info +++ b/editions/multiwikiserver/tiddlywiki.info @@ -11,9 +11,6 @@ "tiddlywiki/snowwhite" ], "build": { - "--mws-list-users": [ - "--mws-list-users" - ], "mws-add-user": [ "--mws-add-permission", "READ", "Allows user to create tiddlers", "--mws-add-permission", "WRITE", "Gives the user the permission to edit and delete tiddlers", diff --git a/plugins/tiddlywiki/multiwikiserver/auth/form/login/form.tid b/plugins/tiddlywiki/multiwikiserver/auth/form/login/form.tid index da027d65d..2dcd1e89d 100644 --- a/plugins/tiddlywiki/multiwikiserver/auth/form/login/form.tid +++ b/plugins/tiddlywiki/multiwikiserver/auth/form/login/form.tid @@ -1,6 +1,5 @@ title: $:/plugins/tiddlywiki/multiwikiserver/auth/form/login/form -<$macrocall $name="loginForm"/>
>/> diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-list-users.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-list-users.js deleted file mode 100644 index b9249cbb4..000000000 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-list-users.js +++ /dev/null @@ -1,46 +0,0 @@ -/*\ -title: $:/plugins/tiddlywiki/multiwikiserver/commands/mws-list-users.js -type: application/javascript -module-type: command - -Command to list users - -\*/ -(function(){ - -/*jslint node: true, browser: true */ -/*global $tw: false */ -"use strict"; - -exports.info = { - name: "mws-list-users", - synchronous: false -}; - -var Command = function(params,commander,callback) { - this.params = params; - this.commander = commander; - this.callback = callback; -}; - -Command.prototype.execute = function() { - var self = this; - - if(!$tw.mws || !$tw.mws.store || !$tw.mws.store.sqlTiddlerDatabase) { - return "Error: MultiWikiServer or SQL database not initialized."; - } - - var users = $tw.mws.store.sqlTiddlerDatabase.listUsers().map(function(user){ - return ({ - username: user.username, - email: user.email, - created_at: user.created_at, - }) - }); - console.log("Users:", users); - self.callback(null, "Users retrieved successfully:\n" + JSON.stringify(users, null, 2)); -}; - -exports.Command = Command; - -})(); \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-test-server.js b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-test-server.js index 680a26326..0c246c93e 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-test-server.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/commands/mws-test-server.js @@ -50,11 +50,18 @@ TestRunner.prototype.runTests = function(callback) { const self = this; let currentTestSpec = 0; let hasFailed = false; + let sessionId; function runNextTest() { if(currentTestSpec < testSpecs.length) { const testSpec = testSpecs[currentTestSpec]; + if(!!sessionId) { + testSpec.headers['Cookie'] = `session=${sessionId}; HttpOnly; Path=/`; + } currentTestSpec += 1; - self.runTest(testSpec,function(err) { + self.runTest(testSpec,function(err, data) { + if(data?.sessionId) { + sessionId = data?.sessionId; + } if(err) { hasFailed = true; console.log(`Failed "${testSpec.description}" with "${err}"`) @@ -96,7 +103,7 @@ TestRunner.prototype.runTest = function(testSpec,callback) { response.on("end", () => { const jsonData = $tw.utils.parseJSONSafe(buffer,function() {return undefined;}); const testResult = testSpec.expectedResult(jsonData,buffer,response.headers); - callback(testResult ? null : "Test failed"); + callback(testResult ? null : "Test failed", jsonData); }); }); request.on("error", (e) => { @@ -112,6 +119,20 @@ TestRunner.prototype.runTest = function(testSpec,callback) { }; const testSpecs = [ + { + description: "Login Test User", + method: "POST", + path: "/login", + headers: { + "Accept": 'application/json', + "Content-Type": 'application/x-www-form-urlencoded', + "User-Agent": 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36' + }, + data: "username=user&password=pass123", + expectedResult: (jsonData,data,headers) => { + return !!jsonData.sessionId; + } + }, { description: "Check index page", method: "GET", diff --git a/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js b/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js index 437c6d3e4..2ff85539a 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js @@ -19,7 +19,8 @@ if($tw.node) { path = require("path"), querystring = require("querystring"), crypto = require("crypto"), - zlib = require("zlib"); + zlib = require("zlib"), + aclMiddleware = require('$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js').middleware; } /* @@ -34,7 +35,7 @@ function Server(options) { this.authenticators = options.authenticators || []; this.wiki = options.wiki; this.boot = options.boot || $tw.boot; - this.sqlTiddlerDatabase = $tw.mws.store.sqlTiddlerDatabase; + this.sqlTiddlerDatabase = options.sqlTiddlerDatabase || $tw.mws.store.sqlTiddlerDatabase; // Initialise the variables this.variables = $tw.utils.extend({},this.defaultVariables); if(options.variables) { @@ -158,9 +159,10 @@ function sendResponse(request,response,statusCode,headers,data,encoding) { data = zlib.gzipSync(data); } } - - response.writeHead(statusCode,headers); - response.end(data,encoding); + if(!response.headersSent) { + response.writeHead(statusCode,headers); + response.end(data,encoding); + } } function redirect(request,response,statusCode,location) { @@ -351,6 +353,13 @@ Server.prototype.methodMappings = { "DELETE": "writers" }; +Server.prototype.methodACLPermMappings = { + "GET": "READ", + "PUT": "WRITE", + "POST": "WRITE", + "DELETE": "WRITE" +} + /* Check whether a given user is authorized for the specified authorizationType ("readers" or "writers"). Pass null or undefined as the username to check for anonymous access */ @@ -411,8 +420,7 @@ Server.prototype.redirectToLogin = function(response, returnUrl) { } else { console.log(`Invalid return URL detected: ${returnUrl}. Redirecting to home page.`); } - response.setHeader('Set-Cookie', `returnUrl=${encodeURIComponent(sanitizedReturnUrl)}; HttpOnly; Path=/`); - const loginUrl = '/login'; + response.setHeader('Set-Cookie', `returnUrl=${encodeURIComponent(sanitizedReturnUrl)}; HttpOnly; Secure; SameSite=Strict; Path=/`); const loginUrl = '/login'; response.writeHead(302, { 'Location': loginUrl }); @@ -468,6 +476,12 @@ Server.prototype.requestHandler = function(request,response,options) { // Find the route that matches this path var route = self.findMatchingRoute(request,state); + + // If the route is configured to use ACL middleware, check that the user has permission + if(route?.useACL) { + const permissionName = this.methodACLPermMappings[route.method]; + aclMiddleware(request,response,state,route.entityName,permissionName) + } // Optionally output debug info if(self.get("debug-level") !== "none") { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag.js index 72c2721d6..7d262b83f 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag.js @@ -13,12 +13,14 @@ GET /bags/:bag_name /*global $tw: false */ "use strict"; -var aclMiddleware = require('$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js').middleware; - exports.method = "GET"; exports.path = /^\/bags\/([^\/]+)(\/?)$/; +exports.useACL = true; + +exports.entityName = "bag" + exports.handler = function (request, response, state) { // Redirect if there is no trailing slash. We do this so that the relative URL specified in the upload form works correctly if (state.params[1] !== "/") { @@ -33,7 +35,6 @@ exports.handler = function (request, response, state) { if (request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) { state.sendResponse(200, { "Content-Type": "application/json" }, JSON.stringify(bagTiddlers), "utf8"); } else { - aclMiddleware(request, response, state, 'bag', 'READ'); if (!response.headersSent) { // This is not a JSON API request, we should return the raw tiddler content response.writeHead(200, "OK", { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddler.js index 77ec9855b..8b23184ad 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddler.js @@ -16,14 +16,15 @@ fallback= // Optional redirect if the tiddler is not found /*global $tw: false */ "use strict"; -var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware; - exports.method = "GET"; exports.path = /^\/recipes\/([^\/]+)\/tiddlers\/(.+)$/; +exports.useACL = true; + +exports.entityName = "recipe" + exports.handler = function(request,response,state) { - aclMiddleware(request, response, state, "recipe", "READ"); // Get the parameters var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]), title = $tw.utils.decodeURIComponentSafe(state.params[1]), diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag-tiddlers.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag-tiddlers.js index 55b5f133c..0f520b1ba 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag-tiddlers.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag-tiddlers.js @@ -12,8 +12,6 @@ POST /bags/:bag_name/tiddlers/ /*global $tw: false */ "use strict"; -var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware; - exports.method = "POST"; exports.path = /^\/bags\/([^\/]+)\/tiddlers\/$/; @@ -22,8 +20,11 @@ exports.bodyFormat = "stream"; exports.csrfDisable = true; +exports.useACL = true; + +exports.entityName = "bag" + exports.handler = function(request,response,state) { - aclMiddleware(request, response, state, "bag", "WRITE"); const path = require("path"), fs = require("fs"), processIncomingStream = require("$:/plugins/tiddlywiki/multiwikiserver/routes/helpers/multipart-forms.js").processIncomingStream; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag.js index 62a019b86..bd59b0642 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-bag.js @@ -17,8 +17,6 @@ description /*global $tw: false */ "use strict"; -var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware; - exports.method = "POST"; exports.path = /^\/bags$/; @@ -27,8 +25,11 @@ exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; +exports.useACL = true; + +exports.entityName = "bag" + exports.handler = function(request,response,state) { - aclMiddleware(request, response, state, "bag", "WRITE"); if(state.data.bag_name) { const result = $tw.mws.store.createBag(state.data.bag_name,state.data.description); if(!result) { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-login.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-login.js index cd32abeb8..1ce6ac881 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-login.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-login.js @@ -37,17 +37,29 @@ exports.handler = function(request,response,state) { var sessionId = auth.createSession(user.user_id); var returnUrl = state.server.parseCookieString(request.headers.cookie).returnUrl response.setHeader('Set-Cookie', `session=${sessionId}; HttpOnly; Path=/`); - response.writeHead(302, { - 'Location': returnUrl || '/' - }); + if(request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) { + state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify({ + "sessionId": sessionId + })); + } else { + response.writeHead(302, { + 'Location': returnUrl || '/' + }); + } } else { $tw.mws.store.adminWiki.addTiddler(new $tw.Tiddler({ title: "$:/temp/mws/login/error", text: "Invalid username or password" })); - response.writeHead(302, { - 'Location': '/login' - }); + if(request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) { + state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify({ + "message": "Invalid username or password" + })); + } else { + response.writeHead(302, { + 'Location': '/login' + }); + } } response.end(); }; diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-recipe.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-recipe.js index 766242338..3f4bb02dc 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-recipe.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-recipe.js @@ -18,8 +18,6 @@ bag_names: space separated list of bags /*global $tw: false */ "use strict"; -var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware; - exports.method = "POST"; exports.path = /^\/recipes$/; @@ -28,8 +26,11 @@ exports.bodyFormat = "www-form-urlencoded"; exports.csrfDisable = true; +exports.useACL = true; + +exports.entityName = "recipe" + exports.handler = function(request,response,state) { - aclMiddleware(request, response, state, "recipe", "WRITE"); if(state.data.recipe_name && state.data.bag_names) { const result = $tw.mws.store.createRecipe(state.data.recipe_name,$tw.utils.parseStringArray(state.data.bag_names),state.data.description); if(!result) { diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-bag.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-bag.js index be67d6a2c..d174ee8ce 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-bag.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-bag.js @@ -12,14 +12,15 @@ PUT /bags/:bag_name /*global $tw: false */ "use strict"; -var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware; - exports.method = "PUT"; exports.path = /^\/bags\/(.+)$/; +exports.useACL = true; + +exports.entityName = "bag" + exports.handler = function(request,response,state) { - aclMiddleware(request, response, state, "bag", "WRITE"); // Get the parameters var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]), data = $tw.utils.parseJSONSafe(state.data); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe-tiddler.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe-tiddler.js index 0b71c087f..25279cdd0 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe-tiddler.js @@ -12,14 +12,15 @@ PUT /recipes/:recipe_name/tiddlers/:title /*global $tw: false */ "use strict"; -var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware; - exports.method = "PUT"; exports.path = /^\/recipes\/([^\/]+)\/tiddlers\/(.+)$/; +exports.useACL = true; + +exports.entityName = "recipe" + exports.handler = function (request, response, state) { - aclMiddleware(request, response, state, "recipe", "WRITE"); // Get the parameters var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]), title = $tw.utils.decodeURIComponentSafe(state.params[1]), diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe.js index 99f7ed841..002c5e4db 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe.js @@ -12,14 +12,15 @@ PUT /recipes/:recipe_name /*global $tw: false */ "use strict"; -var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware; - exports.method = "PUT"; exports.path = /^\/recipes\/(.+)$/; +exports.useACL = true; + +exports.entityName = "recipe" + exports.handler = function (request, response, state) { - aclMiddleware(request, response, state, "recipe", "WRITE"); // Get the parameters var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]), data = $tw.utils.parseJSONSafe(state.data); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js index 9b16d5957..20d238c20 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js @@ -214,15 +214,13 @@ SqlTiddlerDatabase.prototype.createBag = function(bag_name,description,accesscon $accesscontrol: accesscontrol, $description: description }); - - // update the permissions on ACL records - const admin = this.getRoleByName('ADMIN'); + const admin = this.getRoleByName("ADMIN"); if(admin) { - const readPermission = this.getPermissionByName('READ'); - const writePermission = this.getPermissionByName('WRITE'); - this.createACL(bag_name, 'bag', admin.role_id, readPermission.permission_id); - this.createACL(bag_name, 'bag', admin.role_id, writePermission.permission_id); + const readPermission = this.getPermissionByName("READ"); + const writePermission = this.getPermissionByName("WRITE"); + this.createACL(bag_name, "bag", admin.role_id, readPermission.permission_id); + this.createACL(bag_name, "bag", admin.role_id, writePermission.permission_id); } return updateBags.lastInsertRowid; }; @@ -290,12 +288,12 @@ SqlTiddlerDatabase.prototype.createRecipe = function(recipe_name,bag_names,descr // update the permissions on ACL records - const admin = this.getRoleByName('ADMIN'); + const admin = this.getRoleByName("ADMIN"); if(admin) { - const readPermission = this.getPermissionByName('READ'); - const writePermission = this.getPermissionByName('WRITE'); - this.createACL(recipe_name, 'recipe', admin.role_id, readPermission.permission_id); - this.createACL(recipe_name, 'recipe', admin.role_id, writePermission.permission_id); + const readPermission = this.getPermissionByName("READ"); + const writePermission = this.getPermissionByName("WRITE"); + this.createACL(recipe_name, "recipe", admin.role_id, readPermission.permission_id); + this.createACL(recipe_name, "recipe", admin.role_id, writePermission.permission_id); } return updateRecipes.lastInsertRowid; }; @@ -495,34 +493,34 @@ SqlTiddlerDatabase.prototype.getRecipeTiddler = function(title,recipe_name) { Checks if a user has permission to access a recipe */ SqlTiddlerDatabase.prototype.hasRecipePermission = function(userId, recipeName, permissionName) { - return this.checkACLPermission(userId, 'recipe', recipeName, permissionName) + return this.checkACLPermission(userId, "recipe", recipeName, permissionName) }; /* Checks if a user has permission to access a bag */ SqlTiddlerDatabase.prototype.hasBagPermission = function(userId, bagName, permissionName) { - return this.checkACLPermission(userId, 'bag', bagName, permissionName) + return this.checkACLPermission(userId, "bag", bagName, permissionName) }; SqlTiddlerDatabase.prototype.checkACLPermission = function(userId, entityType, entityName, permissionName) { const entityTypeToTableMap = { bag: { - table: 'bags', - column: 'bag_name' + table: "bags", + column: "bag_name" }, recipe: { - table: 'recipes', - column: 'recipe_name' + table: "recipes", + column: "recipe_name" } }; const entityInfo = entityTypeToTableMap[entityType]; if (!entityInfo) { - throw new Error('Invalid entity type: ' + entityType); + throw new Error("Invalid entity type: " + entityType); } - // if the entityName starts with "$:/", we'll assume its a system tiddler, then grant the user permission + // if the entityName starts with "$:/", we'll assume its a system bag/recipe, then grant the user permission if(entityName.startsWith("$:/")){ return true } diff --git a/plugins/tiddlywiki/multiwikiserver/templates/manage-user.tid b/plugins/tiddlywiki/multiwikiserver/templates/manage-user.tid index 3fe501712..434ebd66a 100644 --- a/plugins/tiddlywiki/multiwikiserver/templates/manage-user.tid +++ b/plugins/tiddlywiki/multiwikiserver/templates/manage-user.tid @@ -5,43 +5,45 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-user <$transclude/> - -