riju/backend/users.js

107 lines
2.9 KiB
JavaScript

import { spawn } from "child_process";
import { promises as fs } from "fs";
import os from "os";
import AsyncLock from "async-lock";
import _ from "lodash";
import parsePasswd from "parse-passwd";
import { asBool, privilegedUseradd, run, uuidRegexp } from "./util.js";
// Keep in sync with system/src/riju-system-privileged.c
export const MIN_UID = 2000;
export const MAX_UID = 65000;
function validUID(uid) {
return uid >= MIN_UID && uid < MAX_UID;
}
const CUR_UID = os.userInfo().uid;
const ASSUME_SINGLE_PROCESS = asBool(
process.env.RIJU_ASSUME_SINGLE_PROCESS,
false
);
let initialized = false;
let nextUserToCreate = null;
let locallyBorrowedUsers = new Set();
let availableUsers = new Set();
let lock = new AsyncLock();
async function getCreatedUsers() {
return new Set(
parsePasswd(await fs.readFile("/etc/passwd", "utf-8"))
.map(({ uid }) => parseInt(uid))
.filter((uid) => !isNaN(uid) && validUID(uid))
);
}
async function getActiveUsers() {
return new Set(
(
await Promise.all(
(await fs.readdir("/tmp/riju"))
.filter((name) => name.match(uuidRegexp))
.map((name) => fs.stat(`/tmp/riju/${name}`))
)
)
.map(({ uid }) => uid)
.filter(validUID)
);
}
async function createUser(log) {
if (nextUserToCreate >= MAX_UID) {
throw new Error("too many users");
}
const uid = nextUserToCreate;
await run(privilegedUseradd(uid), log);
nextUserToCreate += 1;
return uid;
}
export async function borrowUser(log) {
return await lock.acquire("key", async () => {
if (!initialized || !ASSUME_SINGLE_PROCESS) {
const createdUsers = await getCreatedUsers();
const activeUsers = await getActiveUsers();
if (createdUsers.size > 0) {
nextUserToCreate = _.max([...createdUsers]) + 1;
} else {
nextUserToCreate = MIN_UID;
}
// If there are new users created, we want to make them
// available (unless they are already active). Similarly, if
// there are users that have become inactive, we want to make
// them available (unless they are already borrowed locally).
for (const user of createdUsers) {
if (!activeUsers.has(user) && !locallyBorrowedUsers.has(user)) {
availableUsers.add(user);
}
}
// If there are users that have become active, we want to make
// them unavailable.
for (const user of activeUsers) {
availableUsers.delete(user);
}
initialized = true;
}
if (availableUsers.size === 0) {
availableUsers.add(await createUser(log));
}
// https://stackoverflow.com/a/32539929/3538165
const user = availableUsers.values().next().value;
locallyBorrowedUsers.add(user);
availableUsers.delete(user);
return {
uid: user,
returnUser: async () => {
await lock.acquire("key", () => {
locallyBorrowedUsers.delete(user);
availableUsers.add(user);
});
},
};
});
}