Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/database/postgres/project-queries.ts
5647 views
1
/*
2
* This file is part of CoCalc: Copyright © 2020 – 2025 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import debug from "debug";
7
import { omit } from "lodash";
8
9
import { callback2 } from "@cocalc/util/async-utils";
10
import {
11
DUMMY_SECRET,
12
PORT_MAX,
13
PORT_MIN,
14
validatePortNumber,
15
} from "@cocalc/util/consts";
16
import { DatastoreConfig } from "@cocalc/util/types";
17
import { query } from "./query";
18
import { PostgreSQL } from "./types";
19
20
const L = debug("hub:project-queries");
21
22
export async function project_has_network_access(
23
db: PostgreSQL,
24
project_id: string,
25
): Promise<boolean> {
26
let x;
27
try {
28
x = await callback2(db.get_project, {
29
project_id,
30
columns: ["users", "settings"],
31
});
32
} catch (err) {
33
// error probably means there is no such project or project_id is badly formatted.
34
return false;
35
}
36
if (x.settings != null && x.settings.network) {
37
return true;
38
}
39
if (x.users != null) {
40
for (const account_id in x.users) {
41
if (
42
x.users[account_id] != null &&
43
x.users[account_id].upgrades != null &&
44
x.users[account_id].upgrades.network
45
) {
46
return true;
47
}
48
}
49
}
50
return false;
51
}
52
53
// get/set/del datastore configurations in addons
54
55
interface GetDSOpts {
56
db: PostgreSQL;
57
account_id: string;
58
project_id: string;
59
}
60
61
async function get_datastore(
62
opts: GetDSOpts,
63
): Promise<{ [key: string]: DatastoreConfig }> {
64
const { db, account_id, project_id } = opts;
65
const q: { users: any; addons?: any } = await query({
66
db,
67
table: "projects",
68
select: ["addons", "users"],
69
where: { project_id },
70
one: true,
71
});
72
73
// this access test is absolutely critial to have! (only project queries set access_check to false)
74
if (q.users[account_id] == null) throw Error(`access denied`);
75
76
return q.addons?.datastore;
77
}
78
79
export async function project_datastore_set(
80
db: PostgreSQL,
81
account_id: string,
82
project_id: string,
83
config: any,
84
): Promise<void> {
85
// L("project_datastore_set", config);
86
87
if (config.name == null) throw Error("configuration 'name' is not defined");
88
if (typeof config.type !== "string")
89
throw Error(
90
"configuration 'type' is not defined (must be 'gcs', 'sshfs', ...)",
91
);
92
93
// check data from user
94
for (const [key, val] of Object.entries(config)) {
95
if (val == null) continue;
96
if (key === "port") {
97
const port = validatePortNumber(val);
98
if (port == null) {
99
throw new Error(
100
`Invalid value -- 'port' must be an integer between ${PORT_MIN} and ${PORT_MAX}`,
101
);
102
}
103
config.port = port;
104
continue;
105
}
106
if (
107
typeof val !== "string" &&
108
typeof val !== "boolean" &&
109
typeof val !== "number"
110
) {
111
throw new Error(`Invalid value -- '${key}' is not a valid type`);
112
}
113
if (typeof val === "string" && val.length > 100000) {
114
throw new Error(`Invalid value -- '${key}' is too long`);
115
}
116
}
117
118
const old_name = config.__old_name;
119
const conf_new = omit(config, "name", "secret", "__old_name");
120
121
// this is implicitly a test if the user has access to modify this -- don't catch it
122
const ds_prev = await get_datastore({ db, account_id, project_id });
123
124
// there is a situation where datastore is renamed, i.e. "name" is a new one,
125
// while the previous secret is stored under a different key. So, if __old_name
126
// is set, we pick that one instead.
127
const prev_name = old_name != null ? old_name : config.name;
128
129
// if a user wants to update the settings, they don't need to have the secret.
130
// an empty value or the dummy text signals to keep the secret as it is...
131
if (
132
ds_prev != null &&
133
ds_prev[prev_name] != null &&
134
(config.secret === DUMMY_SECRET || config.secret === "")
135
) {
136
conf_new.secret = ds_prev[prev_name].secret;
137
} else {
138
conf_new.secret = Buffer.from(config.secret ?? "").toString("base64");
139
}
140
141
await query({
142
db,
143
query: "UPDATE projects",
144
where: { "project_id = $::UUID": project_id },
145
jsonb_merge: { addons: { datastore: { [config.name]: conf_new } } },
146
});
147
}
148
149
export async function project_datastore_del(
150
db: PostgreSQL,
151
account_id: string,
152
project_id: string,
153
name: string,
154
): Promise<void> {
155
L("project_datastore_del", name);
156
if (typeof name !== "string" || name.length == 0) {
157
throw Error("Datastore name not properly set.");
158
}
159
160
// this is implicitly a test if the user has access to modify this -- don't catch it
161
const ds = await get_datastore({ db, account_id, project_id });
162
delete ds[name];
163
await query({
164
db,
165
query: "UPDATE projects",
166
where: { "project_id = $::UUID": project_id },
167
jsonb_set: { addons: { datastore: ds } },
168
});
169
}
170
171
export async function project_datastore_get(
172
db: PostgreSQL,
173
account_id: string,
174
project_id: string,
175
): Promise<any> {
176
try {
177
const ds = await get_datastore({
178
db,
179
account_id,
180
project_id,
181
});
182
if (ds != null) {
183
for (const [k, v] of Object.entries(ds)) {
184
ds[k] = omit(v, "secret") as any;
185
}
186
}
187
return {
188
addons: { datastore: ds },
189
};
190
} catch (err) {
191
return { type: "error", error: `${err}` };
192
}
193
}
194
195