Use MSCSecurity for all auth

This commit is contained in:
Travis Ralston 2019-07-10 19:30:06 -06:00
parent 7d54948d7e
commit 3b4aa7b0ba
26 changed files with 376 additions and 343 deletions

View File

@ -26,7 +26,7 @@ export class ApiError {
* then converted to JSON as {message: "your_message"}) * then converted to JSON as {message: "your_message"})
* @param {string} errCode The internal error code to describe what went wrong * @param {string} errCode The internal error code to describe what went wrong
*/ */
constructor(statusCode: number, json: string | object, errCode = "D_UNKNOWN") { constructor(statusCode: number, json: string | object, errCode = "M_UNKNOWN") {
// Because typescript is just plain dumb // Because typescript is just plain dumb
// https://stackoverflow.com/questions/31626231/custom-error-class-in-typescript // https://stackoverflow.com/questions/31626231/custom-error-class-in-typescript
Error.apply(this, ["ApiError"]); Error.apply(this, ["ApiError"]);
@ -37,5 +37,8 @@ export class ApiError {
this.errorCode = errCode; this.errorCode = errCode;
this.jsonResponse["dim_errcode"] = this.errorCode; this.jsonResponse["dim_errcode"] = this.errorCode;
if (!this.jsonResponse['error']) this.jsonResponse['error'] = this.jsonResponse['message'];
if (!this.jsonResponse['errcode']) this.jsonResponse['errcode'] = errCode;
} }
} }

View File

@ -71,6 +71,11 @@ export default class Webserver {
next(); next();
}); });
this.app.use((_req, res, next) => { this.app.use((_req, res, next) => {
if (res.headersSent) {
next();
return;
}
res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next(); next();

View File

@ -1,10 +1,10 @@
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest"; import { Context, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
import { AdminService } from "./AdminService";
import AppService from "../../db/models/AppService"; import AppService from "../../db/models/AppService";
import { AppserviceStore } from "../../db/AppserviceStore"; import { AppserviceStore } from "../../db/AppserviceStore";
import { ApiError } from "../ApiError"; import { ApiError } from "../ApiError";
import { MatrixAppserviceClient } from "../../matrix/MatrixAppserviceClient"; import { MatrixAppserviceClient } from "../../matrix/MatrixAppserviceClient";
import { LogService } from "matrix-js-snippets"; import { LogService } from "matrix-js-snippets";
import { ROLE_MSC_ADMIN, ROLE_MSC_USER } from "../security/MSCSecurity";
interface AppserviceResponse { interface AppserviceResponse {
id: string; id: string;
@ -23,18 +23,20 @@ interface AppserviceCreateRequest {
@Path("/api/v1/dimension/admin/appservices") @Path("/api/v1/dimension/admin/appservices")
export class AdminAppserviceService { export class AdminAppserviceService {
@Context
private context: ServiceContext;
@GET @GET
@Path("all") @Path("all")
public async getAppservices(@QueryParam("scalar_token") scalarToken: string): Promise<AppserviceResponse[]> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async getAppservices(): Promise<AppserviceResponse[]> {
return (await AppService.findAll()).map(a => this.mapAppservice(a)); return (await AppService.findAll()).map(a => this.mapAppservice(a));
} }
@GET @GET
@Path(":appserviceId") @Path(":appserviceId")
public async getAppservice(@QueryParam("scalar_token") scalarToken: string, @PathParam("appserviceId") asId: string): Promise<AppserviceResponse> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async getAppservice(@PathParam("appserviceId") asId: string): Promise<AppserviceResponse> {
try { try {
const appservice = await AppserviceStore.getAppservice(asId); const appservice = await AppserviceStore.getAppservice(asId);
return this.mapAppservice(appservice); return this.mapAppservice(appservice);
@ -46,8 +48,9 @@ export class AdminAppserviceService {
@POST @POST
@Path("new") @Path("new")
public async createAppservice(@QueryParam("scalar_token") scalarToken: string, request: AppserviceCreateRequest): Promise<AppserviceResponse> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async createAppservice(request: AppserviceCreateRequest): Promise<AppserviceResponse> {
const userId = this.context.request.user.userId;
// Trim off the @ sign if it's on the prefix // Trim off the @ sign if it's on the prefix
if (request.userPrefix[0] === "@") { if (request.userPrefix[0] === "@") {
@ -66,9 +69,8 @@ export class AdminAppserviceService {
@POST @POST
@Path(":appserviceId/test") @Path(":appserviceId/test")
public async test(@QueryParam("scalar_token") scalarToken: string, @PathParam("appserviceId") asId: string): Promise<any> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async test(@PathParam("appserviceId") asId: string): Promise<any> {
const appservice = await AppserviceStore.getAppservice(asId); const appservice = await AppserviceStore.getAppservice(asId);
const client = new MatrixAppserviceClient(appservice); const client = new MatrixAppserviceClient(appservice);
const userId = await client.whoAmI(); const userId = await client.whoAmI();

View File

@ -1,9 +1,9 @@
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest"; import { Context, DELETE, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
import { AdminService } from "./AdminService";
import { ApiError } from "../ApiError"; import { ApiError } from "../ApiError";
import { LogService } from "matrix-js-snippets"; import { LogService } from "matrix-js-snippets";
import { BotStore } from "../../db/BotStore"; import { BotStore } from "../../db/BotStore";
import { Cache, CACHE_INTEGRATIONS } from "../../MemoryCache"; import { Cache, CACHE_INTEGRATIONS } from "../../MemoryCache";
import { ROLE_MSC_ADMIN, ROLE_MSC_USER } from "../security/MSCSecurity";
interface BotResponse extends BotRequest { interface BotResponse extends BotRequest {
id: number; id: number;
@ -31,18 +31,20 @@ interface BotProfile {
@Path("/api/v1/dimension/admin/bots/simple/custom") @Path("/api/v1/dimension/admin/bots/simple/custom")
export class AdminCustomSimpleBotService { export class AdminCustomSimpleBotService {
@Context
private context: ServiceContext;
@GET @GET
@Path("all") @Path("all")
public async getBots(@QueryParam("scalar_token") scalarToken: string): Promise<BotResponse[]> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async getBots(): Promise<BotResponse[]> {
return BotStore.getCustomBots(); return BotStore.getCustomBots();
} }
@GET @GET
@Path(":botId") @Path(":botId")
public async getBot(@QueryParam("scalar_token") scalarToken: string, @PathParam("botId") botId: number): Promise<BotResponse> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async getBot(@PathParam("botId") botId: number): Promise<BotResponse> {
const bot = await BotStore.getCustomBot(botId); const bot = await BotStore.getCustomBot(botId);
if (!bot) throw new ApiError(404, "Bot not found"); if (!bot) throw new ApiError(404, "Bot not found");
return bot; return bot;
@ -50,9 +52,9 @@ export class AdminCustomSimpleBotService {
@POST @POST
@Path("new") @Path("new")
public async createBot(@QueryParam("scalar_token") scalarToken: string, request: BotRequest): Promise<BotResponse> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async createBot(request: BotRequest): Promise<BotResponse> {
const userId = this.context.request.user.userId;
const bot = await BotStore.createCustom(request); const bot = await BotStore.createCustom(request);
LogService.info("AdminCustomSimpleBotService", userId + " created a simple bot"); LogService.info("AdminCustomSimpleBotService", userId + " created a simple bot");
Cache.for(CACHE_INTEGRATIONS).clear(); Cache.for(CACHE_INTEGRATIONS).clear();
@ -61,9 +63,9 @@ export class AdminCustomSimpleBotService {
@POST @POST
@Path(":botId") @Path(":botId")
public async updateBot(@QueryParam("scalar_token") scalarToken: string, @PathParam("botId") botId: number, request: BotRequest): Promise<BotResponse> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async updateBot(@PathParam("botId") botId: number, request: BotRequest): Promise<BotResponse> {
const userId = this.context.request.user.userId;
const bot = await BotStore.updateCustom(botId, request); const bot = await BotStore.updateCustom(botId, request);
LogService.info("AdminCustomSimpleBotService", userId + " updated a simple bot"); LogService.info("AdminCustomSimpleBotService", userId + " updated a simple bot");
Cache.for(CACHE_INTEGRATIONS).clear(); Cache.for(CACHE_INTEGRATIONS).clear();
@ -72,9 +74,9 @@ export class AdminCustomSimpleBotService {
@DELETE @DELETE
@Path(":botId") @Path(":botId")
public async deleteBot(@QueryParam("scalar_token") scalarToken: string, @PathParam("botId") botId: number): Promise<any> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async deleteBot(@PathParam("botId") botId: number): Promise<any> {
const userId = this.context.request.user.userId;
await BotStore.deleteCustom(botId); await BotStore.deleteCustom(botId);
LogService.info("AdminCustomSimpleBotService", userId + " deleted a simple bot"); LogService.info("AdminCustomSimpleBotService", userId + " deleted a simple bot");
Cache.for(CACHE_INTEGRATIONS).clear(); Cache.for(CACHE_INTEGRATIONS).clear();
@ -83,9 +85,8 @@ export class AdminCustomSimpleBotService {
@GET @GET
@Path("profile/:userId") @Path("profile/:userId")
public async getProfile(@QueryParam("scalar_token") scalarToken: string, @PathParam("userId") userId: string): Promise<BotProfile> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async getProfile(@PathParam("userId") userId: string): Promise<BotProfile> {
const profile = await BotStore.getProfile(userId); const profile = await BotStore.getProfile(userId);
return {name: profile.displayName, avatarUrl: profile.avatarMxc}; return {name: profile.displayName, avatarUrl: profile.avatarMxc};
} }

View File

@ -1,10 +1,10 @@
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest"; import { Context, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
import { AdminService } from "./AdminService";
import { Cache, CACHE_GITTER_BRIDGE, CACHE_INTEGRATIONS } from "../../MemoryCache"; import { Cache, CACHE_GITTER_BRIDGE, CACHE_INTEGRATIONS } from "../../MemoryCache";
import { LogService } from "matrix-js-snippets"; import { LogService } from "matrix-js-snippets";
import { ApiError } from "../ApiError"; import { ApiError } from "../ApiError";
import GitterBridgeRecord from "../../db/models/GitterBridgeRecord"; import GitterBridgeRecord from "../../db/models/GitterBridgeRecord";
import Upstream from "../../db/models/Upstream"; import Upstream from "../../db/models/Upstream";
import { ROLE_MSC_ADMIN, ROLE_MSC_USER } from "../security/MSCSecurity";
interface CreateWithUpstream { interface CreateWithUpstream {
upstreamId: number; upstreamId: number;
@ -27,11 +27,13 @@ interface BridgeResponse {
@Path("/api/v1/dimension/admin/gitter") @Path("/api/v1/dimension/admin/gitter")
export class AdminGitterService { export class AdminGitterService {
@Context
private context: ServiceContext;
@GET @GET
@Path("all") @Path("all")
public async getBridges(@QueryParam("scalar_token") scalarToken: string): Promise<BridgeResponse[]> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async getBridges(): Promise<BridgeResponse[]> {
const bridges = await GitterBridgeRecord.findAll(); const bridges = await GitterBridgeRecord.findAll();
return Promise.all(bridges.map(async b => { return Promise.all(bridges.map(async b => {
return { return {
@ -45,9 +47,8 @@ export class AdminGitterService {
@GET @GET
@Path(":bridgeId") @Path(":bridgeId")
public async getBridge(@QueryParam("scalar_token") scalarToken: string, @PathParam("bridgeId") bridgeId: number): Promise<BridgeResponse> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async getBridge(@PathParam("bridgeId") bridgeId: number): Promise<BridgeResponse> {
const telegramBridge = await GitterBridgeRecord.findByPk(bridgeId); const telegramBridge = await GitterBridgeRecord.findByPk(bridgeId);
if (!telegramBridge) throw new ApiError(404, "Gitter Bridge not found"); if (!telegramBridge) throw new ApiError(404, "Gitter Bridge not found");
@ -61,9 +62,9 @@ export class AdminGitterService {
@POST @POST
@Path(":bridgeId") @Path(":bridgeId")
public async updateBridge(@QueryParam("scalar_token") scalarToken: string, @PathParam("bridgeId") bridgeId: number, request: CreateSelfhosted): Promise<BridgeResponse> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async updateBridge(@PathParam("bridgeId") bridgeId: number, request: CreateSelfhosted): Promise<BridgeResponse> {
const userId = this.context.request.user.userId;
const bridge = await GitterBridgeRecord.findByPk(bridgeId); const bridge = await GitterBridgeRecord.findByPk(bridgeId);
if (!bridge) throw new ApiError(404, "Bridge not found"); if (!bridge) throw new ApiError(404, "Bridge not found");
@ -74,14 +75,14 @@ export class AdminGitterService {
Cache.for(CACHE_GITTER_BRIDGE).clear(); Cache.for(CACHE_GITTER_BRIDGE).clear();
Cache.for(CACHE_INTEGRATIONS).clear(); Cache.for(CACHE_INTEGRATIONS).clear();
return this.getBridge(scalarToken, bridge.id); return this.getBridge(bridge.id);
} }
@POST @POST
@Path("new/upstream") @Path("new/upstream")
public async newConfigForUpstream(@QueryParam("scalar_token") scalarToken: string, request: CreateWithUpstream): Promise<BridgeResponse> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async newConfigForUpstream(request: CreateWithUpstream): Promise<BridgeResponse> {
const userId = this.context.request.user.userId;
const upstream = await Upstream.findByPk(request.upstreamId); const upstream = await Upstream.findByPk(request.upstreamId);
if (!upstream) throw new ApiError(400, "Upstream not found"); if (!upstream) throw new ApiError(400, "Upstream not found");
@ -93,14 +94,14 @@ export class AdminGitterService {
Cache.for(CACHE_GITTER_BRIDGE).clear(); Cache.for(CACHE_GITTER_BRIDGE).clear();
Cache.for(CACHE_INTEGRATIONS).clear(); Cache.for(CACHE_INTEGRATIONS).clear();
return this.getBridge(scalarToken, bridge.id); return this.getBridge(bridge.id);
} }
@POST @POST
@Path("new/selfhosted") @Path("new/selfhosted")
public async newSelfhosted(@QueryParam("scalar_token") scalarToken: string, request: CreateSelfhosted): Promise<BridgeResponse> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async newSelfhosted(request: CreateSelfhosted): Promise<BridgeResponse> {
const userId = this.context.request.user.userId;
const bridge = await GitterBridgeRecord.create({ const bridge = await GitterBridgeRecord.create({
provisionUrl: request.provisionUrl, provisionUrl: request.provisionUrl,
isEnabled: true, isEnabled: true,
@ -109,6 +110,6 @@ export class AdminGitterService {
Cache.for(CACHE_GITTER_BRIDGE).clear(); Cache.for(CACHE_GITTER_BRIDGE).clear();
Cache.for(CACHE_INTEGRATIONS).clear(); Cache.for(CACHE_INTEGRATIONS).clear();
return this.getBridge(scalarToken, bridge.id); return this.getBridge(bridge.id);
} }
} }

View File

@ -1,12 +1,12 @@
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest"; import { Context, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
import { ApiError } from "../ApiError"; import { ApiError } from "../ApiError";
import { AdminService } from "./AdminService";
import { DimensionIntegrationsService } from "../dimension/DimensionIntegrationsService"; import { DimensionIntegrationsService } from "../dimension/DimensionIntegrationsService";
import { WidgetStore } from "../../db/WidgetStore"; import { WidgetStore } from "../../db/WidgetStore";
import { Cache, CACHE_INTEGRATIONS } from "../../MemoryCache"; import { Cache, CACHE_INTEGRATIONS } from "../../MemoryCache";
import { Integration } from "../../integrations/Integration"; import { Integration } from "../../integrations/Integration";
import { LogService } from "matrix-js-snippets"; import { LogService } from "matrix-js-snippets";
import { BridgeStore } from "../../db/BridgeStore"; import { BridgeStore } from "../../db/BridgeStore";
import { ROLE_MSC_ADMIN, ROLE_MSC_USER } from "../security/MSCSecurity";
interface SetEnabledRequest { interface SetEnabledRequest {
enabled: boolean; enabled: boolean;
@ -23,10 +23,14 @@ interface SetOptionsRequest {
@Path("/api/v1/dimension/admin/integrations") @Path("/api/v1/dimension/admin/integrations")
export class AdminIntegrationsService { export class AdminIntegrationsService {
@Context
private context: ServiceContext;
@POST @POST
@Path(":category/:type/options") @Path(":category/:type/options")
public async setOptions(@QueryParam("scalar_token") scalarToken: string, @PathParam("category") category: string, @PathParam("type") type: string, body: SetOptionsRequest): Promise<any> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async setOptions(@PathParam("category") category: string, @PathParam("type") type: string, body: SetOptionsRequest): Promise<any> {
const userId = this.context.request.user.userId;
if (category === "widget") await WidgetStore.setOptions(type, body.options); if (category === "widget") await WidgetStore.setOptions(type, body.options);
else throw new ApiError(400, "Unrecognized category"); else throw new ApiError(400, "Unrecognized category");
@ -39,8 +43,9 @@ export class AdminIntegrationsService {
@POST @POST
@Path(":category/:type/enabled") @Path(":category/:type/enabled")
public async setEnabled(@QueryParam("scalar_token") scalarToken: string, @PathParam("category") category: string, @PathParam("type") type: string, body: SetEnabledRequest): Promise<any> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async setEnabled(@PathParam("category") category: string, @PathParam("type") type: string, body: SetEnabledRequest): Promise<any> {
const userId = this.context.request.user.userId;
if (category === "widget") await WidgetStore.setEnabled(type, body.enabled); if (category === "widget") await WidgetStore.setEnabled(type, body.enabled);
else if (category === "bridge") await BridgeStore.setEnabled(type, body.enabled); else if (category === "bridge") await BridgeStore.setEnabled(type, body.enabled);
@ -53,8 +58,9 @@ export class AdminIntegrationsService {
@GET @GET
@Path(":category/all") @Path(":category/all")
public async getAllIntegrations(@QueryParam("scalar_token") scalarToken: string, @PathParam("category") category: string): Promise<Integration[]> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async getAllIntegrations(@PathParam("category") category: string): Promise<Integration[]> {
const userId = this.context.request.user.userId;
if (category === "widget") return await DimensionIntegrationsService.getWidgets(false); if (category === "widget") return await DimensionIntegrationsService.getWidgets(false);
else if (category === "bridge") return await DimensionIntegrationsService.getBridges(false, userId); else if (category === "bridge") return await DimensionIntegrationsService.getBridges(false, userId);

View File

@ -1,5 +1,4 @@
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest"; import { Context, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
import { AdminService } from "./AdminService";
import { Cache, CACHE_INTEGRATIONS, CACHE_IRC_BRIDGE } from "../../MemoryCache"; import { Cache, CACHE_INTEGRATIONS, CACHE_IRC_BRIDGE } from "../../MemoryCache";
import { LogService } from "matrix-js-snippets"; import { LogService } from "matrix-js-snippets";
import { ApiError } from "../ApiError"; import { ApiError } from "../ApiError";
@ -7,6 +6,7 @@ import IrcBridgeRecord from "../../db/models/IrcBridgeRecord";
import { AvailableNetworks, IrcBridge } from "../../bridges/IrcBridge"; import { AvailableNetworks, IrcBridge } from "../../bridges/IrcBridge";
import Upstream from "../../db/models/Upstream"; import Upstream from "../../db/models/Upstream";
import IrcBridgeNetwork from "../../db/models/IrcBridgeNetwork"; import IrcBridgeNetwork from "../../db/models/IrcBridgeNetwork";
import { ROLE_MSC_ADMIN, ROLE_MSC_USER } from "../security/MSCSecurity";
interface CreateWithUpstream { interface CreateWithUpstream {
upstreamId: number; upstreamId: number;
@ -35,10 +35,14 @@ interface SetEnabledRequest {
@Path("/api/v1/dimension/admin/irc") @Path("/api/v1/dimension/admin/irc")
export class AdminIrcService { export class AdminIrcService {
@Context
private context: ServiceContext;
@GET @GET
@Path("all") @Path("all")
public async getBridges(@QueryParam("scalar_token") scalarToken: string): Promise<BridgeResponse[]> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async getBridges(): Promise<BridgeResponse[]> {
const userId = this.context.request.user.userId;
const bridges = await IrcBridgeRecord.findAll(); const bridges = await IrcBridgeRecord.findAll();
const client = new IrcBridge(userId); const client = new IrcBridge(userId);
@ -64,8 +68,9 @@ export class AdminIrcService {
@GET @GET
@Path(":bridgeId") @Path(":bridgeId")
public async getBridge(@QueryParam("scalar_token") scalarToken: string, @PathParam("bridgeId") bridgeId: number): Promise<BridgeResponse> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async getBridge(@PathParam("bridgeId") bridgeId: number): Promise<BridgeResponse> {
const userId = this.context.request.user.userId;
const ircBridge = await IrcBridgeRecord.findByPk(bridgeId); const ircBridge = await IrcBridgeRecord.findByPk(bridgeId);
if (!ircBridge) throw new ApiError(404, "IRC Bridge not found"); if (!ircBridge) throw new ApiError(404, "IRC Bridge not found");
@ -91,8 +96,9 @@ export class AdminIrcService {
@POST @POST
@Path(":bridgeId/network/:networkId/enabled") @Path(":bridgeId/network/:networkId/enabled")
public async setNetworkEnabled(@QueryParam("scalar_token") scalarToken: string, @PathParam("bridgeId") bridgeId: number, @PathParam("networkId") networkId: string, request: SetEnabledRequest): Promise<any> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async setNetworkEnabled(@PathParam("bridgeId") bridgeId: number, @PathParam("networkId") networkId: string, request: SetEnabledRequest): Promise<any> {
const userId = this.context.request.user.userId;
const ircBridge = await IrcBridgeRecord.findByPk(bridgeId); const ircBridge = await IrcBridgeRecord.findByPk(bridgeId);
if (!ircBridge) throw new ApiError(404, "IRC Bridge not found"); if (!ircBridge) throw new ApiError(404, "IRC Bridge not found");
@ -116,8 +122,9 @@ export class AdminIrcService {
@POST @POST
@Path("new/upstream") @Path("new/upstream")
public async newConfigForUpstream(@QueryParam("scalar_token") scalarToken: string, request: CreateWithUpstream): Promise<BridgeResponse> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async newConfigForUpstream(request: CreateWithUpstream): Promise<BridgeResponse> {
const userId = this.context.request.user.userId;
const upstream = await Upstream.findByPk(request.upstreamId); const upstream = await Upstream.findByPk(request.upstreamId);
if (!upstream) throw new ApiError(400, "Upstream not found"); if (!upstream) throw new ApiError(400, "Upstream not found");
@ -130,13 +137,14 @@ export class AdminIrcService {
Cache.for(CACHE_IRC_BRIDGE).clear(); Cache.for(CACHE_IRC_BRIDGE).clear();
Cache.for(CACHE_INTEGRATIONS).clear(); Cache.for(CACHE_INTEGRATIONS).clear();
return this.getBridge(scalarToken, bridge.id); return this.getBridge(bridge.id);
} }
@POST @POST
@Path("new/selfhosted") @Path("new/selfhosted")
public async newSelfhosted(@QueryParam("scalar_token") scalarToken: string, request: CreateSelfhosted): Promise<BridgeResponse> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async newSelfhosted(request: CreateSelfhosted): Promise<BridgeResponse> {
const userId = this.context.request.user.userId;
const bridge = await IrcBridgeRecord.create({ const bridge = await IrcBridgeRecord.create({
provisionUrl: request.provisionUrl, provisionUrl: request.provisionUrl,
@ -146,6 +154,6 @@ export class AdminIrcService {
Cache.for(CACHE_IRC_BRIDGE).clear(); Cache.for(CACHE_IRC_BRIDGE).clear();
Cache.for(CACHE_INTEGRATIONS).clear(); Cache.for(CACHE_INTEGRATIONS).clear();
return this.getBridge(scalarToken, bridge.id); return this.getBridge(bridge.id);
} }
} }

View File

@ -1,10 +1,10 @@
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest"; import { Context, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
import { AdminService } from "./AdminService";
import { Cache, CACHE_INTEGRATIONS, CACHE_NEB } from "../../MemoryCache"; import { Cache, CACHE_INTEGRATIONS, CACHE_NEB } from "../../MemoryCache";
import { NebStore } from "../../db/NebStore"; import { NebStore } from "../../db/NebStore";
import { NebConfig } from "../../models/neb"; import { NebConfig } from "../../models/neb";
import { LogService } from "matrix-js-snippets"; import { LogService } from "matrix-js-snippets";
import { ApiError } from "../ApiError"; import { ApiError } from "../ApiError";
import { ROLE_MSC_ADMIN, ROLE_MSC_USER } from "../security/MSCSecurity";
interface CreateWithUpstream { interface CreateWithUpstream {
upstreamId: number; upstreamId: number;
@ -26,11 +26,13 @@ interface SetEnabledRequest {
@Path("/api/v1/dimension/admin/neb") @Path("/api/v1/dimension/admin/neb")
export class AdminNebService { export class AdminNebService {
@Context
private context: ServiceContext;
@GET @GET
@Path("all") @Path("all")
public async getNebConfigs(@QueryParam("scalar_token") scalarToken: string): Promise<NebConfig[]> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async getNebConfigs(): Promise<NebConfig[]> {
const cachedConfigs = Cache.for(CACHE_NEB).get("configurations"); const cachedConfigs = Cache.for(CACHE_NEB).get("configurations");
if (cachedConfigs) return cachedConfigs; if (cachedConfigs) return cachedConfigs;
@ -41,8 +43,9 @@ export class AdminNebService {
@GET @GET
@Path(":id/config") @Path(":id/config")
public async getNebConfig(@QueryParam("scalar_token") scalarToken: string, @PathParam("id") nebId: number): Promise<NebConfig> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const configs = await this.getNebConfigs(scalarToken); // does auth for us public async getNebConfig(@PathParam("id") nebId: number): Promise<NebConfig> {
const configs = await this.getNebConfigs();
const firstConfig = configs.filter(c => c.id === nebId)[0]; const firstConfig = configs.filter(c => c.id === nebId)[0];
if (!firstConfig) throw new ApiError(404, "Configuration not found"); if (!firstConfig) throw new ApiError(404, "Configuration not found");
return firstConfig; return firstConfig;
@ -50,8 +53,9 @@ export class AdminNebService {
@POST @POST
@Path(":id/integration/:type/enabled") @Path(":id/integration/:type/enabled")
public async setIntegrationEnabled(@QueryParam("scalar_token") scalarToken: string, @PathParam("id") nebId: number, @PathParam("type") integrationType: string, request: SetEnabledRequest): Promise<any> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async setIntegrationEnabled(@PathParam("id") nebId: number, @PathParam("type") integrationType: string, request: SetEnabledRequest): Promise<any> {
const userId = this.context.request.user.userId;
await NebStore.setIntegrationEnabled(nebId, integrationType, request.enabled); await NebStore.setIntegrationEnabled(nebId, integrationType, request.enabled);
LogService.info("AdminNebService", userId + " set the " + integrationType + " on NEB " + nebId + " to " + (request.enabled ? "enabled" : "disabled")); LogService.info("AdminNebService", userId + " set the " + integrationType + " on NEB " + nebId + " to " + (request.enabled ? "enabled" : "disabled"));
@ -63,8 +67,9 @@ export class AdminNebService {
@POST @POST
@Path(":id/integration/:type/config") @Path(":id/integration/:type/config")
public async setIntegrationConfig(@QueryParam("scalar_token") scalarToken: string, @PathParam("id") nebId: number, @PathParam("type") integrationType: string, newConfig: any): Promise<any> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async setIntegrationConfig(@PathParam("id") nebId: number, @PathParam("type") integrationType: string, newConfig: any): Promise<any> {
const userId = this.context.request.user.userId;
await NebStore.setIntegrationConfig(nebId, integrationType, newConfig); await NebStore.setIntegrationConfig(nebId, integrationType, newConfig);
LogService.info("AdminNebService", userId + " updated the configuration for " + integrationType + " on NEB " + nebId); LogService.info("AdminNebService", userId + " updated the configuration for " + integrationType + " on NEB " + nebId);
@ -76,15 +81,16 @@ export class AdminNebService {
@GET @GET
@Path(":id/integration/:type/config") @Path(":id/integration/:type/config")
public async getIntegrationConfig(@QueryParam("scalar_token") scalarToken: string, @PathParam("id") nebId: number, @PathParam("type") integrationType: string): Promise<any> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async getIntegrationConfig(@PathParam("id") nebId: number, @PathParam("type") integrationType: string): Promise<any> {
return NebStore.getIntegrationConfig(nebId, integrationType); return NebStore.getIntegrationConfig(nebId, integrationType);
} }
@POST @POST
@Path("new/upstream") @Path("new/upstream")
public async newConfigForUpstream(@QueryParam("scalar_token") scalarToken: string, request: CreateWithUpstream): Promise<NebConfig> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async newConfigForUpstream(request: CreateWithUpstream): Promise<NebConfig> {
const userId = this.context.request.user.userId;
try { try {
const neb = await NebStore.createForUpstream(request.upstreamId); const neb = await NebStore.createForUpstream(request.upstreamId);
@ -101,8 +107,9 @@ export class AdminNebService {
@POST @POST
@Path("new/appservice") @Path("new/appservice")
public async newConfigForAppservice(@QueryParam("scalar_token") scalarToken: string, request: CreateWithAppservice): Promise<NebConfig> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async newConfigForAppservice(request: CreateWithAppservice): Promise<NebConfig> {
const userId = this.context.request.user.userId;
try { try {
const neb = await NebStore.createForAppservice(request.appserviceId, request.adminUrl); const neb = await NebStore.createForAppservice(request.appserviceId, request.adminUrl);

View File

@ -1,12 +1,11 @@
import { GET, Path, POST, QueryParam } from "typescript-rest"; import { GET, Path, POST, QueryParam, Security } from "typescript-rest";
import config from "../../config"; import config from "../../config";
import { ApiError } from "../ApiError";
import { MatrixLiteClient } from "../../matrix/MatrixLiteClient"; import { MatrixLiteClient } from "../../matrix/MatrixLiteClient";
import { CURRENT_VERSION } from "../../version"; import { CURRENT_VERSION } from "../../version";
import { getFederationConnInfo } from "../../matrix/helpers"; import { getFederationConnInfo } from "../../matrix/helpers";
import UserScalarToken from "../../db/models/UserScalarToken"; import UserScalarToken from "../../db/models/UserScalarToken";
import { Cache, CACHE_SCALAR_ACCOUNTS } from "../../MemoryCache"; import { Cache, CACHE_SCALAR_ACCOUNTS } from "../../MemoryCache";
import AccountController from "../controllers/AccountController"; import { ROLE_MSC_ADMIN, ROLE_MSC_USER } from "../security/MSCSecurity";
interface DimensionVersionResponse { interface DimensionVersionResponse {
version: string; version: string;
@ -33,49 +32,24 @@ interface DimensionConfigResponse {
@Path("/api/v1/dimension/admin") @Path("/api/v1/dimension/admin")
export class AdminService { export class AdminService {
/**
* Determines if a given user is an administrator
* @param {string} userId The user ID to validate
* @returns {boolean} True if the user is an administrator
*/
public static isAdmin(userId: string) {
return config.admins.indexOf(userId) >= 0;
}
/**
* Validates the given scalar token to ensure the owner is an administrator. If the
* given scalar token does not belong to an administrator, an ApiError is raised.
* @param {string} scalarToken The scalar token to validate
* @returns {Promise<string>} Resolves to the owner's user ID if they are an administrator
* @throws {ApiError} Thrown with a status code of 401 if the owner is not an administrator
*/
public static async validateAndGetAdminTokenOwner(scalarToken: string): Promise<string> {
const accountController = new AccountController();
const userId = await accountController.getTokenOwner(scalarToken, true);
if (!AdminService.isAdmin(userId))
throw new ApiError(401, "You must be an administrator to use this API");
return userId;
}
@GET @GET
@Path("check") @Path("check")
public async checkIfAdmin(@QueryParam("scalar_token") scalarToken: string): Promise<{}> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async checkIfAdmin(): Promise<{}> {
return {}; // A 200 OK essentially means "you're an admin". return {}; // A 200 OK essentially means "you're an admin".
} }
@GET @GET
@Path("version") @Path("version")
public async getVersion(@QueryParam("scalar_token") scalarToken: string): Promise<DimensionVersionResponse> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async getVersion(): Promise<DimensionVersionResponse> {
return {version: CURRENT_VERSION}; return {version: CURRENT_VERSION};
} }
@GET @GET
@Path("config") @Path("config")
public async getConfig(@QueryParam("scalar_token") scalarToken: string): Promise<DimensionConfigResponse> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async getConfig(): Promise<DimensionConfigResponse> {
const client = new MatrixLiteClient(config.homeserver.accessToken); const client = new MatrixLiteClient(config.homeserver.accessToken);
const fedInfo = await getFederationConnInfo(config.homeserver.name); const fedInfo = await getFederationConnInfo(config.homeserver.name);
return { return {
@ -96,9 +70,8 @@ export class AdminService {
@GET @GET
@Path("test/federation") @Path("test/federation")
public async testFederationRouting(@QueryParam("scalar_token") scalarToken: string, @QueryParam("server_name") serverName: string): Promise<any> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async testFederationRouting(@QueryParam("server_name") serverName: string): Promise<any> {
return { return {
inputServerName: serverName, inputServerName: serverName,
resolvedServer: await getFederationConnInfo(serverName), resolvedServer: await getFederationConnInfo(serverName),
@ -107,9 +80,8 @@ export class AdminService {
@POST @POST
@Path("sessions/logout/all") @Path("sessions/logout/all")
public async logoutAll(@QueryParam("scalar_token") scalarToken: string): Promise<any> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async logoutAll(): Promise<any> {
// Clear the cache first to hopefully invalidate a bunch of them // Clear the cache first to hopefully invalidate a bunch of them
Cache.for(CACHE_SCALAR_ACCOUNTS).clear(); Cache.for(CACHE_SCALAR_ACCOUNTS).clear();

View File

@ -1,10 +1,10 @@
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest"; import { Context, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
import { AdminService } from "./AdminService";
import { Cache, CACHE_INTEGRATIONS, CACHE_SLACK_BRIDGE } from "../../MemoryCache"; import { Cache, CACHE_INTEGRATIONS, CACHE_SLACK_BRIDGE } from "../../MemoryCache";
import { LogService } from "matrix-js-snippets"; import { LogService } from "matrix-js-snippets";
import { ApiError } from "../ApiError"; import { ApiError } from "../ApiError";
import Upstream from "../../db/models/Upstream"; import Upstream from "../../db/models/Upstream";
import SlackBridgeRecord from "../../db/models/SlackBridgeRecord"; import SlackBridgeRecord from "../../db/models/SlackBridgeRecord";
import { ROLE_MSC_ADMIN, ROLE_MSC_USER } from "../security/MSCSecurity";
interface CreateWithUpstream { interface CreateWithUpstream {
upstreamId: number; upstreamId: number;
@ -27,11 +27,13 @@ interface BridgeResponse {
@Path("/api/v1/dimension/admin/slack") @Path("/api/v1/dimension/admin/slack")
export class AdminSlackService { export class AdminSlackService {
@Context
private context: ServiceContext;
@GET @GET
@Path("all") @Path("all")
public async getBridges(@QueryParam("scalar_token") scalarToken: string): Promise<BridgeResponse[]> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async getBridges(): Promise<BridgeResponse[]> {
const bridges = await SlackBridgeRecord.findAll(); const bridges = await SlackBridgeRecord.findAll();
return Promise.all(bridges.map(async b => { return Promise.all(bridges.map(async b => {
return { return {
@ -45,9 +47,8 @@ export class AdminSlackService {
@GET @GET
@Path(":bridgeId") @Path(":bridgeId")
public async getBridge(@QueryParam("scalar_token") scalarToken: string, @PathParam("bridgeId") bridgeId: number): Promise<BridgeResponse> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async getBridge(@PathParam("bridgeId") bridgeId: number): Promise<BridgeResponse> {
const telegramBridge = await SlackBridgeRecord.findByPk(bridgeId); const telegramBridge = await SlackBridgeRecord.findByPk(bridgeId);
if (!telegramBridge) throw new ApiError(404, "Slack Bridge not found"); if (!telegramBridge) throw new ApiError(404, "Slack Bridge not found");
@ -61,9 +62,9 @@ export class AdminSlackService {
@POST @POST
@Path(":bridgeId") @Path(":bridgeId")
public async updateBridge(@QueryParam("scalar_token") scalarToken: string, @PathParam("bridgeId") bridgeId: number, request: CreateSelfhosted): Promise<BridgeResponse> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async updateBridge(@PathParam("bridgeId") bridgeId: number, request: CreateSelfhosted): Promise<BridgeResponse> {
const userId = this.context.request.user.userId;
const bridge = await SlackBridgeRecord.findByPk(bridgeId); const bridge = await SlackBridgeRecord.findByPk(bridgeId);
if (!bridge) throw new ApiError(404, "Bridge not found"); if (!bridge) throw new ApiError(404, "Bridge not found");
@ -74,14 +75,14 @@ export class AdminSlackService {
Cache.for(CACHE_SLACK_BRIDGE).clear(); Cache.for(CACHE_SLACK_BRIDGE).clear();
Cache.for(CACHE_INTEGRATIONS).clear(); Cache.for(CACHE_INTEGRATIONS).clear();
return this.getBridge(scalarToken, bridge.id); return this.getBridge(bridge.id);
} }
@POST @POST
@Path("new/upstream") @Path("new/upstream")
public async newConfigForUpstream(@QueryParam("scalar_token") scalarToken: string, request: CreateWithUpstream): Promise<BridgeResponse> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async newConfigForUpstream(request: CreateWithUpstream): Promise<BridgeResponse> {
const userId = this.context.request.user.userId;
const upstream = await Upstream.findByPk(request.upstreamId); const upstream = await Upstream.findByPk(request.upstreamId);
if (!upstream) throw new ApiError(400, "Upstream not found"); if (!upstream) throw new ApiError(400, "Upstream not found");
@ -93,14 +94,14 @@ export class AdminSlackService {
Cache.for(CACHE_SLACK_BRIDGE).clear(); Cache.for(CACHE_SLACK_BRIDGE).clear();
Cache.for(CACHE_INTEGRATIONS).clear(); Cache.for(CACHE_INTEGRATIONS).clear();
return this.getBridge(scalarToken, bridge.id); return this.getBridge(bridge.id);
} }
@POST @POST
@Path("new/selfhosted") @Path("new/selfhosted")
public async newSelfhosted(@QueryParam("scalar_token") scalarToken: string, request: CreateSelfhosted): Promise<BridgeResponse> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async newSelfhosted(request: CreateSelfhosted): Promise<BridgeResponse> {
const userId = this.context.request.user.userId;
const bridge = await SlackBridgeRecord.create({ const bridge = await SlackBridgeRecord.create({
provisionUrl: request.provisionUrl, provisionUrl: request.provisionUrl,
isEnabled: true, isEnabled: true,
@ -109,6 +110,6 @@ export class AdminSlackService {
Cache.for(CACHE_SLACK_BRIDGE).clear(); Cache.for(CACHE_SLACK_BRIDGE).clear();
Cache.for(CACHE_INTEGRATIONS).clear(); Cache.for(CACHE_INTEGRATIONS).clear();
return this.getBridge(scalarToken, bridge.id); return this.getBridge(bridge.id);
} }
} }

View File

@ -1,5 +1,4 @@
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest"; import { Context, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
import { AdminService } from "./AdminService";
import StickerPack from "../../db/models/StickerPack"; import StickerPack from "../../db/models/StickerPack";
import { ApiError } from "../ApiError"; import { ApiError } from "../ApiError";
import { DimensionStickerService, MemoryStickerPack } from "../dimension/DimensionStickerService"; import { DimensionStickerService, MemoryStickerPack } from "../dimension/DimensionStickerService";
@ -10,6 +9,7 @@ import config from "../../config";
import Sticker from "../../db/models/Sticker"; import Sticker from "../../db/models/Sticker";
import { LogService } from "matrix-js-snippets"; import { LogService } from "matrix-js-snippets";
import * as sharp from "sharp"; import * as sharp from "sharp";
import { ROLE_MSC_ADMIN, ROLE_MSC_USER } from "../security/MSCSecurity";
interface SetEnabledRequest { interface SetEnabledRequest {
isEnabled: boolean; isEnabled: boolean;
@ -25,17 +25,20 @@ interface ImportTelegramRequest {
@Path("/api/v1/dimension/admin/stickers") @Path("/api/v1/dimension/admin/stickers")
export class AdminStickerService { export class AdminStickerService {
@Context
private context: ServiceContext;
@GET @GET
@Path("packs") @Path("packs")
public async getStickerPacks(@QueryParam("scalar_token") scalarToken: string): Promise<MemoryStickerPack[]> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async getStickerPacks(): Promise<MemoryStickerPack[]> {
return await DimensionStickerService.getStickerPacks(false); return await DimensionStickerService.getStickerPacks(false);
} }
@POST @POST
@Path("packs/:id/enabled") @Path("packs/:id/enabled")
public async setPackEnabled(@QueryParam("scalar_token") scalarToken: string, @PathParam("id") packId: number, request: SetEnabledRequest): Promise<any> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async setPackEnabled(@PathParam("id") packId: number, request: SetEnabledRequest): Promise<any> {
const pack = await StickerPack.findByPk(packId); const pack = await StickerPack.findByPk(packId);
if (!pack) throw new ApiError(404, "Sticker pack not found"); if (!pack) throw new ApiError(404, "Sticker pack not found");
@ -48,8 +51,9 @@ export class AdminStickerService {
@POST @POST
@Path("packs/import/telegram") @Path("packs/import/telegram")
public async importFromTelegram(@QueryParam("scalar_token") scalarToken: string, request: ImportTelegramRequest): Promise<MemoryStickerPack> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async importFromTelegram(request: ImportTelegramRequest): Promise<MemoryStickerPack> {
const userId = this.context.request.user.userId;
if (!request.packUrl || (!request.packUrl.startsWith("https://t.me/addstickers/") && !request.packUrl.startsWith("https://telegram.me/addstickers/"))) { if (!request.packUrl || (!request.packUrl.startsWith("https://t.me/addstickers/") && !request.packUrl.startsWith("https://telegram.me/addstickers/"))) {
throw new ApiError(400, "Invalid pack URL"); throw new ApiError(400, "Invalid pack URL");

View File

@ -1,9 +1,9 @@
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest"; import { Context, GET, Path, PathParam, POST, QueryParam, Security, ServiceContext } from "typescript-rest";
import { AdminService } from "./AdminService";
import { Cache, CACHE_INTEGRATIONS, CACHE_TELEGRAM_BRIDGE } from "../../MemoryCache"; import { Cache, CACHE_INTEGRATIONS, CACHE_TELEGRAM_BRIDGE } from "../../MemoryCache";
import { LogService } from "matrix-js-snippets"; import { LogService } from "matrix-js-snippets";
import { ApiError } from "../ApiError"; import { ApiError } from "../ApiError";
import TelegramBridgeRecord from "../../db/models/TelegramBridgeRecord"; import TelegramBridgeRecord from "../../db/models/TelegramBridgeRecord";
import { ROLE_MSC_ADMIN, ROLE_MSC_USER } from "../security/MSCSecurity";
interface CreateWithUpstream { interface CreateWithUpstream {
upstreamId: number; upstreamId: number;
@ -32,11 +32,13 @@ interface BridgeResponse {
@Path("/api/v1/dimension/admin/telegram") @Path("/api/v1/dimension/admin/telegram")
export class AdminTelegramService { export class AdminTelegramService {
@Context
private context: ServiceContext;
@GET @GET
@Path("all") @Path("all")
public async getBridges(@QueryParam("scalar_token") scalarToken: string): Promise<BridgeResponse[]> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async getBridges(): Promise<BridgeResponse[]> {
const bridges = await TelegramBridgeRecord.findAll(); const bridges = await TelegramBridgeRecord.findAll();
return Promise.all(bridges.map(async b => { return Promise.all(bridges.map(async b => {
return { return {
@ -53,9 +55,8 @@ export class AdminTelegramService {
@GET @GET
@Path(":bridgeId") @Path(":bridgeId")
public async getBridge(@QueryParam("scalar_token") scalarToken: string, @PathParam("bridgeId") bridgeId: number): Promise<BridgeResponse> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async getBridge(@PathParam("bridgeId") bridgeId: number): Promise<BridgeResponse> {
const telegramBridge = await TelegramBridgeRecord.findByPk(bridgeId); const telegramBridge = await TelegramBridgeRecord.findByPk(bridgeId);
if (!telegramBridge) throw new ApiError(404, "Telegram Bridge not found"); if (!telegramBridge) throw new ApiError(404, "Telegram Bridge not found");
@ -72,8 +73,9 @@ export class AdminTelegramService {
@POST @POST
@Path(":bridgeId") @Path(":bridgeId")
public async updateBridge(@QueryParam("scalar_token") scalarToken: string, @PathParam("bridgeId") bridgeId: number, request: CreateSelfhosted): Promise<BridgeResponse> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async updateBridge(@PathParam("bridgeId") bridgeId: number, request: CreateSelfhosted): Promise<BridgeResponse> {
const userId = this.context.request.user.userId;
const bridge = await TelegramBridgeRecord.findByPk(bridgeId); const bridge = await TelegramBridgeRecord.findByPk(bridgeId);
if (!bridge) throw new ApiError(404, "Bridge not found"); if (!bridge) throw new ApiError(404, "Bridge not found");
@ -88,19 +90,21 @@ export class AdminTelegramService {
Cache.for(CACHE_TELEGRAM_BRIDGE).clear(); Cache.for(CACHE_TELEGRAM_BRIDGE).clear();
Cache.for(CACHE_INTEGRATIONS).clear(); Cache.for(CACHE_INTEGRATIONS).clear();
return this.getBridge(scalarToken, bridge.id); return this.getBridge(bridge.id);
} }
@POST @POST
@Path("new/upstream") @Path("new/upstream")
@Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
public async newConfigForUpstream(@QueryParam("scalar_token") _scalarToken: string, _request: CreateWithUpstream): Promise<BridgeResponse> { public async newConfigForUpstream(@QueryParam("scalar_token") _scalarToken: string, _request: CreateWithUpstream): Promise<BridgeResponse> {
throw new ApiError(400, "Cannot create a telegram bridge from an upstream"); throw new ApiError(400, "Cannot create a telegram bridge from an upstream");
} }
@POST @POST
@Path("new/selfhosted") @Path("new/selfhosted")
public async newSelfhosted(@QueryParam("scalar_token") scalarToken: string, request: CreateSelfhosted): Promise<BridgeResponse> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async newSelfhosted(request: CreateSelfhosted): Promise<BridgeResponse> {
const userId = this.context.request.user.userId;
const bridge = await TelegramBridgeRecord.create({ const bridge = await TelegramBridgeRecord.create({
provisionUrl: request.provisionUrl, provisionUrl: request.provisionUrl,
@ -113,6 +117,6 @@ export class AdminTelegramService {
Cache.for(CACHE_TELEGRAM_BRIDGE).clear(); Cache.for(CACHE_TELEGRAM_BRIDGE).clear();
Cache.for(CACHE_INTEGRATIONS).clear(); Cache.for(CACHE_INTEGRATIONS).clear();
return this.getBridge(scalarToken, bridge.id); return this.getBridge(bridge.id);
} }
} }

View File

@ -1,8 +1,8 @@
import { GET, Path, POST, QueryParam } from "typescript-rest"; import { Context, GET, Path, POST, Security, ServiceContext } from "typescript-rest";
import { AdminService } from "./AdminService";
import { Cache, CACHE_SCALAR_ACCOUNTS, CACHE_UPSTREAM } from "../../MemoryCache"; import { Cache, CACHE_SCALAR_ACCOUNTS, CACHE_UPSTREAM } from "../../MemoryCache";
import Upstream from "../../db/models/Upstream"; import Upstream from "../../db/models/Upstream";
import { LogService } from "matrix-js-snippets"; import { LogService } from "matrix-js-snippets";
import { ROLE_MSC_ADMIN, ROLE_MSC_USER } from "../security/MSCSecurity";
interface UpstreamRepsonse { interface UpstreamRepsonse {
id: number; id: number;
@ -26,11 +26,13 @@ interface NewUpstreamRequest {
@Path("/api/v1/dimension/admin/upstreams") @Path("/api/v1/dimension/admin/upstreams")
export class AdminUpstreamService { export class AdminUpstreamService {
@Context
private context: ServiceContext;
@GET @GET
@Path("all") @Path("all")
public async getUpstreams(@QueryParam("scalar_token") scalarToken: string): Promise<UpstreamRepsonse[]> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async getUpstreams(): Promise<UpstreamRepsonse[]> {
const cachedUpstreams = Cache.for(CACHE_UPSTREAM).get("upstreams"); const cachedUpstreams = Cache.for(CACHE_UPSTREAM).get("upstreams");
if (cachedUpstreams) return cachedUpstreams; if (cachedUpstreams) return cachedUpstreams;
@ -42,8 +44,9 @@ export class AdminUpstreamService {
@POST @POST
@Path("new") @Path("new")
public async createUpstream(@QueryParam("scalar_token") scalarToken: string, request: NewUpstreamRequest): Promise<UpstreamRepsonse> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async createUpstream(request: NewUpstreamRequest): Promise<UpstreamRepsonse> {
const userId = this.context.request.user.userId;
const upstream = await Upstream.create({ const upstream = await Upstream.create({
name: request.name, name: request.name,

View File

@ -1,9 +1,9 @@
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest"; import { Context, GET, Path, PathParam, POST, QueryParam, Security, ServiceContext } from "typescript-rest";
import { AdminService } from "./AdminService";
import { Cache, CACHE_INTEGRATIONS, CACHE_WEBHOOKS_BRIDGE } from "../../MemoryCache"; import { Cache, CACHE_INTEGRATIONS, CACHE_WEBHOOKS_BRIDGE } from "../../MemoryCache";
import { LogService } from "matrix-js-snippets"; import { LogService } from "matrix-js-snippets";
import { ApiError } from "../ApiError"; import { ApiError } from "../ApiError";
import WebhookBridgeRecord from "../../db/models/WebhookBridgeRecord"; import WebhookBridgeRecord from "../../db/models/WebhookBridgeRecord";
import { ROLE_MSC_ADMIN, ROLE_MSC_USER } from "../security/MSCSecurity";
interface CreateWithUpstream { interface CreateWithUpstream {
upstreamId: number; upstreamId: number;
@ -28,11 +28,13 @@ interface BridgeResponse {
@Path("/api/v1/dimension/admin/webhooks") @Path("/api/v1/dimension/admin/webhooks")
export class AdminWebhooksService { export class AdminWebhooksService {
@Context
private context: ServiceContext;
@GET @GET
@Path("all") @Path("all")
public async getBridges(@QueryParam("scalar_token") scalarToken: string): Promise<BridgeResponse[]> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async getBridges(): Promise<BridgeResponse[]> {
const bridges = await WebhookBridgeRecord.findAll(); const bridges = await WebhookBridgeRecord.findAll();
return Promise.all(bridges.map(async b => { return Promise.all(bridges.map(async b => {
return { return {
@ -47,9 +49,8 @@ export class AdminWebhooksService {
@GET @GET
@Path(":bridgeId") @Path(":bridgeId")
public async getBridge(@QueryParam("scalar_token") scalarToken: string, @PathParam("bridgeId") bridgeId: number): Promise<BridgeResponse> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async getBridge(@PathParam("bridgeId") bridgeId: number): Promise<BridgeResponse> {
const webhookBridge = await WebhookBridgeRecord.findByPk(bridgeId); const webhookBridge = await WebhookBridgeRecord.findByPk(bridgeId);
if (!webhookBridge) throw new ApiError(404, "Webhook Bridge not found"); if (!webhookBridge) throw new ApiError(404, "Webhook Bridge not found");
@ -64,8 +65,9 @@ export class AdminWebhooksService {
@POST @POST
@Path(":bridgeId") @Path(":bridgeId")
public async updateBridge(@QueryParam("scalar_token") scalarToken: string, @PathParam("bridgeId") bridgeId: number, request: CreateSelfhosted): Promise<BridgeResponse> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async updateBridge(@PathParam("bridgeId") bridgeId: number, request: CreateSelfhosted): Promise<BridgeResponse> {
const userId = this.context.request.user.userId;
const bridge = await WebhookBridgeRecord.findByPk(bridgeId); const bridge = await WebhookBridgeRecord.findByPk(bridgeId);
if (!bridge) throw new ApiError(404, "Bridge not found"); if (!bridge) throw new ApiError(404, "Bridge not found");
@ -78,19 +80,21 @@ export class AdminWebhooksService {
Cache.for(CACHE_WEBHOOKS_BRIDGE).clear(); Cache.for(CACHE_WEBHOOKS_BRIDGE).clear();
Cache.for(CACHE_INTEGRATIONS).clear(); Cache.for(CACHE_INTEGRATIONS).clear();
return this.getBridge(scalarToken, bridge.id); return this.getBridge(bridge.id);
} }
@POST @POST
@Path("new/upstream") @Path("new/upstream")
@Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
public async newConfigForUpstream(@QueryParam("scalar_token") _scalarToken: string, _request: CreateWithUpstream): Promise<BridgeResponse> { public async newConfigForUpstream(@QueryParam("scalar_token") _scalarToken: string, _request: CreateWithUpstream): Promise<BridgeResponse> {
throw new ApiError(400, "Cannot create a webhook bridge from an upstream"); throw new ApiError(400, "Cannot create a webhook bridge from an upstream");
} }
@POST @POST
@Path("new/selfhosted") @Path("new/selfhosted")
public async newSelfhosted(@QueryParam("scalar_token") scalarToken: string, request: CreateSelfhosted): Promise<BridgeResponse> { @Security([ROLE_MSC_USER, ROLE_MSC_ADMIN])
const userId = await AdminService.validateAndGetAdminTokenOwner(scalarToken); public async newSelfhosted(request: CreateSelfhosted): Promise<BridgeResponse> {
const userId = this.context.request.user.userId;
const bridge = await WebhookBridgeRecord.create({ const bridge = await WebhookBridgeRecord.create({
provisionUrl: request.provisionUrl, provisionUrl: request.provisionUrl,
@ -101,6 +105,6 @@ export class AdminWebhooksService {
Cache.for(CACHE_WEBHOOKS_BRIDGE).clear(); Cache.for(CACHE_WEBHOOKS_BRIDGE).clear();
Cache.for(CACHE_INTEGRATIONS).clear(); Cache.for(CACHE_INTEGRATIONS).clear();
return this.getBridge(scalarToken, bridge.id); return this.getBridge(bridge.id);
} }
} }

View File

@ -31,16 +31,15 @@ export default class AccountController {
/** /**
* Gets the owner of a given scalar token, throwing an ApiError if the token is invalid. * Gets the owner of a given scalar token, throwing an ApiError if the token is invalid.
* @param {string} scalarToken The scalar token to validate * @param {string} scalarToken The scalar token to validate
* @param {boolean} ignoreUpstreams True to consider the token valid if it is missing links to other upstreams
* @returns {Promise<string>} Resolves to the owner's user ID if the token is valid. * @returns {Promise<string>} Resolves to the owner's user ID if the token is valid.
* @throws {ApiError} Thrown with a status code of 401 if the token is invalid. * @throws {ApiError} Thrown with a status code of 401 if the token is invalid.
*/ */
public async getTokenOwner(scalarToken: string, ignoreUpstreams = false): Promise<string> { public async getTokenOwner(scalarToken: string): Promise<string> {
const cachedUserId = Cache.for(CACHE_SCALAR_ACCOUNTS).get(scalarToken); const cachedUserId = Cache.for(CACHE_SCALAR_ACCOUNTS).get(scalarToken);
if (cachedUserId) return cachedUserId; if (cachedUserId) return cachedUserId;
try { try {
const user = await ScalarStore.getTokenOwner(scalarToken, ignoreUpstreams); const user = await ScalarStore.getTokenOwner(scalarToken);
Cache.for(CACHE_SCALAR_ACCOUNTS).put(scalarToken, user.userId, 30 * 60 * 1000); // 30 minutes Cache.for(CACHE_SCALAR_ACCOUNTS).put(scalarToken, user.userId, 30 * 60 * 1000); // 30 minutes
return user.userId; return user.userId;
} catch (err) { } catch (err) {

View File

@ -1,9 +1,8 @@
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest"; import { Context, DELETE, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
import { ApiError } from "../ApiError"; import { ApiError } from "../ApiError";
import { BridgedRoom, GitterBridge } from "../../bridges/GitterBridge"; import { BridgedRoom, GitterBridge } from "../../bridges/GitterBridge";
import { LogService } from "matrix-js-snippets"; import { LogService } from "matrix-js-snippets";
import { AutoWired, Inject } from "typescript-ioc/es6"; import { ROLE_MSC_USER } from "../security/MSCSecurity";
import AccountController from "../controllers/AccountController";
interface BridgeRoomRequest { interface BridgeRoomRequest {
gitterRoomName: string; gitterRoomName: string;
@ -13,17 +12,16 @@ interface BridgeRoomRequest {
* API for interacting with the Gitter bridge * API for interacting with the Gitter bridge
*/ */
@Path("/api/v1/dimension/gitter") @Path("/api/v1/dimension/gitter")
@AutoWired
export class DimensionGitterService { export class DimensionGitterService {
@Inject @Context
private accountController: AccountController; private context: ServiceContext;
@GET @GET
@Path("room/:roomId/link") @Path("room/:roomId/link")
public async getLink(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<BridgedRoom> { @Security(ROLE_MSC_USER)
const userId = await this.accountController.getTokenOwner(scalarToken); public async getLink(@PathParam("roomId") roomId: string): Promise<BridgedRoom> {
const userId = this.context.request.user.userId;
try { try {
const gitter = new GitterBridge(userId); const gitter = new GitterBridge(userId);
return gitter.getLink(roomId); return gitter.getLink(roomId);
@ -35,9 +33,9 @@ export class DimensionGitterService {
@POST @POST
@Path("room/:roomId/link") @Path("room/:roomId/link")
public async bridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<BridgedRoom> { @Security(ROLE_MSC_USER)
const userId = await this.accountController.getTokenOwner(scalarToken); public async bridgeRoom(@PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<BridgedRoom> {
const userId = this.context.request.user.userId;
try { try {
const gitter = new GitterBridge(userId); const gitter = new GitterBridge(userId);
await gitter.requestLink(roomId, request.gitterRoomName); await gitter.requestLink(roomId, request.gitterRoomName);
@ -50,9 +48,9 @@ export class DimensionGitterService {
@DELETE @DELETE
@Path("room/:roomId/link") @Path("room/:roomId/link")
public async unbridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<any> { @Security(ROLE_MSC_USER)
const userId = await this.accountController.getTokenOwner(scalarToken); public async unbridgeRoom(@PathParam("roomId") roomId: string): Promise<any> {
const userId = this.context.request.user.userId;
try { try {
const gitter = new GitterBridge(userId); const gitter = new GitterBridge(userId);
const link = await gitter.getLink(roomId); const link = await gitter.getLink(roomId);

View File

@ -1,4 +1,4 @@
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest"; import { Context, DELETE, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
import { Widget } from "../../integrations/Widget"; import { Widget } from "../../integrations/Widget";
import { Cache, CACHE_INTEGRATIONS } from "../../MemoryCache"; import { Cache, CACHE_INTEGRATIONS } from "../../MemoryCache";
import { Integration } from "../../integrations/Integration"; import { Integration } from "../../integrations/Integration";
@ -10,8 +10,7 @@ import { ComplexBot } from "../../integrations/ComplexBot";
import { Bridge } from "../../integrations/Bridge"; import { Bridge } from "../../integrations/Bridge";
import { BridgeStore } from "../../db/BridgeStore"; import { BridgeStore } from "../../db/BridgeStore";
import { BotStore } from "../../db/BotStore"; import { BotStore } from "../../db/BotStore";
import AccountController from "../controllers/AccountController"; import { ROLE_MSC_USER } from "../security/MSCSecurity";
import { AutoWired, Inject } from "typescript-ioc/es6";
export interface IntegrationsResponse { export interface IntegrationsResponse {
widgets: Widget[], widgets: Widget[],
@ -24,11 +23,10 @@ export interface IntegrationsResponse {
* API for managing integrations, primarily for a given room * API for managing integrations, primarily for a given room
*/ */
@Path("/api/v1/dimension/integrations") @Path("/api/v1/dimension/integrations")
@AutoWired
export class DimensionIntegrationsService { export class DimensionIntegrationsService {
@Inject @Context
private accountController: AccountController; private context: ServiceContext;
/** /**
* Gets a list of widgets * Gets a list of widgets
@ -90,8 +88,9 @@ export class DimensionIntegrationsService {
@GET @GET
@Path("room/:roomId") @Path("room/:roomId")
public async getIntegrationsInRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<IntegrationsResponse> { @Security(ROLE_MSC_USER)
const userId = await this.accountController.getTokenOwner(scalarToken); public async getIntegrationsInRoom(@PathParam("roomId") roomId: string): Promise<IntegrationsResponse> {
const userId = this.context.request.user.userId;
return { return {
widgets: await DimensionIntegrationsService.getWidgets(true), widgets: await DimensionIntegrationsService.getWidgets(true),
bots: await DimensionIntegrationsService.getSimpleBots(userId), bots: await DimensionIntegrationsService.getSimpleBots(userId),
@ -102,8 +101,9 @@ export class DimensionIntegrationsService {
@GET @GET
@Path("room/:roomId/integrations/:category/:type") @Path("room/:roomId/integrations/:category/:type")
public async getIntegrationInRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, @PathParam("category") category: string, @PathParam("type") integrationType: string): Promise<any> { @Security(ROLE_MSC_USER)
const roomConfig = await this.getIntegrationsInRoom(scalarToken, roomId); // does auth for us public async getIntegrationInRoom(@PathParam("roomId") roomId: string, @PathParam("category") category: string, @PathParam("type") integrationType: string): Promise<any> {
const roomConfig = await this.getIntegrationsInRoom(roomId); // does auth for us
if (category === "widget") return roomConfig.widgets.find(i => i.type === integrationType); if (category === "widget") return roomConfig.widgets.find(i => i.type === integrationType);
else if (category === "bot") return roomConfig.bots.find(i => i.type === integrationType); else if (category === "bot") return roomConfig.bots.find(i => i.type === integrationType);
@ -114,8 +114,9 @@ export class DimensionIntegrationsService {
@POST @POST
@Path("room/:roomId/integrations/:category/:type/config") @Path("room/:roomId/integrations/:category/:type/config")
public async setIntegrationConfigurationInRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, @PathParam("category") category: string, @PathParam("type") integrationType: string, newConfig: any): Promise<any> { @Security(ROLE_MSC_USER)
const userId = await this.accountController.getTokenOwner(scalarToken); public async setIntegrationConfigurationInRoom(@PathParam("roomId") roomId: string, @PathParam("category") category: string, @PathParam("type") integrationType: string, newConfig: any): Promise<any> {
const userId = this.context.request.user.userId;
if (category === "complex-bot") await NebStore.setComplexBotConfig(userId, integrationType, roomId, newConfig); if (category === "complex-bot") await NebStore.setComplexBotConfig(userId, integrationType, roomId, newConfig);
else if (category === "bridge") await BridgeStore.setBridgeRoomConfig(userId, integrationType, roomId, newConfig); else if (category === "bridge") await BridgeStore.setBridgeRoomConfig(userId, integrationType, roomId, newConfig);
@ -127,8 +128,9 @@ export class DimensionIntegrationsService {
@DELETE @DELETE
@Path("room/:roomId/integrations/:category/:type") @Path("room/:roomId/integrations/:category/:type")
public async removeIntegrationInRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, @PathParam("category") category: string, @PathParam("type") integrationType: string): Promise<any> { @Security(ROLE_MSC_USER)
const userId = await this.accountController.getTokenOwner(scalarToken); public async removeIntegrationInRoom(@PathParam("roomId") roomId: string, @PathParam("category") category: string, @PathParam("type") integrationType: string): Promise<any> {
const userId = this.context.request.user.userId;
if (category === "widget") throw new ApiError(400, "Widgets should be removed client-side"); if (category === "widget") throw new ApiError(400, "Widgets should be removed client-side");
else if (category === "bot") { else if (category === "bot") {

View File

@ -1,10 +1,9 @@
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest"; import { Context, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
import { LogService } from "matrix-js-snippets"; import { LogService } from "matrix-js-snippets";
import { IrcBridge } from "../../bridges/IrcBridge"; import { IrcBridge } from "../../bridges/IrcBridge";
import IrcBridgeRecord from "../../db/models/IrcBridgeRecord"; import IrcBridgeRecord from "../../db/models/IrcBridgeRecord";
import { ApiError } from "../ApiError"; import { ApiError } from "../ApiError";
import AccountController from "../controllers/AccountController"; import { ROLE_MSC_USER } from "../security/MSCSecurity";
import { AutoWired, Inject } from "typescript-ioc/es6";
interface RequestLinkRequest { interface RequestLinkRequest {
op: string; op: string;
@ -14,17 +13,16 @@ interface RequestLinkRequest {
* API for interacting with the IRC bridge * API for interacting with the IRC bridge
*/ */
@Path("/api/v1/dimension/irc") @Path("/api/v1/dimension/irc")
@AutoWired
export class DimensionIrcService { export class DimensionIrcService {
@Inject @Context
private accountController: AccountController; private context: ServiceContext;
@GET @GET
@Path(":networkId/channel/:channel/ops") @Path(":networkId/channel/:channel/ops")
public async getOps(@QueryParam("scalar_token") scalarToken: string, @PathParam("networkId") networkId: string, @PathParam("channel") channelNoHash: string): Promise<string[]> { @Security(ROLE_MSC_USER)
const userId = await this.accountController.getTokenOwner(scalarToken); public async getOps(@PathParam("networkId") networkId: string, @PathParam("channel") channelNoHash: string): Promise<string[]> {
const userId = this.context.request.user.userId;
const parsed = IrcBridge.parseNetworkId(networkId); const parsed = IrcBridge.parseNetworkId(networkId);
const bridge = await IrcBridgeRecord.findByPk(parsed.bridgeId); const bridge = await IrcBridgeRecord.findByPk(parsed.bridgeId);
if (!bridge) throw new ApiError(404, "Bridge not found"); if (!bridge) throw new ApiError(404, "Bridge not found");
@ -38,9 +36,9 @@ export class DimensionIrcService {
@POST @POST
@Path(":networkId/channel/:channel/link/:roomId") @Path(":networkId/channel/:channel/link/:roomId")
public async requestLink(@QueryParam("scalar_token") scalarToken: string, @PathParam("networkId") networkId: string, @PathParam("channel") channelNoHash: string, @PathParam("roomId") roomId: string, request: RequestLinkRequest): Promise<any> { @Security(ROLE_MSC_USER)
const userId = await this.accountController.getTokenOwner(scalarToken); public async requestLink(@PathParam("networkId") networkId: string, @PathParam("channel") channelNoHash: string, @PathParam("roomId") roomId: string, request: RequestLinkRequest): Promise<any> {
const userId = this.context.request.user.userId;
const parsed = IrcBridge.parseNetworkId(networkId); const parsed = IrcBridge.parseNetworkId(networkId);
const bridge = await IrcBridgeRecord.findByPk(parsed.bridgeId); const bridge = await IrcBridgeRecord.findByPk(parsed.bridgeId);
if (!bridge) throw new ApiError(404, "Bridge not found"); if (!bridge) throw new ApiError(404, "Bridge not found");
@ -54,9 +52,9 @@ export class DimensionIrcService {
@POST @POST
@Path(":networkId/channel/:channel/unlink/:roomId") @Path(":networkId/channel/:channel/unlink/:roomId")
public async unlink(@QueryParam("scalar_token") scalarToken: string, @PathParam("networkId") networkId: string, @PathParam("channel") channelNoHash: string, @PathParam("roomId") roomId: string): Promise<any> { @Security(ROLE_MSC_USER)
const userId = await this.accountController.getTokenOwner(scalarToken); public async unlink(@PathParam("networkId") networkId: string, @PathParam("channel") channelNoHash: string, @PathParam("roomId") roomId: string): Promise<any> {
const userId = this.context.request.user.userId;
const parsed = IrcBridge.parseNetworkId(networkId); const parsed = IrcBridge.parseNetworkId(networkId);
const bridge = await IrcBridgeRecord.findByPk(parsed.bridgeId); const bridge = await IrcBridgeRecord.findByPk(parsed.bridgeId);
if (!bridge) throw new ApiError(404, "Bridge not found"); if (!bridge) throw new ApiError(404, "Bridge not found");

View File

@ -1,10 +1,9 @@
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest"; import { Context, DELETE, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
import { ApiError } from "../ApiError"; import { ApiError } from "../ApiError";
import { LogService } from "matrix-js-snippets"; import { LogService } from "matrix-js-snippets";
import { BridgedChannel, SlackBridge } from "../../bridges/SlackBridge"; import { BridgedChannel, SlackBridge } from "../../bridges/SlackBridge";
import { SlackChannel, SlackTeam } from "../../bridges/models/slack"; import { SlackChannel, SlackTeam } from "../../bridges/models/slack";
import { AutoWired, Inject } from "typescript-ioc/es6"; import { ROLE_MSC_USER } from "../security/MSCSecurity";
import AccountController from "../controllers/AccountController";
interface BridgeRoomRequest { interface BridgeRoomRequest {
teamId: string; teamId: string;
@ -15,16 +14,16 @@ interface BridgeRoomRequest {
* API for interacting with the Slack bridge * API for interacting with the Slack bridge
*/ */
@Path("/api/v1/dimension/slack") @Path("/api/v1/dimension/slack")
@AutoWired
export class DimensionSlackService { export class DimensionSlackService {
@Inject @Context
private accountController: AccountController; private context: ServiceContext;
@GET @GET
@Path("room/:roomId/link") @Path("room/:roomId/link")
public async getLink(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<BridgedChannel> { @Security(ROLE_MSC_USER)
const userId = await this.accountController.getTokenOwner(scalarToken); public async getLink(@PathParam("roomId") roomId: string): Promise<BridgedChannel> {
const userId = this.context.request.user.userId;
try { try {
const slack = new SlackBridge(userId); const slack = new SlackBridge(userId);
@ -37,8 +36,9 @@ export class DimensionSlackService {
@POST @POST
@Path("room/:roomId/link") @Path("room/:roomId/link")
public async bridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<BridgedChannel> { @Security(ROLE_MSC_USER)
const userId = await this.accountController.getTokenOwner(scalarToken); public async bridgeRoom(@PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<BridgedChannel> {
const userId = this.context.request.user.userId;
try { try {
const slack = new SlackBridge(userId); const slack = new SlackBridge(userId);
@ -52,8 +52,8 @@ export class DimensionSlackService {
@DELETE @DELETE
@Path("room/:roomId/link") @Path("room/:roomId/link")
public async unbridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<any> { public async unbridgeRoom(@PathParam("roomId") roomId: string): Promise<any> {
const userId = await this.accountController.getTokenOwner(scalarToken); const userId = this.context.request.user.userId;
try { try {
const slack = new SlackBridge(userId); const slack = new SlackBridge(userId);
@ -69,8 +69,9 @@ export class DimensionSlackService {
@GET @GET
@Path("teams") @Path("teams")
public async getTeams(@QueryParam("scalar_token") scalarToken: string): Promise<SlackTeam[]> { @Security(ROLE_MSC_USER)
const userId = await this.accountController.getTokenOwner(scalarToken); public async getTeams(): Promise<SlackTeam[]> {
const userId = this.context.request.user.userId;
const slack = new SlackBridge(userId); const slack = new SlackBridge(userId);
const teams = await slack.getTeams(); const teams = await slack.getTeams();
@ -80,8 +81,9 @@ export class DimensionSlackService {
@GET @GET
@Path("teams/:teamId/channels") @Path("teams/:teamId/channels")
public async getChannels(@QueryParam("scalar_token") scalarToken: string, @PathParam("teamId") teamId: string): Promise<SlackChannel[]> { @Security(ROLE_MSC_USER)
const userId = await this.accountController.getTokenOwner(scalarToken); public async getChannels(@PathParam("teamId") teamId: string): Promise<SlackChannel[]> {
const userId = this.context.request.user.userId;
try { try {
const slack = new SlackBridge(userId); const slack = new SlackBridge(userId);
@ -94,8 +96,9 @@ export class DimensionSlackService {
@GET @GET
@Path("auth") @Path("auth")
public async getAuthUrl(@QueryParam("scalar_token") scalarToken: string): Promise<{ authUrl: string }> { @Security(ROLE_MSC_USER)
const userId = await this.accountController.getTokenOwner(scalarToken); public async getAuthUrl(): Promise<{ authUrl: string }> {
const userId = this.context.request.user.userId;
try { try {
const slack = new SlackBridge(userId); const slack = new SlackBridge(userId);

View File

@ -1,4 +1,4 @@
import { GET, Path, PathParam, POST, QueryParam } from "typescript-rest"; import { Context, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
import { Cache, CACHE_STICKERS } from "../../MemoryCache"; import { Cache, CACHE_STICKERS } from "../../MemoryCache";
import StickerPack from "../../db/models/StickerPack"; import StickerPack from "../../db/models/StickerPack";
import Sticker from "../../db/models/Sticker"; import Sticker from "../../db/models/Sticker";
@ -7,8 +7,7 @@ import { ApiError } from "../ApiError";
import { StickerpackMetadataDownloader } from "../../utils/StickerpackMetadataDownloader"; import { StickerpackMetadataDownloader } from "../../utils/StickerpackMetadataDownloader";
import { MatrixStickerBot } from "../../matrix/MatrixStickerBot"; import { MatrixStickerBot } from "../../matrix/MatrixStickerBot";
import config from "../../config"; import config from "../../config";
import { AutoWired, Inject } from "typescript-ioc/es6"; import { ROLE_MSC_USER } from "../security/MSCSecurity";
import AccountController from "../controllers/AccountController";
export interface MemoryStickerPack { export interface MemoryStickerPack {
id: number; id: number;
@ -64,11 +63,10 @@ interface StickerConfig {
* API for stickers * API for stickers
*/ */
@Path("/api/v1/dimension/stickers") @Path("/api/v1/dimension/stickers")
@AutoWired
export class DimensionStickerService { export class DimensionStickerService {
@Inject @Context
private accountController: AccountController; private context: ServiceContext;
public static async getStickerPacks(enabledOnly = false): Promise<MemoryStickerPack[]> { public static async getStickerPacks(enabledOnly = false): Promise<MemoryStickerPack[]> {
const cachedPacks = Cache.for(CACHE_STICKERS).get("packs"); const cachedPacks = Cache.for(CACHE_STICKERS).get("packs");
@ -90,9 +88,8 @@ export class DimensionStickerService {
@GET @GET
@Path("config") @Path("config")
public async getConfig(@QueryParam("scalar_token") scalarToken: string): Promise<StickerConfig> { @Security(ROLE_MSC_USER)
await this.accountController.getTokenOwner(scalarToken); public async getConfig(): Promise<StickerConfig> {
return { return {
enabled: config.stickers.enabled, enabled: config.stickers.enabled,
stickerBot: config.stickers.stickerBot, stickerBot: config.stickers.stickerBot,
@ -102,9 +99,9 @@ export class DimensionStickerService {
@GET @GET
@Path("packs") @Path("packs")
public async getStickerPacks(@QueryParam("scalar_token") scalarToken: string): Promise<MemoryStickerPack[]> { @Security(ROLE_MSC_USER)
const userId = await this.accountController.getTokenOwner(scalarToken); public async getStickerPacks(): Promise<MemoryStickerPack[]> {
const userId = this.context.request.user.userId;
const cachedPacks = Cache.for(CACHE_STICKERS).get("packs_" + userId); const cachedPacks = Cache.for(CACHE_STICKERS).get("packs_" + userId);
if (cachedPacks) return cachedPacks; if (cachedPacks) return cachedPacks;
@ -129,9 +126,9 @@ export class DimensionStickerService {
@POST @POST
@Path("packs/:packId/selected") @Path("packs/:packId/selected")
public async setPackSelected(@QueryParam("scalar_token") scalarToken: string, @PathParam("packId") packId: number, request: SetSelectedRequest): Promise<any> { @Security(ROLE_MSC_USER)
const userId = await this.accountController.getTokenOwner(scalarToken); public async setPackSelected(@PathParam("packId") packId: number, request: SetSelectedRequest): Promise<any> {
const userId = this.context.request.user.userId;
const pack = await StickerPack.findByPk(packId); const pack = await StickerPack.findByPk(packId);
if (!pack) throw new ApiError(404, "Sticker pack not found"); if (!pack) throw new ApiError(404, "Sticker pack not found");
@ -153,9 +150,8 @@ export class DimensionStickerService {
@POST @POST
@Path("packs/import") @Path("packs/import")
public async importPack(@QueryParam("scalar_token") scalarToken: string, request: ImportPackRequest): Promise<MemoryUserStickerPack> { @Security(ROLE_MSC_USER)
await this.accountController.getTokenOwner(scalarToken); public async importPack(request: ImportPackRequest): Promise<MemoryUserStickerPack> {
if (!config.stickers.enabled) { if (!config.stickers.enabled) {
throw new ApiError(400, "Custom stickerpacks are disabled on this homeserver"); throw new ApiError(400, "Custom stickerpacks are disabled on this homeserver");
} }
@ -171,7 +167,7 @@ export class DimensionStickerService {
const pack = stickerPacks[0]; const pack = stickerPacks[0];
// Simulate a call to setPackSelected // Simulate a call to setPackSelected
await this.setPackSelected(scalarToken, pack.id, {isSelected: true}); await this.setPackSelected(pack.id, {isSelected: true});
const memoryPack = await DimensionStickerService.packToMemory(pack); const memoryPack = await DimensionStickerService.packToMemory(pack);
return Object.assign({isSelected: true}, memoryPack); return Object.assign({isSelected: true}, memoryPack);

View File

@ -1,8 +1,7 @@
import { DELETE, GET, Path, PathParam, POST, QueryParam } from "typescript-rest"; import { Context, DELETE, GET, Path, PathParam, POST, QueryParam, Security, ServiceContext } from "typescript-rest";
import { TelegramBridge } from "../../bridges/TelegramBridge"; import { TelegramBridge } from "../../bridges/TelegramBridge";
import { ApiError } from "../ApiError"; import { ApiError } from "../ApiError";
import { AutoWired, Inject } from "typescript-ioc/es6"; import { ROLE_MSC_USER } from "../security/MSCSecurity";
import AccountController from "../controllers/AccountController";
interface PortalInfoResponse { interface PortalInfoResponse {
bridged: boolean; bridged: boolean;
@ -20,16 +19,16 @@ interface BridgeRoomRequest {
* API for interacting with the Telegram bridge * API for interacting with the Telegram bridge
*/ */
@Path("/api/v1/dimension/telegram") @Path("/api/v1/dimension/telegram")
@AutoWired
export class DimensionTelegramService { export class DimensionTelegramService {
@Inject @Context
private accountController: AccountController; private context: ServiceContext;
@GET @GET
@Path("chat/:chatId") @Path("chat/:chatId")
public async getPortalInfo(@QueryParam("scalar_token") scalarToken: string, @PathParam("chatId") chatId: number, @QueryParam("roomId") roomId: string): Promise<PortalInfoResponse> { @Security(ROLE_MSC_USER)
const userId = await this.accountController.getTokenOwner(scalarToken); public async getPortalInfo(@PathParam("chatId") chatId: number, @QueryParam("roomId") roomId: string): Promise<PortalInfoResponse> {
const userId = this.context.request.user.userId;
try { try {
const telegram = new TelegramBridge(userId); const telegram = new TelegramBridge(userId);
@ -51,8 +50,9 @@ export class DimensionTelegramService {
@POST @POST
@Path("chat/:chatId/room/:roomId") @Path("chat/:chatId/room/:roomId")
public async bridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("chatId") chatId: number, @PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<PortalInfoResponse> { @Security(ROLE_MSC_USER)
const userId = await this.accountController.getTokenOwner(scalarToken); public async bridgeRoom(@PathParam("chatId") chatId: number, @PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise<PortalInfoResponse> {
const userId = this.context.request.user.userId;
try { try {
const telegram = new TelegramBridge(userId); const telegram = new TelegramBridge(userId);
@ -73,8 +73,9 @@ export class DimensionTelegramService {
@DELETE @DELETE
@Path("room/:roomId") @Path("room/:roomId")
public async unbridgeRoom(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string): Promise<PortalInfoResponse> { @Security(ROLE_MSC_USER)
const userId = await this.accountController.getTokenOwner(scalarToken); public async unbridgeRoom(@PathParam("roomId") roomId: string): Promise<PortalInfoResponse> {
const userId = this.context.request.user.userId;
try { try {
const telegram = new TelegramBridge(userId); const telegram = new TelegramBridge(userId);

View File

@ -1,23 +1,31 @@
import { DELETE, FormParam, HeaderParam, Path, PathParam, POST, QueryParam } from "typescript-rest"; import {
Context,
DELETE,
FormParam,
HeaderParam,
Path,
PathParam,
POST,
Security,
ServiceContext
} from "typescript-rest";
import { SuccessResponse, WebhookConfiguration, WebhookOptions } from "../../bridges/models/webhooks"; import { SuccessResponse, WebhookConfiguration, WebhookOptions } from "../../bridges/models/webhooks";
import { WebhooksBridge } from "../../bridges/WebhooksBridge"; import { WebhooksBridge } from "../../bridges/WebhooksBridge";
import Webhook from "../../db/models/Webhook"; import Webhook from "../../db/models/Webhook";
import { ApiError } from "../ApiError"; import { ApiError } from "../ApiError";
import { LogService } from "matrix-js-snippets"; import { LogService } from "matrix-js-snippets";
import * as request from "request"; import * as request from "request";
import { AutoWired, Inject } from "typescript-ioc/es6"; import { ROLE_MSC_USER } from "../security/MSCSecurity";
import AccountController from "../controllers/AccountController";
/** /**
* API for interacting with the Webhooks bridge, and for setting up proxies to other * API for interacting with the Webhooks bridge, and for setting up proxies to other
* services. * services.
*/ */
@Path("/api/v1/dimension/webhooks") @Path("/api/v1/dimension/webhooks")
@AutoWired
export class DimensionWebhooksService { export class DimensionWebhooksService {
@Inject @Context
private accountController: AccountController; private context: ServiceContext;
@POST @POST
@Path("/travisci/:webhookId") @Path("/travisci/:webhookId")
@ -47,27 +55,27 @@ export class DimensionWebhooksService {
@POST @POST
@Path("room/:roomId/webhooks/new") @Path("room/:roomId/webhooks/new")
public async newWebhook(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, options: WebhookOptions): Promise<WebhookConfiguration> { @Security(ROLE_MSC_USER)
const userId = await this.accountController.getTokenOwner(scalarToken); public async newWebhook(@PathParam("roomId") roomId: string, options: WebhookOptions): Promise<WebhookConfiguration> {
const userId = this.context.request.user.userId;
const webhooks = new WebhooksBridge(userId); const webhooks = new WebhooksBridge(userId);
return webhooks.createWebhook(roomId, options); return webhooks.createWebhook(roomId, options);
} }
@POST @POST
@Path("room/:roomId/webhooks/:hookId") @Path("room/:roomId/webhooks/:hookId")
public async updateWebhook(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, @PathParam("hookId") hookId: string, options: WebhookOptions): Promise<WebhookConfiguration> { @Security(ROLE_MSC_USER)
const userId = await this.accountController.getTokenOwner(scalarToken); public async updateWebhook(@PathParam("roomId") roomId: string, @PathParam("hookId") hookId: string, options: WebhookOptions): Promise<WebhookConfiguration> {
const userId = this.context.request.user.userId;
const webhooks = new WebhooksBridge(userId); const webhooks = new WebhooksBridge(userId);
return webhooks.updateWebhook(roomId, hookId, options); return webhooks.updateWebhook(roomId, hookId, options);
} }
@DELETE @DELETE
@Path("room/:roomId/webhooks/:hookId") @Path("room/:roomId/webhooks/:hookId")
public async deleteWebhook(@QueryParam("scalar_token") scalarToken: string, @PathParam("roomId") roomId: string, @PathParam("hookId") hookId: string): Promise<SuccessResponse> { @Security(ROLE_MSC_USER)
const userId = await this.accountController.getTokenOwner(scalarToken); public async deleteWebhook(@PathParam("roomId") roomId: string, @PathParam("hookId") hookId: string): Promise<SuccessResponse> {
const userId = this.context.request.user.userId;
const webhooks = new WebhooksBridge(userId); const webhooks = new WebhooksBridge(userId);
return webhooks.deleteWebhook(roomId, hookId); return webhooks.deleteWebhook(roomId, hookId);
} }

View File

@ -1,4 +1,4 @@
import { GET, Path, POST, QueryParam, Security } from "typescript-rest"; import { Context, GET, Path, POST, QueryParam, Security, ServiceContext } from "typescript-rest";
import { ApiError } from "../ApiError"; import { ApiError } from "../ApiError";
import { OpenId } from "../../models/OpenId"; import { OpenId } from "../../models/OpenId";
import { ScalarAccountResponse, ScalarRegisterResponse } from "../../models/ScalarResponses"; import { ScalarAccountResponse, ScalarRegisterResponse } from "../../models/ScalarResponses";
@ -17,6 +17,9 @@ export class ScalarService {
@Inject @Inject
private accountController: AccountController; private accountController: AccountController;
@Context
private context: ServiceContext;
@POST @POST
@Path("register") @Path("register")
public async register(request: OpenId, @QueryParam("v") apiVersion: string): Promise<ScalarRegisterResponse> { public async register(request: OpenId, @QueryParam("v") apiVersion: string): Promise<ScalarRegisterResponse> {
@ -31,13 +34,12 @@ export class ScalarService {
@GET @GET
@Path("account") @Path("account")
@Security(ROLE_MSC_USER) @Security(ROLE_MSC_USER)
public async getAccount(@QueryParam("scalar_token") scalarToken: string, @QueryParam("v") apiVersion: string): Promise<ScalarAccountResponse> { public async getAccount(@QueryParam("v") apiVersion: string): Promise<ScalarAccountResponse> {
if (apiVersion !== "1.1") { if (apiVersion !== "1.1") {
throw new ApiError(401, "Invalid API version."); throw new ApiError(401, "Invalid API version.");
} }
const userId = await this.accountController.getTokenOwner(scalarToken); return {user_id: this.context.request.user.userId};
return {user_id: userId};
} }
@GET @GET

View File

@ -1,11 +1,10 @@
import { GET, Path, QueryParam } from "typescript-rest"; import { GET, Path, QueryParam, Security } from "typescript-rest";
import { LogService } from "matrix-js-snippets"; import { LogService } from "matrix-js-snippets";
import { Cache, CACHE_WIDGET_TITLES } from "../../MemoryCache"; import { Cache, CACHE_WIDGET_TITLES } from "../../MemoryCache";
import { MatrixLiteClient } from "../../matrix/MatrixLiteClient"; import { MatrixLiteClient } from "../../matrix/MatrixLiteClient";
import config from "../../config"; import config from "../../config";
import { ROLE_MSC_USER } from "../security/MSCSecurity";
import moment = require("moment"); import moment = require("moment");
import { AutoWired, Inject } from "typescript-ioc/es6";
import AccountController from "../controllers/AccountController";
interface UrlPreviewResponse { interface UrlPreviewResponse {
cached_response: boolean; cached_response: boolean;
@ -23,17 +22,12 @@ interface UrlPreviewResponse {
* API for the minimum Scalar API for widget functionality in clients. * API for the minimum Scalar API for widget functionality in clients.
*/ */
@Path("/api/v1/scalar/widgets") @Path("/api/v1/scalar/widgets")
@AutoWired
export class ScalarWidgetService { export class ScalarWidgetService {
@Inject
private accountController: AccountController;
@GET @GET
@Path("title_lookup") @Path("title_lookup")
public async titleLookup(@QueryParam("scalar_token") scalarToken: string, @QueryParam("curl") url: string): Promise<UrlPreviewResponse> { @Security(ROLE_MSC_USER)
await this.accountController.getTokenOwner(scalarToken); public async titleLookup(@QueryParam("curl") url: string): Promise<UrlPreviewResponse> {
const cachedResult = Cache.for(CACHE_WIDGET_TITLES).get(url); const cachedResult = Cache.for(CACHE_WIDGET_TITLES).get(url);
if (cachedResult) { if (cachedResult) {
cachedResult.cached_response = true; cachedResult.cached_response = true;

View File

@ -5,6 +5,7 @@ import { LogService } from "matrix-js-snippets";
import AccountController from "../controllers/AccountController"; import AccountController from "../controllers/AccountController";
import TermsController from "../controllers/TermsController"; import TermsController from "../controllers/TermsController";
import config from "../../config"; import config from "../../config";
import { ScalarStore } from "../../db/ScalarStore";
export interface IMSCUser { export interface IMSCUser {
userId: string; userId: string;
@ -22,6 +23,10 @@ const TERMS_IGNORED_ROUTES = [
{method: "POST", path: "/_matrix/integrations/v1/logout"}, {method: "POST", path: "/_matrix/integrations/v1/logout"},
]; ];
const ADMIN_ROUTES = [
{method: "*", path: "/api/v1/dimension/admin/"},
];
export default class MSCSecurity implements ServiceAuthenticator { export default class MSCSecurity implements ServiceAuthenticator {
private accountController = new AccountController(); private accountController = new AccountController();
@ -62,20 +67,15 @@ export default class MSCSecurity implements ServiceAuthenticator {
token: token, token: token,
}; };
let needTerms = true; const needUpstreams = !this.matchesAnyRoute(req, ADMIN_ROUTES);
if (req.method !== "OPTIONS") { if (needUpstreams) {
for (const route of TERMS_IGNORED_ROUTES) { const hasUpstreams = await ScalarStore.doesUserHaveTokensForAllUpstreams(req.user.userId);
if (route.method === "*" && req.path.startsWith(route.path)) { if (!hasUpstreams) {
needTerms = false; return res.status(401).json({errcode: "M_INVALID_TOKEN", error: "Invalid token"});
break;
}
if (route.method === req.method && route.path === req.path) {
needTerms = false;
break;
}
} }
} else needTerms = false; }
const needTerms = !this.matchesAnyRoute(req, TERMS_IGNORED_ROUTES);
if (needTerms) { if (needTerms) {
const signatureNeeded = await this.termsController.doesUserNeedToSignTerms(req.user); const signatureNeeded = await this.termsController.doesUserNeedToSignTerms(req.user);
if (signatureNeeded) { if (signatureNeeded) {
@ -86,14 +86,17 @@ export default class MSCSecurity implements ServiceAuthenticator {
} }
} }
if (this.matchesAnyRoute(req, ADMIN_ROUTES, false) && !this.getRoles(req).includes(ROLE_MSC_ADMIN)) {
return res.status(403).json({errcode: "M_UNAUTHORIZED", error: "User is not an admin"});
}
return next(); return next();
} else { } else {
return res.status(401).json({errcode: "M_INVALID_TOKEN", error: "Invalid token"}); return res.status(401).json({errcode: "M_INVALID_TOKEN", error: "Invalid token"});
} }
} catch (e) { } catch (e) {
if (e instanceof ApiError) { if (e instanceof ApiError) {
// TODO: Proper error message res.status(e.statusCode).json(e.jsonResponse);
res.status(e.statusCode).json({errcode: e.errorCode, error: "Error"});
} else { } else {
LogService.error("MSCSecurity", e); LogService.error("MSCSecurity", e);
res.status(500).json({errcode: "M_UNKNOWN", error: "Unknown server error"}); res.status(500).json({errcode: "M_UNKNOWN", error: "Unknown server error"});
@ -105,4 +108,19 @@ export default class MSCSecurity implements ServiceAuthenticator {
public initialize(_router: Router): void { public initialize(_router: Router): void {
} }
private matchesAnyRoute(req: Request, routes: { method: string, path: string }[], valForOptions = true): boolean {
if (req.method === 'OPTIONS') return valForOptions;
for (const route of routes) {
if (route.method === '*' && req.path.startsWith(route.path)) {
return true;
}
if (route.method === req.method && route.path === req.path) {
return true;
}
}
return false;
}
} }

View File

@ -4,7 +4,7 @@ import Upstream from "./models/Upstream";
import User from "./models/User"; import User from "./models/User";
import { MatrixStickerBot } from "../matrix/MatrixStickerBot"; import { MatrixStickerBot } from "../matrix/MatrixStickerBot";
import { ScalarClient } from "../scalar/ScalarClient"; import { ScalarClient } from "../scalar/ScalarClient";
import { CACHE_SCALAR_ONLINE_STATE, Cache } from "../MemoryCache"; import { Cache, CACHE_SCALAR_ONLINE_STATE } from "../MemoryCache";
export class ScalarStore { export class ScalarStore {
@ -33,21 +33,14 @@ export class ScalarStore {
return true; return true;
} }
public static async getTokenOwner(scalarToken: string, ignoreUpstreams = false): Promise<User> { public static async getTokenOwner(scalarToken: string): Promise<User> {
const tokens = await UserScalarToken.findAll({ const tokens = await UserScalarToken.findAll({
where: {isDimensionToken: true, scalarToken: scalarToken}, where: {isDimensionToken: true, scalarToken: scalarToken},
include: [User] include: [User]
}); });
if (!tokens || tokens.length === 0) throw new Error("Invalid token"); if (!tokens || tokens.length === 0) throw new Error("Invalid token");
const user = tokens[0].user; return tokens[0].user;
if (ignoreUpstreams) return user; // skip upstreams check
const hasAllTokens = await ScalarStore.doesUserHaveTokensForAllUpstreams(user.userId);
if (!hasAllTokens) {
throw new Error("Invalid token"); // They are missing an upstream, so we'll lie and say they are not authorized
}
return user;
} }
public static async isUpstreamOnline(upstream: Upstream): Promise<boolean> { public static async isUpstreamOnline(upstream: Upstream): Promise<boolean> {