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