Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the ability to deactivate users to prevent them from ever being bridged #1021

Merged
merged 12 commits into from
Apr 7, 2020
1 change: 1 addition & 0 deletions changelog.d/1021.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add ability to deactivate users permanently via the DebugAPI.
46 changes: 37 additions & 9 deletions spec/integ/irc-connections.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,10 @@ describe("IRC connections", function() {
});

it("should be made once per client, regardless of how many messages are " +
"to be sent to IRC", function(done) {
"to be sent to IRC", async function() {
let connectCount = 0;

env.ircMock._whenClient(roomMapping.server, testUser.nick, "connect",
function(client, cb) {
env.ircMock._whenClient(roomMapping.server, testUser.nick, "connect", (client, cb) => {
connectCount += 1;
// add an artificially long delay to make sure it isn't connecting
// twice
Expand All @@ -243,7 +242,7 @@ describe("IRC connections", function() {
}, 500);
});

let promises = [];
const promises = [];

promises.push(env.mockAppService._trigger("type:m.room.message", {
content: {
Expand All @@ -264,11 +263,8 @@ describe("IRC connections", function() {
room_id: roomMapping.roomId,
type: "m.room.message"
}));

Promise.all(promises).done(function() {
expect(connectCount).toBe(1);
done();
});
await Promise.all(promises);
expect(connectCount).toBe(1);
});

// BOTS-41
Expand Down Expand Up @@ -563,4 +559,36 @@ describe("IRC connections", function() {
}
throw Error("Should have thrown");
});

it("should not bridge matrix users who are deactivated", async function() {
const deactivatedUserId = "@deactivated:hs";
const nick = "M-deactivated";

const store = env.ircBridge.getStore();
await store.deactivateUser(deactivatedUserId);
expect(await store.isUserDeactivated(deactivatedUserId)).toBe(true);

env.ircMock._whenClient(roomMapping.server, nick, "connect",
function() {
throw Error("Client should not be saying anything")
});
try {
await env.mockAppService._trigger("type:m.room.message", {
content: {
body: "Text that should never be sent",
msgtype: "m.text"
},
user_id: deactivatedUserId,
room_id: roomMapping.roomId,
type: "m.room.message"
});
}
catch (ex) {
expect(ex.message).toBe(
"Cannot create bridged client - user has been deactivated"
);
return;
}
throw Error("Should have thrown");
});
});
7 changes: 5 additions & 2 deletions src/DebugApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export class DebugApi {
promise = Promise.reject(new Error("Need user_id and reason"));
}
else {
promise = this.killUser(body.user_id, body.reason);
promise = this.killUser(body.user_id, body.reason, body.deactivate);
}
}
catch (err) {
Expand Down Expand Up @@ -232,8 +232,11 @@ export class DebugApi {
return inspect(client, { colors:true, depth:7 });
}

private killUser(userId: string, reason: string) {
private async killUser(userId: string, reason: string, deactivate: boolean) {
const req = new BridgeRequest(this.ircBridge.getAppServiceBridge().getRequestFactory().newRequest());
if (deactivate) {
await this.ircBridge.getStore().deactivateUser(userId);
}
const clients = this.pool.getBridgedClientsForUserId(userId);
return this.ircBridge.matrixHandler.quitUser(req, userId, clients, null, reason);
}
Expand Down
4 changes: 4 additions & 0 deletions src/datastore/DataStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,5 +172,9 @@ export interface DataStore {

setRoomVisibility(roomId: string, vis: "public"|"private"): Promise<void>;

isUserDeactivated(userId: string): Promise<boolean>;

deactivateUser(userId: string): Promise<void>;

destroy(): Promise<void>;
}
14 changes: 14 additions & 0 deletions src/datastore/NedbDataStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,20 @@ export class NeDBDataStore implements DataStore {
await this.roomStore.setMatrixRoom(room);
}

public async deactivateUser(userId: string) {
let user = await this.userStore.getMatrixUser(userId);
if (!user) {
user = new MatrixUser(userId);
}
user.set("deactivated", true);
await this.userStore.setMatrixUser(user);
}

public async isUserDeactivated(userId: string) {
const user = await this.userStore.getMatrixUser(userId);
return user?.get("deactivated") === true;
}

public async roomUpgradeOnRoomMigrated(oldRoomId: string, newRoomId: string) {
const ircRooms = await this.getIrcChannelsForRoomId(oldRoomId);
for (const ircRoom of ircRooms) {
Expand Down
11 changes: 10 additions & 1 deletion src/datastore/postgres/PgDataStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const log = getLogger("PgDatastore");
export class PgDataStore implements DataStore {
private serverMappings: {[domain: string]: IrcServer} = {};

public static readonly LATEST_SCHEMA = 3;
public static readonly LATEST_SCHEMA = 4;
private pgPool: Pool;
private hasEnded = false;
private cryptoStore?: StringCrypto;
Expand Down Expand Up @@ -560,6 +560,15 @@ export class PgDataStore implements DataStore {
log.info(`setRoomVisibility ${roomId} => ${visibility}`);
}

public async isUserDeactivated(userId: string): Promise<boolean> {
const res = await this.pgPool.query(`SELECT user_id FROM deactivated_users WHERE user_id = $1`, [userId]);
return res.rowCount > 0;
}

public async deactivateUser(userId: string) {
this.pgPool.query("INSERT INTO deactivated_users VALUES ($1, $2)", [userId, Date.now()]);
}

public async ensureSchema() {
log.info("Starting postgres database engine");
let currentVersion = await this.getSchemaVersion();
Expand Down
11 changes: 11 additions & 0 deletions src/datastore/postgres/schema/v4.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { PoolClient } from "pg";

export async function runSchema(connection: PoolClient) {
// Create schema
await connection.query(`
CREATE TABLE deactivated_users (
user_id TEXT UNIQUE NOT NULL,
ts BIGINT NOT NULL
);
`);
}
4 changes: 4 additions & 0 deletions src/irc/ClientPool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@ export class ClientPool {
return bridgedClient;
}

if (await this.ircBridge.getStore().isUserDeactivated(userId)) {
throw Error("Cannot create bridged client - user has been deactivated");
}

const mxUser = new MatrixUser(userId);
if (displayName) {
mxUser.setDisplayName(displayName);
Expand Down