diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag.js index 49df49f4a..a4f9a41c6 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-bag.js @@ -7,49 +7,56 @@ GET /bags/:bag_name/ GET /bags/:bag_name \*/ -(function() { +(function () { -/*jslint node: true, browser: true */ -/*global $tw: false */ -"use strict"; + /*jslint node: true, browser: true */ + /*global $tw: false */ + "use strict"; -exports.method = "GET"; + var aclMiddleware = require('$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js').middleware; -exports.path = /^\/bags\/([^\/]+)(\/?)$/; + exports.method = "GET"; -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] !== "/") { - state.redirect(301,state.urlInfo.path + "/"); - return; - } - // Get the parameters - var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]), - bagTiddlers = bag_name && $tw.mws.store.getBagTiddlers(bag_name); - if(bag_name && bagTiddlers) { - // If application/json is requested then this is an API request, and gets the response in JSON - if(request.headers.accept && request.headers.accept.indexOf("application/json") !== -1) { - state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(bagTiddlers),"utf8"); - } else { - // This is not a JSON API request, we should return the raw tiddler content - response.writeHead(200, "OK",{ - "Content-Type": "text/html" - }); - var html = $tw.mws.store.adminWiki.renderTiddler("text/plain","$:/plugins/tiddlywiki/multiwikiserver/templates/page",{ - variables: { - "page-content": "$:/plugins/tiddlywiki/multiwikiserver/templates/get-bag", - "bag-name": bag_name, - "bag-titles": JSON.stringify(bagTiddlers.map(bagTiddler => bagTiddler.title)), - "bag-tiddlers": JSON.stringify(bagTiddlers) - } - }); - response.write(html); - response.end(); + exports.path = /^\/bags\/([^\/]+)(\/?)$/; + + 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] !== "/") { + state.redirect(301, state.urlInfo.path + "/"); + return; } - } else { - response.writeHead(404); - response.end(); - } -}; + // Get the parameters + var bag_name = $tw.utils.decodeURIComponentSafe(state.params[0]), + bagTiddlers = bag_name && $tw.mws.store.getBagTiddlers(bag_name); + if (bag_name && bagTiddlers) { + // If application/json is requested then this is an API request, and gets the response in JSON + 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", { + "Content-Type": "text/html" + }); + var html = $tw.mws.store.adminWiki.renderTiddler("text/plain", "$:/plugins/tiddlywiki/multiwikiserver/templates/page", { + variables: { + "page-content": "$:/plugins/tiddlywiki/multiwikiserver/templates/get-bag", + "bag-name": bag_name, + "bag-titles": JSON.stringify(bagTiddlers.map(bagTiddler => bagTiddler.title)), + "bag-tiddlers": JSON.stringify(bagTiddlers) + } + }); + response.write(html); + response.end(); + } + } + } else { + if (!response.headersSent) { + response.writeHead(404); + response.end(); + } + } + }; }()); 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 22a1f94e4..e860bfacc 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe-tiddler.js @@ -6,40 +6,46 @@ module-type: mws-route PUT /recipes/:recipe_name/tiddlers/:title \*/ -(function() { +(function () { -/*jslint node: true, browser: true */ -/*global $tw: false */ -"use strict"; + /*jslint node: true, browser: true */ + /*global $tw: false */ + "use strict"; -exports.method = "PUT"; + var aclMiddleware = require("$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js").middleware; -exports.path = /^\/recipes\/([^\/]+)\/tiddlers\/(.+)$/; + exports.method = "PUT"; -exports.handler = function(request,response,state) { - // Get the parameters - var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]), - title = $tw.utils.decodeURIComponentSafe(state.params[1]), - fields = $tw.utils.parseJSONSafe(state.data); - if(recipe_name && title === fields.title) { - var result = $tw.mws.store.saveRecipeTiddler(fields,recipe_name); - if(result) { - response.writeHead(204, "OK",{ - "X-Revision-Number": result.tiddler_id.toString(), - "X-Bag-Name": result.bag_name, - Etag: state.makeTiddlerEtag(result), - "Content-Type": "text/plain" - }); - } else { - response.writeHead(400); + exports.path = /^\/recipes\/([^\/]+)\/tiddlers\/(.+)$/; + + 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]), + fields = $tw.utils.parseJSONSafe(state.data); + if(recipe_name && title === fields.title) { + var result = $tw.mws.store.saveRecipeTiddler(fields, recipe_name); + if(!response.headersSent) { + if(result) { + response.writeHead(204, "OK", { + "X-Revision-Number": result.tiddler_id.toString(), + "X-Bag-Name": result.bag_name, + Etag: state.makeTiddlerEtag(result), + "Content-Type": "text/plain" + }); + } else { + response.writeHead(400); + } + response.end(); + } + return; } - response.end(); - return; - } - // Fail if something went wrong - response.writeHead(404); - response.end(); - -}; + // Fail if something went wrong + if(!response.headersSent) { + response.writeHead(404); + response.end(); + } + }; }()); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe.js index 8c260b36b..764a9e7da 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/put-recipe.js @@ -6,37 +6,42 @@ module-type: mws-route PUT /recipes/:recipe_name \*/ -(function() { +(function () { -/*jslint node: true, browser: true */ -/*global $tw: false */ -"use strict"; + /*jslint node: true, browser: true */ + /*global $tw: false */ + "use strict"; -exports.method = "PUT"; + var aclMiddleware = require('$:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js').middleware; -exports.path = /^\/recipes\/(.+)$/; + exports.method = "PUT"; -exports.handler = function(request,response,state) { - // Get the parameters - var recipe_name = $tw.utils.decodeURIComponentSafe(state.params[0]), - data = $tw.utils.parseJSONSafe(state.data); - if(recipe_name && data) { - const result = $tw.mws.store.createRecipe(recipe_name,data.bag_names,data.description); - if(!result) { - state.sendResponse(204,{ - "Content-Type": "text/plain" - }); + exports.path = /^\/recipes\/(.+)$/; + + 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); + if (recipe_name && data) { + const result = $tw.mws.store.createRecipe(recipe_name, data.bag_names, data.description); + if (!result) { + state.sendResponse(204, { + "Content-Type": "text/plain" + }); + } else { + state.sendResponse(400, { + "Content-Type": "text/plain" + }, + result.message, + "utf8"); + } } else { - state.sendResponse(400,{ - "Content-Type": "text/plain" - }, - result.message, - "utf8"); + if (!response.headersSent) { + response.writeHead(404); + response.end(); + } } - } else { - response.writeHead(404); - response.end(); - } -}; + }; }()); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js new file mode 100644 index 000000000..37da7129b --- /dev/null +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js @@ -0,0 +1,51 @@ +/*\ +title: $:/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js +type: application/javascript +module-type: library + +Middleware to handle ACL permissions + +\*/ + +(function () { + + /*jslint node: true, browser: true */ + /*global $tw: false */ + "use strict"; + + /* + ACL Middleware factory function + */ + exports.middleware = function (request, response, state, entityType, permissionName) { + + var server = state.server, + sqlTiddlerDatabase = server.sqlTiddlerDatabase, + entityName = 13; + // Extract entity ID based on entityType + if(entityType === "recipe") { + entityName = state.params[0]; // Assuming recipe name is the first parameter + } else if(entityType === "bag") { + entityName = state.params[0]; // Adjust as needed for bag + } + console.log("middleware =>", { entityType, permissionName, entityName }) + + // Check if user is authenticated + if(!state.authenticatedUser && !response.headersSent) { + response.writeHead(401, "Unauthorized"); + response.end(); + return; + } + + // Check ACL permission + var hasPermission = sqlTiddlerDatabase.checkACLPermission(state.authenticatedUser.user_id, entityType, entityName, permissionName) + console.log("hasPermission =>", hasPermission) + if(!hasPermission) { + if(!response.headersSent) { + response.writeHead(403, "Forbidden"); + response.end(); + } + return; + } + }; + +})(); \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js index 8685c69e6..91f61db12 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js @@ -521,6 +521,82 @@ SqlTiddlerDatabase.prototype.hasBagPermission = function(userId, bagName, permis return hasBagPermission; }; +SqlTiddlerDatabase.prototype.checkACLPermission = function(userId, entityType, entityName, permissionName) { + const entityTypeToTableMap = { + bag: { + table: 'bags', + column: 'bag_name' + }, + recipe: { + table: 'recipes', + column: 'recipe_name' + } + }; + + const entityInfo = entityTypeToTableMap[entityType]; + if (!entityInfo) { + throw new Error('Invalid entity type: ' + entityType); + } + + console.log("Starting ACL permission check:", { userId, entityType, entityName, permissionName, entityInfo }); + + // Step 1: Get the entity ID + const entityQuery = ` + SELECT ${entityInfo.table.slice(0, -1)}_id as entity_id + FROM ${entityInfo.table} + WHERE ${entityInfo.column} = $entity_name + `; + const entityResult = this.engine.runStatementGet(entityQuery, { $entity_name: entityName }); + console.log("Entity query result:", entityResult); + + if (!entityResult) { + console.log(`${entityType} not found: ${entityName}`); + return false; + } + + const entityId = entityResult.entity_id; + + // Step 2: Get user's roles + const userRolesQuery = ` + SELECT r.role_id + FROM users u + JOIN user_roles ur ON u.user_id = ur.user_id + JOIN roles r ON ur.role_id = r.role_id + WHERE u.user_id = $user_id + `; + const userRoles = this.engine.runStatementGetAll(userRolesQuery, { $user_id: userId }); + console.log("User roles:", userRoles); + + if (userRoles.length === 0) { + console.log(`No roles found for user: ${userId}`); + return false; + } + + // Step 3: Check for permission + const roleIds = userRoles.map(role => role.role_id); + const permissionQuery = ` + SELECT 1 + FROM acl a + JOIN permissions p ON a.permission_id = p.permission_id + WHERE a.role_id IN (${roleIds.join(',')}) + AND a.entity_type = $entity_type + AND a.entity_id = $entity_id + AND p.permission_name = $permission_name + LIMIT 1 + `; + const permissionResult = this.engine.runStatementGet(permissionQuery, { + $entity_type: entityType, + $entity_id: entityId, + $permission_name: permissionName + }); + console.log("Permission query result:", permissionResult); + + const hasPermission = permissionResult !== undefined; + console.log("Final permission check result:", hasPermission); + + return hasPermission; +}; + /* Get the titles of the tiddlers in a bag. Returns an empty array for bags that do not exist */