Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/database/postgres/project/user-set-query-project-users.ts
5608 views
1
/*
2
* This file is part of CoCalc: Copyright © 2025 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { PROJECT_UPGRADES } from "@cocalc/util/schema";
7
import {
8
assert_valid_account_id,
9
is_object,
10
is_valid_uuid_string,
11
} from "@cocalc/util/misc";
12
import { type UserGroup } from "@cocalc/util/project-ownership";
13
14
type AllowedUserFields = {
15
group?: UserGroup;
16
hide?: boolean;
17
upgrades?: Record<string, unknown>;
18
ssh_keys?: Record<string, Record<string, unknown> | undefined>;
19
};
20
21
function ensureAllowedKeys(
22
user: Record<string, unknown>,
23
allowGroupChanges: boolean,
24
): void {
25
const allowed = new Set(["hide", "upgrades", "ssh_keys"]);
26
for (const key of Object.keys(user)) {
27
if (key === "group") {
28
if (!allowGroupChanges) {
29
throw Error(
30
"changing collaborator group via user_set_query is not allowed",
31
);
32
}
33
continue;
34
}
35
if (!allowed.has(key)) {
36
throw Error(`unknown field '${key}'`);
37
}
38
}
39
}
40
41
function sanitizeUpgrades(upgrades: unknown): Record<string, unknown> {
42
if (!is_object(upgrades)) {
43
throw Error("invalid type for field 'upgrades'");
44
}
45
const allowedUpgrades = PROJECT_UPGRADES.params;
46
for (const key of Object.keys(upgrades)) {
47
if (!Object.prototype.hasOwnProperty.call(allowedUpgrades, key)) {
48
throw Error(`invalid upgrades field '${key}'`);
49
}
50
}
51
return upgrades as Record<string, unknown>;
52
}
53
54
function sanitizeSshKeys(
55
ssh_keys: unknown,
56
): Record<string, Record<string, unknown> | undefined> {
57
if (!is_object(ssh_keys)) {
58
throw Error("ssh_keys must be an object");
59
}
60
const sanitized: Record<string, Record<string, unknown> | undefined> = {};
61
for (const fingerprint of Object.keys(ssh_keys)) {
62
const key = (ssh_keys as Record<string, unknown>)[fingerprint];
63
if (!key) {
64
sanitized[fingerprint] = undefined;
65
continue;
66
}
67
if (!is_object(key)) {
68
throw Error("each key in ssh_keys must be an object");
69
}
70
for (const field of Object.keys(key)) {
71
if (
72
!["title", "value", "creation_date", "last_use_date"].includes(field)
73
) {
74
throw Error(`invalid ssh_keys field '${field}'`);
75
}
76
}
77
sanitized[fingerprint] = key as Record<string, unknown>;
78
}
79
return sanitized;
80
}
81
82
/**
83
* Sanitize and security-check project user mutations submitted via user set query.
84
*
85
* Only permits modifying the requesting user's own entry (hide/upgrades/ssh_keys).
86
* Collaborator role changes must use dedicated APIs that enforce ownership rules.
87
*/
88
export function sanitizeUserSetQueryProjectUsers(
89
obj: { users?: unknown } | undefined,
90
account_id?: string,
91
): Record<string, AllowedUserFields> | undefined {
92
if (obj?.users == null) {
93
return undefined;
94
}
95
if (account_id != null) {
96
assert_valid_account_id(account_id);
97
}
98
if (!is_object(obj.users)) {
99
throw Error("users must be an object");
100
}
101
102
const sanitized: Record<string, AllowedUserFields> = {};
103
const usersInput = obj.users as Record<string, unknown>;
104
105
for (const id of Object.keys(usersInput)) {
106
if (!is_valid_uuid_string(id)) {
107
throw Error(`invalid account_id '${id}'`);
108
}
109
const user = usersInput[id];
110
if (!is_object(user)) {
111
throw Error("user entry must be an object");
112
}
113
114
const isSelf = account_id == null || id === account_id;
115
ensureAllowedKeys(user as Record<string, unknown>, account_id == null);
116
117
const entry: AllowedUserFields = {};
118
if ("group" in user) {
119
if (account_id != null) {
120
throw Error(
121
"changing collaborator group via user_set_query is not allowed",
122
);
123
}
124
const group = (user as any).group;
125
if (group !== "owner" && group !== "collaborator") {
126
throw Error(
127
`invalid group value '${group}' - must be 'owner' or 'collaborator'`,
128
);
129
}
130
entry.group = group;
131
}
132
if ("hide" in user) {
133
if (typeof (user as any).hide !== "boolean") {
134
throw Error("invalid type for field 'hide'");
135
}
136
entry.hide = (user as any).hide;
137
}
138
if ("upgrades" in user) {
139
if (!isSelf) {
140
throw Error(
141
"users set queries may only change upgrades for the requesting account",
142
);
143
}
144
entry.upgrades = sanitizeUpgrades((user as any).upgrades);
145
}
146
if ("ssh_keys" in user) {
147
if (!isSelf) {
148
throw Error(
149
"users set queries may only change ssh_keys for the requesting account",
150
);
151
}
152
entry.ssh_keys = sanitizeSshKeys((user as any).ssh_keys);
153
}
154
sanitized[id] = entry;
155
}
156
157
return sanitized;
158
}
159
160