Files
2024-07-15 23:05:27 +02:00

493 lines
13 KiB
TypeScript

// deno-lint-ignore-file no-explicit-any
import { requireLogin } from "./context.ts";
import {
GlobalOptions,
isSuperset,
removeType,
removePathPrefix,
} from "./types.ts";
import { compareInstanceObjects } from "./instance.ts";
import {
colors,
Command,
GlobalUserInfo,
log,
passwordGenerator,
Table,
UserService,
GroupService,
WorkspaceService,
GranularAclService,
yamlStringify,
yamlParse,
ExportedUser,
} from "./deps.ts";
import { InstanceGroup } from "./deps.ts";
import { SettingService } from "./deps.ts";
import { ExportedInstanceGroup } from "./deps.ts";
async function list(opts: GlobalOptions) {
await requireLogin(opts);
const perPage = 10;
let page = 0;
const total: GlobalUserInfo[] = [];
while (true) {
const res = await UserService.listUsersAsSuperAdmin({ page, perPage });
total.push(...res);
page += 1;
if (res.length < perPage) {
break;
}
}
new Table()
.header(["email", "name", "company", "verified", "super admin"])
.padding(2)
.border(true)
.body(
total.map((x) => [
x.email,
x.name ?? "-",
x.company ?? "-",
x.verified ? "true" : "false",
x.super_admin ? "true" : "false",
])
)
.render();
}
async function add(
opts: GlobalOptions & {
superadmin?: boolean;
company?: string;
name?: string;
},
email: string,
password?: string
) {
await requireLogin(opts);
const password_final = password ?? passwordGenerator("*", 15);
await UserService.createUserGlobally({
requestBody: {
email,
password: password_final,
super_admin: opts.superadmin ?? false,
company: opts.company,
name: opts.name,
},
});
if (!password) {
log.info(colors.red("New Password: " + password_final));
}
log.info(colors.underline.green("New User Created."));
}
async function remove(opts: GlobalOptions, email: string) {
await requireLogin(opts);
await UserService.globalUserDelete({ email });
log.info(colors.green("Deleted User " + email));
}
async function createToken(
opts: GlobalOptions & { email: string; password: string }
) {
if (opts.email && opts.password) {
log.info(
"Token: " +
(await UserService.login({
requestBody: {
email: opts.email,
password: opts.password,
},
}))
);
}
await requireLogin(opts);
log.info("Token: " + (await UserService.createToken({ requestBody: {} })));
}
interface SimplifiedUser {
role: string;
username: string;
disabled: boolean;
}
export async function pushWorkspaceUser(
workspace: string,
path: string,
user: SimplifiedUser | undefined,
localUser: SimplifiedUser
): Promise<void> {
const email = removePathPrefix(removeType(path, "user"), "users");
log.debug(`Processing local user ${email}`);
if (!["operator", "developer", "admin"].includes(localUser.role)) {
throw new Error(`Invalid role for user ${email}: ${localUser.role}`);
}
try {
const remoteUser = await UserService.getUser({
workspace,
username: localUser.username,
});
user = {
role: remoteUser.is_admin
? "admin"
: remoteUser.operator
? "operator"
: "developer",
username: remoteUser.username,
disabled: remoteUser.disabled,
};
log.debug(`User ${email} exists on remote`);
} catch {
log.debug(`User ${email} does not exist on remote`);
//ignore
}
if (user && user.username !== localUser.username) {
throw new Error("Username cannot be changed");
}
if (user) {
if (isSuperset(localUser, user)) {
log.debug(`User ${email} is up to date`);
return;
}
log.debug(`User ${email} is not up-to-date, updating...`);
try {
await UserService.updateUser({
workspace: workspace,
username: localUser.username,
requestBody: {
is_admin: localUser.role === "admin",
operator: localUser.role === "operator",
disabled: localUser.disabled,
},
});
} catch (e) {
console.error(e.body);
throw e;
}
} else {
console.log(colors.bold.yellow("Creating new user: " + email));
try {
const automatedUsernameCreation: boolean =
(await SettingService.getGlobal({
key: "automate_username_creation",
})) as boolean;
await WorkspaceService.addUser({
workspace: workspace,
requestBody: {
email: email,
is_admin: localUser.role === "admin",
operator: localUser.role === "operator",
username: !automatedUsernameCreation ? localUser.username : undefined,
},
});
} catch (e) {
console.error(e.body);
throw e;
}
}
}
interface SimplifiedGroup {
summary: string | undefined;
admins: string[];
members: string[];
}
export async function pushGroup(
workspace: string,
path: string,
group: SimplifiedGroup | undefined,
localGroup: SimplifiedGroup
): Promise<void> {
const name = removePathPrefix(removeType(path, "group"), "groups");
log.debug(`Processing local group ${name}`);
try {
const remoteGroup = await GroupService.getGroup({
workspace,
name,
});
// only consider extra_perms that concern actual members of the group
const admins = Object.entries(remoteGroup.extra_perms ?? {})
.filter(([k, v]) => v && remoteGroup.members?.includes(k.slice(2)))
.map(([k, _]) => k)
.sort();
group = {
summary: remoteGroup.summary,
admins,
// remove members that are also admins as they are already in the admins list
members: (remoteGroup.members ?? [])
.map((m) => "u/" + m)
.filter((m) => !admins.includes(m)),
};
log.debug(`Group ${name} exists on remote`);
} catch {
log.debug(`Group ${name} does not exist on remote`);
//ignore
}
if (group) {
if (isSuperset(localGroup, group)) {
log.debug(`Group ${name} is up to date`);
return;
}
log.debug(`Group ${name} is not up-to-date, updating...`);
try {
await GroupService.updateGroup({
workspace: workspace,
name,
requestBody: {
summary: localGroup.summary,
},
});
} catch (e) {
console.error(e.body);
throw e;
}
for (const member of [...localGroup.members, ...localGroup.admins]) {
try {
if ([...group.members, ...group.admins].includes(member)) {
log.debug(`${member} is already in group ${name}`);
} else {
log.debug(`Adding ${member} to group ${name}`);
await GroupService.addUserToGroup({
workspace: workspace,
name,
requestBody: {
username: member.slice(2),
},
});
}
if (
localGroup.admins.includes(member) &&
!group.admins.includes(member)
) {
log.debug(`Setting role of ${member} as admin in group ${name}`);
await GranularAclService.addGranularAcls({
workspace: workspace,
kind: "group_",
path: name,
requestBody: {
owner: member,
write: true,
},
});
}
if (
localGroup.members.includes(member) &&
!group.members.includes(member)
) {
log.debug(`Setting role of ${member} as member in group ${name}`);
await GranularAclService.addGranularAcls({
workspace: workspace,
kind: "group_",
path: name,
requestBody: {
owner: member,
write: false,
},
});
}
} catch (e) {
console.error(e.body);
throw e;
}
}
for (const member of [...group.members, ...group.admins]) {
if (![...localGroup.members, ...localGroup.admins].includes(member)) {
log.debug(
`Removing ${member} and any associated role from group ${name}`
);
try {
await GroupService.removeUserToGroup({
workspace: workspace,
name,
requestBody: {
username: member.slice(2),
},
});
await GranularAclService.removeGranularAcls({
workspace: workspace,
kind: "group_",
path: name,
requestBody: {
owner: member,
},
});
} catch (e) {
console.error(e.body);
throw e;
}
}
}
} else {
console.log(colors.bold.yellow("Creating new user: " + name));
try {
await GroupService.createGroup({
workspace: workspace,
requestBody: {
name,
summary: localGroup.summary,
},
});
for (const member of [...localGroup.members, ...localGroup.admins]) {
log.debug(`Adding user ${member} to group ${name}`);
try {
await GroupService.addUserToGroup({
workspace: workspace,
name,
requestBody: {
username: member.slice(2),
},
});
if (localGroup.admins.includes(member)) {
log.debug(`Setting role of ${member} as admin in group ${name}`);
await GranularAclService.addGranularAcls({
workspace: workspace,
kind: "group_",
path: name,
requestBody: {
owner: member,
write: true,
},
});
}
} catch (e) {
console.error(e.body);
throw e;
}
}
} catch (e) {
console.error(e.body);
throw e;
}
}
}
export async function pullInstanceUsers(preview: boolean = false) {
const remoteUsers = await UserService.globalUsersExport();
if (preview) {
let localUsers: ExportedUser[] = [];
try {
const raw = await Deno.readTextFile("instance_users.yaml");
localUsers = yamlParse(raw) as ExportedUser[];
} catch {}
return compareInstanceObjects(remoteUsers, localUsers, "email", "user");
} else {
log.info("Pulling users from instance...");
await Deno.writeTextFile(
"instance_users.yaml",
yamlStringify(remoteUsers as any)
);
log.info(colors.green("Users written to instance_users.yaml"));
}
}
export async function pushInstanceUsers(preview: boolean = false) {
const remoteUsers = await UserService.globalUsersExport();
const localUsers = (await Deno.readTextFile("instance_users.yaml")
.then((raw) => yamlParse(raw))
.catch(() => [])) as ExportedUser[];
if (preview) {
return compareInstanceObjects(localUsers, remoteUsers, "email", "user");
} else {
log.info("Pushing users to instance...");
await UserService.globalUsersOverwrite({
requestBody: localUsers,
});
log.info(colors.green("Users pushed to the instance"));
}
}
export async function pullInstanceGroups(preview = false) {
const remoteGroups = await GroupService.exportInstanceGroups();
if (preview) {
let localGroups: InstanceGroup[] = [];
try {
const raw = await Deno.readTextFile("instance_groups.yaml");
localGroups = yamlParse(raw) as InstanceGroup[];
} catch {}
return compareInstanceObjects(remoteGroups, localGroups, "name", "group");
} else {
log.info("Pulling groups from instance...");
await Deno.writeTextFile(
"instance_groups.yaml",
yamlStringify(remoteGroups as any)
);
log.info(colors.green("Groups written to instance_groups.yaml"));
}
}
export async function pushInstanceGroups(preview: boolean = false) {
const remoteGroups = await GroupService.exportInstanceGroups();
const localGroups = (await Deno.readTextFile("instance_groups.yaml")
.then((raw) => yamlParse(raw))
.catch(() => [])) as ExportedInstanceGroup[];
if (preview) {
return compareInstanceObjects(localGroups, remoteGroups, "name", "group");
} else {
log.info("Pushing groups to instance...");
await GroupService.overwriteInstanceGroups({
requestBody: localGroups,
});
log.info(colors.green("Groups pushed to the instance"));
}
}
const command = new Command()
.description("user related commands")
.action(list as any)
.command("add", "Create a user")
.arguments("<email:string> [password:string]")
.option("--superadmin", "Specify to make the new user superadmin.")
.option(
"--company <company:string>",
"Specify to set the company of the new user."
)
.option("--name <name:string>", "Specify to set the name of the new user.")
.action(add as any)
.command("remove", "Delete a user")
.arguments("<email:string>")
.action(remove as any)
.command("create-token")
.option(
"--email <email:string>",
"Specify credentials to use for authentication. This will not be stored. It will only be used to exchange for a token with the API server, which will not be stored either.",
{
depends: ["password"],
}
)
.option(
"--password <password:string>",
"Specify credentials to use for authentication. This will not be stored. It will only be used to exchange for a token with the API server, which will not be stored either.",
{
depends: ["email"],
}
)
.action(createToken as any);
export default command;