mirror of
https://github.com/matrix-org/matrix-hookshot.git
synced 2025-03-10 13:17:08 +00:00
Add Admin command from listing and disconnecting connections (#367)
* Add Admin command from listing and disconnecting connections (Only lists Github for now). * Require connectionManager permissions to manipulate connections Gives connection management its own config section and switches AdminRoom categories to be enums. * Fail more descriptively if connectionManager is not up in time for adminRoom * Fix Github API URLs (#366) * Fix Github API URLs * Add changelog entry * Fixes * Tidyup Co-authored-by: Will Hunt <will@half-shot.uk> Co-authored-by: Tadeusz Sośnierz <tadeusz@sosnierz.com> * Fix URLs *again* * Block private repos from being publically bridged * Ensure check looks at service type * Finish up deleting connections impl Co-authored-by: Tadeusz Sośnierz <tadeusz@sosnierz.com> Co-authored-by: Will Hunt <will@half-shot.uk>
This commit is contained in:
parent
ee2fd5bbcc
commit
86b9a83b97
1
changelog.d/367.feature
Normal file
1
changelog.d/367.feature
Normal file
@ -0,0 +1 @@
|
||||
Add ability for bridge admins to remove GitHub connections using the admin room.
|
@ -113,7 +113,7 @@ The `level` can be:
|
||||
- `login` All the above, and can also log in to the bridge.
|
||||
- `notifications` All the above, and can also bridge their notifications.
|
||||
- `manageConnections` All the above, and can create and delete connections (either via the provisioner, setup commands, or state events).
|
||||
- `admin` All permissions. Currently, there are no admin features so this exists as a placeholder.
|
||||
- `admin` All permissions. This allows you to perform administrative tasks like deleting connections from all rooms.
|
||||
|
||||
When permissions are checked, if a user matches any of the permission set and one
|
||||
of those grants the right level for a service, they are allowed access. If none of the
|
||||
|
101
src/AdminRoom.ts
101
src/AdminRoom.ts
@ -5,6 +5,8 @@ import { botCommand, compileBotCommands, handleCommand, BotCommands, HelpFunctio
|
||||
import { BridgeConfig, BridgePermissionLevel } from "./Config/Config";
|
||||
import { BridgeRoomState, BridgeRoomStateGitHub } from "./Widgets/BridgeWidgetInterface";
|
||||
import { Endpoints } from "@octokit/types";
|
||||
import { GitHubDiscussionSpace, GitHubIssueConnection, GitHubRepoConnection } from "./Connections";
|
||||
import { ConnectionManager } from "./ConnectionManager";
|
||||
import { FormatUtil } from "./FormatUtil";
|
||||
import { GetUserResponse } from "./Gitlab/Types";
|
||||
import { GitHubBotCommands } from "./Github/AdminCommands";
|
||||
@ -20,6 +22,12 @@ import markdown from "markdown-it";
|
||||
type ProjectsListForRepoResponseData = Endpoints["GET /repos/{owner}/{repo}/projects"]["response"];
|
||||
type ProjectsListForUserResponseData = Endpoints["GET /users/{username}/projects"]["response"];
|
||||
|
||||
enum Category {
|
||||
ConnectionManagement = "Connection Management",
|
||||
Github = "Github",
|
||||
Gitlab = "Gitlab",
|
||||
Jira = "Jira",
|
||||
}
|
||||
|
||||
const md = new markdown();
|
||||
const log = new LogWrapper('AdminRoom');
|
||||
@ -44,7 +52,9 @@ export class AdminRoom extends AdminRoomCommandHandler {
|
||||
notifContent: NotificationFilterStateContent,
|
||||
botIntent: Intent,
|
||||
tokenStore: UserTokenStore,
|
||||
config: BridgeConfig) {
|
||||
config: BridgeConfig,
|
||||
private connectionManager: ConnectionManager,
|
||||
) {
|
||||
super(botIntent, roomId, tokenStore, config, data);
|
||||
this.notifFilter = new NotifFilter(notifContent);
|
||||
}
|
||||
@ -126,17 +136,40 @@ export class AdminRoom extends AdminRoomCommandHandler {
|
||||
});
|
||||
}
|
||||
|
||||
@botCommand("help", "This help text")
|
||||
@botCommand("help", { help: "This help text" })
|
||||
public async helpCommand() {
|
||||
const enabledCategories = [
|
||||
this.config.github ? "github" : "",
|
||||
this.config.gitlab ? "gitlab" : "",
|
||||
this.config.jira ? "jira" : "",
|
||||
this.config.github ? Category.Github : "",
|
||||
this.config.gitlab ? Category.Gitlab : "",
|
||||
this.config.jira ? Category.Jira : "",
|
||||
this.canAdminConnections('github') ? Category.ConnectionManagement : '',
|
||||
];
|
||||
return this.botIntent.sendEvent(this.roomId, AdminRoom.helpMessage(undefined, enabledCategories));
|
||||
}
|
||||
|
||||
@botCommand("github notifications toggle", { help: "Toggle enabling/disabling GitHub notifications in this room", category: "github"})
|
||||
@botCommand("disconnect", { help: "Remove a connection", requiredArgs: ['roomId', 'id'], category: Category.ConnectionManagement })
|
||||
public async disconnect(roomId: string, id: string) {
|
||||
if (!this.canAdminConnections('github')) {
|
||||
await this.sendNotice("Insufficient permissions.");
|
||||
return;
|
||||
}
|
||||
|
||||
// it's stupid that we need roomId -- shouldn't `id` identify the connection?
|
||||
const conn = this.connectionManager.getConnectionById(roomId, id);
|
||||
if (!conn) {
|
||||
await this.sendNotice("Connection not found");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.connectionManager.purgeConnection(conn.roomId, conn.connectionId);
|
||||
await this.sendNotice('Connection removed successfully');
|
||||
} catch (err: unknown) {
|
||||
log.debug(`Failed to purge connection: ${err}`);
|
||||
await this.sendNotice('Connection could not be removed: see debug logs for details');
|
||||
}
|
||||
}
|
||||
|
||||
@botCommand("github notifications toggle", { help: "Toggle enabling/disabling GitHub notifications in this room", category: Category.Github})
|
||||
public async setGitHubNotificationsStateToggle() {
|
||||
const newData = await this.saveAccountData((data) => {
|
||||
return {
|
||||
@ -152,7 +185,7 @@ export class AdminRoom extends AdminRoomCommandHandler {
|
||||
await this.sendNotice(`${newData.github?.notifications?.enabled ? "En" : "Dis"}abled GitHub notifcations`);
|
||||
}
|
||||
|
||||
@botCommand("github notifications filter participating", {help: "Toggle enabling/disabling GitHub notifications in this room", category: "github"})
|
||||
@botCommand("github notifications filter participating", {help: "Toggle enabling/disabling GitHub notifications in this room", category: Category.Github})
|
||||
private async setGitHubNotificationsStateParticipating() {
|
||||
const newData = await this.saveAccountData((data) => {
|
||||
if (!data.github?.notifications?.enabled) {
|
||||
@ -175,7 +208,7 @@ export class AdminRoom extends AdminRoomCommandHandler {
|
||||
return this.sendNotice(`Showing all events.`);
|
||||
}
|
||||
|
||||
@botCommand("github notifications", {help: "Show the current notification settings", category: "github"})
|
||||
@botCommand("github notifications", {help: "Show the current notification settings", category: Category.Github})
|
||||
public async getGitHubNotificationsState() {
|
||||
if (!this.notificationsEnabled("github")) {
|
||||
return this.sendNotice(`Notifications are disabled.`);
|
||||
@ -183,8 +216,42 @@ export class AdminRoom extends AdminRoomCommandHandler {
|
||||
return this.sendNotice(`Notifications are enabled, ${this.notificationsParticipating("github") ? "Showing only events you are particiapting in." : "Showing all events."}`);
|
||||
}
|
||||
|
||||
@botCommand("github list-connections", {help: "List currently bridged Github rooms", category: Category.ConnectionManagement})
|
||||
public async listGithubConnections() {
|
||||
if (!this.config.github) {
|
||||
return this.sendNotice("The bridge is not configured with GitHub support.");
|
||||
}
|
||||
|
||||
@botCommand("github project list-for-user", {help: "List GitHub projects for a user", optionalArgs:['user', 'repo'], category: "github"})
|
||||
if (!this.canAdminConnections('github')) {
|
||||
await this.sendNotice("Insufficient permissions.");
|
||||
return;
|
||||
}
|
||||
|
||||
const connections = {
|
||||
repos: this.connectionManager.getAllConnectionsOfType(GitHubRepoConnection),
|
||||
issues: this.connectionManager.getAllConnectionsOfType(GitHubIssueConnection),
|
||||
discussions: this.connectionManager.getAllConnectionsOfType(GitHubDiscussionSpace),
|
||||
};
|
||||
|
||||
const reposFormatted = connections.repos.map(c => ` - ${c.org}/${c.repo} (ID: \`${c.connectionId}\`, Room: \`${c.roomId}\`)`).join('\n');
|
||||
const issuesFormatted = connections.issues.map(c => ` - ${c.org}/${c.repo}/${c.issueNumber} (Room: \`${c.roomId}\`, ID: \`${c.connectionId}\`)`).join('\n');
|
||||
const discussionsFormatted = connections.discussions.map(c => ` - ${c.owner}/${c.repo} (Room: \`${c.roomId}\`, ID: \`${c.connectionId}\`)`).join('\n');
|
||||
|
||||
const content = [
|
||||
connections.repos.length > 0 ? `Repositories:\n${reposFormatted}` : '',
|
||||
connections.issues.length > 0 ? `Issues:\n${issuesFormatted}` : '',
|
||||
connections.discussions.length > 0 ? `Discussions:\n${discussionsFormatted}` : '',
|
||||
].join('\n\n') || 'No Github bridges';
|
||||
|
||||
return this.botIntent.sendEvent(this.roomId,{
|
||||
msgtype: "m.notice",
|
||||
body: content,
|
||||
formatted_body: md.render(content),
|
||||
format: "org.matrix.custom.html"
|
||||
});
|
||||
}
|
||||
|
||||
@botCommand("github project list-for-user", {help: "List GitHub projects for a user", optionalArgs:['user', 'repo'], category: Category.Github})
|
||||
private async listGitHubProjectsForUser(username?: string, repo?: string) {
|
||||
if (!this.config.github) {
|
||||
return this.sendNotice("The bridge is not configured with GitHub support.");
|
||||
@ -225,7 +292,7 @@ export class AdminRoom extends AdminRoomCommandHandler {
|
||||
});
|
||||
}
|
||||
|
||||
@botCommand("github project list-for-org", {help: "List GitHub projects for an org", requiredArgs: ['org'], optionalArgs: ['repo'], category: "github"})
|
||||
@botCommand("github project list-for-org", {help: "List GitHub projects for an org", requiredArgs: ['org'], optionalArgs: ['repo'], category: Category.Github})
|
||||
private async listGitHubProjectsForOrg(org: string, repo?: string) {
|
||||
if (!this.config.github) {
|
||||
return this.sendNotice("The bridge is not configured with GitHub support.");
|
||||
@ -263,7 +330,7 @@ export class AdminRoom extends AdminRoomCommandHandler {
|
||||
});
|
||||
}
|
||||
|
||||
@botCommand("github project open", {help: "Open a GitHub project as a room", requiredArgs: ['projectId'], category: "github"})
|
||||
@botCommand("github project open", {help: "Open a GitHub project as a room", requiredArgs: ['projectId'], category: Category.Github})
|
||||
private async openProject(projectId: string) {
|
||||
if (!this.config.github) {
|
||||
return this.sendNotice("The bridge is not configured with GitHub support.");
|
||||
@ -287,7 +354,7 @@ export class AdminRoom extends AdminRoomCommandHandler {
|
||||
}
|
||||
}
|
||||
|
||||
@botCommand("github discussion open", {help: "Open a discussion room", requiredArgs: ['owner', 'repo', 'number'], category: "github"})
|
||||
@botCommand("github discussion open", {help: "Open a discussion room", requiredArgs: ['owner', 'repo', 'number'], category: Category.Github})
|
||||
private async listDiscussions(owner: string, repo: string, numberStr: string) {
|
||||
const number = parseInt(numberStr);
|
||||
if (!this.config.github) {
|
||||
@ -313,7 +380,7 @@ export class AdminRoom extends AdminRoomCommandHandler {
|
||||
|
||||
/* GitLab commands */
|
||||
|
||||
@botCommand("gitlab open issue", {help: "Open or join a issue room for GitLab", requiredArgs: ['url'], category: "gitlab"})
|
||||
@botCommand("gitlab open issue", {help: "Open or join a issue room for GitLab", requiredArgs: ['url'], category: Category.Gitlab})
|
||||
private async gitLabOpenIssue(url: string) {
|
||||
if (!this.config.gitlab) {
|
||||
return this.sendNotice("The bridge is not configured with GitLab support.");
|
||||
@ -338,7 +405,7 @@ export class AdminRoom extends AdminRoomCommandHandler {
|
||||
return this.emit('open.gitlab-issue', getIssueOpts, issue, instanceName, instance);
|
||||
}
|
||||
|
||||
@botCommand("gitlab personaltoken", {help: "Set your personal access token for GitLab", requiredArgs: ['instanceName', 'accessToken'], category: "gitlab"})
|
||||
@botCommand("gitlab personaltoken", {help: "Set your personal access token for GitLab", requiredArgs: ['instanceName', 'accessToken'], category: Category.Gitlab})
|
||||
public async setGitLabPersonalAccessToken(instanceName: string, accessToken: string) {
|
||||
let me: GetUserResponse;
|
||||
if (!this.config.gitlab) {
|
||||
@ -359,7 +426,7 @@ export class AdminRoom extends AdminRoomCommandHandler {
|
||||
return this.tokenStore.storeUserToken("gitlab", this.userId, accessToken, instance.url);
|
||||
}
|
||||
|
||||
@botCommand("gitlab hastoken", {help: "Check if you have a token stored for GitLab", requiredArgs: ["instanceName"], category: "gitlab"})
|
||||
@botCommand("gitlab hastoken", {help: "Check if you have a token stored for GitLab", requiredArgs: ["instanceName"], category: Category.Gitlab})
|
||||
public async gitlabHasPersonalToken(instanceName: string) {
|
||||
if (!this.config.gitlab) {
|
||||
return this.sendNotice("The bridge is not configured with GitLab support.");
|
||||
@ -423,6 +490,10 @@ export class AdminRoom extends AdminRoomCommandHandler {
|
||||
return this.botIntent.underlyingClient.sendStateEvent(this.roomId, NotifFilter.StateType, "", this.notifFilter.getStateContent());
|
||||
}
|
||||
|
||||
private canAdminConnections(service: string): boolean {
|
||||
return this.config.checkPermission(this.userId, service, BridgePermissionLevel.admin);
|
||||
}
|
||||
|
||||
private async saveAccountData(updateFn: (record: AdminAccountData) => AdminAccountData) {
|
||||
let oldData: AdminAccountData|null = await this.botIntent.underlyingClient.getSafeRoomAccountData(BRIDGE_ROOM_TYPE, this.roomId, null);
|
||||
if (!oldData) {
|
||||
|
@ -1141,9 +1141,14 @@ export class Bridge {
|
||||
}
|
||||
|
||||
private async setUpAdminRoom(roomId: string, accountData: AdminAccountData, notifContent: NotificationFilterStateContent) {
|
||||
if (!this.connectionManager) {
|
||||
throw Error('setUpAdminRoom() called before connectionManager was ready');
|
||||
}
|
||||
|
||||
const adminRoom = new AdminRoom(
|
||||
roomId, accountData, notifContent, this.as.botIntent, this.tokenStore, this.config,
|
||||
roomId, accountData, notifContent, this.as.botIntent, this.tokenStore, this.config, this.connectionManager,
|
||||
);
|
||||
|
||||
adminRoom.on("settings.changed", this.onAdminRoomSettingsChanged.bind(this));
|
||||
adminRoom.on("open.project", async (project: ProjectsGetResponseData) => {
|
||||
const [connection] = this.connectionManager?.getForGitHubProject(project.id) || [];
|
||||
|
@ -155,4 +155,16 @@ export class GitHubDiscussionConnection extends BaseConnection implements IConne
|
||||
'uk.half-shot.matrix-hookshot.github.discussion.comment_id': data.comment.id,
|
||||
}, 'm.room.message', intent.userId);
|
||||
}
|
||||
|
||||
public async onRemove() {
|
||||
log.info(`Removing ${this.toString()} for ${this.roomId}`);
|
||||
// Do a sanity check that the event exists.
|
||||
try {
|
||||
await this.as.botClient.getRoomStateEvent(this.roomId, GitHubDiscussionConnection.CanonicalEventType, this.stateKey);
|
||||
await this.as.botClient.sendStateEvent(this.roomId, GitHubDiscussionConnection.CanonicalEventType, this.stateKey, { disabled: true });
|
||||
} catch (ex) {
|
||||
await this.as.botClient.getRoomStateEvent(this.roomId, GitHubDiscussionConnection.LegacyCanonicalEventType, this.stateKey);
|
||||
await this.as.botClient.sendStateEvent(this.roomId, GitHubDiscussionConnection.LegacyCanonicalEventType, this.stateKey, { disabled: true });
|
||||
}
|
||||
}
|
||||
}
|
@ -60,6 +60,9 @@ export class GitHubDiscussionSpace extends BaseConnection implements IConnection
|
||||
if (!repoRes.owner) {
|
||||
throw Error('Repo has no owner!');
|
||||
}
|
||||
if (repoRes.private) {
|
||||
throw Error('Refusing to bridge private repo');
|
||||
}
|
||||
} catch (ex) {
|
||||
log.error("Failed to get repo:", ex);
|
||||
throw Error("Could not find repo");
|
||||
@ -164,4 +167,17 @@ export class GitHubDiscussionSpace extends BaseConnection implements IConnection
|
||||
log.info(`Adding connection to ${this.toString()}`);
|
||||
await this.space.addChildRoom(discussion.roomId);
|
||||
}
|
||||
|
||||
public async onRemove() {
|
||||
log.info(`Removing ${this.toString()} for ${this.roomId}`);
|
||||
// Do a sanity check that the event exists.
|
||||
try {
|
||||
|
||||
await this.space.client.getRoomStateEvent(this.roomId, GitHubDiscussionSpace.CanonicalEventType, this.stateKey);
|
||||
await this.space.client.sendStateEvent(this.roomId, GitHubDiscussionSpace.CanonicalEventType, this.stateKey, { disabled: true });
|
||||
} catch (ex) {
|
||||
await this.space.client.getRoomStateEvent(this.roomId, GitHubDiscussionSpace.LegacyCanonicalEventType, this.stateKey);
|
||||
await this.space.client.sendStateEvent(this.roomId, GitHubDiscussionSpace.LegacyCanonicalEventType, this.stateKey, { disabled: true });
|
||||
}
|
||||
}
|
||||
}
|
@ -93,6 +93,9 @@ export class GitHubIssueConnection extends BaseConnection implements IConnection
|
||||
owner,
|
||||
repo: repoName,
|
||||
})).data;
|
||||
if (repo.private) {
|
||||
throw Error('Refusing to bridge private repo');
|
||||
}
|
||||
} catch (ex) {
|
||||
log.error("Failed to get issue:", ex);
|
||||
throw Error("Could not find issue");
|
||||
@ -342,6 +345,18 @@ export class GitHubIssueConnection extends BaseConnection implements IConnection
|
||||
}
|
||||
}
|
||||
|
||||
public async onRemove() {
|
||||
log.info(`Removing ${this.toString()} for ${this.roomId}`);
|
||||
// Do a sanity check that the event exists.
|
||||
try {
|
||||
await this.as.botClient.getRoomStateEvent(this.roomId, GitHubIssueConnection.CanonicalEventType, this.stateKey);
|
||||
await this.as.botClient.sendStateEvent(this.roomId, GitHubIssueConnection.CanonicalEventType, this.stateKey, { disabled: true });
|
||||
} catch (ex) {
|
||||
await this.as.botClient.getRoomStateEvent(this.roomId, GitHubIssueConnection.LegacyCanonicalEventType, this.stateKey);
|
||||
await this.as.botClient.sendStateEvent(this.roomId, GitHubIssueConnection.LegacyCanonicalEventType, this.stateKey, { disabled: true });
|
||||
}
|
||||
}
|
||||
|
||||
public onIssueStateChange() {
|
||||
return this.syncIssueState();
|
||||
}
|
||||
|
@ -302,6 +302,9 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti
|
||||
owner,
|
||||
repo,
|
||||
})).data;
|
||||
if (repoRes.private) {
|
||||
throw Error('Refusing to bridge private repo');
|
||||
}
|
||||
} catch (ex) {
|
||||
log.error("Failed to get repo:", ex);
|
||||
throw Error("Could not find repo");
|
||||
|
@ -2,6 +2,7 @@
|
||||
import { expect } from "chai";
|
||||
import { AdminRoom } from "../src/AdminRoom";
|
||||
import { DefaultConfig } from "../src/Config/Defaults";
|
||||
import { ConnectionManager } from "../src/ConnectionManager";
|
||||
import { NotifFilter } from "../src/NotificationFilters";
|
||||
import { UserTokenStore } from "../src/UserTokenStore";
|
||||
import { IntentMock } from "./utils/IntentMock";
|
||||
@ -14,7 +15,7 @@ function createAdminRoom(data: any = {admin_user: "@admin:bar"}): [AdminRoom, In
|
||||
data.admin_user = "@admin:bar";
|
||||
}
|
||||
const tokenStore = new UserTokenStore("notapath", intent, DefaultConfig);
|
||||
return [new AdminRoom(ROOM_ID, data, NotifFilter.getDefaultContent(), intent, tokenStore, DefaultConfig), intent];
|
||||
return [new AdminRoom(ROOM_ID, data, NotifFilter.getDefaultContent(), intent, tokenStore, DefaultConfig, {} as ConnectionManager), intent];
|
||||
}
|
||||
|
||||
describe("AdminRoom", () => {
|
||||
@ -24,7 +25,7 @@ describe("AdminRoom", () => {
|
||||
expect(intent.sentEvents).to.have.lengthOf(1);
|
||||
expect(intent.sentEvents[0]).to.deep.equal({
|
||||
roomId: ROOM_ID,
|
||||
content: AdminRoom.helpMessage(undefined, ["github", "gitlab", "jira"]),
|
||||
content: AdminRoom.helpMessage(undefined, ["Github", "Gitlab", "Jira"]),
|
||||
});
|
||||
});
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user