diff --git a/changelog.d/931.removal b/changelog.d/931.removal new file mode 100644 index 000000000..559e21ef9 --- /dev/null +++ b/changelog.d/931.removal @@ -0,0 +1,2 @@ +The legacy provisioning API has been removed (used by services such as Dimension). Developers should seek to update +to use the widget API, which supports more features and is regularly updated. diff --git a/src/Bridge.ts b/src/Bridge.ts index 311bb8279..99e728dfa 100644 --- a/src/Bridge.ts +++ b/src/Bridge.ts @@ -26,7 +26,6 @@ import { UserNotificationsEvent } from "./Notifications/UserNotificationWatcher" import { UserTokenStore } from "./tokens/UserTokenStore"; import * as GitHubWebhookTypes from "@octokit/webhooks-types"; import { Logger } from "matrix-appservice-bridge"; -import { Provisioner } from "./provisioning/provisioner"; import { JiraProvisionerRouter } from "./jira/Router"; import { GitHubProvisionerRouter } from "./github/Router"; import { OAuthRequest } from "./WebhookTypes"; @@ -56,7 +55,6 @@ export class Bridge { private adminRooms: Map = new Map(); private feedReader?: FeedReader; private houndReader?: HoundReader; - private provisioningApi?: Provisioner; private replyProcessor = new RichRepliesPreprocessor(true); private ready = false; @@ -135,34 +133,6 @@ export class Bridge { this.github, ); - if (this.config.provisioning) { - const routers = []; - if (this.config.jira) { - routers.push({ - route: "/v1/jira", - router: new JiraProvisionerRouter(this.config.jira, this.tokenStore).getRouter(), - }); - this.connectionManager.registerProvisioningConnection(JiraProjectConnection); - } - if (this.config.github && this.github) { - routers.push({ - route: "/v1/github", - router: new GitHubProvisionerRouter(this.config.github, this.tokenStore, this.github).getRouter(), - }); - this.connectionManager.registerProvisioningConnection(GitHubRepoConnection); - } - if (this.config.generic) { - this.connectionManager.registerProvisioningConnection(GenericHookConnection); - } - this.provisioningApi = new Provisioner( - this.config.provisioning, - this.connectionManager, - this.botUsersManager, - this.as, - routers, - ); - } - this.as.on("query.room", async (roomAlias, cb) => { try { cb(await this.onQueryRoom(roomAlias)); @@ -775,9 +745,6 @@ export class Bridge { ); } - if (this.provisioningApi) { - this.listener.bindResource('provisioning', this.provisioningApi.expressRouter); - } if (this.config.metrics?.enabled) { this.listener.bindResource('metrics', Metrics.expressRouter); } diff --git a/src/ConnectionManager.ts b/src/ConnectionManager.ts index 5f8508c82..0072469e9 100644 --- a/src/ConnectionManager.ts +++ b/src/ConnectionManager.ts @@ -11,7 +11,7 @@ import { CommentProcessor } from "./CommentProcessor"; import { ConnectionDeclaration, ConnectionDeclarations, GenericHookConnection, GitHubDiscussionConnection, GitHubDiscussionSpace, GitHubIssueConnection, GitHubProjectConnection, GitHubRepoConnection, GitHubUserSpace, GitLabIssueConnection, GitLabRepoConnection, IConnection, IConnectionState, JiraProjectConnection } from "./Connections"; import { FigmaFileConnection, FeedConnection } from "./Connections"; -import { GetConnectionTypeResponseItem } from "./provisioning/api"; +import { GetConnectionTypeResponseItem } from "./Widgets/api"; import { GitLabClient } from "./Gitlab/Client"; import { GithubInstance } from "./github/GithubInstance"; import { IBridgeStorageProvider } from "./Stores/StorageProvider"; diff --git a/src/Connections/FeedConnection.ts b/src/Connections/FeedConnection.ts index 26f427f38..9f6182792 100644 --- a/src/Connections/FeedConnection.ts +++ b/src/Connections/FeedConnection.ts @@ -6,7 +6,7 @@ import { Logger } from "matrix-appservice-bridge"; import { BaseConnection } from "./BaseConnection"; import markdown from "markdown-it"; import { Connection, ProvisionConnectionOpts } from "./IConnection"; -import { GetConnectionsResponseItem } from "../provisioning/api"; +import { GetConnectionsResponseItem } from "../Widgets/api"; import { readFeed, sanitizeHtml } from "../libRs"; import UserAgent from "../UserAgent"; import { retry, retryMatrixErrorFilter } from "../PromiseUtil"; diff --git a/src/Connections/GenericHook.ts b/src/Connections/GenericHook.ts index 5aa87de04..36537687e 100644 --- a/src/Connections/GenericHook.ts +++ b/src/Connections/GenericHook.ts @@ -7,7 +7,7 @@ import { MatrixEvent } from "../MatrixEvent"; import { Appservice, Intent, StateEvent } from "matrix-bot-sdk"; import { ApiError, ErrCode } from "../api"; import { BaseConnection } from "./BaseConnection"; -import { GetConnectionsResponseItem } from "../provisioning/api"; +import { GetConnectionsResponseItem } from "../Widgets/api"; import { BridgeConfigGenericWebhooks } from "../config/Config"; import { ensureUserIsInRoom } from "../IntentUtils"; import { randomUUID } from 'node:crypto'; diff --git a/src/Connections/GithubRepo.ts b/src/Connections/GithubRepo.ts index dc4e452d8..622e1cfc0 100644 --- a/src/Connections/GithubRepo.ts +++ b/src/Connections/GithubRepo.ts @@ -4,7 +4,7 @@ import { CommentProcessor } from "../CommentProcessor"; import { FormatUtil, LooseMinimalGitHubRepo } from "../FormatUtil"; import { Octokit } from "@octokit/rest"; import { Connection, IConnection, IConnectionState, InstantiateConnectionOpts, ProvisionConnectionOpts } from "./IConnection"; -import { GetConnectionsResponseItem } from "../provisioning/api"; +import { GetConnectionsResponseItem } from "../Widgets/api"; import { IssuesOpenedEvent, IssuesReopenedEvent, IssuesEditedEvent, PullRequestOpenedEvent, IssuesClosedEvent, PullRequestClosedEvent, PullRequestReadyForReviewEvent, PullRequestReviewSubmittedEvent, ReleasePublishedEvent, ReleaseCreatedEvent, IssuesLabeledEvent, IssuesUnlabeledEvent, WorkflowRunCompletedEvent, IssueCommentCreatedEvent, PushEvent diff --git a/src/Connections/GitlabRepo.ts b/src/Connections/GitlabRepo.ts index 29e7f3e14..1cf4447ef 100644 --- a/src/Connections/GitlabRepo.ts +++ b/src/Connections/GitlabRepo.ts @@ -8,7 +8,7 @@ import { BridgeConfigGitLab, GitLabInstance } from "../config/Config"; import { IGitlabMergeRequest, IGitlabProject, IGitlabUser, IGitLabWebhookMREvent, IGitLabWebhookNoteEvent, IGitLabWebhookPushEvent, IGitLabWebhookReleaseEvent, IGitLabWebhookTagPushEvent, IGitLabWebhookWikiPageEvent } from "../Gitlab/WebhookTypes"; import { CommandConnection } from "./CommandConnection"; import { Connection, IConnection, IConnectionState, InstantiateConnectionOpts, ProvisionConnectionOpts } from "./IConnection"; -import { ConnectionWarning, GetConnectionsResponseItem } from "../provisioning/api"; +import { ConnectionWarning, GetConnectionsResponseItem } from "../Widgets/api"; import { ErrCode, ApiError, ValidatorApiError } from "../api" import { AccessLevel, SerializedGitlabDiscussionThreads } from "../Gitlab/Types"; import Ajv, { JSONSchemaType } from "ajv"; diff --git a/src/Connections/IConnection.ts b/src/Connections/IConnection.ts index c01c8878d..78293dcdf 100644 --- a/src/Connections/IConnection.ts +++ b/src/Connections/IConnection.ts @@ -1,6 +1,6 @@ import { MatrixEvent, MatrixMessageContent } from "../MatrixEvent"; import { IssuesOpenedEvent, IssuesEditedEvent } from "@octokit/webhooks-types"; -import { ConnectionWarning, GetConnectionsResponseItem } from "../provisioning/api"; +import { ConnectionWarning, GetConnectionsResponseItem } from "../Widgets/api"; import { Appservice, Intent, IRichReplyMetadata, StateEvent } from "matrix-bot-sdk"; import { BridgeConfig, BridgePermissionLevel } from "../config/Config"; import { UserTokenStore } from "../tokens/UserTokenStore"; diff --git a/src/Connections/JiraProject.ts b/src/Connections/JiraProject.ts index 05aabec1d..d342b644c 100644 --- a/src/Connections/JiraProject.ts +++ b/src/Connections/JiraProject.ts @@ -13,7 +13,7 @@ import { UserTokenStore } from "../tokens/UserTokenStore"; import { CommandError, NotLoggedInError } from "../errors"; import { ApiError, ErrCode } from "../api"; import JiraApi from "jira-client"; -import { GetConnectionsResponseItem } from "../provisioning/api"; +import { GetConnectionsResponseItem } from "../Widgets/api"; import { BridgeConfigJira } from "../config/Config"; import { HookshotJiraApi } from "../jira/Client"; import { GrantChecker } from "../grants/GrantCheck"; diff --git a/src/ListenerService.ts b/src/ListenerService.ts index 62f2168ec..b45cfce46 100644 --- a/src/ListenerService.ts +++ b/src/ListenerService.ts @@ -5,8 +5,8 @@ import { errorMiddleware } from "./api"; // Appserices can't be handled yet because the bot-sdk maintains control of it. // See https://github.com/turt2live/matrix-bot-sdk/issues/191 -export type ResourceName = "webhooks"|"widgets"|"metrics"|"provisioning"; -export const ResourceTypeArray: ResourceName[] = ["webhooks","widgets","metrics","provisioning"]; +export type ResourceName = "webhooks"|"widgets"|"metrics"; +export const ResourceTypeArray: ResourceName[] = ["webhooks","widgets","metrics"]; import { Handlers } from "@sentry/node"; export interface BridgeConfigListener { bindAddress?: string; diff --git a/src/Widgets/BridgeWidgetApi.ts b/src/Widgets/BridgeWidgetApi.ts index 0962d5d00..42cbc0aea 100644 --- a/src/Widgets/BridgeWidgetApi.ts +++ b/src/Widgets/BridgeWidgetApi.ts @@ -8,7 +8,7 @@ import { ProvisioningApi, ProvisioningRequest } from "matrix-appservice-bridge"; import { IBridgeStorageProvider } from "../Stores/StorageProvider"; import { ConnectionManager } from "../ConnectionManager"; import BotUsersManager, {BotUser} from "../Managers/BotUsersManager"; -import { assertUserPermissionsInRoom, GetConnectionsResponseItem } from "../provisioning/api"; +import { assertUserPermissionsInRoom, GetConnectionsResponseItem } from "./api"; import { Appservice, PowerLevelsEvent } from "matrix-bot-sdk"; import { GithubInstance } from '../github/GithubInstance'; import { AllowedTokenTypes, TokenType, UserTokenStore } from '../tokens/UserTokenStore'; diff --git a/src/Widgets/BridgeWidgetInterface.ts b/src/Widgets/BridgeWidgetInterface.ts index aa292c27c..7725950a8 100644 --- a/src/Widgets/BridgeWidgetInterface.ts +++ b/src/Widgets/BridgeWidgetInterface.ts @@ -1,4 +1,4 @@ -import { GetConnectionsResponseItem } from "../provisioning/api"; +import { GetConnectionsResponseItem } from "./api"; export interface BridgeRoomStateGitHub { enabled: boolean; diff --git a/src/provisioning/api.ts b/src/Widgets/api.ts similarity index 100% rename from src/provisioning/api.ts rename to src/Widgets/api.ts diff --git a/src/config/Config.ts b/src/config/Config.ts index 83288f97f..b6aca58b6 100644 --- a/src/config/Config.ts +++ b/src/config/Config.ts @@ -434,12 +434,6 @@ export interface BridgeConfigServiceBot { service: string; } -export interface BridgeConfigProvisioning { - bindAddress?: string; - port?: number; - secret: string; -} - export interface BridgeConfigMetrics { enabled: boolean; bindAddress?: string; @@ -472,7 +466,6 @@ export interface BridgeConfigRoot { metrics?: BridgeConfigMetrics; passFile: string; permissions?: BridgeConfigActorPermission[]; - provisioning?: BridgeConfigProvisioning; queue?: BridgeConfigQueue; sentry?: BridgeConfigSentry; serviceBots?: BridgeConfigServiceBot[]; @@ -524,8 +517,6 @@ export class BridgeConfig { public readonly serviceBots?: BridgeConfigServiceBot[]; @configKey("EXPERIMENTAL support for complimentary widgets", true) public readonly widgets?: BridgeWidgetConfig; - @configKey("Provisioning API for integration managers", true) - public readonly provisioning?: BridgeConfigProvisioning; @configKey("Prometheus metrics support", true) public readonly metrics?: BridgeConfigMetrics; @@ -559,7 +550,6 @@ export class BridgeConfig { this.jira = configData.jira && new BridgeConfigJira(configData.jira); this.generic = configData.generic && new BridgeConfigGenericWebhooks(configData.generic); this.feeds = configData.feeds && new BridgeConfigFeeds(configData.feeds); - this.provisioning = configData.provisioning; this.passFile = configData.passFile ?? "./passkey.pem"; this.bot = configData.bot; this.serviceBots = configData.serviceBots; @@ -627,7 +617,11 @@ export class BridgeConfig { } if ('goNebMigrator' in configData) { - log.warn(`The GoNEB migrator has been removed from this release. You should remove the 'goNebMigrator' from your config.`); + log.warn(`The GoNEB migrator has been removed from Hookshot. You should remove the 'goNebMigrator' from your config.`); + } + + if ('provisioning' in configData) { + log.warn(`The provisioning API has been removed from Hookshot. You should remove the 'provisioning' from your config.`); } // Listeners is a bit special @@ -650,15 +644,6 @@ export class BridgeConfig { }) } - if (this.provisioning?.port) { - this.listeners.push({ - resources: ['provisioning'], - port: this.provisioning.port, - bindAddress: this.provisioning.bindAddress, - }) - log.warn("The `provisioning` configuration still specifies a port/bindAddress. This should be moved to the `listeners` config."); - } - if (this.metrics?.port) { this.listeners.push({ resources: ['metrics'], diff --git a/src/config/Defaults.ts b/src/config/Defaults.ts index 2ea9e1f5b..36c300353 100644 --- a/src/config/Defaults.ts +++ b/src/config/Defaults.ts @@ -126,9 +126,6 @@ export const DefaultConfigRoot: BridgeConfigRoot = { pollTimeoutSeconds: 30, pollConcurrency: 4, }, - provisioning: { - secret: "!secretToken" - }, metrics: { enabled: true, }, @@ -141,7 +138,7 @@ export const DefaultConfigRoot: BridgeConfigRoot = { { port: 9001, bindAddress: '127.0.0.1', - resources: ['metrics', 'provisioning'], + resources: ['metrics'], }, { port: 9002, diff --git a/src/provisioning/api.md b/src/provisioning/api.md deleted file mode 100644 index 5c2a36418..000000000 --- a/src/provisioning/api.md +++ /dev/null @@ -1,295 +0,0 @@ -Provisioning API for matrix-hookshot ------------------------------ - -# Overview - -This document describes how to integrate with `matrix-hookshot`'s provisioning API. - -Requests made to the bridge must be against the API listener defined in the config under `provisioning`, not -the appservice or webhook listeners. - -Requests should always be authenticated with the secret given in the config, inside the `Authorization` header. -Requests being made on behalf of users (most provisioning APIs) should include the userId as a query parameter. - -``` -GET /v1/health?userId=%40Half-Shot%3Ahalf-shot.uk -Authorization: Bearer secret -``` - -APIs are versioned independently so two endpoints on the latest version may not always have the same version. - -# APIs - -## GET /v1/health - -Request the status of the provisioning API. - -### Response - -``` -HTTP 200 -{} -``` - -Any other response should be considered a failed request (e.g. 404, 502 etc). - -## GET /v1/connectiontypes - -Request the connection types enabled for this bridge. - -### Response - -```json5 -{ - "JiraProject": { - "type": "JiraProject", // The name of the connection - "eventType": "uk.half-shot.matrix-hookshot.jira.project", // Corresponds to the state type for the connection - "service": "jira", // or github, webhook. A human-readable service name to make things look pretty - "botUserId": "@hookshot:yourdomain.com", // The bot mxid for the service. Currently, this is the sender_localpart, but may change in the future. - } -} -``` - -## GET /v1/{roomId}/connections - -Request the connections for a given room. The `{roomId}` parameter is the target Matrix room. - -### Response - -```json5 -[{ - "type": "JiraProject", // The name of the connection - "eventType": "uk.half-shot.matrix-hookshot.jira.project", // Corresponds to the state type in the connection - "id": "opaque-unique-id", // An opaque ID used to refer to this connection. Should **NOT** be assumed to be stable. - "service": "jira", // or github, webhook. A human-readable service name to make things look pretty - "botUserId": "@hookshot:yourdomain.com", // The bot mxid for the service. Currently, this is the sender_localpart, but may change in the future. - "config": { - // ... connection specific details, can be configured. - } -}] -``` - - -## GET /v1/{roomId}/connections/{id} - -Request details of a single connection. The `{roomId}` parameter is the target Matrix room. - -### Response - -```json5 -{ - "type": "JiraProject", // The name of the connection - "eventType": "uk.half-shot.matrix-hookshot.jira.project", // Corresponds to the state type in the connection - "id": "opaque-unique-id", // An opaque ID used to refer to this connection. Should **NOT** be assumed to be stable. - "service": "jira", // or github, webhook. A human-readable service name to make things look pretty - "botUserId": "@hookshot:yourdomain.com", // The bot mxid for the service. Currently, this is the sender_localpart, but may change in the future. - "config": { - // ... connection specific details, can be configured. - } -} -``` - -## PUT /v1/{roomId}/connections/{type} - -Create a new connection of a given type. The type refers to the `eventType` (`IConnection.CanonicalEventType`). The `{roomId}` parameter is the target Matrix room. - -The body of the request is the configuration for the connection, which will be the "ConnectionState" interface for each connection. - - -### Request body -```json5 -{ - // ... connection specific details, can be configured. -} -``` -### Response - -```json5 -{ - "type": "JiraProject", // The name of the connection - "eventType": "uk.half-shot.matrix-hookshot.jira.project", // Corresponds to the state type in the connection - "id": "opaque-unique-id", // An opaque ID used to refer to this connection. Should **NOT** be assumed to be stable. - "service": "jira", // or github, webhook. A human-readable service name to make things look pretty - "botUserId": "@hookshot:yourdomain.com", // The bot mxid for the service. Currently, this is the sender_localpart, but may change in the future. - "config": { - // ... connection specific details, can be configured. - } -} -``` - -## PATCH /v1/{roomId}/connections/{id} - -Update a connection's configuration. The `id` refers to the `id` returned in the GET response. - -The body of the request is the configuration for the connection, which will be the "ConnectionState" interface for each connection. - -### Request body -```json5 -{ - // ... connection specific details, can be configured. -} -``` -### Response - -```json5 -{ - - "type": "JiraProject", // The name of the connection - "eventType": "uk.half-shot.matrix-hookshot.jira.project", // Corresponds to the state type in the connection - "id": "opaque-unique-id", // An opaque ID used to refer to this connection. Should **NOT** be assumed to be stable. - "service": "jira", // or github, webhook. A human-readable service name to make things look pretty - "botUserId": "@hookshot:yourdomain.com", // The bot mxid for the service. Currently, this is the sender_localpart, but may change in the future. - "config": { - // ... connection specific details, can be configured. - } -} -``` - -## DELETE /v1/{roomId}/connections/{id} - -Delete a connection. The `id` refers to the `id` returned in the GET response. -### Response - -```json5 -{ - "ok": true -} -``` - -# Service specific APIs - -Some services have specific APIs for additional functionality, like OAuth. - -## GitHub - - -### GET /v1/github/oauth?userId={userId} - - -Request an OAuth url for the given user. Once the user has completed the steps in the OAuth process, -the bridge will be granted access. - -### Response - -```json5 -[{ - "user_url": "https://github.com/login/oauth/authorize?...", - "org_url": "https://github.com/apps/matrix-bridge/installations/new", -}] -``` - -### GET /v1/github/account?userId={userId} - -Request the status of the users account. This will return a `loggedIn` value to determine if the -bridge has a GitHub identity stored for the user, and any organisations they have access to. - -### Response - -```json5 -{ - "loggedIn": true, - "organisations":[{ - "name": "half-shot", - "avatarUrl": "https://avatars.githubusercontent.com/u/8418310?v=4" - }] -} -``` - -### GET /v1/github/orgs/{orgName}/repositories?userId={userId}&page={page}&perPage={perPage} - -Request a list of all repositories a user is a member of in the given org. The `owner` and `name` value of a repository can be given to create a new GitHub connection. - -This request is paginated, and `page` sets the page (defaults to `1`) while `perPage` (defaults to `10`) sets the number of entries per page. - -This request can be retried until the number of entries is less than the value of `perPage`. - -### Response - -```json5 -{ - "loggedIn": true, - "repositories":[{ - "name": "matrix-hookshot", - "owner": "matrix-org", - "fullName": "matrix-org/matrix-hookshot", - "avatarUrl": "https://avatars.githubusercontent.com/u/8418310?v=4", - "description": "A bridge between Matrix and multiple project management services, such as GitHub, GitLab and JIRA. " - }] -} -``` - -### GET /v1/github/repositories?userId={userId}&page={page}&perPage={perPage} - -Request a list of all repositories a user is a member of (including those not belonging to an org). The `owner` and `name` value of a repository can be given to create a new GitHub connection. - -If the user has only allowed a subset of repositories to be bridged, `changeSelectionUrl` will be defined and can be used to expand the search query. - -This request is paginated, and `page` sets the page (defaults to `1`) while `perPage` (defaults to `10`) sets the number of entries per page. - -This request can be retried until the number of entries is less than the value of `perPage`. - -### Response - -```json5 -{ - "loggedIn": true, - "changeSelectionUrl": "https://github.com/settings/installations/12345", - "repositories":[{ - "name": "matrix-hookshot", - "owner": "matrix-org", - "fullName": "matrix-org/matrix-hookshot", - "avatarUrl": "https://avatars.githubusercontent.com/u/8418310?v=4", - "description": "A bridge between Matrix and multiple project management services, such as GitHub, GitLab and JIRA. " - }] -} -``` - -## JIRA - - -### GET /v1/jira/oauth?userId={userId} - - -Request an OAuth url for the given user. Once the user has completed the steps in the OAuth process, -the bridge will be granted access. - -### Response - -```json5 -{ - "url": "https://auth.atlassian.com/authorize?..." -} -``` - -### GET /v1/jira/account?userId={userId} - - -Request the status of the users account. This will return a `loggedIn` value to determine if the -bridge has a JIRA identity stored for the user, and any instances they have access to. Note that if a -user does not have access to an instance, they can authenticate again to gain access to it (if they are able -to consent). -### Response - -```json5 -{ - "loggedIn": true, - "instances":[{ - "name": "acme", - "url": "https://acme.atlassian.net" - }] -} -``` - -### GET /v1/jira/instances/{instanceName}/projects?userId={userId} - -Request a list of all projects a user can see in a given instance. The `url` value of a project can be given to create -a new JIRA connection. -### Response - -```json5 -[{ - "key": "PLAY", - "name": "Jira Playground", - "url": "https://acme.atlassian.net/projects/PLAY" -}] -``` diff --git a/src/provisioning/provisioner.ts b/src/provisioning/provisioner.ts deleted file mode 100644 index 73417123a..000000000 --- a/src/provisioning/provisioner.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { BridgeConfigProvisioning } from "../config/Config"; -import { Router, default as express, NextFunction, Request, Response } from "express"; -import { ConnectionManager } from "../ConnectionManager"; -import { Logger } from "matrix-appservice-bridge"; -import { assertUserPermissionsInRoom, GetConnectionsResponseItem, GetConnectionTypeResponseItem } from "./api"; -import { ApiError, ErrCode } from "../api"; -import { Appservice } from "matrix-bot-sdk"; -import Metrics from "../Metrics"; -import BotUsersManager from "../Managers/BotUsersManager"; - -const log = new Logger("Provisioner"); - -// Simple validator -const ROOM_ID_VALIDATOR = /!.+:.+/; -const USER_ID_VALIDATOR = /@.+:.+/; - - -export class Provisioner { - public readonly expressRouter: Router = Router(); - constructor( - private readonly config: BridgeConfigProvisioning, - private readonly connMan: ConnectionManager, - private readonly botUsersManager: BotUsersManager, - private readonly as: Appservice, - additionalRoutes: {route: string, router: Router}[]) { - if (!this.config.secret) { - throw Error('Missing secret in provisioning config'); - } - this.expressRouter.use("/v1", (req, _res, next) => { - Metrics.provisioningHttpRequest.inc({path: req.path, method: req.method}); - next(); - }); - this.expressRouter.get("/v1/health", this.getHealth); - this.expressRouter.use("/v1", this.checkAuth.bind(this)); - this.expressRouter.use(express.json()); - // Room Routes - this.expressRouter.get( - "/v1/connectiontypes", - this.getConnectionTypes.bind(this), - ); - this.expressRouter.use("/v1", this.checkUserId.bind(this)); - additionalRoutes.forEach(route => { - this.expressRouter.use(route.route, route.router); - }); - this.expressRouter.get<{roomId: string}, unknown, unknown, {userId: string}>( - "/v1/:roomId/connections", - this.checkRoomId.bind(this), - (...args) => this.checkUserPermission("read", ...args), - this.getConnections.bind(this), - ); - this.expressRouter.get<{roomId: string, connectionId: string}, unknown, unknown, {userId: string}>( - "/v1/:roomId/connections/:connectionId", - this.checkRoomId.bind(this), - (...args) => this.checkUserPermission("read", ...args), - this.getConnection.bind(this), - ); - this.expressRouter.put<{roomId: string, type: string}, unknown, Record, {userId: string}>( - "/v1/:roomId/connections/:type", - this.checkRoomId.bind(this), - (...args) => this.checkUserPermission("write", ...args), - this.putConnection.bind(this), - ); - this.expressRouter.patch<{roomId: string, connectionId: string}, unknown, Record, {userId: string}>( - "/v1/:roomId/connections/:connectionId", - this.checkRoomId.bind(this), - (...args) => this.checkUserPermission("write", ...args), - this.patchConnection.bind(this), - ); - this.expressRouter.delete<{roomId: string, connectionId: string}, unknown, unknown, {userId: string}>( - "/v1/:roomId/connections/:connectionId", - this.checkRoomId.bind(this), - (...args) => this.checkUserPermission("write", ...args), - this.deleteConnection.bind(this), - ); - } - - private checkAuth(req: Request, _res: Response, next: NextFunction) { - if (req.headers.authorization === `Bearer ${this.config.secret}`) { - return next(); - } - throw new ApiError("Unauthorized", ErrCode.BadToken); - } - - private checkRoomId(req: Request<{roomId: string}>, _res: Response, next: NextFunction) { - if (!req.params.roomId || !ROOM_ID_VALIDATOR.exec(req.params.roomId)) { - throw new ApiError("Invalid roomId", ErrCode.BadValue); - } - next(); - } - - private checkUserId(req: Request, _res: Response, next: NextFunction) { - if (typeof req.query.userId !== "string" || !USER_ID_VALIDATOR.exec(req.query.userId)) { - throw new ApiError("Invalid userId", ErrCode.BadValue); - } - next(); - } - - private async checkUserPermission(requiredPermission: "read"|"write", req: Request<{roomId: string}, unknown, unknown, {userId: string}>, res: Response, next: NextFunction) { - const userId = req.query.userId; - const roomId = req.params.roomId; - - const botUser = this.botUsersManager.getBotUserInRoom(roomId); - if (!botUser) { - throw new ApiError("Bot is not joined to the room.", ErrCode.NotInRoom); - } - - try { - await assertUserPermissionsInRoom(userId, roomId, requiredPermission, botUser.intent); - next(); - } catch (ex) { - next(ex); - } - } - - private getHealth(_req: Request, res: Response) { - return res.send({}) - } - - private getConnectionTypes(_req: Request, res: Response>) { - return res.send(this.connMan.enabledForProvisioning); - } - - private getConnections(req: Request<{roomId: string}>, res: Response) { - const connections = this.connMan.getAllConnectionsForRoom(req.params.roomId); - const details = connections.map(c => c.getProvisionerDetails?.()).filter(c => !!c) as GetConnectionsResponseItem[]; - return res.send(details); - } - - private getConnection(req: Request<{roomId: string, connectionId: string}>, res: Response) { - const connection = this.connMan.getConnectionById(req.params.roomId, req.params.connectionId); - if (!connection) { - throw new ApiError("Connection does not exist.", ErrCode.NotFound); - } - if (!connection.getProvisionerDetails) { - throw new ApiError("Connection type does not support updates.", ErrCode.UnsupportedOperation); - } - return res.send(connection.getProvisionerDetails()); - } - - private async putConnection(req: Request<{roomId: string, type: string}, unknown, Record, {userId: string}>, res: Response, next: NextFunction) { - const roomId = req.params.roomId; - const userId = req.query.userId; - const eventType = req.params.type; - const connectionType = this.connMan.getConnectionTypeForEventType(eventType); - if (!connectionType) { - throw new ApiError("Unknown event type", ErrCode.NotFound); - } - const serviceType = connectionType.ServiceCategory; - - // Need to figure out which connections are available - try { - if (!req.body || typeof req.body !== "object") { - throw new ApiError("A JSON body must be provided", ErrCode.BadValue); - } - this.connMan.validateCommandPrefix(roomId, req.body); - - const botUser = this.botUsersManager.getBotUserInRoom(roomId, serviceType); - if (!botUser) { - throw new ApiError("Bot is not joined to the room.", ErrCode.NotInRoom); - } - - const result = await this.connMan.provisionConnection(roomId, botUser.intent, userId, connectionType, req.body); - if (!result.connection.getProvisionerDetails) { - throw new Error('Connection supported provisioning but not getProvisionerDetails'); - } - - res.send({ - ...result.connection.getProvisionerDetails(true), - warning: result.warning, - }); - } catch (ex) { - log.error(`Failed to create connection for ${roomId}`, ex); - return next(ex); - } - } - - private async patchConnection(req: Request<{roomId: string, connectionId: string}, unknown, Record, {userId: string}>, res: Response, next: NextFunction) { - try { - const connection = this.connMan.getConnectionById(req.params.roomId, req.params.connectionId); - if (!connection) { - return next(new ApiError("Connection does not exist.", ErrCode.NotFound)); - } - if (!connection.provisionerUpdateConfig || !connection.getProvisionerDetails) { - return next(new ApiError("Connection type does not support updates.", ErrCode.UnsupportedOperation)); - } - this.connMan.validateCommandPrefix(req.params.roomId, req.body, connection); - await connection.provisionerUpdateConfig(req.query.userId, req.body); - res.send(connection.getProvisionerDetails(true)); - } catch (ex) { - next(ex); - } - } - - private async deleteConnection(req: Request<{roomId: string, connectionId: string}>, res: Response<{ok: true}>, next: NextFunction) { - try { - const connection = this.connMan.getConnectionById(req.params.roomId, req.params.connectionId); - if (!connection) { - return next(new ApiError("Connection does not exist.", ErrCode.NotFound)); - } - if (!connection.onRemove) { - return next(new ApiError("Connection does not support removal.", ErrCode.UnsupportedOperation)); - } - await this.connMan.purgeConnection(req.params.roomId, req.params.connectionId); - res.send({ok: true}); - } catch (ex) { - return next(ex); - } - } -}