diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 03ebaa7368..e4c46dd9ee 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -1,6 +1,8 @@ import { program } from "commander"; +import { OssServeConfigurator } from "./oss-serve-configurator"; import { registerOssPrograms } from "./register-oss-programs"; +import { ServeProgram } from "./serve.program"; import { ServiceContainer } from "./service-container"; async function main() { @@ -9,6 +11,10 @@ async function main() { await registerOssPrograms(serviceContainer); + // ServeProgram is registered separately so it can be overridden by bit-cli + const serveConfigurator = new OssServeConfigurator(serviceContainer); + new ServeProgram(serviceContainer, serveConfigurator).register(); + program.parse(process.argv); } diff --git a/apps/cli/src/commands/serve.command.ts b/apps/cli/src/commands/serve.command.ts index 8949e5b71e..05603a3c24 100644 --- a/apps/cli/src/commands/serve.command.ts +++ b/apps/cli/src/commands/serve.command.ts @@ -1,4 +1,3 @@ -import * as koaMulter from "@koa/multer"; import * as koaRouter from "@koa/router"; import { OptionValues } from "commander"; import * as koa from "koa"; @@ -7,170 +6,14 @@ import * as koaJson from "koa-json"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { ConfirmCommand } from "../admin-console/commands/confirm.command"; -import { ShareCommand } from "../admin-console/commands/share.command"; -import { LockCommand } from "../auth/commands/lock.command"; -import { UnlockCommand } from "../auth/commands/unlock.command"; -import { Response } from "../models/response"; -import { FileResponse } from "../models/response/file.response"; +import { OssServeConfigurator } from "../oss-serve-configurator"; import { ServiceContainer } from "../service-container"; -import { GenerateCommand } from "../tools/generate.command"; -import { - SendEditCommand, - SendCreateCommand, - SendDeleteCommand, - SendGetCommand, - SendListCommand, - SendRemovePasswordCommand, -} from "../tools/send"; -import { CreateCommand } from "../vault/create.command"; -import { DeleteCommand } from "../vault/delete.command"; -import { SyncCommand } from "../vault/sync.command"; - -import { EditCommand } from "./edit.command"; -import { GetCommand } from "./get.command"; -import { ListCommand } from "./list.command"; -import { RestoreCommand } from "./restore.command"; -import { StatusCommand } from "./status.command"; export class ServeCommand { - private listCommand: ListCommand; - private getCommand: GetCommand; - private createCommand: CreateCommand; - private editCommand: EditCommand; - private generateCommand: GenerateCommand; - private shareCommand: ShareCommand; - private statusCommand: StatusCommand; - private syncCommand: SyncCommand; - private deleteCommand: DeleteCommand; - private confirmCommand: ConfirmCommand; - private restoreCommand: RestoreCommand; - private lockCommand: LockCommand; - private unlockCommand: UnlockCommand; - - private sendCreateCommand: SendCreateCommand; - private sendDeleteCommand: SendDeleteCommand; - private sendEditCommand: SendEditCommand; - private sendGetCommand: SendGetCommand; - private sendListCommand: SendListCommand; - private sendRemovePasswordCommand: SendRemovePasswordCommand; - - constructor(protected serviceContainer: ServiceContainer) { - this.getCommand = new GetCommand( - this.serviceContainer.cipherService, - this.serviceContainer.folderService, - this.serviceContainer.collectionService, - this.serviceContainer.totpService, - this.serviceContainer.auditService, - this.serviceContainer.cryptoService, - this.serviceContainer.stateService, - this.serviceContainer.searchService, - this.serviceContainer.apiService, - this.serviceContainer.organizationService, - this.serviceContainer.eventCollectionService, - this.serviceContainer.billingAccountProfileStateService, - ); - this.listCommand = new ListCommand( - this.serviceContainer.cipherService, - this.serviceContainer.folderService, - this.serviceContainer.collectionService, - this.serviceContainer.organizationService, - this.serviceContainer.searchService, - this.serviceContainer.organizationUserService, - this.serviceContainer.apiService, - this.serviceContainer.eventCollectionService, - ); - this.createCommand = new CreateCommand( - this.serviceContainer.cipherService, - this.serviceContainer.folderService, - this.serviceContainer.cryptoService, - this.serviceContainer.apiService, - this.serviceContainer.folderApiService, - this.serviceContainer.billingAccountProfileStateService, - this.serviceContainer.organizationService, - ); - this.editCommand = new EditCommand( - this.serviceContainer.cipherService, - this.serviceContainer.folderService, - this.serviceContainer.cryptoService, - this.serviceContainer.apiService, - this.serviceContainer.folderApiService, - ); - this.generateCommand = new GenerateCommand( - this.serviceContainer.passwordGenerationService, - this.serviceContainer.stateService, - ); - this.syncCommand = new SyncCommand(this.serviceContainer.syncService); - this.statusCommand = new StatusCommand( - this.serviceContainer.environmentService, - this.serviceContainer.syncService, - this.serviceContainer.accountService, - this.serviceContainer.authService, - ); - this.deleteCommand = new DeleteCommand( - this.serviceContainer.cipherService, - this.serviceContainer.folderService, - this.serviceContainer.apiService, - this.serviceContainer.folderApiService, - this.serviceContainer.billingAccountProfileStateService, - ); - this.confirmCommand = new ConfirmCommand( - this.serviceContainer.apiService, - this.serviceContainer.cryptoService, - this.serviceContainer.organizationUserService, - ); - this.restoreCommand = new RestoreCommand(this.serviceContainer.cipherService); - this.shareCommand = new ShareCommand(this.serviceContainer.cipherService); - this.lockCommand = new LockCommand(this.serviceContainer.vaultTimeoutService); - this.unlockCommand = new UnlockCommand( - this.serviceContainer.accountService, - this.serviceContainer.masterPasswordService, - this.serviceContainer.cryptoService, - this.serviceContainer.stateService, - this.serviceContainer.cryptoFunctionService, - this.serviceContainer.apiService, - this.serviceContainer.logService, - this.serviceContainer.keyConnectorService, - this.serviceContainer.environmentService, - this.serviceContainer.syncService, - this.serviceContainer.organizationApiService, - async () => await this.serviceContainer.logout(), - this.serviceContainer.kdfConfigService, - ); - - this.sendCreateCommand = new SendCreateCommand( - this.serviceContainer.sendService, - this.serviceContainer.environmentService, - this.serviceContainer.sendApiService, - this.serviceContainer.billingAccountProfileStateService, - ); - this.sendDeleteCommand = new SendDeleteCommand( - this.serviceContainer.sendService, - this.serviceContainer.sendApiService, - ); - this.sendGetCommand = new SendGetCommand( - this.serviceContainer.sendService, - this.serviceContainer.environmentService, - this.serviceContainer.searchService, - this.serviceContainer.cryptoService, - ); - this.sendEditCommand = new SendEditCommand( - this.serviceContainer.sendService, - this.sendGetCommand, - this.serviceContainer.sendApiService, - this.serviceContainer.billingAccountProfileStateService, - ); - this.sendListCommand = new SendListCommand( - this.serviceContainer.sendService, - this.serviceContainer.environmentService, - this.serviceContainer.searchService, - ); - this.sendRemovePasswordCommand = new SendRemovePasswordCommand( - this.serviceContainer.sendService, - this.serviceContainer.sendApiService, - this.serviceContainer.environmentService, - ); - } + constructor( + protected serviceContainer: ServiceContainer, + protected serveConfigurator: OssServeConfigurator, + ) {} async run(options: OptionValues) { const protectOrigin = !options.disableOriginProtection; @@ -205,207 +48,7 @@ export class ServeCommand { .use(koaBodyParser()) .use(koaJson({ pretty: false, param: "pretty" })); - router.get("/generate", async (ctx, next) => { - const response = await this.generateCommand.run(ctx.request.query); - this.processResponse(ctx.response, response); - await next(); - }); - - router.get("/status", async (ctx, next) => { - const response = await this.statusCommand.run(); - this.processResponse(ctx.response, response); - await next(); - }); - - router.get("/list/object/:object", async (ctx, next) => { - if (await this.errorIfLocked(ctx.response)) { - await next(); - return; - } - let response: Response = null; - if (ctx.params.object === "send") { - response = await this.sendListCommand.run(ctx.request.query); - } else { - response = await this.listCommand.run(ctx.params.object, ctx.request.query); - } - this.processResponse(ctx.response, response); - await next(); - }); - - router.get("/send/list", async (ctx, next) => { - if (await this.errorIfLocked(ctx.response)) { - await next(); - return; - } - const response = await this.sendListCommand.run(ctx.request.query); - this.processResponse(ctx.response, response); - await next(); - }); - - router.post("/sync", async (ctx, next) => { - const response = await this.syncCommand.run(ctx.request.query); - this.processResponse(ctx.response, response); - await next(); - }); - - router.post("/lock", async (ctx, next) => { - const response = await this.lockCommand.run(); - this.processResponse(ctx.response, response); - await next(); - }); - - router.post("/unlock", async (ctx, next) => { - // Do not allow guessing password location through serve command - delete ctx.request.query.passwordFile; - delete ctx.request.query.passwordEnv; - - const response = await this.unlockCommand.run( - ctx.request.body.password == null ? null : (ctx.request.body.password as string), - ctx.request.query, - ); - this.processResponse(ctx.response, response); - await next(); - }); - - router.post("/confirm/:object/:id", async (ctx, next) => { - if (await this.errorIfLocked(ctx.response)) { - await next(); - return; - } - const response = await this.confirmCommand.run( - ctx.params.object, - ctx.params.id, - ctx.request.query, - ); - this.processResponse(ctx.response, response); - await next(); - }); - - router.post("/restore/:object/:id", async (ctx, next) => { - if (await this.errorIfLocked(ctx.response)) { - await next(); - return; - } - const response = await this.restoreCommand.run(ctx.params.object, ctx.params.id); - this.processResponse(ctx.response, response); - await next(); - }); - - router.post("/move/:id/:organizationId", async (ctx, next) => { - if (await this.errorIfLocked(ctx.response)) { - await next(); - return; - } - const response = await this.shareCommand.run( - ctx.params.id, - ctx.params.organizationId, - ctx.request.body, // TODO: Check the format of this body for an array of collection ids - ); - this.processResponse(ctx.response, response); - await next(); - }); - - router.post("/attachment", koaMulter().single("file"), async (ctx, next) => { - if (await this.errorIfLocked(ctx.response)) { - await next(); - return; - } - const response = await this.createCommand.run( - "attachment", - ctx.request.body, - ctx.request.query, - { - fileBuffer: ctx.request.file.buffer, - fileName: ctx.request.file.originalname, - }, - ); - this.processResponse(ctx.response, response); - await next(); - }); - - router.post("/send/:id/remove-password", async (ctx, next) => { - if (await this.errorIfLocked(ctx.response)) { - await next(); - return; - } - const response = await this.sendRemovePasswordCommand.run(ctx.params.id); - this.processResponse(ctx.response, response); - await next(); - }); - - router.post("/object/:object", async (ctx, next) => { - if (await this.errorIfLocked(ctx.response)) { - await next(); - return; - } - let response: Response = null; - if (ctx.params.object === "send") { - response = await this.sendCreateCommand.run(ctx.request.body, ctx.request.query); - } else { - response = await this.createCommand.run( - ctx.params.object, - ctx.request.body, - ctx.request.query, - ); - } - this.processResponse(ctx.response, response); - await next(); - }); - - router.put("/object/:object/:id", async (ctx, next) => { - if (await this.errorIfLocked(ctx.response)) { - await next(); - return; - } - let response: Response = null; - if (ctx.params.object === "send") { - ctx.request.body.id = ctx.params.id; - response = await this.sendEditCommand.run(ctx.request.body, ctx.request.query); - } else { - response = await this.editCommand.run( - ctx.params.object, - ctx.params.id, - ctx.request.body, - ctx.request.query, - ); - } - this.processResponse(ctx.response, response); - await next(); - }); - - router.get("/object/:object/:id", async (ctx, next) => { - if (await this.errorIfLocked(ctx.response)) { - await next(); - return; - } - let response: Response = null; - if (ctx.params.object === "send") { - response = await this.sendGetCommand.run(ctx.params.id, null); - } else { - response = await this.getCommand.run(ctx.params.object, ctx.params.id, ctx.request.query); - } - this.processResponse(ctx.response, response); - await next(); - }); - - router.delete("/object/:object/:id", async (ctx, next) => { - if (await this.errorIfLocked(ctx.response)) { - await next(); - return; - } - let response: Response = null; - if (ctx.params.object === "send") { - response = await this.sendDeleteCommand.run(ctx.params.id); - } else { - response = await this.deleteCommand.run( - ctx.params.object, - ctx.params.id, - ctx.request.query, - ); - } - this.processResponse(ctx.response, response); - await next(); - }); + this.serveConfigurator.configureRouter(router); server .use(router.routes()) @@ -414,31 +57,4 @@ export class ServeCommand { this.serviceContainer.logService.info("Listening on " + hostname + ":" + port); }); } - - private processResponse(res: koa.Response, commandResponse: Response) { - if (!commandResponse.success) { - res.status = 400; - } - if (commandResponse.data instanceof FileResponse) { - res.body = commandResponse.data.data; - res.attachment(commandResponse.data.fileName); - res.set("Content-Type", "application/octet-stream"); - res.set("Content-Length", commandResponse.data.data.length.toString()); - } else { - res.body = commandResponse; - } - } - - private async errorIfLocked(res: koa.Response) { - const authed = await this.serviceContainer.stateService.getIsAuthenticated(); - if (!authed) { - this.processResponse(res, Response.error("You are not logged in.")); - return true; - } - if (await this.serviceContainer.cryptoService.hasUserKey()) { - return false; - } - this.processResponse(res, Response.error("Vault is locked.")); - return true; - } } diff --git a/apps/cli/src/oss-serve-configurator.ts b/apps/cli/src/oss-serve-configurator.ts new file mode 100644 index 0000000000..970be7a4bb --- /dev/null +++ b/apps/cli/src/oss-serve-configurator.ts @@ -0,0 +1,399 @@ +import * as koaMulter from "@koa/multer"; +import * as koaRouter from "@koa/router"; +import * as koa from "koa"; + +import { ConfirmCommand } from "./admin-console/commands/confirm.command"; +import { ShareCommand } from "./admin-console/commands/share.command"; +import { LockCommand } from "./auth/commands/lock.command"; +import { UnlockCommand } from "./auth/commands/unlock.command"; +import { EditCommand } from "./commands/edit.command"; +import { GetCommand } from "./commands/get.command"; +import { ListCommand } from "./commands/list.command"; +import { RestoreCommand } from "./commands/restore.command"; +import { StatusCommand } from "./commands/status.command"; +import { Response } from "./models/response"; +import { FileResponse } from "./models/response/file.response"; +import { ServiceContainer } from "./service-container"; +import { GenerateCommand } from "./tools/generate.command"; +import { + SendEditCommand, + SendCreateCommand, + SendDeleteCommand, + SendGetCommand, + SendListCommand, + SendRemovePasswordCommand, +} from "./tools/send"; +import { CreateCommand } from "./vault/create.command"; +import { DeleteCommand } from "./vault/delete.command"; +import { SyncCommand } from "./vault/sync.command"; + +export class OssServeConfigurator { + private listCommand: ListCommand; + private getCommand: GetCommand; + private createCommand: CreateCommand; + private editCommand: EditCommand; + private generateCommand: GenerateCommand; + private shareCommand: ShareCommand; + private statusCommand: StatusCommand; + private syncCommand: SyncCommand; + private deleteCommand: DeleteCommand; + private confirmCommand: ConfirmCommand; + private restoreCommand: RestoreCommand; + private lockCommand: LockCommand; + private unlockCommand: UnlockCommand; + + private sendCreateCommand: SendCreateCommand; + private sendDeleteCommand: SendDeleteCommand; + private sendEditCommand: SendEditCommand; + private sendGetCommand: SendGetCommand; + private sendListCommand: SendListCommand; + private sendRemovePasswordCommand: SendRemovePasswordCommand; + + constructor(protected serviceContainer: ServiceContainer) { + this.getCommand = new GetCommand( + this.serviceContainer.cipherService, + this.serviceContainer.folderService, + this.serviceContainer.collectionService, + this.serviceContainer.totpService, + this.serviceContainer.auditService, + this.serviceContainer.cryptoService, + this.serviceContainer.stateService, + this.serviceContainer.searchService, + this.serviceContainer.apiService, + this.serviceContainer.organizationService, + this.serviceContainer.eventCollectionService, + this.serviceContainer.billingAccountProfileStateService, + ); + this.listCommand = new ListCommand( + this.serviceContainer.cipherService, + this.serviceContainer.folderService, + this.serviceContainer.collectionService, + this.serviceContainer.organizationService, + this.serviceContainer.searchService, + this.serviceContainer.organizationUserService, + this.serviceContainer.apiService, + this.serviceContainer.eventCollectionService, + ); + this.createCommand = new CreateCommand( + this.serviceContainer.cipherService, + this.serviceContainer.folderService, + this.serviceContainer.cryptoService, + this.serviceContainer.apiService, + this.serviceContainer.folderApiService, + this.serviceContainer.billingAccountProfileStateService, + this.serviceContainer.organizationService, + ); + this.editCommand = new EditCommand( + this.serviceContainer.cipherService, + this.serviceContainer.folderService, + this.serviceContainer.cryptoService, + this.serviceContainer.apiService, + this.serviceContainer.folderApiService, + ); + this.generateCommand = new GenerateCommand( + this.serviceContainer.passwordGenerationService, + this.serviceContainer.stateService, + ); + this.syncCommand = new SyncCommand(this.serviceContainer.syncService); + this.statusCommand = new StatusCommand( + this.serviceContainer.environmentService, + this.serviceContainer.syncService, + this.serviceContainer.accountService, + this.serviceContainer.authService, + ); + this.deleteCommand = new DeleteCommand( + this.serviceContainer.cipherService, + this.serviceContainer.folderService, + this.serviceContainer.apiService, + this.serviceContainer.folderApiService, + this.serviceContainer.billingAccountProfileStateService, + ); + this.confirmCommand = new ConfirmCommand( + this.serviceContainer.apiService, + this.serviceContainer.cryptoService, + this.serviceContainer.organizationUserService, + ); + this.restoreCommand = new RestoreCommand(this.serviceContainer.cipherService); + this.shareCommand = new ShareCommand(this.serviceContainer.cipherService); + this.lockCommand = new LockCommand(this.serviceContainer.vaultTimeoutService); + this.unlockCommand = new UnlockCommand( + this.serviceContainer.accountService, + this.serviceContainer.masterPasswordService, + this.serviceContainer.cryptoService, + this.serviceContainer.stateService, + this.serviceContainer.cryptoFunctionService, + this.serviceContainer.apiService, + this.serviceContainer.logService, + this.serviceContainer.keyConnectorService, + this.serviceContainer.environmentService, + this.serviceContainer.syncService, + this.serviceContainer.organizationApiService, + async () => await this.serviceContainer.logout(), + this.serviceContainer.kdfConfigService, + ); + + this.sendCreateCommand = new SendCreateCommand( + this.serviceContainer.sendService, + this.serviceContainer.environmentService, + this.serviceContainer.sendApiService, + this.serviceContainer.billingAccountProfileStateService, + ); + this.sendDeleteCommand = new SendDeleteCommand( + this.serviceContainer.sendService, + this.serviceContainer.sendApiService, + ); + this.sendGetCommand = new SendGetCommand( + this.serviceContainer.sendService, + this.serviceContainer.environmentService, + this.serviceContainer.searchService, + this.serviceContainer.cryptoService, + ); + this.sendEditCommand = new SendEditCommand( + this.serviceContainer.sendService, + this.sendGetCommand, + this.serviceContainer.sendApiService, + this.serviceContainer.billingAccountProfileStateService, + ); + this.sendListCommand = new SendListCommand( + this.serviceContainer.sendService, + this.serviceContainer.environmentService, + this.serviceContainer.searchService, + ); + this.sendRemovePasswordCommand = new SendRemovePasswordCommand( + this.serviceContainer.sendService, + this.serviceContainer.sendApiService, + this.serviceContainer.environmentService, + ); + } + + configureRouter(router: koaRouter) { + router.get("/generate", async (ctx, next) => { + const response = await this.generateCommand.run(ctx.request.query); + this.processResponse(ctx.response, response); + await next(); + }); + + router.get("/status", async (ctx, next) => { + const response = await this.statusCommand.run(); + this.processResponse(ctx.response, response); + await next(); + }); + + router.get("/list/object/:object", async (ctx, next) => { + if (await this.errorIfLocked(ctx.response)) { + await next(); + return; + } + let response: Response = null; + if (ctx.params.object === "send") { + response = await this.sendListCommand.run(ctx.request.query); + } else { + response = await this.listCommand.run(ctx.params.object, ctx.request.query); + } + this.processResponse(ctx.response, response); + await next(); + }); + + router.get("/send/list", async (ctx, next) => { + if (await this.errorIfLocked(ctx.response)) { + await next(); + return; + } + const response = await this.sendListCommand.run(ctx.request.query); + this.processResponse(ctx.response, response); + await next(); + }); + + router.post("/sync", async (ctx, next) => { + const response = await this.syncCommand.run(ctx.request.query); + this.processResponse(ctx.response, response); + await next(); + }); + + router.post("/lock", async (ctx, next) => { + const response = await this.lockCommand.run(); + this.processResponse(ctx.response, response); + await next(); + }); + + router.post("/unlock", async (ctx, next) => { + // Do not allow guessing password location through serve command + delete ctx.request.query.passwordFile; + delete ctx.request.query.passwordEnv; + + const response = await this.unlockCommand.run( + ctx.request.body.password == null ? null : (ctx.request.body.password as string), + ctx.request.query, + ); + this.processResponse(ctx.response, response); + await next(); + }); + + router.post("/confirm/:object/:id", async (ctx, next) => { + if (await this.errorIfLocked(ctx.response)) { + await next(); + return; + } + const response = await this.confirmCommand.run( + ctx.params.object, + ctx.params.id, + ctx.request.query, + ); + this.processResponse(ctx.response, response); + await next(); + }); + + router.post("/restore/:object/:id", async (ctx, next) => { + if (await this.errorIfLocked(ctx.response)) { + await next(); + return; + } + const response = await this.restoreCommand.run(ctx.params.object, ctx.params.id); + this.processResponse(ctx.response, response); + await next(); + }); + + router.post("/move/:id/:organizationId", async (ctx, next) => { + if (await this.errorIfLocked(ctx.response)) { + await next(); + return; + } + const response = await this.shareCommand.run( + ctx.params.id, + ctx.params.organizationId, + ctx.request.body, // TODO: Check the format of this body for an array of collection ids + ); + this.processResponse(ctx.response, response); + await next(); + }); + + router.post("/attachment", koaMulter().single("file"), async (ctx, next) => { + if (await this.errorIfLocked(ctx.response)) { + await next(); + return; + } + const response = await this.createCommand.run( + "attachment", + ctx.request.body, + ctx.request.query, + { + fileBuffer: ctx.request.file.buffer, + fileName: ctx.request.file.originalname, + }, + ); + this.processResponse(ctx.response, response); + await next(); + }); + + router.post("/send/:id/remove-password", async (ctx, next) => { + if (await this.errorIfLocked(ctx.response)) { + await next(); + return; + } + const response = await this.sendRemovePasswordCommand.run(ctx.params.id); + this.processResponse(ctx.response, response); + await next(); + }); + + router.post("/object/:object", async (ctx, next) => { + if (await this.errorIfLocked(ctx.response)) { + await next(); + return; + } + let response: Response = null; + if (ctx.params.object === "send") { + response = await this.sendCreateCommand.run(ctx.request.body, ctx.request.query); + } else { + response = await this.createCommand.run( + ctx.params.object, + ctx.request.body, + ctx.request.query, + ); + } + this.processResponse(ctx.response, response); + await next(); + }); + + router.put("/object/:object/:id", async (ctx, next) => { + if (await this.errorIfLocked(ctx.response)) { + await next(); + return; + } + let response: Response = null; + if (ctx.params.object === "send") { + ctx.request.body.id = ctx.params.id; + response = await this.sendEditCommand.run(ctx.request.body, ctx.request.query); + } else { + response = await this.editCommand.run( + ctx.params.object, + ctx.params.id, + ctx.request.body, + ctx.request.query, + ); + } + this.processResponse(ctx.response, response); + await next(); + }); + + router.get("/object/:object/:id", async (ctx, next) => { + if (await this.errorIfLocked(ctx.response)) { + await next(); + return; + } + let response: Response = null; + if (ctx.params.object === "send") { + response = await this.sendGetCommand.run(ctx.params.id, null); + } else { + response = await this.getCommand.run(ctx.params.object, ctx.params.id, ctx.request.query); + } + this.processResponse(ctx.response, response); + await next(); + }); + + router.delete("/object/:object/:id", async (ctx, next) => { + if (await this.errorIfLocked(ctx.response)) { + await next(); + return; + } + let response: Response = null; + if (ctx.params.object === "send") { + response = await this.sendDeleteCommand.run(ctx.params.id); + } else { + response = await this.deleteCommand.run( + ctx.params.object, + ctx.params.id, + ctx.request.query, + ); + } + this.processResponse(ctx.response, response); + await next(); + }); + } + + protected processResponse(res: koa.Response, commandResponse: Response) { + if (!commandResponse.success) { + res.status = 400; + } + if (commandResponse.data instanceof FileResponse) { + res.body = commandResponse.data.data; + res.attachment(commandResponse.data.fileName); + res.set("Content-Type", "application/octet-stream"); + res.set("Content-Length", commandResponse.data.data.length.toString()); + } else { + res.body = commandResponse; + } + } + + protected async errorIfLocked(res: koa.Response) { + const authed = await this.serviceContainer.stateService.getIsAuthenticated(); + if (!authed) { + this.processResponse(res, Response.error("You are not logged in.")); + return true; + } + if (await this.serviceContainer.cryptoService.hasUserKey()) { + return false; + } + this.processResponse(res, Response.error("Vault is locked.")); + return true; + } +} diff --git a/apps/cli/src/program.ts b/apps/cli/src/program.ts index 597b388a05..b8ddca11de 100644 --- a/apps/cli/src/program.ts +++ b/apps/cli/src/program.ts @@ -12,7 +12,6 @@ import { BaseProgram } from "./base-program"; import { CompletionCommand } from "./commands/completion.command"; import { ConfigCommand } from "./commands/config.command"; import { EncodeCommand } from "./commands/encode.command"; -import { ServeCommand } from "./commands/serve.command"; import { StatusCommand } from "./commands/status.command"; import { UpdateCommand } from "./commands/update.command"; import { Response } from "./models/response"; @@ -487,34 +486,5 @@ export class Program extends BaseProgram { const response = await command.run(); this.processResponse(response); }); - - program - .command("serve") - .description("Start a RESTful API webserver.") - .option("--hostname ", "The hostname to bind your API webserver to.") - .option("--port ", "The port to run your API webserver on.") - .option( - "--disable-origin-protection", - "If set, allows requests with origin header. Warning, this option exists for backwards compatibility reasons and exposes your environment to known CSRF attacks.", - ) - .on("--help", () => { - writeLn("\n Notes:"); - writeLn(""); - writeLn(" Default hostname is `localhost`."); - writeLn(" Use hostname `all` for no hostname binding."); - writeLn(" Default port is `8087`."); - writeLn(""); - writeLn(" Examples:"); - writeLn(""); - writeLn(" bw serve"); - writeLn(" bw serve --port 8080"); - writeLn(" bw serve --hostname bwapi.mydomain.com --port 80"); - writeLn("", true); - }) - .action(async (cmd) => { - await this.exitIfNotAuthed(); - const command = new ServeCommand(this.serviceContainer); - await command.run(cmd); - }); } } diff --git a/apps/cli/src/serve.program.ts b/apps/cli/src/serve.program.ts new file mode 100644 index 0000000000..bbf66661e5 --- /dev/null +++ b/apps/cli/src/serve.program.ts @@ -0,0 +1,49 @@ +import { program } from "commander"; + +import { BaseProgram } from "./base-program"; +import { ServeCommand } from "./commands/serve.command"; +import { OssServeConfigurator } from "./oss-serve-configurator"; +import { ServiceContainer } from "./service-container"; +import { CliUtils } from "./utils"; + +const writeLn = CliUtils.writeLn; + +export class ServeProgram extends BaseProgram { + constructor( + serviceContainer: ServiceContainer, + private configurator: OssServeConfigurator, + ) { + super(serviceContainer); + } + + register() { + program + .command("serve") + .description("Start a RESTful API webserver.") + .option("--hostname ", "The hostname to bind your API webserver to.") + .option("--port ", "The port to run your API webserver on.") + .option( + "--disable-origin-protection", + "If set, allows requests with origin header. Warning, this option exists for backwards compatibility reasons and exposes your environment to known CSRF attacks.", + ) + .on("--help", () => { + writeLn("\n Notes:"); + writeLn(""); + writeLn(" Default hostname is `localhost`."); + writeLn(" Use hostname `all` for no hostname binding."); + writeLn(" Default port is `8087`."); + writeLn(""); + writeLn(" Examples:"); + writeLn(""); + writeLn(" bw serve"); + writeLn(" bw serve --port 8080"); + writeLn(" bw serve --hostname bwapi.mydomain.com --port 80"); + writeLn("", true); + }) + .action(async (cmd) => { + await this.exitIfNotAuthed(); + const command = new ServeCommand(this.serviceContainer, this.configurator); + await command.run(cmd); + }); + } +} diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/approve-all.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/approve-all.command.ts index 3214a0fc41..bb00c50ab1 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/approve-all.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/approve-all.command.ts @@ -6,6 +6,8 @@ import { MessageResponse } from "@bitwarden/cli/models/response/message.response import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { ServiceContainer } from "../../service-container"; + export class ApproveAllCommand { constructor( private organizationAuthRequestService: OrganizationAuthRequestService, @@ -49,4 +51,11 @@ export class ApproveAllCommand { return Response.error(e); } } + + static create(serviceContainer: ServiceContainer) { + return new ApproveAllCommand( + serviceContainer.organizationAuthRequestService, + serviceContainer.organizationService, + ); + } } diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/approve.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/approve.command.ts index 8efa172296..918bd077b0 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/approve.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/approve.command.ts @@ -5,6 +5,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { Utils } from "@bitwarden/common/platform/misc/utils"; import { OrganizationAuthRequestService } from "../../../../bit-common/src/admin-console/auth-requests"; +import { ServiceContainer } from "../../service-container"; export class ApproveCommand { constructor( @@ -51,4 +52,11 @@ export class ApproveCommand { return Response.error(e); } } + + static create(serviceContainer: ServiceContainer) { + return new ApproveCommand( + serviceContainer.organizationService, + serviceContainer.organizationAuthRequestService, + ); + } } diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/deny-all.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/deny-all.command.ts index 59cc4235eb..db73773f08 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/deny-all.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/deny-all.command.ts @@ -6,6 +6,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { Utils } from "@bitwarden/common/platform/misc/utils"; import { OrganizationAuthRequestService } from "../../../../bit-common/src/admin-console/auth-requests"; +import { ServiceContainer } from "../../service-container"; export class DenyAllCommand { constructor( @@ -46,4 +47,11 @@ export class DenyAllCommand { return Response.error(e); } } + + static create(serviceContainer: ServiceContainer) { + return new DenyAllCommand( + serviceContainer.organizationService, + serviceContainer.organizationAuthRequestService, + ); + } } diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/deny.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/deny.command.ts index a9676d3fc5..3470baaa25 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/deny.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/deny.command.ts @@ -5,6 +5,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { Utils } from "@bitwarden/common/platform/misc/utils"; import { OrganizationAuthRequestService } from "../../../../bit-common/src/admin-console/auth-requests"; +import { ServiceContainer } from "../../service-container"; export class DenyCommand { constructor( @@ -43,4 +44,11 @@ export class DenyCommand { return Response.error(e); } } + + static create(serviceContainer: ServiceContainer) { + return new DenyCommand( + serviceContainer.organizationService, + serviceContainer.organizationAuthRequestService, + ); + } } diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/device-approval.program.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/device-approval.program.ts index 408a5b8d81..984bd15cde 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/device-approval.program.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/device-approval.program.ts @@ -42,11 +42,7 @@ export class DeviceApprovalProgram extends BaseProgram { await this.exitIfFeatureFlagDisabled(FeatureFlag.BulkDeviceApproval); await this.exitIfLocked(); - const cmd = new ListCommand( - this.serviceContainer.organizationAuthRequestService, - this.serviceContainer.organizationService, - ); - + const cmd = ListCommand.create(this.serviceContainer); const response = await cmd.run(options.organizationid); this.processResponse(response); }); @@ -61,10 +57,7 @@ export class DeviceApprovalProgram extends BaseProgram { await this.exitIfFeatureFlagDisabled(FeatureFlag.BulkDeviceApproval); await this.exitIfLocked(); - const cmd = new ApproveCommand( - this.serviceContainer.organizationService, - this.serviceContainer.organizationAuthRequestService, - ); + const cmd = ApproveCommand.create(this.serviceContainer); const response = await cmd.run(options.organizationid, id); this.processResponse(response); }); @@ -78,10 +71,7 @@ export class DeviceApprovalProgram extends BaseProgram { await this.exitIfFeatureFlagDisabled(FeatureFlag.BulkDeviceApproval); await this.exitIfLocked(); - const cmd = new ApproveAllCommand( - this.serviceContainer.organizationAuthRequestService, - this.serviceContainer.organizationService, - ); + const cmd = ApproveAllCommand.create(this.serviceContainer); const response = await cmd.run(options.organizationid); this.processResponse(response); }); @@ -96,10 +86,7 @@ export class DeviceApprovalProgram extends BaseProgram { await this.exitIfFeatureFlagDisabled(FeatureFlag.BulkDeviceApproval); await this.exitIfLocked(); - const cmd = new DenyCommand( - this.serviceContainer.organizationService, - this.serviceContainer.organizationAuthRequestService, - ); + const cmd = DenyCommand.create(this.serviceContainer); const response = await cmd.run(options.organizationid, id); this.processResponse(response); }); @@ -113,10 +100,7 @@ export class DeviceApprovalProgram extends BaseProgram { await this.exitIfFeatureFlagDisabled(FeatureFlag.BulkDeviceApproval); await this.exitIfLocked(); - const cmd = new DenyAllCommand( - this.serviceContainer.organizationService, - this.serviceContainer.organizationAuthRequestService, - ); + const cmd = DenyAllCommand.create(this.serviceContainer); const response = await cmd.run(options.organizationid); this.processResponse(response); }); diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/index.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/index.ts index 399f89623e..0482c8caf1 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/index.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/index.ts @@ -1 +1,6 @@ -export { DeviceApprovalProgram } from "./device-approval.program"; +export * from "./device-approval.program"; +export * from "./approve.command"; +export * from "./approve-all.command"; +export * from "./deny.command"; +export * from "./deny-all.command"; +export * from "./list.command"; diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts index 10da11b35c..972be460df 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts @@ -6,6 +6,8 @@ import { ListResponse } from "@bitwarden/cli/models/response/list.response"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { ServiceContainer } from "../../service-container"; + import { PendingAuthRequestResponse } from "./pending-auth-request.response"; export class ListCommand { @@ -39,4 +41,11 @@ export class ListCommand { return Response.error(e); } } + + static create(serviceContainer: ServiceContainer) { + return new ListCommand( + serviceContainer.organizationAuthRequestService, + serviceContainer.organizationService, + ); + } } diff --git a/bitwarden_license/bit-cli/src/bit-serve-configurator.ts b/bitwarden_license/bit-cli/src/bit-serve-configurator.ts new file mode 100644 index 0000000000..c669eb7092 --- /dev/null +++ b/bitwarden_license/bit-cli/src/bit-serve-configurator.ts @@ -0,0 +1,95 @@ +import * as koaRouter from "@koa/router"; + +import { OssServeConfigurator } from "@bitwarden/cli/oss-serve-configurator"; + +import { + ApproveAllCommand, + ApproveCommand, + DenyAllCommand, + DenyCommand, + ListCommand, +} from "./admin-console/device-approval"; +import { ServiceContainer } from "./service-container"; + +export class BitServeConfigurator extends OssServeConfigurator { + constructor(protected override serviceContainer: ServiceContainer) { + super(serviceContainer); + } + + override configureRouter(router: koaRouter): void { + // Register OSS endpoints + super.configureRouter(router); + + // Register bit endpoints + this.serveDeviceApprovals(router); + } + + private serveDeviceApprovals(router: koaRouter) { + router.get("/device-approval/:organizationId", async (ctx, next) => { + if (await this.errorIfLocked(ctx.response)) { + await next(); + return; + } + + const response = await ListCommand.create(this.serviceContainer).run( + ctx.params.organizationId, + ); + this.processResponse(ctx.response, response); + await next(); + }); + + router.post("/device-approval/:organizationId/approve-all", async (ctx, next) => { + if (await this.errorIfLocked(ctx.response)) { + await next(); + return; + } + + const response = await ApproveAllCommand.create(this.serviceContainer).run( + ctx.params.organizationId, + ); + this.processResponse(ctx.response, response); + await next(); + }); + + router.post("/device-approval/:organizationId/approve/:requestId", async (ctx, next) => { + if (await this.errorIfLocked(ctx.response)) { + await next(); + return; + } + + const response = await ApproveCommand.create(this.serviceContainer).run( + ctx.params.organizationId, + ctx.params.requestId, + ); + this.processResponse(ctx.response, response); + await next(); + }); + + router.post("/device-approval/:organizationId/deny-all", async (ctx, next) => { + if (await this.errorIfLocked(ctx.response)) { + await next(); + return; + } + + const response = await DenyAllCommand.create(this.serviceContainer).run( + ctx.params.organizationId, + ); + this.processResponse(ctx.response, response); + await next(); + }); + + router.post("/device-approval/:organizationId/deny/:requestId", async (ctx, next) => { + if (await this.errorIfLocked(ctx.response)) { + await next(); + return; + } + + const response = await DenyCommand.create(this.serviceContainer).run( + ctx.params.organizationId, + ctx.params.requestId, + ); + this.processResponse(ctx.response, response); + await next(); + }); + } +} diff --git a/bitwarden_license/bit-cli/src/bw.ts b/bitwarden_license/bit-cli/src/bw.ts index d6ebcaf041..ffbc186d9e 100644 --- a/bitwarden_license/bit-cli/src/bw.ts +++ b/bitwarden_license/bit-cli/src/bw.ts @@ -1,7 +1,9 @@ import { program } from "commander"; import { registerOssPrograms } from "@bitwarden/cli/register-oss-programs"; +import { ServeProgram } from "@bitwarden/cli/serve.program"; +import { BitServeConfigurator } from "./bit-serve-configurator"; import { registerBitPrograms } from "./register-bit-programs"; import { ServiceContainer } from "./service-container"; @@ -12,6 +14,9 @@ async function main() { await registerOssPrograms(serviceContainer); await registerBitPrograms(serviceContainer); + const serveConfigurator = new BitServeConfigurator(serviceContainer); + new ServeProgram(serviceContainer, serveConfigurator).register(); + program.parse(process.argv); }