diff --git a/exp_groups/index.ts b/exp_groups/index.ts index 14fe26e0..ae6ba441 100644 --- a/exp_groups/index.ts +++ b/exp_groups/index.ts @@ -1,19 +1,168 @@ import * as lib from "@clusterio/lib"; +import * as messages from "./messages"; declare module "@clusterio/lib" { export interface InstanceConfigFields { + "exp_groups.sync_mode": "enabled" | "disabled" | "bidirectional" } export interface ControllerConfigFields { } } +// Group permissions + +lib.definePermission({ + name: "exp_groups.group.get", + title: "Get Groups", + description: "Retrieve a specific Factorio permission group by ID.", + grantByDefault: true, +}); +lib.definePermission({ + name: "exp_groups.group.list", + title: "List Groups", + description: "List all Factorio permission groups.", + grantByDefault: true, +}); +lib.definePermission({ + name: "exp_groups.group.subscribe", + title: "Subscribe to Group Updates", + description: "Receive updates when Factorio permission groups change.", + grantByDefault: true, +}); +lib.definePermission({ + name: "exp_groups.group.create", + title: "Create Groups", + description: "Create new Factorio permission groups.", + grantByDefault: false, +}); +lib.definePermission({ + name: "exp_groups.group.update", + title: "Update Groups", + description: "Modify existing Factorio permission groups.", + grantByDefault: false, +}); +lib.definePermission({ + name: "exp_groups.group.delete", + title: "Delete Groups", + description: "Delete Factorio permission groups.", + grantByDefault: false, +}); + +// Assignment permissions + +lib.definePermission({ + name: "exp_groups.assignment.get", + title: "Get Assignments", + description: "Retrieve a specific manual group assignment for a player.", + grantByDefault: true, +}); +lib.definePermission({ + name: "exp_groups.assignment.list", + title: "List Assignments", + description: "List all manual group assignments.", + grantByDefault: true, +}); +lib.definePermission({ + name: "exp_groups.assignment.subscribe", + title: "Subscribe to Assignment Updates", + description: "Receive updates when manual group assignments change.", + grantByDefault: true, +}); +lib.definePermission({ + name: "exp_groups.assignment.create", + title: "Create Assignments", + description: "Manually assign players to groups, overriding role mappings.", + grantByDefault: false, +}); +lib.definePermission({ + name: "exp_groups.assignment.update", + title: "Update Assignments", + description: "Modify existing manual group assignments.", + grantByDefault: false, +}); +lib.definePermission({ + name: "exp_groups.assignment.delete", + title: "Delete Assignments", + description: "Remove manual group assignments.", + grantByDefault: false, +}); + +// Role mapping permissions + +lib.definePermission({ + name: "exp_groups.role_mapping.get", + title: "Get Role Mappings", + description: "Retrieve a specific role mapping rule.", + grantByDefault: true, +}); +lib.definePermission({ + name: "exp_groups.role_mapping.list", + title: "List Role Mappings", + description: "List all role mapping rules.", + grantByDefault: true, +}); +lib.definePermission({ + name: "exp_groups.role_mapping.subscribe", + title: "Subscribe to Role Mapping Updates", + description: "Receive updates when role mapping rules change.", + grantByDefault: true, +}); +lib.definePermission({ + name: "exp_groups.role_mapping.create", + title: "Create Role Mappings", + description: "Create rules that map user roles to Factorio permission groups.", + grantByDefault: false, +}); +lib.definePermission({ + name: "exp_groups.role_mapping.update", + title: "Update Role Mappings", + description: "Modify existing role mapping rules.", + grantByDefault: false, +}); +lib.definePermission({ + name: "exp_groups.role_mapping.delete", + title: "Delete Role Mappings", + description: "Delete role mapping rules.", + grantByDefault: false, +}); + export const plugin: lib.PluginDeclaration = { name: "exp_groups", title: "ExpGaming - Permission Groups", description: "Clusterio plugin providing syncing of permission groups", + messages: [ + messages.GroupUpdatedEvent, + messages.AssignmentUpdatedEvent, + messages.RoleMappingUpdatedEvent, + + messages.GroupCreateRequest, + messages.GroupUpdateRequest, + messages.GroupDeleteRequest, + messages.GroupGetRequest, + messages.GroupListRequest, + + messages.AssignmentCreateRequest, + messages.AssignmentUpdateRequest, + messages.AssignmentDeleteRequest, + messages.AssignmentGetRequest, + messages.AssignmentListRequest, + + messages.RoleMappingCreateRequest, + messages.RoleMappingUpdateRequest, + messages.RoleMappingDeleteRequest, + messages.RoleMappingGetRequest, + messages.RoleMappingListRequest, + ], + instanceEntrypoint: "./dist/node/instance", instanceConfigFields: { + "exp_groups.sync_mode": { + description: "Synchronize permission groups with the controller", + type: "string", + enum: ["disabled", "enabled", "bidirectional"], + initialValue: "bidirectional", + }, }, controllerEntrypoint: "./dist/node/controller", diff --git a/exp_groups/messages.ts b/exp_groups/messages.ts new file mode 100644 index 00000000..4e337041 --- /dev/null +++ b/exp_groups/messages.ts @@ -0,0 +1,657 @@ +import * as lib from "@clusterio/lib"; +import { Type, Static } from "@sinclair/typebox"; + +/* + Data records +*/ + +export class GroupPermissions { + constructor( + public isBlacklist: boolean, + public permissions: string[], + ) {} + + static jsonSchema = Type.Object({ + is_blacklist: Type.Boolean(), + permissions: Type.Array(Type.String()), + }); + + toJSON(): Static { + return { + is_blacklist: this.isBlacklist, + permissions: this.permissions, + }; + } + + static fromJSON(json: Static) { + return new this( + json.is_blacklist, + json.permissions, + ); + } +} + +export class GroupRecord { + constructor( + public id: number, + public name: string, + public permissions: GroupPermissions, + public updatedAtMs: number = 0, + public isDeleted: boolean = false, + ) {} + + static jsonSchema = Type.Object({ + id: Type.Integer(), + name: Type.String(), + permissions: GroupPermissions.jsonSchema, + updated_at_ms: Type.Optional(Type.Number()), + is_deleted: Type.Optional(Type.Boolean()), + }); + + toJSON() { + let json: Static = { + id: this.id, + name: this.name, + permissions: this.permissions.toJSON(), + }; + + if (this.updatedAtMs) { + json.updated_at_ms = this.updatedAtMs; + } + + if (this.isDeleted) { + json.is_deleted = true; + } + + return json; + } + + static fromJSON(json: Static) { + return new this( + json.id, + json.name, + GroupPermissions.fromJSON(json.permissions), + json.updated_at_ms ?? 0, + json.is_deleted ?? false, + ); + } +} + +export class AssignmentRecord { + constructor( + public name: string, + public groupId: number, + public updatedAtMs: number = 0, + public isDeleted: boolean = false, + ) {} + + static jsonSchema = Type.Object({ + name: Type.String(), + group_id: Type.Integer(), + updated_at_ms: Type.Optional(Type.Number()), + is_deleted: Type.Optional(Type.Boolean()), + }); + + toJSON(): Static { + let json: Static = { + name: this.name, + group_id: this.groupId, + }; + + if (this.updatedAtMs) { + json.updated_at_ms = this.updatedAtMs; + } + + if (this.isDeleted) { + json.is_deleted = true; + } + + return json; + } + + static fromJSON(json: Static) { + return new this( + json.name, + json.group_id, + json.updated_at_ms ?? 0, + json.is_deleted ?? false, + ); + } +} + +export class RoleMappingRecord { + constructor( + public id: number, + public roleIds: Set, + public groupId: number, + public priority: number, + public enabled: boolean, + public updatedAtMs: number = 0, + public isDeleted: boolean = false, + ) {} + + static jsonSchema = Type.Object({ + id: Type.Integer(), + role_ids: Type.Array(Type.Integer()), + group_id: Type.Integer(), + priority: Type.Number(), + enabled: Type.Boolean(), + updated_at_ms: Type.Optional(Type.Number()), + is_deleted: Type.Optional(Type.Boolean()), + }); + + toJSON(): Static { + let json: Static = { + id: this.id, + role_ids: [...this.roleIds], + group_id: this.groupId, + priority: this.priority, + enabled: this.enabled, + }; + + if (this.updatedAtMs) { + json.updated_at_ms = this.updatedAtMs; + } + + if (this.isDeleted) { + json.is_deleted = true; + } + + return json; + } + + static fromJSON(json: Static) { + return new this( + json.id, + new Set(json.role_ids), + json.group_id, + json.priority, + json.enabled, + json.updated_at_ms ?? 0, + json.is_deleted ?? false, + ); + } +} + +/* + Update events +*/ + +export class GroupUpdatedEvent { + declare ["constructor"]: typeof GroupUpdatedEvent; + static plugin = "exp_groups" as const; + static type = "event" as const; + static src = "controller" as const; + static dst = ["control", "instance"] as const; + static permission = "exp_groups.group.subscribe" as const; + + constructor( + public updates: GroupRecord[], + ) {} + + static jsonSchema = Type.Object({ + updates: Type.Array(GroupRecord.jsonSchema), + }); + + toJSON() { + return { updates: this.updates.map(group => group.toJSON()) }; + } + + static fromJSON(json: Static) { + return new this(json.updates.map(group => GroupRecord.fromJSON(group))); + } +} + +export class AssignmentUpdatedEvent { + declare ["constructor"]: typeof AssignmentUpdatedEvent; + static plugin = "exp_groups" as const; + static type = "event" as const; + static src = "controller" as const; + static dst = ["control", "instance"] as const; + static permission = "exp_groups.assignment.subscribe" as const; + + constructor( + public updates: AssignmentRecord[], + ) {} + + static jsonSchema = Type.Object({ + updates: Type.Array(AssignmentRecord.jsonSchema), + }); + + toJSON() { + return { updates: this.updates.map(assignment => assignment.toJSON()) }; + } + + static fromJSON(json: Static) { + return new this(json.updates.map(assignment => AssignmentRecord.fromJSON(assignment))); + } +} + +export class RoleMappingUpdatedEvent { + declare ["constructor"]: typeof RoleMappingUpdatedEvent; + static plugin = "exp_groups" as const; + static type = "event" as const; + static src = "controller" as const; + static dst = "control" as const; + static permission = "exp_groups.role_mapping.subscribe" as const; + + constructor( + public updates: RoleMappingRecord[], + ) {} + + static jsonSchema = Type.Object({ + updates: Type.Array(RoleMappingRecord.jsonSchema), + }); + + toJSON() { + return { updates: this.updates.map(roleMapping => roleMapping.toJSON()) }; + } + + static fromJSON(json: Static) { + return new this(json.updates.map(roleMapping => RoleMappingRecord.fromJSON(roleMapping))); + } +} + +/* + Group requests +*/ + +export class GroupCreateRequest { + declare ["constructor"]: typeof GroupCreateRequest; + static plugin = "exp_groups" as const; + static type = "request" as const; + static src = ["control", "instance"] as const; + static dst = "controller" as const; + static permission = "exp_groups.group.create" as const; + + constructor( + public name: string, + public permissions: GroupPermissions, + ) {} + + static jsonSchema = Type.Object({ + name: Type.String(), + permissions: GroupPermissions.jsonSchema, + }); + + toJSON() { + return { + name: this.name, + permissions: this.permissions.toJSON(), + }; + } + + static fromJSON(json: Static) { + return new this( + json.name, + GroupPermissions.fromJSON(json.permissions), + ); + } +} + +export class GroupUpdateRequest { + declare ["constructor"]: typeof GroupUpdateRequest; + static plugin = "exp_groups" as const; + static type = "request" as const; + static src = ["control", "instance"] as const; + static dst = "controller" as const; + static permission = "exp_groups.group.update" as const; + + constructor( + public group: GroupRecord, + ) {} + + static jsonSchema = Type.Object({ + group: GroupRecord.jsonSchema, + }); + + toJSON() { + return { + group: this.group.toJSON(), + }; + } + + static fromJSON(json: Static) { + return new this( + GroupRecord.fromJSON(json.group), + ); + } +} + +export class GroupDeleteRequest { + declare ["constructor"]: typeof GroupDeleteRequest; + static plugin = "exp_groups" as const; + static type = "request" as const; + static src = ["control", "instance"] as const; + static dst = "controller" as const; + static permission = "exp_groups.group.delete" as const; + + constructor( + public groupId: number, + ) {} + + static jsonSchema = Type.Object({ + group_id: Type.Integer(), + }); + + toJSON() { + return { + group_id: this.groupId, + }; + } + + static fromJSON(json: Static) { + return new this(json.group_id); + } +} + +export class GroupGetRequest { + declare ["constructor"]: typeof GroupGetRequest; + static plugin = "exp_groups" as const; + static type = "request" as const; + static src = ["control", "instance"] as const; + static dst = "controller" as const; + static permission = "exp_groups.group.get" as const; + static Response = GroupRecord; + + constructor( + public groupId: number, + ) {} + + static jsonSchema = Type.Object({ + group_id: Type.Integer(), + }); + + toJSON() { + return { + group_id: this.groupId, + }; + } + + static fromJSON(json: Static) { + return new this(json.group_id); + } +} + +export class GroupListRequest { + declare ["constructor"]: typeof GroupListRequest; + static plugin = "exp_groups" as const; + static type = "request" as const; + static src = ["control", "instance"] as const; + static dst = "controller" as const; + static permission = "exp_groups.group.list" as const; + static Response = lib.jsonArray(GroupRecord); + + constructor() {} +} + +/* + Assignment requests +*/ + +export class AssignmentCreateRequest { + declare ["constructor"]: typeof AssignmentCreateRequest; + static plugin = "exp_groups" as const; + static type = "request" as const; + static src = ["control", "instance"] as const; + static dst = "controller" as const; + static permission = "exp_groups.assignment.create" as const; + + constructor( + public name: string, + public groupId: number, + ) {} + + static jsonSchema = Type.Object({ + name: Type.String(), + group_id: Type.Integer(), + }); + + toJSON() { + return { + name: this.name, + group_id: this.groupId, + }; + } + + static fromJSON(json: Static) { + return new this(json.name, json.group_id); + } +} + +export class AssignmentUpdateRequest { + declare ["constructor"]: typeof AssignmentUpdateRequest; + static plugin = "exp_groups" as const; + static type = "request" as const; + static src = ["control", "instance"] as const; + static dst = "controller" as const; + static permission = "exp_groups.assignment.update" as const; + + constructor( + public assignment: AssignmentRecord, + ) {} + + static jsonSchema = Type.Object({ + assignment: AssignmentRecord.jsonSchema, + }); + + toJSON() { + return { + assignment: this.assignment.toJSON(), + }; + } + + static fromJSON(json: Static) { + return new this( + AssignmentRecord.fromJSON(json.assignment), + ); + } +} + +export class AssignmentDeleteRequest { + declare ["constructor"]: typeof AssignmentDeleteRequest; + static plugin = "exp_groups" as const; + static type = "request" as const; + static src = ["control", "instance"] as const; + static dst = "controller" as const; + static permission = "exp_groups.assignment.delete" as const; + + constructor( + public name: string, + ) {} + + static jsonSchema = Type.Object({ + name: Type.String(), + }); + + toJSON() { + return { + name: this.name, + }; + } + + static fromJSON(json: Static) { + return new this(json.name); + } +} + +export class AssignmentGetRequest { + declare ["constructor"]: typeof AssignmentGetRequest; + static plugin = "exp_groups" as const; + static type = "request" as const; + static src = ["control", "instance"] as const; + static dst = "controller" as const; + static permission = "exp_groups.assignment.get" as const; + static Response = AssignmentRecord; + + constructor( + public name: string, + ) {} + + static jsonSchema = Type.Object({ + name: Type.String(), + }); + + toJSON() { + return { + name: this.name, + }; + } + + static fromJSON(json: Static) { + return new this(json.name); + } +} + +export class AssignmentListRequest { + declare ["constructor"]: typeof AssignmentListRequest; + static plugin = "exp_groups" as const; + static type = "request" as const; + static src = ["control", "instance"] as const; + static dst = "controller" as const; + static permission = "exp_groups.assignment.list" as const; + static Response = lib.jsonArray(AssignmentRecord); + + constructor() {} +} + +/* + Role mapping requests +*/ + +export class RoleMappingCreateRequest { + declare ["constructor"]: typeof RoleMappingCreateRequest; + static plugin = "exp_groups" as const; + static type = "request" as const; + static src = ["control", "instance"] as const; + static dst = "controller" as const; + static permission = "exp_groups.role_mapping.create" as const; + + constructor( + public roleIds: number[], + public groupId: number, + public priority: number, + public enabled: boolean, + ) {} + + static jsonSchema = Type.Object({ + role_ids: Type.Array(Type.Integer()), + group_id: Type.Integer(), + priority: Type.Number(), + enabled: Type.Boolean(), + }); + + toJSON() { + return { + role_ids: this.roleIds, + group_id: this.groupId, + priority: this.priority, + enabled: this.enabled, + }; + } + + static fromJSON(json: Static) { + return new this( + json.role_ids, + json.group_id, + json.priority, + json.enabled, + ); + } +} + +export class RoleMappingUpdateRequest { + declare ["constructor"]: typeof RoleMappingUpdateRequest; + static plugin = "exp_groups" as const; + static type = "request" as const; + static src = ["control", "instance"] as const; + static dst = "controller" as const; + static permission = "exp_groups.role_mapping.update" as const; + + constructor( + public roleMapping: RoleMappingRecord, + ) {} + + static jsonSchema = Type.Object({ + role_mapping: RoleMappingRecord.jsonSchema, + }); + + toJSON() { + return { + role_mapping: this.roleMapping.toJSON(), + }; + } + + static fromJSON(json: Static) { + return new this( + RoleMappingRecord.fromJSON(json.role_mapping), + ); + } +} + +export class RoleMappingDeleteRequest { + declare ["constructor"]: typeof RoleMappingDeleteRequest; + static plugin = "exp_groups" as const; + static type = "request" as const; + static src = ["control", "instance"] as const; + static dst = "controller" as const; + static permission = "exp_groups.role_mapping.delete" as const; + + constructor( + public id: number, + ) {} + + static jsonSchema = Type.Object({ + id: Type.Integer(), + }); + + toJSON() { + return { + id: this.id, + }; + } + + static fromJSON(json: Static) { + return new this(json.id); + } +} + +export class RoleMappingGetRequest { + declare ["constructor"]: typeof RoleMappingGetRequest; + static plugin = "exp_groups" as const; + static type = "request" as const; + static src = ["control", "instance"] as const; + static dst = "controller" as const; + static permission = "exp_groups.role_mapping.get" as const; + static Response = RoleMappingRecord; + + constructor( + public id: number, + ) {} + + static jsonSchema = Type.Object({ + id: Type.Integer(), + }); + + toJSON() { + return { + id: this.id, + }; + } + + static fromJSON(json: Static) { + return new this(json.id); + } +} + +export class RoleMappingListRequest { + declare ["constructor"]: typeof RoleMappingListRequest; + static plugin = "exp_groups" as const; + static type = "request" as const; + static src = ["control", "instance"] as const; + static dst = "controller" as const; + static permission = "exp_groups.role_mapping.list" as const; + static Response = lib.jsonArray(RoleMappingRecord); + + constructor() {} +}