Skip to content

Commit

Permalink
Appservice convenience (#47)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Gnuxie authored May 3, 2023
1 parent 449e48b commit 99e6f16
Show file tree
Hide file tree
Showing 9 changed files with 91 additions and 15 deletions.
9 changes: 9 additions & 0 deletions src/appservice/AccessControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down Expand Up @@ -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);
}
}
4 changes: 2 additions & 2 deletions src/appservice/AppService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down
54 changes: 54 additions & 0 deletions src/appservice/bot/AccessCommands.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Copyright (C) 2022 Gnuxie <[email protected]>
* 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,
});
1 change: 1 addition & 0 deletions src/appservice/bot/AppserviceCommandHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type AppserviceBaseExecutor = (this: AppserviceContext, ...args: any[]) =

import '../../commands/interface-manager/MatrixPresentations';
import './ListCommand';
import './AccessCommands';
import { AppserviceBotEmitter } from './AppserviceBotEmitter';


Expand Down
2 changes: 1 addition & 1 deletion src/appservice/config/config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 1 addition & 2 deletions src/appservice/config/config.harness.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 0 additions & 2 deletions src/appservice/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
29 changes: 22 additions & 7 deletions src/models/PolicyList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion test/appservice/utils/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

Expand Down

0 comments on commit 99e6f16

Please sign in to comment.