Skip to content

Commit c9f4597

Browse files
committed
checking permissions
- block anonymous writes to wikis and both reads and writes to admin
1 parent 9d19634 commit c9f4597

5 files changed

Lines changed: 31 additions & 44 deletions

File tree

packages/mws/src/RequestState.ts

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,14 @@ export class StateObject<
5959
}
6060

6161
okUser() {
62-
if (!this.user.isLoggedIn) throw "User not authenticated";
62+
if (!this.user.isLoggedIn)
63+
throw new SendError("ACCESS_DENIED", 403, { reason: "User not authenticated" })
6364
}
6465
okAdmin() {
65-
if (!this.user.isLoggedIn) throw "User not authenticated";
66-
if (!this.user.isAdmin) throw "User is not an admin";
66+
if (!this.user.isLoggedIn)
67+
throw new SendError("ACCESS_DENIED", 403, { reason: "User not authenticated" })
68+
if (!this.user.isAdmin)
69+
throw new SendError("ACCESS_DENIED", 403, { reason: "User is not an admin" })
6770
}
6871

6972
async $transaction<T>(fn: (prisma: PrismaTxnClient) => Promise<T>): Promise<T> {
@@ -78,30 +81,20 @@ export class StateObject<
7881
return this.engine.$transaction(arg(this.engine), options);
7982
}
8083

81-
makeTiddlerEtag(options: { bag_name: string; revision_id: string | number; }) {
82-
// why do we need revision_id AND bag_name? revision_id is unique across all tiddlers
83-
if (options.bag_name && options.revision_id) {
84-
return `"tiddler:${options.bag_name}/${options.revision_id}"`;
85-
} else {
86-
throw "Missing bag_name or revision_id";
87-
}
88-
}
89-
90-
9184
assertWikiReferer(recipe_slug: string) {
9285
const state = this;
9386
if (!state.headers.referer) return;
9487
const referer = new URL(state.headers.referer);
9588
// console.log("Referer", state.headers.referer, referer);
9689
if (!referer.pathname.startsWith(state.pathPrefix))
97-
throw state.sendEmpty(404, { "x-reason": "invalid path prefix" });
90+
throw new SendError("ACCESS_DENIED", 403, { reason: "Referer check failed" })
9891
if (!referer.pathname.startsWith(state.pathPrefix + "/wiki/"))
9992
return; // keep going
10093
// we now get the recipe name from the referer
10194
const recipe_name = referer.pathname.substring(state.pathPrefix.length + "/wiki/".length);
10295
const referer_slug = decodeURIComponent(recipe_name) as PrismaField<"Recipe", "id">;
10396
if (referer_slug !== recipe_slug)
104-
throw new SendError("CROSS_WIKI_ACCESS_DENIED", 403, null);
97+
throw new SendError("ACCESS_DENIED", 403, { reason: "Referer check failed" })
10598
}
10699

107100
assertAdminReferer() {
@@ -111,9 +104,9 @@ export class StateObject<
111104
const referer = new URL(state.headers.referer);
112105
// deny referers from outside our pathprefix
113106
if (!referer.pathname.startsWith(state.pathPrefix))
114-
throw new SendError("ADMIN_ACCESS_DENIED", 403, null);
107+
throw new SendError("ACCESS_DENIED", 403, { reason: "Referer check failed" })
115108
if (referer.pathname.startsWith(state.pathPrefix + "/wiki/"))
116-
throw new SendError("ADMIN_ACCESS_DENIED", 403, null);
109+
throw new SendError("ACCESS_DENIED", 403, { reason: "Referer check failed" })
117110
}
118111

119112

packages/mws/src/SendError.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,8 @@ declare module "@tiddlywiki/server" {
7575
"OPERATION_NOT_PERMITTED":
7676
SendErrorItem<403, { reason: string }>
7777

78-
"CROSS_WIKI_ACCESS_DENIED":
79-
SendErrorItem<403, null>
80-
"ADMIN_ACCESS_DENIED":
81-
SendErrorItem<403, null>
78+
"ACCESS_DENIED":
79+
SendErrorItem<403, { reason: string }>
8280
}
8381
}
8482
SendError.oninstance.push(e => {

packages/mws/src/new-managers/RecipeResolver.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ export class RecipeResolver {
165165

166166
if (!isAdmin && needsWrite) {
167167

168+
state.okUser();
169+
168170
const hasWriteAccess = recipe.recipe_bags
169171
.filter(recipeBag => recipeBag.is_writable)
170172
.some(recipeBag =>

packages/mws/src/new-managers/TabDataAdapter.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,7 @@ export const AdminSave = zodRoute({
533533
zodRequestBody: z => z.any(),
534534
inner: async (state) => {
535535
state.assertAdminReferer();
536-
state.asserted = state.user.isAdmin;
536+
state.asserted = state.user.isLoggedIn;
537537
state.data = JSON.parse(JSON.stringify(state.data), (key: any, val: any) => {
538538
if (typeof val === "string" && val.startsWith(IdString.prefix))
539539
return new IdString(val.slice(IdString.prefix.length));
@@ -584,7 +584,6 @@ export const AdminLoad = zodRoute({
584584
securityChecks: { requestedWithHeader: true },
585585
zodPathParams: z => ({}),
586586
inner: async (state) => {
587-
// access is checked in each list
588587
state.assertAdminReferer();
589588
state.asserted = state.user.isLoggedIn;
590589
const res = await state.$transaction(async prisma => {

packages/mws/src/new-managers/TabUpserts.ts

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,14 @@ export abstract class PerClassImportWriter<Modal extends PrismaModalKeys> {
8888

8989
async checkExisting(id: IdString, name: string, user: ServerRequest["user"]) {
9090
if (user.isAdmin) { this.asserted = true; }
91-
if(this.initStore && typeof user.isLoggedIn === "boolean")
91+
if (this.initStore && typeof user.isLoggedIn === "boolean")
9292
throw new Error("This shouldn't happen.");
93+
9394
if (!user.isAdmin && this.adminLevel) {
94-
if (!id.toString()) {
95-
throw new SendError("OPERATION_NOT_PERMITTED", 403, {
96-
reason: "You don't have permission to create " + this.tabid + "."
97-
});
98-
}
95+
96+
if (!id.toString())
97+
throw new SendError("ACCESS_DENIED", 403, { reason: "You don't have permission to create " + this.tabid + "." });
98+
9999
const hasPermission: number = await (this.tx[this.modal] as any).count({
100100
where: {
101101
id: id.toString(),
@@ -106,26 +106,21 @@ export abstract class PerClassImportWriter<Modal extends PrismaModalKeys> {
106106
}
107107
}
108108
}
109-
})
110-
if (!hasPermission) {
111-
throw new SendError("OPERATION_NOT_PERMITTED", 403, {
112-
reason: "You don't have permission to modify " + this.tabid + "."
113-
});
114-
}
115-
this.asserted = true;
109+
});
110+
111+
if (!hasPermission)
112+
throw new SendError("ACCESS_DENIED", 403, { reason: "You don't have permission to modify " + this.tabid + "." });
113+
else
114+
this.asserted = true;
116115
}
117116
if (!user.isAdmin && this.tabid === "users") {
118117
if (user.user_id !== id.toString())
119-
throw new SendError("OPERATION_NOT_PERMITTED", 403, {
120-
reason: "You must be an admin to edit other users"
121-
});
122-
this.asserted = true;
118+
throw new SendError("ACCESS_DENIED", 403, { reason: "You must be an admin to edit other users" });
119+
else
120+
this.asserted = true;
123121
}
124122
if (!user.isAdmin && this.tabid === "roles") {
125-
throw new SendError("OPERATION_NOT_PERMITTED", 403, {
126-
reason: "You must be an admin to edit other users"
127-
});
128-
// this.asserted = true;
123+
throw new SendError("ACCESS_DENIED", 403, { reason: "You must be an admin to edit roles" });
129124
}
130125
if (id.toString()) {
131126
const existing = await (this.tx[this.modal] as any).findUnique({

0 commit comments

Comments
 (0)