diff --git a/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js b/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js index be46a53d2d3..febcd97d781 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/mws-server.js @@ -414,11 +414,13 @@ Server.prototype.requestAuthentication = function(response) { Server.prototype.getAnonymousAccessConfig = function() { const allowReadsTiddler = this.wiki.getTiddlerText("$:/config/MultiWikiServer/AllowAnonymousReads", "undefined"); const allowWritesTiddler = this.wiki.getTiddlerText("$:/config/MultiWikiServer/AllowAnonymousWrites", "undefined"); + const showAnonymousAccessModal = this.wiki.getTiddlerText("$:/config/MultiWikiServer/ShowAnonymousAccessModal", "undefined"); return { allowReads: allowReadsTiddler === "yes", allowWrites: allowWritesTiddler === "yes", - isEnabled: allowReadsTiddler !== "undefined" && allowWritesTiddler !== "undefined" + isEnabled: allowReadsTiddler !== "undefined" && allowWritesTiddler !== "undefined", + showAnonConfig: showAnonymousAccessModal === "yes" }; } @@ -452,11 +454,12 @@ Server.prototype.requestHandler = function(request,response,options) { // Check whether anonymous access is granted state.allowAnon = false; //this.isAuthorized(state.authorizationType,null); - var {allowReads, allowWrites, isEnabled} = this.getAnonymousAccessConfig(); + var {allowReads, allowWrites, isEnabled, showAnonConfig} = this.getAnonymousAccessConfig(); + state.anonAccessConfigured = isEnabled; state.allowAnon = isEnabled && (request.method === 'GET' ? allowReads : allowWrites); state.allowAnonReads = allowReads; state.allowAnonWrites = allowWrites; - state.showAnonConfig = !!state.authenticatedUser?.isAdmin && !isEnabled; + state.showAnonConfig = !!state.authenticatedUser?.isAdmin && showAnonConfig; state.firstGuestUser = this.sqlTiddlerDatabase.listUsers().length === 0 && !state.authenticatedUser; // Authorize with the authenticated username 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 037f7489daf..a50657ce58a 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddler.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/get-recipe-tiddler.js @@ -20,7 +20,7 @@ exports.method = "GET"; exports.path = /^\/recipes\/([^\/]+)\/tiddlers\/(.+)$/; -exports.useACL = true; +// exports.useACL = true; exports.entityName = "recipe" diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-acl.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-acl.js index 45982b07997..63a9f414f1b 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-acl.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-acl.js @@ -29,35 +29,40 @@ exports.handler = function (request, response, state) { var permission_id = state.data.permission_id; var isRecipe = entity_type === "recipe" - var entityAclRecords = sqlTiddlerDatabase.getACLByName(entity_type, isRecipe ? recipe_name : bag_name, true); + try { + var entityAclRecords = sqlTiddlerDatabase.getACLByName(entity_type, isRecipe ? recipe_name : bag_name, true); - var aclExists = entityAclRecords.some((record) => ( - record.role_id == role_id && record.permission_id == permission_id - )) + var aclExists = entityAclRecords.some((record) => ( + record.role_id == role_id && record.permission_id == permission_id + )) - // This ensures that the user attempting to modify the ACL has permission to do so - // if(!state.authenticatedUser || (entityAclRecords.length > 0 && !sqlTiddlerDatabase[isRecipe ? 'hasRecipePermission' : 'hasBagPermission'](state.authenticatedUser.user_id, isRecipe ? recipe_name : bag_name, 'WRITE'))){ - // response.writeHead(403, "Forbidden"); - // response.end(); - // return - // } + // This ensures that the user attempting to modify the ACL has permission to do so + // if(!state.authenticatedUser || (entityAclRecords.length > 0 && !sqlTiddlerDatabase[isRecipe ? 'hasRecipePermission' : 'hasBagPermission'](state.authenticatedUser.user_id, isRecipe ? recipe_name : bag_name, 'WRITE'))){ + // response.writeHead(403, "Forbidden"); + // response.end(); + // return + // } - if (aclExists) { - // do nothing, return the user back to the form + if (aclExists) { + // do nothing, return the user back to the form + response.writeHead(302, { "Location": "/admin/acl/" + recipe_name + "/" + bag_name }); + response.end(); + return + } + + sqlTiddlerDatabase.createACL( + isRecipe ? recipe_name : bag_name, + entity_type, + role_id, + permission_id + ) + + response.writeHead(302, { "Location": "/admin/acl/" + recipe_name + "/" + bag_name }); + response.end(); + } catch (error) { response.writeHead(302, { "Location": "/admin/acl/" + recipe_name + "/" + bag_name }); response.end(); - return } - - sqlTiddlerDatabase.createACL( - isRecipe ? recipe_name : bag_name, - entity_type, - role_id, - permission_id - ) - - response.writeHead(302, { "Location": "/admin/acl/" + recipe_name + "/" + bag_name }); - response.end(); }; }()); \ No newline at end of file diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon-config.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon-config.js index c20806540c1..e1e841516c4 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon-config.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon-config.js @@ -42,6 +42,10 @@ exports.handler = function(request, response, state) { text: allowWrites ? "yes" : "no" }); + wiki.addTiddler({ + title: "$:/config/MultiWikiServer/ShowAnonymousAccessModal", + text: "no" + }); // Redirect back to admin page response.writeHead(302, {"Location": "/"}); response.end(); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon.js index 288a81bc779..911b6ef971c 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/handlers/post-anon.js @@ -32,12 +32,8 @@ exports.handler = function(request, response, state) { // Update the configuration tiddlers var wiki = $tw.wiki; wiki.addTiddler({ - title: "$:/config/MultiWikiServer/AllowAnonymousReads", - text: "undefined" - }); - wiki.addTiddler({ - title: "$:/config/MultiWikiServer/AllowAnonymousWrites", - text: "undefined" + title: "$:/config/MultiWikiServer/ShowAnonymousAccessModal", + text: "yes" }); // Redirect back to admin page diff --git a/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js b/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js index adbdcff7a02..6b9095ac467 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/routes/helpers/acl-middleware.js @@ -36,9 +36,10 @@ function redirectToLogin(response, returnUrl) { }; exports.middleware = function (request, response, state, entityType, permissionName) { + var extensionRegex = /\.[A-Za-z0-9]{1,4}$/; var server = state.server, - sqlTiddlerDatabase = server.sqlTiddlerDatabase, + sqlTiddlerDatabase = $tw.mws.store.sqlTiddlerDatabase || server.sqlTiddlerDatabase, entityName = state.data ? (state.data[entityType+"_name"] || state.params[0]) : state.params[0]; // First, replace '%3A' with ':' to handle TiddlyWiki's system tiddlers @@ -48,10 +49,15 @@ exports.middleware = function (request, response, state, entityType, permissionN var aclRecord = sqlTiddlerDatabase.getACLByName(entityType, decodedEntityName); var isGetRequest = request.method === "GET"; var hasAnonymousAccess = state.allowAnon ? (isGetRequest ? state.allowAnonReads : state.allowAnonWrites) : false; + var anonymousAccessConfigured = state.anonAccessConfigured; var entity = sqlTiddlerDatabase.getEntityByName(entityType, decodedEntityName); if(entity?.owner_id) { if(state.authenticatedUser?.user_id && (state.authenticatedUser?.user_id !== entity.owner_id) || !state.authenticatedUser?.user_id && !hasAnonymousAccess) { - if(!response.headersSent) { + const hasPermission = state.authenticatedUser?.user_id ? + entityType === 'recipe' ? sqlTiddlerDatabase.hasRecipePermission(state.authenticatedUser?.user_id, decodedEntityName, isGetRequest ? 'READ' : 'WRITE') + : sqlTiddlerDatabase.hasBagPermission(state.authenticatedUser?.user_id, decodedEntityName, isGetRequest ? 'READ' : 'WRITE') + : false + if(!response.headersSent && !hasPermission) { response.writeHead(403, "Forbidden"); response.end(); } @@ -59,8 +65,8 @@ exports.middleware = function (request, response, state, entityType, permissionN } } else { // First, we need to check if anonymous access is allowed - if(!state.authenticatedUser?.user_id && !hasAnonymousAccess) { - if(!response.headersSent) { + if(!state.authenticatedUser?.user_id && (anonymousAccessConfigured && !hasAnonymousAccess)) { + if(!response.headersSent && !extensionRegex.test(request.url)) { response.writeHead(401, "Unauthorized"); response.end(); } @@ -80,7 +86,7 @@ exports.middleware = function (request, response, state, entityType, permissionN } // Check ACL permission - var hasPermission = request.method === "POST" || sqlTiddlerDatabase.checkACLPermission(state.authenticatedUser.user_id, entityType, decodedEntityName, permissionName) + var hasPermission = request.method === "POST" || sqlTiddlerDatabase.checkACLPermission(state.authenticatedUser.user_id, entityType, decodedEntityName, permissionName, entity?.owner_id) if(!hasPermission && !hasAnonymousAccess) { if(!response.headersSent) { response.writeHead(403, "Forbidden"); diff --git a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js index df68b08df57..c4be2b66dde 100644 --- a/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js +++ b/plugins/tiddlywiki/multiwikiserver/modules/store/sql-tiddler-database.js @@ -500,19 +500,27 @@ SqlTiddlerDatabase.prototype.getRecipeTiddler = function(title,recipe_name) { Checks if a user has permission to access a recipe */ SqlTiddlerDatabase.prototype.hasRecipePermission = function(userId, recipeName, permissionName) { - // check if the user is the owner of the entity - const recipe = this.engine.runStatementGet(` - SELECT owner_id - FROM recipes - WHERE recipe_name = $recipe_name - `, { - $recipe_name: recipeName - }); + try { + // check if the user is the owner of the entity + const recipe = this.engine.runStatementGet(` + SELECT owner_id + FROM recipes + WHERE recipe_name = $recipe_name + `, { + $recipe_name: recipeName + }); - if(recipe?.owner_id) { - return recipe.owner_id === userId; + if(!!recipe?.owner_id && recipe?.owner_id === userId) { + return true; + } else { + var permission = this.checkACLPermission(userId, "recipe", recipeName, permissionName, recipe?.owner_id) + return permission; + } + + } catch (error) { + console.error(error) + return false } - return this.checkACLPermission(userId, "recipe", recipeName, permissionName) }; /* @@ -530,10 +538,11 @@ SqlTiddlerDatabase.prototype.getACLByName = function(entityType, entityName, fet // First, check if there's an ACL record for the entity and get the permission_id var checkACLExistsQuery = ` - SELECT * + SELECT acl.*, permissions.permission_name FROM acl - WHERE entity_type = $entity_type - AND entity_name = $entity_name + LEFT JOIN permissions ON acl.permission_id = permissions.permission_id + WHERE acl.entity_type = $entity_type + AND acl.entity_name = $entity_name `; if (!fetchAll) { @@ -548,43 +557,50 @@ SqlTiddlerDatabase.prototype.getACLByName = function(entityType, entityName, fet return aclRecord; } -SqlTiddlerDatabase.prototype.checkACLPermission = function(userId, entityType, entityName) { - // if the entityName starts with "$:/", we'll assume its a system bag/recipe, then grant the user permission - if(entityName.startsWith("$:/")) { - return true; - } +SqlTiddlerDatabase.prototype.checkACLPermission = function(userId, entityType, entityName, permissionName, ownerId) { + try { + // if the entityName starts with "$:/", we'll assume its a system bag/recipe, then grant the user permission + if(entityName.startsWith("$:/")) { + return true; + } - const aclRecord = this.getACLByName(entityType, entityName); + const aclRecords = this.getACLByName(entityType, entityName, true); + const aclRecord = aclRecords.find(record => record.permission_name === permissionName); - // If no ACL record exists, return true for hasPermission - if (!aclRecord) { - return true; - } + // If no ACL record exists, return true for hasPermission + if ((!aclRecord && !ownerId) || ((!!aclRecord && !!ownerId) && ownerId === userId)) { + return true; + } - // If ACL record exists, check for user permission using the retrieved permission_id - const checkPermissionQuery = ` - SELECT 1 - FROM users u - JOIN user_roles ur ON u.user_id = ur.user_id - JOIN roles r ON ur.role_id = r.role_id - JOIN acl a ON r.role_id = a.role_id - WHERE u.user_id = $user_id - AND a.entity_type = $entity_type - AND a.entity_name = $entity_name - AND a.permission_id = $permission_id - LIMIT 1 - `; + // If ACL record exists, check for user permission using the retrieved permission_id + const checkPermissionQuery = ` + SELECT * + FROM users u + JOIN user_roles ur ON u.user_id = ur.user_id + JOIN roles r ON ur.role_id = r.role_id + JOIN acl a ON r.role_id = a.role_id + WHERE u.user_id = $user_id + AND a.entity_type = $entity_type + AND a.entity_name = $entity_name + AND a.permission_id = $permission_id + LIMIT 1 + `; - const result = this.engine.runStatementGet(checkPermissionQuery, { - $user_id: userId, - $entity_type: entityType, - $entity_name: entityName, - $permission_id: aclRecord.permission_id - }); - - let hasPermission = result !== undefined; + const result = this.engine.runStatementGet(checkPermissionQuery, { + $user_id: userId, + $entity_type: entityType, + $entity_name: entityName, + $permission_id: aclRecord?.permission_id + }); + + let hasPermission = result !== undefined; - return hasPermission; + return hasPermission; + + } catch (error) { + console.error(error); + return false + } }; /** diff --git a/plugins/tiddlywiki/multiwikiserver/templates/manage-acl.tid b/plugins/tiddlywiki/multiwikiserver/templates/manage-acl.tid index 024f5a3a781..696e02f1e3e 100644 --- a/plugins/tiddlywiki/multiwikiserver/templates/manage-acl.tid +++ b/plugins/tiddlywiki/multiwikiserver/templates/manage-acl.tid @@ -16,7 +16,7 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-acl jsonget[recipe_name]] }}}/> jsonget[bag_name]] }}}/> - <$list filter="[jsonindexes[]]" variable="role-index"> <$let role={{{ [jsonextract] }}}> @@ -25,7 +25,7 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-acl - <$list filter="[jsonindexes[]]" variable="permission-index"> <$let permission={{{ [jsonextract] }}}> @@ -86,7 +86,7 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-acl jsonget[recipe_name]] }}}/> jsonget[bag_name]] }}}/> - <$list filter="[jsonindexes[]]" variable="role-index"> <$let role={{{ [jsonextract] }}}> @@ -95,7 +95,7 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-acl - <$list filter="[jsonindexes[]]" variable="permission-index"> <$let permission={{{ [jsonextract] }}}>