Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#8832 fix favicon and access control issue #8846

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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"
};
}

Expand Down Expand Up @@ -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
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
Loading