Skip to content

Commit

Permalink
#8832 fix favicon and access control issue (#8846)
Browse files Browse the repository at this point in the history
  • Loading branch information
webplusai authored Dec 22, 2024
1 parent 1c56488 commit 0198eb2
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 88 deletions.
9 changes: 6 additions & 3 deletions plugins/tiddlywiki/multiwikiserver/modules/mws-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -415,11 +415,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"
};
}

Expand Down Expand Up @@ -453,11 +455,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ exports.method = "GET";

exports.path = /^\/recipes\/([^\/]+)\/tiddlers\/(.+)$/;

exports.useACL = true;
// exports.useACL = true;

exports.entityName = "recipe"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
};

}());
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -48,19 +49,24 @@ 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();
}
return;
}
} 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();
}
Expand All @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
};

/*
Expand All @@ -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) {
Expand All @@ -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
}
};

/**
Expand Down
8 changes: 4 additions & 4 deletions plugins/tiddlywiki/multiwikiserver/templates/manage-acl.tid
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-acl
<input type="hidden" name="entity_type" value="recipe" />
<input type="hidden" name="recipe_name" value={{{ [<recipe>jsonget[recipe_name]] }}}/>
<input type="hidden" name="bag_name" value={{{ [<bag>jsonget[bag_name]] }}}/>
<select name="role_id" class="tc-select">
<select name="role_id" class="tc-select" required>
<option value="">Select Role</option>
<$list filter="[<roles-list>jsonindexes[]]" variable="role-index">
<$let role={{{ [<roles-list>jsonextract<role-index>] }}}>
Expand All @@ -25,7 +25,7 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-acl
</$list>
</select>

<select name="permission_id" class="tc-select">
<select name="permission_id" class="tc-select" required>
<option value="">Select Permission</option>
<$list filter="[<permissions-list>jsonindexes[]]" variable="permission-index">
<$let permission={{{ [<permissions-list>jsonextract<permission-index>] }}}>
Expand Down Expand Up @@ -86,7 +86,7 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-acl
<input type="hidden" name="entity_type" value="bag" />
<input type="hidden" name="recipe_name" value={{{ [<recipe>jsonget[recipe_name]] }}}/>
<input type="hidden" name="bag_name" value={{{ [<bag>jsonget[bag_name]] }}}/>
<select name="role_id" class="tc-select">
<select name="role_id" class="tc-select" required>
<option value="">Select Role</option>
<$list filter="[<roles-list>jsonindexes[]]" variable="role-index">
<$let role={{{ [<roles-list>jsonextract<role-index>] }}}>
Expand All @@ -95,7 +95,7 @@ title: $:/plugins/tiddlywiki/multiwikiserver/templates/manage-acl
</$list>
</select>

<select name="permission_id" class="tc-select">
<select name="permission_id" class="tc-select" required>
<option value="">Select Permission</option>
<$list filter="[<permissions-list>jsonindexes[]]" variable="permission-index">
<$let permission={{{ [<permissions-list>jsonextract<permission-index>] }}}>
Expand Down

0 comments on commit 0198eb2

Please sign in to comment.