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/hub/proxy/check-for-access-to-project.ts
Views: 687
import LRU from "lru-cache";1import { callback2 } from "@cocalc/util/async-utils";2import getLogger from "../logger";3import { database } from "../servers/database";4const {5user_has_write_access_to_project,6user_has_read_access_to_project,7} = require("../access");8import generateHash from "@cocalc/server/auth/hash";9import addUserToProject from "@cocalc/server/projects/add-user-to-project";10import isSandboxProject from "@cocalc/server/projects/is-sandbox";11import { getAccountWithApiKey } from "@cocalc/server/api/manage";12import isCollaborator from "@cocalc/server/projects/is-collaborator";13import isBanned from "@cocalc/server/accounts/is-banned";1415const logger = getLogger("proxy:has-access");1617interface Options {18project_id: string;19remember_me?: string;20api_key?: string;21type: "write" | "read";22isPersonal: boolean;23}2425// 1 minute cache: grant "yes" for a while26const yesCache = new LRU({ max: 20000, ttl: 1000 * 60 * 1 });27// 5 second cache: recheck "no" much more frequently28const noCache = new LRU({ max: 20000, ttl: 1000 * 5 });2930export default async function hasAccess(opts: Options): Promise<boolean> {31if (opts.isPersonal) {32// In personal mode, anyone who can access localhost has full33// access to everything, since this is meant to be used on34// single-user personal computer in a context where there is no35// security requirement at all.36return true;37}3839const { project_id, remember_me, api_key, type } = opts;40const key = `${project_id}${remember_me}${api_key}${type}`;4142for (const cache of [yesCache, noCache]) {43if (cache.has(key)) {44return !!cache.get(key);45}46}4748// not cached, so we determine access.49let access: boolean;50const dbg = (...args) => {51logger.debug(type, " access to ", project_id, ...args);52};5354try {55access = await checkForAccess({56project_id,57remember_me,58api_key,59type,60dbg,61});62} catch (err) {63dbg("error trying to determine access; denying for now", `${err}`);64access = false;65}66dbg("determined that access=", access);6768if (access) {69yesCache.set(key, access);70} else {71noCache.set(key, access);72}73return access;74}7576async function checkForAccess({77project_id,78remember_me,79api_key,80type,81dbg,82}): Promise<boolean> {83if (remember_me) {84const { access, error } = await checkForRememberMeAccess({85project_id,86remember_me,87type,88dbg,89});90if (access) {91return access;92}93if (!api_key) {94// only finish if no api key:95if (error) {96throw Error(error);97} else {98return access;99}100}101}102103if (api_key) {104const { access, error } = await checkForApiKeyAccess({105project_id,106api_key,107type,108dbg,109});110if (access) {111return access;112}113if (error) {114throw Error(error);115}116return access;117}118119throw Error(120"you must authenticate with either an api_key or remember_me cookie, but neither is set",121);122}123124async function checkForRememberMeAccess({125project_id,126remember_me,127type,128dbg,129}): Promise<{ access: boolean; error?: string }> {130dbg("get remember_me message");131const x = remember_me.split("$");132const hash = generateHash(x[0], x[1], parseInt(x[2]), x[3]);133const signed_in_mesg = await callback2(database.get_remember_me, {134hash,135cache: true,136});137if (signed_in_mesg == null) {138return { access: false, error: "not signed in via remember_me" };139}140141let access: boolean = false;142const { account_id, email_address } = signed_in_mesg;143if (await isBanned(account_id)) {144return { access: false, error: "banned" };145}146dbg({ account_id, email_address });147148dbg(`now check if user has access to project`);149if (type === "write") {150access = await callback2(user_has_write_access_to_project, {151database,152project_id,153account_id,154});155if (!access) {156// if the project is a sandbox project, we add the user as a collaborator157// and grant access.158if (await isSandboxProject(project_id)) {159dbg("granting sandbox access");160await addUserToProject({ project_id, account_id });161access = true;162}163}164165if (access) {166// Record that user is going to actively access167// this project. This is important since it resets168// the idle timeout.169database.touch({170account_id,171project_id,172});173}174} else if (type == "read") {175access = await callback2(user_has_read_access_to_project, {176database,177project_id,178account_id,179});180} else {181return { access: false, error: `invalid access type ${type}` };182}183return { access };184}185186async function checkForApiKeyAccess({ project_id, api_key, type, dbg }) {187// we don't have a notion of "read" access, for type.188dbg("checkForApiKeyAccess", { project_id, type });189const account_id = await getAccountWithApiKey(api_key);190if (!account_id) {191dbg("api key is not valid (probably expired)");192return { access: false, error: "invalid or expired api key" };193}194return { access: await isCollaborator({ account_id, project_id }) };195}196197198