CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/util/db-schema/public-paths.ts
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { deep_copy } from "../misc";
7
import { SCHEMA as schema } from "./index";
8
import { Table } from "./types";
9
import { checkPublicPathName } from "./name-rules";
10
11
export interface PublicPath {
12
id: string;
13
project_id: string;
14
path: string;
15
name?: string;
16
description?: string;
17
disabled?: boolean;
18
unlisted?: boolean;
19
authenticated?: boolean; // if true, only authenticated users are allowed to access
20
created?: Date;
21
license?: string;
22
last_edited?: Date;
23
last_saved?: Date;
24
counter?: number;
25
vhost?: string;
26
auth?: string;
27
compute_image?: string;
28
site_license_id?: string;
29
redirect?: string;
30
jupyter_api?: boolean;
31
}
32
33
// Get publicly available information about a project.
34
Table({
35
name: "public_projects",
36
rules: {
37
anonymous: true,
38
virtual: "projects",
39
user_query: {
40
get: {
41
pg_where: [{ "project_id = $::UUID": "project_id-public" }],
42
fields: {
43
project_id: true,
44
title: true,
45
description: true,
46
name: true,
47
},
48
},
49
},
50
},
51
});
52
53
Table({
54
name: "public_paths",
55
fields: {
56
id: {
57
type: "string",
58
pg_type: "CHAR(40)",
59
desc: "sha1 hash derived from project_id and path",
60
},
61
project_id: {
62
type: "uuid",
63
},
64
path: {
65
type: "string",
66
},
67
name: {
68
type: "string",
69
pg_type: "VARCHAR(100)",
70
desc: "The optional name of this public path. Must be globally unique (up to case) across all public paths in a given project. It can be between 1 and 100 characters from a-z A-Z 0-9 period and dash.",
71
render: {
72
type: "text",
73
editable: true,
74
},
75
},
76
description: {
77
type: "string",
78
render: {
79
type: "markdown",
80
maxLen: 1024,
81
editable: true,
82
},
83
},
84
disabled: {
85
type: "boolean",
86
desc: "if true then disabled",
87
render: {
88
type: "boolean",
89
editable: true,
90
},
91
},
92
unlisted: {
93
type: "boolean",
94
desc: "if true then unlisted, so does not appear in /share listing page.",
95
render: {
96
type: "boolean",
97
editable: true,
98
},
99
},
100
authenticated: {
101
type: "boolean",
102
desc: "if true, then only authenticated users have access",
103
render: {
104
type: "boolean",
105
editable: true,
106
},
107
},
108
license: {
109
type: "string",
110
desc: "The license that the content of the share is made available under.",
111
},
112
created: {
113
type: "timestamp",
114
desc: "when this path was created",
115
},
116
last_edited: {
117
type: "timestamp",
118
desc: "when this path was last edited",
119
},
120
last_saved: {
121
type: "timestamp",
122
desc: "when this path was last saved (or deleted if disabled) by manage-storage",
123
},
124
counter: {
125
type: "number",
126
desc: "the number of times this public path has been accessed",
127
render: { type: "number", editable: true, integer: true, min: 0 },
128
},
129
vhost: {
130
// For now, this will only be used *manually* for now; at some point users will be able to specify this,
131
// though maybe they have to prove they own it. This will be like "github pages", basically.
132
// For now we will only serve the vhost files statically with no special support, except we do support
133
// basic http auth. However, we will add
134
// special server support for certain file types (e.g., math typesetting, markdown, sagews, ipynb, etc.)
135
// so static websites can just be written in a mix of md, html, ipynb, etc. files with no javascript needed.
136
// This could be a non-default option.
137
// IMPORTANT: right now if vhost is set, then the share is not visible at all to the normal share server.
138
// This is intentional for security reasons, since vhosts actually serve html files in a way that can be
139
// directly viewed in the browser, and they could contain dangerous content, so must be served on a different
140
// domain to avoid them somehow being an attack vector.
141
// BUG: I also can't get this to work for new domains; it only works for foo.cocalc.com for subdomains, and my
142
// old domains like vertramp.org. WEIRD.
143
type: "string",
144
desc: 'Request for the given host (which must not contain the string "cocalc") will be served by this public share. Only one public path can have a given vhost. The vhost field can be a comma-separated string for multiple vhosts that point to the same public path.',
145
unique: true,
146
render: {
147
type: "text",
148
editable: true,
149
},
150
},
151
cross_origin_isolation: {
152
// This is used by https://python-wasm.cocalc.com. But it can't be used by https://sagelets.cocalc.com/
153
// since that loads third party javascript from the sage cell server. The only safe and secure way to
154
// allow this functionality is in a minimal page that doesn't load content from other pages, and that's
155
// just the way it is. You can't embed such a minimal page in an iframe. See
156
// https://stackoverflow.com/questions/69322834/is-it-possible-to-embed-a-cross-origin-isolated-iframe-inside-a-normal-page
157
// for a discussion.
158
type: "boolean",
159
desc: "Set to true to enable cross-origin isolation for this shared path. It will be served with COOP and COEP headers set to enable access to web APIs including SharedArrayBuffer and Atomics and prevent outer attacks (Spectre attacks, cross-origin attacks, etc.). Setting this will break loading any third party javascript that requires communication with cross-origin windows, e.g., the Sage Cell Server.",
160
},
161
auth: {
162
type: "map",
163
desc: "Map from relative path inside the share to array of {path:[{name:[string], pass:[password-hash]}, ...], ...}. Used both by vhost and share server, but not user editable yet. Later it will be user editable. The password hash is from packages/hub/auth.password_hash (so 1000 iterations of sha512)",
164
},
165
token: {
166
type: "string",
167
desc: "Random token that must be passed in as query parameter to see this share; this increases security. Only used for unlisted shares.",
168
render: {
169
type: "text",
170
editable: true,
171
},
172
},
173
compute_image: {
174
type: "string",
175
desc: "The underlying compute image, which defines the associated software stack. e.g. 'default', 'custom/some-id/latest', ...",
176
},
177
site_license_id: {
178
type: "string",
179
desc: "Site license to apply to projects editing a copy of this.",
180
},
181
url: {
182
type: "string",
183
desc: "If given, use this relative URL to open this share. ONLY set this for proxy urls! For example: 'gist/darribas/4121857' or 'github/cocalc/sagemathinc' or 'url/wstein.org/Tables/modjac/curves.txt'. The point is that we need to store the url somewhere, and don't want to end up using the ugly id in this case. This is different than the urls that come from setting a name for the owner and public path, since that's for files shared *from* within cocalc.",
184
},
185
image: {
186
type: "string",
187
desc: "Image that illustrates this shared content.",
188
render: { type: "image" },
189
},
190
redirect: {
191
type: "string",
192
desc: "Redirect path for this share",
193
render: {
194
type: "text",
195
editable: true,
196
},
197
},
198
jupyter_api: {
199
type: "boolean",
200
desc: "If true, enable stateless jupyter api so users can evaluate code",
201
render: {
202
type: "boolean",
203
editable: true,
204
},
205
},
206
},
207
rules: {
208
primary_key: "id",
209
db_standby: "unsafe",
210
anonymous: true, // allow user *read* access, even if not signed in
211
212
pg_indexes: [
213
"project_id",
214
"url",
215
"last_edited",
216
"vhost",
217
"disabled",
218
"unlisted",
219
"authenticated",
220
"(substring(project_id::text from 1 for 1))",
221
"(substring(project_id::text from 1 for 2))",
222
],
223
224
user_query: {
225
get: {
226
pg_where: [{ "project_id = $::UUID": "project_id" }],
227
throttle_changes: 2000,
228
fields: {
229
id: null,
230
project_id: null,
231
path: null,
232
name: null,
233
url: null, // user can get this but NOT set it (below) since it's set when path is created only (it defines the path).
234
description: null,
235
image: null,
236
disabled: null, // if true then disabled
237
unlisted: null, // if true then do not show in main listing (so doesn't get google indexed)
238
authenticated: null, // if true, only authenticated users can have access
239
license: null,
240
last_edited: null,
241
created: null,
242
last_saved: null,
243
counter: null,
244
// don't use DEFAULT_COMPUTE_IMAGE, because old shares without that val set will always be "default"!
245
compute_image: "default",
246
site_license_id: null,
247
cross_origin_isolation: null,
248
redirect: null,
249
jupyter_api: null,
250
},
251
},
252
set: {
253
fields: {
254
id(obj, db) {
255
return db.sha1(obj.project_id, obj.path);
256
},
257
project_id: "project_write",
258
path: true,
259
name: true,
260
description: true,
261
image: true,
262
disabled: true,
263
unlisted: true,
264
authenticated: true,
265
license: true,
266
last_edited: true,
267
created: true,
268
compute_image: true,
269
site_license_id: true, // user with write access to project can set this.
270
cross_origin_isolation: true,
271
redirect: true,
272
jupyter_api: true,
273
},
274
required_fields: {
275
id: true,
276
project_id: true,
277
path: true,
278
},
279
check_hook(db, obj, _account_id, _project_id, cb) {
280
if (!obj["name"]) {
281
cb();
282
return;
283
}
284
// confirm that the name is valid:
285
try {
286
checkPublicPathName(obj["name"]);
287
} catch (err) {
288
cb(err.toString());
289
return;
290
}
291
// It's a valid name, so next check that it is not already in use in this project
292
db._query({
293
query: "SELECT path FROM public_paths",
294
where: {
295
"project_id = $::UUID": obj["project_id"],
296
"path != $::TEXT": obj["path"],
297
"LOWER(name) = $::TEXT": obj["name"].toLowerCase(),
298
},
299
cb: (err, result) => {
300
if (err) {
301
cb(err);
302
return;
303
}
304
if (result.rows.length > 0) {
305
cb(
306
`There is already a public path "${result.rows[0].path}" in this project with the name "${obj["name"]}". Names are not case sensitive.`,
307
);
308
return;
309
}
310
// success
311
cb();
312
},
313
});
314
},
315
},
316
},
317
},
318
});
319
320
schema.public_paths.project_query = deep_copy(schema.public_paths.user_query);
321
322
/* Look up a single public path by its id. */
323
324
Table({
325
name: "public_paths_by_id",
326
rules: {
327
anonymous: true,
328
virtual: "public_paths",
329
user_query: {
330
get: {
331
check_hook(_db, obj, _account_id, _project_id, cb): void {
332
if (typeof obj.id == "string" && obj.id.length == 40) {
333
cb(); // good
334
} else {
335
cb("id must be a sha1 hash");
336
}
337
},
338
fields: {
339
id: null,
340
project_id: null,
341
path: null,
342
name: null,
343
description: null,
344
disabled: null, // if true then disabled
345
unlisted: null, // if true then do not show in main listing (so doesn't get google indexed)
346
authenticated: null, // if true, only authenticated users can have access
347
license: null,
348
last_edited: null,
349
created: null,
350
last_saved: null,
351
counter: null,
352
compute_image: null,
353
redirect: null,
354
jupyter_api: null,
355
},
356
},
357
},
358
},
359
});
360
361
// WARNING: the fields in queries to all_publics_paths are ignored; all of them are always returned.
362
Table({
363
name: "all_public_paths",
364
rules: {
365
virtual: "public_paths",
366
user_query: {
367
get: {
368
async instead_of_query(database, opts, cb): Promise<void> {
369
try {
370
cb(undefined, await database.get_all_public_paths(opts.account_id));
371
} catch (err) {
372
cb(err);
373
}
374
},
375
fields: {
376
id: null,
377
project_id: null,
378
path: null,
379
name: null,
380
description: null,
381
disabled: null, // if true then disabled
382
unlisted: null, // if true then do not show in main listing (so doesn't get google indexed)
383
authenticated: null, // if true, only authenticated users can have access
384
license: null,
385
last_edited: null,
386
created: null,
387
last_saved: null,
388
counter: null,
389
compute_image: null,
390
},
391
},
392
},
393
},
394
});
395
396
// This is the only way to get the site_license_id for a given public path.
397
// Requester must have write access to the project. This is just like the
398
// public_paths table, but NOT anonymous, and only provides a get query
399
// with access to the site_license_id.
400
Table({
401
name: "public_paths_site_license_id",
402
rules: {
403
virtual: "public_paths",
404
user_query: {
405
get: {
406
pg_where: [{ "project_id = $::UUID": "project_id" }],
407
fields: {
408
id: null,
409
project_id: null,
410
path: null,
411
site_license_id: null,
412
},
413
},
414
},
415
},
416
});
417
418
Table({
419
name: "crm_public_paths",
420
fields: schema.public_paths.fields,
421
rules: {
422
primary_key: schema.public_paths.primary_key,
423
virtual: "public_paths",
424
user_query: {
425
get: {
426
admin: true, // only admins can do get queries on this table
427
// (without this, users who have read access could read)
428
pg_where: [],
429
options: [{ limit: 300, order_by: "-last_edited" }],
430
// @ts-ignore
431
fields: schema.public_paths.user_query.get.fields,
432
},
433
set: {
434
admin: true,
435
fields: {
436
id: true,
437
name: true,
438
description: true,
439
counter: true,
440
image: true,
441
disabled: true,
442
unlisted: true,
443
authenticated: true,
444
license: true,
445
last_edited: true,
446
created: true,
447
compute_image: true,
448
site_license_id: true,
449
redirect: true,
450
jupyter_api: true,
451
},
452
// not doing this since don't want to require project_id and path to
453
// be set, and this is for admin use only anyways:
454
// check_hook: schema.public_paths.user_query.set.check_hook,
455
},
456
},
457
},
458
});
459
460