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