From 009b5107792283b2bf5a72e315e9d50180ebcf6c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 25 Mar 2018 13:13:50 -0600 Subject: [PATCH] Special case each integration, forcing simple bots to go through an NebProxy This is so the different needs of each can be accounted for. For example, widgets are fairly unrestricted, so nothing really needs to prevent them. Bots on the other hand require an upstream token otherwise we can't get the bot IDs from Modular. --- src/api/admin/AdminIntegrationsService.ts | 11 ++-- src/api/admin/AdminNebService.ts | 6 +- .../dimension/DimensionIntegrationsService.ts | 46 ++++++++----- src/db/NebStore.ts | 12 ++-- src/models/ModularResponses.ts | 3 + src/neb/NebClient.ts | 4 +- src/neb/NebProxy.ts | 66 +++++++++++++++++++ web/app/admin/widgets/widgets.component.ts | 2 +- .../admin/admin-integrations-api.service.ts | 4 +- 9 files changed, 122 insertions(+), 32 deletions(-) create mode 100644 src/models/ModularResponses.ts create mode 100644 src/neb/NebProxy.ts diff --git a/src/api/admin/AdminIntegrationsService.ts b/src/api/admin/AdminIntegrationsService.ts index 5509734..47beddd 100644 --- a/src/api/admin/AdminIntegrationsService.ts +++ b/src/api/admin/AdminIntegrationsService.ts @@ -1,9 +1,10 @@ import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest"; import { ApiError } from "../ApiError"; import { AdminService } from "./AdminService"; -import { DimensionIntegrationsService, IntegrationsResponse } from "../dimension/DimensionIntegrationsService"; +import { DimensionIntegrationsService } from "../dimension/DimensionIntegrationsService"; import { WidgetStore } from "../../db/WidgetStore"; import { Cache, CACHE_INTEGRATIONS } from "../../MemoryCache"; +import { Integration } from "../../integrations/Integration"; interface SetEnabledRequest { enabled: boolean; @@ -42,9 +43,11 @@ export class AdminIntegrationsService { } @GET - @Path("all") - public async getAllIntegrations(@QueryParam("scalar_token") scalarToken: string): Promise { + @Path(":category/all") + public async getAllIntegrations(@QueryParam("scalar_token") scalarToken: string, @QueryParam("category") category: string): Promise { await AdminService.validateAndGetAdminTokenOwner(scalarToken); - return DimensionIntegrationsService.getIntegrations(null); + + if (category === "widget") return await DimensionIntegrationsService.getWidgets(false); + else throw new ApiError(400, "Unrecongized category"); } } \ No newline at end of file diff --git a/src/api/admin/AdminNebService.ts b/src/api/admin/AdminNebService.ts index 91121c9..06d1911 100644 --- a/src/api/admin/AdminNebService.ts +++ b/src/api/admin/AdminNebService.ts @@ -1,6 +1,6 @@ import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest"; import { AdminService } from "./AdminService"; -import { Cache, CACHE_NEB } from "../../MemoryCache"; +import { Cache, CACHE_INTEGRATIONS, CACHE_NEB } from "../../MemoryCache"; import { NebStore } from "../../db/NebStore"; import { NebConfig } from "../../models/neb"; import { LogService } from "matrix-js-snippets"; @@ -51,6 +51,7 @@ export class AdminNebService { await AdminService.validateAndGetAdminTokenOwner(scalarToken); await NebStore.setIntegrationEnabled(nebId, integrationType, request.enabled); Cache.for(CACHE_NEB).clear(); + Cache.for(CACHE_INTEGRATIONS).clear(); return {}; // 200 OK } @@ -60,6 +61,7 @@ export class AdminNebService { await AdminService.validateAndGetAdminTokenOwner(scalarToken); await NebStore.setIntegrationConfig(nebId, integrationType, newConfig); Cache.for(CACHE_NEB).clear(); + Cache.for(CACHE_INTEGRATIONS).clear(); return {}; // 200 OK } @@ -78,6 +80,7 @@ export class AdminNebService { try { const neb = await NebStore.createForUpstream(request.upstreamId); Cache.for(CACHE_NEB).clear(); + Cache.for(CACHE_INTEGRATIONS).clear(); return neb; } catch (err) { LogService.error("DimensionNebAdminService", err); @@ -93,6 +96,7 @@ export class AdminNebService { try { const neb = await NebStore.createForAppservice(request.appserviceId, request.adminUrl); Cache.for(CACHE_NEB).clear(); + Cache.for(CACHE_INTEGRATIONS).clear(); return neb; } catch (err) { LogService.error("DimensionNebAdminService", err); diff --git a/src/api/dimension/DimensionIntegrationsService.ts b/src/api/dimension/DimensionIntegrationsService.ts index 6cb11fc..9367c87 100644 --- a/src/api/dimension/DimensionIntegrationsService.ts +++ b/src/api/dimension/DimensionIntegrationsService.ts @@ -5,35 +5,43 @@ import { Cache, CACHE_INTEGRATIONS } from "../../MemoryCache"; import { Integration } from "../../integrations/Integration"; import { ApiError } from "../ApiError"; import { WidgetStore } from "../../db/WidgetStore"; +import { SimpleBot } from "../../integrations/SimpleBot"; import { NebStore } from "../../db/NebStore"; export interface IntegrationsResponse { widgets: Widget[], + bots: SimpleBot[], } @Path("/api/v1/dimension/integrations") export class DimensionIntegrationsService { - public static async getIntegrations(isEnabledCheck?: boolean): Promise { - const cachedIntegrations = Cache.for(CACHE_INTEGRATIONS).get("integrations_" + isEnabledCheck); - if (cachedIntegrations) { - return cachedIntegrations; - } + public static async getWidgets(enabledOnly: boolean): Promise { + const cached = Cache.for(CACHE_INTEGRATIONS).get("widgets"); + if (cached) return cached; - const integrations = { - widgets: await WidgetStore.listAll(isEnabledCheck), - bots: await NebStore.listSimpleBots(), // No enabled check - managed internally - }; + const widgets = await WidgetStore.listAll(enabledOnly ? true : null); + Cache.for(CACHE_INTEGRATIONS).put("widgets", widgets); + return widgets; + } - Cache.for(CACHE_INTEGRATIONS).put("integrations_" + isEnabledCheck, integrations); - return integrations; + public static async getSimpleBots(userId: string): Promise { + const cached = Cache.for(CACHE_INTEGRATIONS).get("simple_bots"); + if (cached) return cached; + + const bots = await NebStore.listSimpleBots(userId); + Cache.for(CACHE_INTEGRATIONS).put("simple_bots", bots); + return bots; } @GET @Path("enabled") public async getEnabledIntegrations(@QueryParam("scalar_token") scalarToken: string): Promise { - await ScalarService.getTokenOwner(scalarToken); - return DimensionIntegrationsService.getIntegrations(true); + const userId = await ScalarService.getTokenOwner(scalarToken); + return { + widgets: await DimensionIntegrationsService.getWidgets(true), + bots: await DimensionIntegrationsService.getSimpleBots(userId), + }; } @GET @@ -48,10 +56,14 @@ export class DimensionIntegrationsService { @Path(":category/:type") public async getIntegration(@PathParam("category") category: string, @PathParam("type") type: string): Promise { // This is intentionally an unauthed endpoint to ensure we can use it in widgets - const integrationsResponse = await DimensionIntegrationsService.getIntegrations(true); - for (const key in integrationsResponse) { - for (const integration of integrationsResponse[key]) { - if (integration.category === category && integration.type === type) return integration; + + let integrations: Integration[] = []; + if (category === "widget") integrations = await DimensionIntegrationsService.getWidgets(true); + else throw new ApiError(400, "Unsupported category"); + + for (const integration of integrations) { + if (integration.category === category && integration.type === type) { + return integration; } } diff --git a/src/db/NebStore.ts b/src/db/NebStore.ts index 745a00e..f782d6f 100644 --- a/src/db/NebStore.ts +++ b/src/db/NebStore.ts @@ -10,6 +10,7 @@ import NebNotificationUser from "./models/NebNotificationUser"; import { AppserviceStore } from "./AppserviceStore"; import config from "../config"; import { SimpleBot } from "../integrations/SimpleBot"; +import { NebProxy } from "../neb/NebProxy"; export interface SupportedIntegration { type: string; @@ -89,7 +90,7 @@ export class NebStore { }, }; - public static async listSimpleBots(): Promise { + public static async listSimpleBots(requestingUserId: string): Promise { const configs = await NebStore.getAllConfigs(); const integrations: { integration: NebIntegration, userId: string }[] = []; const hasTypes: string[] = []; @@ -102,10 +103,11 @@ export class NebStore { if (!metadata || !metadata.simple) continue; if (hasTypes.indexOf(integration.type) !== -1) continue; - // TODO: Handle case of upstream bots - const user = await NebStore.getOrCreateBotUser(config.id, integration.type); - - integrations.push({integration: integration, userId: user.appserviceUserId}); + const proxy = new NebProxy(config, requestingUserId); + integrations.push({ + integration: integration, + userId: await proxy.getBotUserId(integration), + }); hasTypes.push(integration.type); } } diff --git a/src/models/ModularResponses.ts b/src/models/ModularResponses.ts new file mode 100644 index 0000000..01c0720 --- /dev/null +++ b/src/models/ModularResponses.ts @@ -0,0 +1,3 @@ +export interface ModularIntegrationInfoResponse { + bot_user_id: string; +} \ No newline at end of file diff --git a/src/neb/NebClient.ts b/src/neb/NebClient.ts index 4b6755f..18208a0 100644 --- a/src/neb/NebClient.ts +++ b/src/neb/NebClient.ts @@ -15,7 +15,7 @@ export class NebClient { return user.accessToken; } - private getNebType(type: string): string { + public static getNebType(type: string): string { if (type === "rss") return "rssbot"; if (type === "travisci") return "travis-ci"; @@ -37,7 +37,7 @@ export class NebClient { public async setServiceConfig(serviceId: string, userId: string, type: string, serviceConfig: any): Promise { const nebRequest: Service = { ID: serviceId, - Type: this.getNebType(type), + Type: NebClient.getNebType(type), UserID: userId, Config: serviceConfig, }; diff --git a/src/neb/NebProxy.ts b/src/neb/NebProxy.ts new file mode 100644 index 0000000..e5e8de9 --- /dev/null +++ b/src/neb/NebProxy.ts @@ -0,0 +1,66 @@ +import { NebConfig } from "../models/neb"; +import NebIntegration from "../db/models/NebIntegration"; +import { NebStore } from "../db/NebStore"; +import { LogService } from "matrix-js-snippets"; +import * as request from "request"; +import Upstream from "../db/models/Upstream"; +import UserScalarToken from "../db/models/UserScalarToken"; +import { NebClient } from "./NebClient"; +import { ModularIntegrationInfoResponse } from "../models/ModularResponses"; + +export class NebProxy { + constructor(private neb: NebConfig, private requestingUserId: string) { + + } + + public async getBotUserId(integration: NebIntegration) { + if (integration.nebId !== this.neb.id) throw new Error("Integration is not for this NEB proxy"); + + if (this.neb.upstreamId) { + try { + const response = await this.doUpstreamRequest("/integrations/" + NebClient.getNebType(integration.type)); + return response.bot_user_id; + } catch (err) { + LogService.error("NebProxy", err); + return null; + } + } else { + return (await NebStore.getOrCreateBotUser(this.neb.id, integration.type)).appserviceUserId; + } + } + + private async doUpstreamRequest(endpoint: string, body?: any): Promise { + const upstream = await Upstream.findByPrimary(this.neb.upstreamId); + const token = await UserScalarToken.findOne({ + where: { + upstreamId: upstream.id, + isDimensionToken: false, + userId: this.requestingUserId, + }, + }); + + const apiUrl = upstream.apiUrl.endsWith("/") ? upstream.apiUrl.substring(0, upstream.apiUrl.length - 1) : upstream.apiUrl; + const url = apiUrl + (endpoint.startsWith("/") ? endpoint : "/" + endpoint); + + return new Promise((resolve, reject) => { + request({ + method: "POST", + url: url, + qs: {scalar_token: token.scalarToken}, + json: body, + }, (err, res, _body) => { + if (err) { + LogService.error("NebProxy", "Error calling" + url); + LogService.error("NebProxy", err); + reject(err); + } else if (res.statusCode !== 200) { + LogService.error("NebProxy", "Got status code " + res.statusCode + " when calling " + url); + reject(new Error("Request failed")); + } else { + if (typeof(res.body) === "string") res.body = JSON.parse(res.body); + resolve(res.body); + } + }); + }); + } +} \ No newline at end of file diff --git a/web/app/admin/widgets/widgets.component.ts b/web/app/admin/widgets/widgets.component.ts index 24b341c..3a98c00 100644 --- a/web/app/admin/widgets/widgets.component.ts +++ b/web/app/admin/widgets/widgets.component.ts @@ -22,7 +22,7 @@ export class AdminWidgetsComponent { public widgets: FE_Widget[]; constructor(private adminIntegrationsApi: AdminIntegrationsApiService, private toaster: ToasterService, private modal: Modal) { - this.adminIntegrationsApi.getAllIntegrations().then(integrations => { + this.adminIntegrationsApi.getAllWidgets().then(integrations => { this.isLoading = false; this.widgets = integrations.widgets; }); diff --git a/web/app/shared/services/admin/admin-integrations-api.service.ts b/web/app/shared/services/admin/admin-integrations-api.service.ts index b1933a1..633a368 100644 --- a/web/app/shared/services/admin/admin-integrations-api.service.ts +++ b/web/app/shared/services/admin/admin-integrations-api.service.ts @@ -9,8 +9,8 @@ export class AdminIntegrationsApiService extends AuthedApi { super(http); } - public getAllIntegrations(): Promise { - return this.authedGet("/api/v1/dimension/admin/integrations/all").map(r => r.json()).toPromise(); + public getAllWidgets(): Promise { + return this.authedGet("/api/v1/dimension/admin/integrations/widget/all").map(r => r.json()).toPromise(); } public toggleIntegration(category: string, type: string, enabled: boolean): Promise {