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