mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2025-12-27 03:25:23 +09:00
283 lines
10 KiB
TypeScript
283 lines
10 KiB
TypeScript
import * as lib from "@clusterio/lib";
|
|
import { BaseControllerPlugin, InstanceInfo } from "@clusterio/controller";
|
|
|
|
import {
|
|
PermissionStrings, PermissionStringsUpdate,
|
|
PermissionGroup, PermissionGroupUpdate,
|
|
InstancePermissionGroups,
|
|
PermissionInstanceId,
|
|
PermissionGroupEditEvent,
|
|
} from "./messages";
|
|
|
|
import path from "path";
|
|
import fs from "fs-extra";
|
|
|
|
export class ControllerPlugin extends BaseControllerPlugin {
|
|
static permissionGroupsPath = "exp_groups.json";
|
|
static userGroupsPath = "exp_user_groups.json";
|
|
|
|
userToGroup: Map<lib.User["id"], PermissionGroup> = new Map(); // TODO this needs to be per instance
|
|
permissionStrings!: Map<PermissionStrings["id"], PermissionStrings>;
|
|
permissionGroups!: Map<InstancePermissionGroups["id"], InstancePermissionGroups>;
|
|
|
|
async init() {
|
|
this.controller.handle(PermissionStringsUpdate, this.handlePermissionStringsUpdate.bind(this));
|
|
this.controller.handle(PermissionGroupUpdate, this.handlePermissionGroupUpdate.bind(this));
|
|
this.controller.handle(PermissionGroupEditEvent, this.handlePermissionGroupEditEvent.bind(this));
|
|
this.controller.subscriptions.handle(PermissionStringsUpdate, this.handlePermissionStringsSubscription.bind(this));
|
|
this.controller.subscriptions.handle(PermissionGroupUpdate, this.handlePermissionGroupSubscription.bind(this));
|
|
this.controller.subscriptions.handle(PermissionGroupEditEvent);
|
|
this.permissionStrings = new Map([["Global", new PermissionStrings("Global", new Set())]]);
|
|
this.permissionGroups = new Map([["Global", new InstancePermissionGroups("Global")]]);
|
|
await this.loadData();
|
|
|
|
// Add the default group if missing and add any missing cluster roles
|
|
const clusterRoles = [...this.controller.userManager.roles.values()]
|
|
for (const instanceGroups of this.permissionGroups.values()) {
|
|
const groups = instanceGroups.groups;
|
|
const instanceRoles = [...groups.values()].flatMap(group => [...group.roleIds.values()]);
|
|
const missingRoles = clusterRoles.filter(role => instanceRoles.includes(role.id));
|
|
const defaultGroup = groups.get("Default");
|
|
if (defaultGroup) {
|
|
for (const role of missingRoles) {
|
|
defaultGroup.roleIds.add(role.id)
|
|
}
|
|
} else {
|
|
groups.set("Default", new PermissionGroup(
|
|
instanceGroups.instanceId,
|
|
"Default",
|
|
groups.size,
|
|
new Set(missingRoles.map(role => role.id))
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
async onControllerConfigFieldChanged(field: string, curr: unknown, prev: unknown) {
|
|
if (field === "exp_groups.allow_role_inconsistency") {
|
|
// Do something with this.userToGroup
|
|
}
|
|
}
|
|
|
|
async onInstanceConfigFieldChanged(instance: InstanceInfo, field: string, curr: unknown, prev: unknown) {
|
|
this.logger.info(`controller::onInstanceConfigFieldChanged ${instance.id} ${field}`);
|
|
if (field === "exp_groups.sync_permission_groups") {
|
|
const updates = []
|
|
const now = Date.now();
|
|
if (curr) {
|
|
// Global sync enabled, we dont need the instance config
|
|
const instanceGroups = this.permissionGroups.get(instance.id);
|
|
if (instanceGroups) {
|
|
this.permissionGroups.delete(instance.id);
|
|
for (const group of instanceGroups.groups.values()) {
|
|
group.updatedAtMs = now;
|
|
group.isDeleted = true;
|
|
updates.push(group);
|
|
}
|
|
}
|
|
} else {
|
|
// Global sync disabled, make a copy of the global config as a base
|
|
const global = this.permissionGroups.get("Global")!;
|
|
const oldInstanceGroups = this.permissionGroups.get(instance.id);
|
|
const instanceGroups = new InstancePermissionGroups(
|
|
instance.id, new Map([...global.groups.values()].map(group => [group.name, group.copy(instance.id)]))
|
|
)
|
|
this.permissionGroups.set(instance.id, instanceGroups);
|
|
for (const group of instanceGroups.groups.values()) {
|
|
group.updatedAtMs = now;
|
|
updates.push(group);
|
|
}
|
|
// If it has an old config (unexpected) then deal with it
|
|
if (oldInstanceGroups) {
|
|
for (const group of oldInstanceGroups.groups.values()) {
|
|
if (!instanceGroups.groups.has(group.name)) {
|
|
group.updatedAtMs = now;
|
|
group.isDeleted = true;
|
|
updates.push(group);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Send the updates to all instances and controls
|
|
if (updates.length) {
|
|
this.controller.subscriptions.broadcast(new PermissionGroupUpdate(updates));
|
|
}
|
|
}
|
|
}
|
|
|
|
async loadPermissionGroups() {
|
|
const file = path.resolve(this.controller.config.get("controller.database_directory"), ControllerPlugin.permissionGroupsPath);
|
|
this.logger.verbose(`Loading ${file}`);
|
|
try {
|
|
const content = await fs.readFile(file, { encoding: "utf8" });
|
|
for (const groupRaw of JSON.parse(content)) {
|
|
const group = PermissionGroup.fromJSON(groupRaw);
|
|
const instanceGroups = this.permissionGroups.get(group.instanceId);
|
|
if (instanceGroups) {
|
|
instanceGroups.groups.set(group.name, group);
|
|
} else {
|
|
this.permissionGroups.set(group.instanceId,
|
|
new InstancePermissionGroups(group.instanceId, new Map([[group.name, group]]))
|
|
);
|
|
}
|
|
};
|
|
|
|
} catch (err: any) {
|
|
if (err.code === "ENOENT") {
|
|
this.logger.verbose("Creating new permission group database");
|
|
return;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async savePermissionGroups() {
|
|
const file = path.resolve(this.controller.config.get("controller.database_directory"), ControllerPlugin.permissionGroupsPath);
|
|
this.logger.verbose(`Writing ${file}`);
|
|
await lib.safeOutputFile(file, JSON.stringify(
|
|
[...this.permissionGroups.values()].flatMap(instanceGroups => [...instanceGroups.groups.values()])
|
|
));
|
|
}
|
|
|
|
async loadUserGroups() {
|
|
if (!this.controller.config.get("exp_groups.allow_role_inconsistency")) return;
|
|
const file = path.resolve(this.controller.config.get("controller.database_directory"), ControllerPlugin.userGroupsPath);
|
|
this.logger.verbose(`Loading ${file}`);
|
|
try {
|
|
const content = await fs.readFile(file, { encoding: "utf8" });
|
|
this.userToGroup = new Map(JSON.parse(content));
|
|
|
|
} catch (err: any) {
|
|
if (err.code === "ENOENT") {
|
|
this.logger.verbose("Creating new user group database");
|
|
return;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async saveUserGroups() {
|
|
if (!this.controller.config.get("exp_groups.allow_role_inconsistency")) return;
|
|
const file = path.resolve(this.controller.config.get("controller.database_directory"), ControllerPlugin.userGroupsPath);
|
|
this.logger.verbose(`Writing ${file}`);
|
|
await lib.safeOutputFile(file, JSON.stringify([...this.permissionGroups.entries()]));
|
|
}
|
|
|
|
async loadData() {
|
|
await Promise.all([
|
|
this.loadPermissionGroups(),
|
|
this.loadUserGroups(),
|
|
])
|
|
}
|
|
|
|
async onSaveData() {
|
|
await Promise.all([
|
|
this.savePermissionGroups(),
|
|
this.saveUserGroups(),
|
|
])
|
|
}
|
|
|
|
addPermisisonGroup(instanceId: PermissionInstanceId, name: string, permissions = new Set<string>(), silent = false) {
|
|
const instanceGroups = this.permissionGroups.get(instanceId);
|
|
if (!instanceGroups) {
|
|
throw new Error("Instance ID does not exist");
|
|
}
|
|
if (instanceGroups.groups.has(name)) {
|
|
return instanceGroups.groups.get(name)!;
|
|
}
|
|
for (const group of instanceGroups.groups.values()) {
|
|
group.order += 1;
|
|
}
|
|
const group = new PermissionGroup(instanceId, name, 0, new Set(), permissions, Date.now(), false);
|
|
instanceGroups.groups.set(group.id, group);
|
|
if (!silent) {
|
|
this.controller.subscriptions.broadcast(new PermissionGroupUpdate([group]));
|
|
}
|
|
return group;
|
|
}
|
|
|
|
removePermissionGroup(instanceId: PermissionInstanceId, name: string, silent = false) {
|
|
const instanceGroups = this.permissionGroups.get(instanceId);
|
|
if (!instanceGroups) {
|
|
throw new Error("Instance ID does not exist");
|
|
}
|
|
const group = instanceGroups.groups.get(name)
|
|
if (!group) {
|
|
return null;
|
|
}
|
|
for (const nextGroup of instanceGroups.groups.values()) {
|
|
if (nextGroup.order > group.order) {
|
|
nextGroup.order -= 1;
|
|
}
|
|
}
|
|
instanceGroups.groups.delete(group.id);
|
|
group.updatedAtMs = Date.now();
|
|
group.isDeleted = true;
|
|
if (!silent) {
|
|
this.controller.subscriptions.broadcast(new PermissionGroupUpdate([group]));
|
|
}
|
|
return group;
|
|
}
|
|
|
|
async handlePermissionGroupEditEvent(event: PermissionGroupEditEvent) {
|
|
// TODO
|
|
}
|
|
|
|
async handlePermissionStringsUpdate(event: PermissionStringsUpdate) {
|
|
for (const update of event.updates) {
|
|
const global = this.permissionStrings.get("Global")!
|
|
this.permissionStrings.set(update.instanceId as number, update)
|
|
global.updatedAtMs = Math.max(global.updatedAtMs, update.updatedAtMs)
|
|
for (const permission of update.permissions) {
|
|
global.permissions.add(permission)
|
|
}
|
|
// TODO maybe check if changes have happened rather than always pushing updates
|
|
this.controller.subscriptions.broadcast(new PermissionStringsUpdate([global, update]))
|
|
}
|
|
}
|
|
|
|
async handlePermissionGroupUpdate(event: PermissionGroupUpdate) {
|
|
const updates = [];
|
|
for (const group of event.updates) {
|
|
const groups = this.permissionGroups.get(group.instanceId);
|
|
if (!groups) continue;
|
|
const existingGroup = groups.groups.get(group.id);
|
|
let update
|
|
if (!existingGroup) {
|
|
update = this.addPermisisonGroup(group.instanceId, group.name, group.permissions, true);
|
|
} else if (group.isDeleted) {
|
|
update = this.removePermissionGroup(group.instanceId, group.name, true);
|
|
} else {
|
|
existingGroup.permissions = group.permissions;
|
|
existingGroup.updatedAtMs = Date.now();
|
|
update = existingGroup;
|
|
}
|
|
if (update) updates.push(update);
|
|
}
|
|
this.controller.subscriptions.broadcast(new PermissionGroupUpdate(updates));
|
|
}
|
|
|
|
async handlePermissionStringsSubscription(request: lib.SubscriptionRequest, src: lib.Address) {
|
|
const updates = [ ...this.permissionStrings.values() ]
|
|
.filter(
|
|
value => value.updatedAtMs > request.lastRequestTimeMs,
|
|
)
|
|
return updates.length ? new PermissionStringsUpdate(updates) : null;
|
|
}
|
|
|
|
async handlePermissionGroupSubscription(request: lib.SubscriptionRequest, src: lib.Address) {
|
|
const updates = [ ...this.permissionGroups.values() ]
|
|
.flatMap(instanceGroups => [...instanceGroups.groups.values()])
|
|
.filter(
|
|
value => value.updatedAtMs > request.lastRequestTimeMs,
|
|
)
|
|
if (src.type === lib.Address.instance) {
|
|
const instanceUpdates = updates.filter(group => group.instanceId === src.id || group.instanceId === "Global");
|
|
this.logger.info(JSON.stringify(updates))
|
|
this.logger.info(JSON.stringify(instanceUpdates))
|
|
return instanceUpdates.length ? new PermissionGroupUpdate(instanceUpdates) : null;
|
|
}
|
|
return updates.length ? new PermissionGroupUpdate(updates) : null;
|
|
}
|
|
}
|