Appservice convenience (#47)

Canonicalise the existence of the "admin room" for managing the appservice and Draupnir instances

* Add utilities for managing users in the admin room

* Merge the appservice admin room and access control list.

The majority of admins will need to use the draupnir admin commands
to manage the list.

* Utility methods for creating generic rules in PolicyLists.

* Commands for managing appservice users.
This commit is contained in:
Gnuxie authored and GitHub committed 2023-05-03 16:31:28 +01:00
1 parent 449e48b486
commit 99e6f168ca
9 files changed
+91 -15

No files matched your search

+9
View File
@@ -28,6 +28,7 @@ limitations under the License.
import { Bridge } from "matrix-appservice-bridge";
import { Permalinks } from "../commands/interface-manager/Permalinks";
import AccessControlUnit, { EntityAccess } from "../models/AccessControlUnit";
import { EntityType, Recommendation } from "../models/ListRule";
import PolicyList from "../models/PolicyList";
/**
@@ -74,4 +75,12 @@ export class AccessControl {
public getUserAccess(mxid: string): EntityAccess {
return this.accessControlUnit.getAccessForUser(mxid, "CHECK_SERVER");
}
public async allow(mxid: string): Promise<void> {
await this.accessControlList.createPolicy(EntityType.RULE_USER, Recommendation.Allow, mxid);
}
public async remove(mxid: string): Promise<void> {
await this.accessControlList.unbanEntity(EntityType.RULE_USER, mxid);
}
}
+2 -2
View File
@@ -52,7 +52,7 @@ export class MjolnirAppService {
public readonly config: IConfig,
public readonly bridge: Bridge,
public readonly mjolnirManager: MjolnirManager,
private readonly accessControl: AccessControl,
public readonly accessControl: AccessControl,
private readonly dataStore: DataStore,
) {
this.api = new Api(config.homeserver.url, mjolnirManager);
@@ -81,7 +81,7 @@ export class MjolnirAppService {
suppressEcho: false,
});
await bridge.initialise();
const accessControlListId = await bridge.getBot().getClient().resolveRoom(config.accessControlList);
const accessControlListId = await bridge.getBot().getClient().resolveRoom(config.adminRoom);
const accessControl = await AccessControl.setupAccessControl(accessControlListId, bridge);
const mjolnirManager = await MjolnirManager.makeMjolnirManager(dataStore, bridge, accessControl);
const appService = new MjolnirAppService(
+54
View File
@@ -0,0 +1,54 @@
/**
* Copyright (C) 2022 Gnuxie <Gnuxie@protonmail.com>
* All rights reserved.
*/
import { defineInterfaceCommand, findTableCommand } from "../../commands/interface-manager/InterfaceCommand";
import { findPresentationType, parameters, ParsedKeywords } from "../../commands/interface-manager/ParameterParsing";
import { CommandResult } from "../../commands/interface-manager/Validation";
import { AppserviceContext } from "./AppserviceCommandHandler";
import { UserID } from "matrix-bot-sdk";
import { defineMatrixInterfaceAdaptor } from "../../commands/interface-manager/MatrixInterfaceAdaptor";
import { tickCrossRenderer } from "../../commands/interface-manager/MatrixHelpRenderer";
defineInterfaceCommand({
designator: ["allow"],
table: "appservice bot",
parameters: parameters([
{
name: 'user',
acceptor: findPresentationType('UserID'),
}
]),
command: async function (this: AppserviceContext, _keywords: ParsedKeywords, user: UserID): Promise<CommandResult<void>> {
await this.appservice.accessControl.allow(user.toString());
return CommandResult.Ok(undefined);
},
summary: "Allow a user to provision themselves a draupnir using the appservice."
})
defineMatrixInterfaceAdaptor({
interfaceCommand: findTableCommand("appservice bot", "allow"),
renderer: tickCrossRenderer,
});
defineInterfaceCommand({
designator: ["remove"],
table: "appservice bot",
parameters: parameters([
{
name: 'user',
acceptor: findPresentationType('UserID'),
}
]),
command: async function (this: AppserviceContext, _keywords: ParsedKeywords, user: UserID): Promise<CommandResult<void>> {
await this.appservice.accessControl.remove(user.toString());
return CommandResult.Ok(undefined);
},
summary: "Stop a user from using any provisioned draupnir in the appservice."
})
defineMatrixInterfaceAdaptor({
interfaceCommand: findTableCommand("appservice bot", "remove"),
renderer: tickCrossRenderer,
});
@@ -22,6 +22,7 @@ export type AppserviceBaseExecutor = (this: AppserviceContext, ...args: any[]) =
import '../../commands/interface-manager/MatrixPresentations';
import './ListCommand';
import './AccessCommands';
import { AppserviceBotEmitter } from './AppserviceBotEmitter';
+1 -1
View File
@@ -12,7 +12,7 @@ db:
# A room you have created that scopes who can access the appservice.
# See docs/access_control.md
accessControlList: "#access-control-list:localhost:9999"
adminRoom: "#draupnir-admin:localhost:9999"
# This is a web api that the widget connects to in order to interact with the appservice.
webAPI:
+1 -2
View File
@@ -7,8 +7,7 @@ db:
engine: "postgres"
connectionString: "postgres://mjolnir-tester:mjolnir-test@localhost:8083/mjolnir-test-db"
accessControlList: "#access-control-list:localhost:9999"
adminRoom: "#access-control-list:localhost:9999"
adminRoom: "#draupnir-admin:localhost:9999"
webAPI:
port: 9001
-2
View File
@@ -46,8 +46,6 @@ export interface IConfig {
webAPI: {
port: number
},
/** A policy room for controlling access to the appservice -- just replace with mangement room tbh bloody hell m8 */
accessControlList: string,
/** The admin room for the appservice bot. Not called managementRoom like mjolnir on purpose, so they're not mixed in code somehow. */
adminRoom: string,
/** configuration for matrix-appservice-bridge's Logger */
+22 -7
View File
@@ -314,21 +314,36 @@ export class PolicyList extends EventEmitter {
}
}
/**
* Create a new policy rule in the list.
* @param entityType The type of entity the rule applies to e.g. RULE_USER for users.
* @param recommendation The recommendation e.g. `Recommendation.Ban` that the policy represents.
* @param entity The entity to be added to the policy list.
* @param additionalProperties Any other properties to embed in the rule such as a reason.
* @returns The event id of the policy.
*/
public async createPolicy(entityType: EntityType, recommendation: Recommendation, entity: string, additionalProperties = {}): Promise<string> {
// '@' at the beginning of state keys is reserved.
const stateKey = entityType === RULE_USER ? '_' + entity.substring(1) : entity;
const eventId = await this.client.sendStateEvent(this.roomId, entityType, stateKey, {
recommendation,
entity,
...additionalProperties
});
this.updateForEvent(eventId);
return eventId;
}
/**
* Ban an entity with Recommendation.Ban from the list.
* @param ruleType The type of rule e.g. RULE_USER.
* @param entity The entity to ban.
* @param reason A reason we are banning them.
*/
public async banEntity(ruleType: string, entity: string, reason?: string): Promise<void> {
// '@' at the beginning of state keys is reserved.
const stateKey = ruleType === RULE_USER ? '_' + entity.substring(1) : entity;
const event_id = await this.client.sendStateEvent(this.roomId, ruleType, stateKey, {
entity,
recommendation: Recommendation.Ban,
public async banEntity(ruleType: EntityType, entity: string, reason?: string): Promise<void> {
await this.createPolicy(ruleType, Recommendation.Ban, entity, {
reason: reason || '<no reason supplied>',
});
this.updateForEvent(event_id);
}
/**
+1 -1
View File
@@ -13,7 +13,7 @@ export function readTestConfig(): IConfig {
export async function setupHarness(): Promise<MjolnirAppService> {
const config = readTestConfig();
const utilityUser = await newTestUser(config.homeserver.url, { name: { contains: "utility" }});
await ensureAliasedRoomExists(utilityUser, config.accessControlList);
await ensureAliasedRoomExists(utilityUser, config.adminRoom);
return await MjolnirAppService.run(9000, config, "mjolnir-registration.yaml");
}