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