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.
This commit is contained in:
Travis Ralston 2018-03-25 13:13:50 -06:00
parent 04bfccc95f
commit 009b510779
9 changed files with 122 additions and 32 deletions

View file

@ -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<IntegrationsResponse> {
@Path(":category/all")
public async getAllIntegrations(@QueryParam("scalar_token") scalarToken: string, @QueryParam("category") category: string): Promise<Integration[]> {
await AdminService.validateAndGetAdminTokenOwner(scalarToken);
return DimensionIntegrationsService.getIntegrations(null);
if (category === "widget") return await DimensionIntegrationsService.getWidgets(false);
else throw new ApiError(400, "Unrecongized category");
}
}

View file

@ -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);

View file

@ -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<IntegrationsResponse> {
const cachedIntegrations = Cache.for(CACHE_INTEGRATIONS).get("integrations_" + isEnabledCheck);
if (cachedIntegrations) {
return cachedIntegrations;
}
public static async getWidgets(enabledOnly: boolean): Promise<Widget[]> {
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<SimpleBot[]> {
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<IntegrationsResponse> {
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<Integration> {
// 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;
}
}

View file

@ -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<SimpleBot[]> {
public static async listSimpleBots(requestingUserId: string): Promise<SimpleBot[]> {
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);
}
}

View file

@ -0,0 +1,3 @@
export interface ModularIntegrationInfoResponse {
bot_user_id: string;
}

View file

@ -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<any> {
const nebRequest: Service = {
ID: serviceId,
Type: this.getNebType(type),
Type: NebClient.getNebType(type),
UserID: userId,
Config: serviceConfig,
};

66
src/neb/NebProxy.ts Normal file
View file

@ -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<ModularIntegrationInfoResponse>("/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<T>(endpoint: string, body?: any): Promise<T> {
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<T>((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);
}
});
});
}
}

View file

@ -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;
});

View file

@ -9,8 +9,8 @@ export class AdminIntegrationsApiService extends AuthedApi {
super(http);
}
public getAllIntegrations(): Promise<FE_IntegrationsResponse> {
return this.authedGet("/api/v1/dimension/admin/integrations/all").map(r => r.json()).toPromise();
public getAllWidgets(): Promise<FE_IntegrationsResponse> {
return this.authedGet("/api/v1/dimension/admin/integrations/widget/all").map(r => r.json()).toPromise();
}
public toggleIntegration(category: string, type: string, enabled: boolean): Promise<any> {