diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2ec9d3b1b40..118f9dab277 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -52,6 +52,7 @@ apps/cli/src/admin-console @bitwarden/team-admin-console-dev apps/desktop/src/admin-console @bitwarden/team-admin-console-dev apps/web/src/app/admin-console @bitwarden/team-admin-console-dev bitwarden_license/bit-web/src/app/admin-console @bitwarden/team-admin-console-dev +bitwarden_license/bit-cli/src/admin-console @bitwarden/team-admin-console-dev libs/angular/src/admin-console @bitwarden/team-admin-console-dev libs/common/src/admin-console @bitwarden/team-admin-console-dev libs/admin-console @bitwarden/team-admin-console-dev diff --git a/apps/cli/src/base-program.ts b/apps/cli/src/base-program.ts new file mode 100644 index 00000000000..46aadc323c3 --- /dev/null +++ b/apps/cli/src/base-program.ts @@ -0,0 +1,173 @@ +import * as chalk from "chalk"; +import { firstValueFrom, map } from "rxjs"; + +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; + +import { UnlockCommand } from "./auth/commands/unlock.command"; +import { Response } from "./models/response"; +import { ListResponse } from "./models/response/list.response"; +import { MessageResponse } from "./models/response/message.response"; +import { StringResponse } from "./models/response/string.response"; +import { TemplateResponse } from "./models/response/template.response"; +import { ServiceContainer } from "./service-container"; +import { CliUtils } from "./utils"; + +const writeLn = CliUtils.writeLn; + +export abstract class BaseProgram { + constructor(protected serviceContainer: ServiceContainer) {} + + protected processResponse(response: Response, exitImmediately = false) { + if (!response.success) { + if (process.env.BW_QUIET !== "true") { + if (process.env.BW_RESPONSE === "true") { + writeLn(this.getJson(response), true, false); + } else { + writeLn(chalk.redBright(response.message), true, true); + } + } + const exitCode = process.env.BW_CLEANEXIT ? 0 : 1; + if (exitImmediately) { + process.exit(exitCode); + } else { + process.exitCode = exitCode; + } + return; + } + + if (process.env.BW_RESPONSE === "true") { + writeLn(this.getJson(response), true, false); + } else if (response.data != null) { + let out: string = null; + + if (response.data.object === "template") { + out = this.getJson((response.data as TemplateResponse).template); + } + + if (out == null) { + if (response.data.object === "string") { + const data = (response.data as StringResponse).data; + if (data != null) { + out = data; + } + } else if (response.data.object === "list") { + out = this.getJson((response.data as ListResponse).data); + } else if (response.data.object === "message") { + out = this.getMessage(response); + } else { + out = this.getJson(response.data); + } + } + + if (out != null && process.env.BW_QUIET !== "true") { + writeLn(out, true, false); + } + } + if (exitImmediately) { + process.exit(0); + } else { + process.exitCode = 0; + } + } + + private getJson(obj: any): string { + if (process.env.BW_PRETTY === "true") { + return JSON.stringify(obj, null, " "); + } else { + return JSON.stringify(obj); + } + } + + protected getMessage(response: Response): string { + const message = response.data as MessageResponse; + if (process.env.BW_RAW === "true") { + return message.raw; + } + + let out = ""; + if (message.title != null) { + if (message.noColor) { + out = message.title; + } else { + out = chalk.greenBright(message.title); + } + } + if (message.message != null) { + if (message.title != null) { + out += "\n"; + } + out += message.message; + } + return out.trim() === "" ? null : out; + } + + protected async exitIfAuthed() { + const authed = await firstValueFrom( + this.serviceContainer.authService.activeAccountStatus$.pipe( + map((status) => status > AuthenticationStatus.LoggedOut), + ), + ); + if (authed) { + const email = await firstValueFrom( + this.serviceContainer.accountService.activeAccount$.pipe(map((a) => a?.email)), + ); + this.processResponse(Response.error("You are already logged in as " + email + "."), true); + } + } + + protected async exitIfNotAuthed() { + const authed = await this.serviceContainer.stateService.getIsAuthenticated(); + if (!authed) { + this.processResponse(Response.error("You are not logged in."), true); + } + } + + protected async exitIfLocked() { + await this.exitIfNotAuthed(); + if (await this.serviceContainer.cryptoService.hasUserKey()) { + return; + } else if (process.env.BW_NOINTERACTION !== "true") { + // must unlock + if (await this.serviceContainer.keyConnectorService.getUsesKeyConnector()) { + const response = Response.error( + "Your vault is locked. You must unlock your vault using your session key.\n" + + "If you do not have your session key, you can get a new one by logging out and logging in again.", + ); + this.processResponse(response, true); + } else { + const command = 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, + this.serviceContainer.logout, + this.serviceContainer.kdfConfigService, + ); + const response = await command.run(null, null); + if (!response.success) { + this.processResponse(response, true); + } + } + } else { + this.processResponse(Response.error("Vault is locked."), true); + } + } + + protected async exitIfFeatureFlagDisabled(featureFlag: FeatureFlag) { + const enabled = await firstValueFrom( + this.serviceContainer.configService.getFeatureFlag$(featureFlag), + ); + + if (!enabled) { + this.processResponse(Response.error("This command is temporarily unavailable."), true); + } + } +} diff --git a/apps/cli/src/program.ts b/apps/cli/src/program.ts index 667e0f683fd..e0311beb247 100644 --- a/apps/cli/src/program.ts +++ b/apps/cli/src/program.ts @@ -1,6 +1,6 @@ import * as chalk from "chalk"; import { program, Command, OptionValues } from "commander"; -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -8,6 +8,7 @@ import { LockCommand } from "./auth/commands/lock.command"; import { LoginCommand } from "./auth/commands/login.command"; import { LogoutCommand } from "./auth/commands/logout.command"; import { UnlockCommand } from "./auth/commands/unlock.command"; +import { BaseProgram } from "./base-program"; import { CompletionCommand } from "./commands/completion.command"; import { ConfigCommand } from "./commands/config.command"; import { EncodeCommand } from "./commands/encode.command"; @@ -15,20 +16,14 @@ import { ServeCommand } from "./commands/serve.command"; import { StatusCommand } from "./commands/status.command"; import { UpdateCommand } from "./commands/update.command"; import { Response } from "./models/response"; -import { ListResponse } from "./models/response/list.response"; import { MessageResponse } from "./models/response/message.response"; -import { StringResponse } from "./models/response/string.response"; -import { TemplateResponse } from "./models/response/template.response"; -import { ServiceContainer } from "./service-container"; import { GenerateCommand } from "./tools/generate.command"; import { CliUtils } from "./utils"; import { SyncCommand } from "./vault/sync.command"; const writeLn = CliUtils.writeLn; -export class Program { - constructor(protected serviceContainer: ServiceContainer) {} - +export class Program extends BaseProgram { async register() { program .option("--pretty", "Format output. JSON is tabbed with two spaces.") @@ -517,147 +512,4 @@ export class Program { await command.run(cmd); }); } - - protected processResponse(response: Response, exitImmediately = false) { - if (!response.success) { - if (process.env.BW_QUIET !== "true") { - if (process.env.BW_RESPONSE === "true") { - writeLn(this.getJson(response), true, false); - } else { - writeLn(chalk.redBright(response.message), true, true); - } - } - const exitCode = process.env.BW_CLEANEXIT ? 0 : 1; - if (exitImmediately) { - process.exit(exitCode); - } else { - process.exitCode = exitCode; - } - return; - } - - if (process.env.BW_RESPONSE === "true") { - writeLn(this.getJson(response), true, false); - } else if (response.data != null) { - let out: string = null; - - if (response.data.object === "template") { - out = this.getJson((response.data as TemplateResponse).template); - } - - if (out == null) { - if (response.data.object === "string") { - const data = (response.data as StringResponse).data; - if (data != null) { - out = data; - } - } else if (response.data.object === "list") { - out = this.getJson((response.data as ListResponse).data); - } else if (response.data.object === "message") { - out = this.getMessage(response); - } else { - out = this.getJson(response.data); - } - } - - if (out != null && process.env.BW_QUIET !== "true") { - writeLn(out, true, false); - } - } - if (exitImmediately) { - process.exit(0); - } else { - process.exitCode = 0; - } - } - - private getJson(obj: any): string { - if (process.env.BW_PRETTY === "true") { - return JSON.stringify(obj, null, " "); - } else { - return JSON.stringify(obj); - } - } - - private getMessage(response: Response): string { - const message = response.data as MessageResponse; - if (process.env.BW_RAW === "true") { - return message.raw; - } - - let out = ""; - if (message.title != null) { - if (message.noColor) { - out = message.title; - } else { - out = chalk.greenBright(message.title); - } - } - if (message.message != null) { - if (message.title != null) { - out += "\n"; - } - out += message.message; - } - return out.trim() === "" ? null : out; - } - - private async exitIfAuthed() { - const authed = await firstValueFrom( - this.serviceContainer.authService.activeAccountStatus$.pipe( - map((status) => status > AuthenticationStatus.LoggedOut), - ), - ); - if (authed) { - const email = await firstValueFrom( - this.serviceContainer.accountService.activeAccount$.pipe(map((a) => a?.email)), - ); - this.processResponse(Response.error("You are already logged in as " + email + "."), true); - } - } - - private async exitIfNotAuthed() { - const authed = await this.serviceContainer.stateService.getIsAuthenticated(); - if (!authed) { - this.processResponse(Response.error("You are not logged in."), true); - } - } - - protected async exitIfLocked() { - await this.exitIfNotAuthed(); - if (await this.serviceContainer.cryptoService.hasUserKey()) { - return; - } else if (process.env.BW_NOINTERACTION !== "true") { - // must unlock - if (await this.serviceContainer.keyConnectorService.getUsesKeyConnector()) { - const response = Response.error( - "Your vault is locked. You must unlock your vault using your session key.\n" + - "If you do not have your session key, you can get a new one by logging out and logging in again.", - ); - this.processResponse(response, true); - } else { - const command = 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, - this.serviceContainer.logout, - this.serviceContainer.kdfConfigService, - ); - const response = await command.run(null, null); - if (!response.success) { - this.processResponse(response, true); - } - } - } else { - this.processResponse(Response.error("Vault is locked."), true); - } - } } diff --git a/apps/cli/src/register-oss-programs.ts b/apps/cli/src/register-oss-programs.ts index f47aa528543..d8aa54118d7 100644 --- a/apps/cli/src/register-oss-programs.ts +++ b/apps/cli/src/register-oss-programs.ts @@ -15,8 +15,8 @@ export async function registerOssPrograms(serviceContainer: ServiceContainer) { await program.register(); const vaultProgram = new VaultProgram(serviceContainer); - await vaultProgram.register(); + vaultProgram.register(); const sendProgram = new SendProgram(serviceContainer); - await sendProgram.register(); + sendProgram.register(); } diff --git a/apps/cli/src/tools/send/send.program.ts b/apps/cli/src/tools/send/send.program.ts index 86edd28f098..670683e7a2a 100644 --- a/apps/cli/src/tools/send/send.program.ts +++ b/apps/cli/src/tools/send/send.program.ts @@ -7,9 +7,9 @@ import { program, Command, OptionValues } from "commander"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; +import { BaseProgram } from "../../base-program"; import { GetCommand } from "../../commands/get.command"; import { Response } from "../../models/response"; -import { Program } from "../../program"; import { CliUtils } from "../../utils"; import { @@ -27,8 +27,8 @@ import { SendResponse } from "./models/send.response"; const writeLn = CliUtils.writeLn; -export class SendProgram extends Program { - async register() { +export class SendProgram extends BaseProgram { + register() { program.addCommand(this.sendCommand()); // receive is accessible both at `bw receive` and `bw send receive` program.addCommand(this.receiveCommand()); diff --git a/apps/cli/src/vault.program.ts b/apps/cli/src/vault.program.ts index 52857ed5424..c8e0701845b 100644 --- a/apps/cli/src/vault.program.ts +++ b/apps/cli/src/vault.program.ts @@ -2,12 +2,12 @@ import { program, Command } from "commander"; import { ConfirmCommand } from "./admin-console/commands/confirm.command"; import { ShareCommand } from "./admin-console/commands/share.command"; +import { BaseProgram } from "./base-program"; 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 { Response } from "./models/response"; -import { Program } from "./program"; import { ExportCommand } from "./tools/export.command"; import { ImportCommand } from "./tools/import.command"; import { CliUtils } from "./utils"; @@ -16,8 +16,8 @@ import { DeleteCommand } from "./vault/delete.command"; const writeLn = CliUtils.writeLn; -export class VaultProgram extends Program { - async register() { +export class VaultProgram extends BaseProgram { + register() { program .addCommand(this.listCommand()) .addCommand(this.getCommand()) 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 new file mode 100644 index 00000000000..a3a6c4943f8 --- /dev/null +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/approve-all.command.ts @@ -0,0 +1,9 @@ +import { Response } from "@bitwarden/cli/models/response"; + +export class ApproveAllCommand { + constructor() {} + + async run(organizationId: string): Promise { + throw new Error("Not implemented"); + } +} 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 new file mode 100644 index 00000000000..b3a30165ce3 --- /dev/null +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/approve.command.ts @@ -0,0 +1,9 @@ +import { Response } from "@bitwarden/cli/models/response"; + +export class ApproveCommand { + constructor() {} + + async run(id: string): Promise { + throw new Error("Not implemented"); + } +} 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 new file mode 100644 index 00000000000..521a7e8ded6 --- /dev/null +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/deny-all.command.ts @@ -0,0 +1,9 @@ +import { Response } from "@bitwarden/cli/models/response"; + +export class DenyAllCommand { + constructor() {} + + async run(organizationId: string): Promise { + throw new Error("Not implemented"); + } +} 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 new file mode 100644 index 00000000000..a366bfb05a0 --- /dev/null +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/deny.command.ts @@ -0,0 +1,9 @@ +import { Response } from "@bitwarden/cli/models/response"; + +export class DenyCommand { + constructor() {} + + async run(id: string): Promise { + throw new Error("Not implemented"); + } +} 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 new file mode 100644 index 00000000000..342ea9bc52e --- /dev/null +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/device-approval.program.ts @@ -0,0 +1,96 @@ +import { program, Command } from "commander"; + +import { BaseProgram } from "@bitwarden/cli/base-program"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; + +import { ApproveAllCommand } from "./approve-all.command"; +import { ApproveCommand } from "./approve.command"; +import { DenyAllCommand } from "./deny-all.command"; +import { DenyCommand } from "./deny.command"; +import { ListCommand } from "./list.command"; + +export class DeviceApprovalProgram extends BaseProgram { + register() { + program.addCommand(this.deviceApprovalCommand()); + } + + private deviceApprovalCommand() { + return new Command("device-approval") + .description("Manage device approvals") + .addCommand(this.listCommand()) + .addCommand(this.approveCommand()) + .addCommand(this.approveAllCommand()) + .addCommand(this.denyCommand()) + .addCommand(this.denyAllCommand()); + } + + private listCommand(): Command { + return new Command("list") + .description("List all pending requests for an organization") + .argument("") + .action(async (organizationId: string) => { + await this.exitIfFeatureFlagDisabled(FeatureFlag.BulkDeviceApproval); + await this.exitIfNotAuthed(); + + const cmd = new ListCommand(); + const response = await cmd.run(organizationId); + this.processResponse(response); + }); + } + + private approveCommand(): Command { + return new Command("approve") + .argument("") + .description("Approve a pending request") + .action(async (id: string) => { + await this.exitIfFeatureFlagDisabled(FeatureFlag.BulkDeviceApproval); + await this.exitIfLocked(); + + const cmd = new ApproveCommand(); + const response = await cmd.run(id); + this.processResponse(response); + }); + } + + private approveAllCommand(): Command { + return new Command("approveAll") + .description("Approve all pending requests for an organization") + .argument("") + .action(async (organizationId: string) => { + await this.exitIfFeatureFlagDisabled(FeatureFlag.BulkDeviceApproval); + await this.exitIfLocked(); + + const cmd = new ApproveAllCommand(); + const response = await cmd.run(organizationId); + this.processResponse(response); + }); + } + + private denyCommand(): Command { + return new Command("deny") + .argument("") + .description("Deny a pending request") + .action(async (id: string) => { + await this.exitIfFeatureFlagDisabled(FeatureFlag.BulkDeviceApproval); + await this.exitIfLocked(); + + const cmd = new DenyCommand(); + const response = await cmd.run(id); + this.processResponse(response); + }); + } + + private denyAllCommand(): Command { + return new Command("denyAll") + .description("Deny all pending requests for an organization") + .argument("") + .action(async (organizationId: string) => { + await this.exitIfFeatureFlagDisabled(FeatureFlag.BulkDeviceApproval); + await this.exitIfLocked(); + + const cmd = new DenyAllCommand(); + const response = await cmd.run(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 new file mode 100644 index 00000000000..399f89623ec --- /dev/null +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/index.ts @@ -0,0 +1 @@ +export { DeviceApprovalProgram } from "./device-approval.program"; 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 new file mode 100644 index 00000000000..11fb6ec3ee2 --- /dev/null +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts @@ -0,0 +1,9 @@ +import { Response } from "@bitwarden/cli/models/response"; + +export class ListCommand { + constructor() {} + + async run(organizationId: string): Promise { + throw new Error("Not implemented"); + } +} diff --git a/bitwarden_license/bit-cli/src/register-bit-programs.ts b/bitwarden_license/bit-cli/src/register-bit-programs.ts index 859574644ad..0d3f5e39f25 100644 --- a/bitwarden_license/bit-cli/src/register-bit-programs.ts +++ b/bitwarden_license/bit-cli/src/register-bit-programs.ts @@ -1,3 +1,4 @@ +import { DeviceApprovalProgram } from "./admin-console/device-approval"; import { ServiceContainer } from "./service-container"; /** @@ -7,4 +8,6 @@ import { ServiceContainer } from "./service-container"; * myProgram.register(); * @param serviceContainer A class that instantiates services and makes them available for dependency injection */ -export async function registerBitPrograms(serviceContainer: ServiceContainer) {} +export async function registerBitPrograms(serviceContainer: ServiceContainer) { + new DeviceApprovalProgram(serviceContainer).register(); +} diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index af022f4e549..f3bea6964ec 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -16,6 +16,7 @@ export enum FeatureFlag { ExtensionRefresh = "extension-refresh", RestrictProviderAccess = "restrict-provider-access", UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection", + BulkDeviceApproval = "bulk-device-approval", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -42,6 +43,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.ExtensionRefresh]: FALSE, [FeatureFlag.RestrictProviderAccess]: FALSE, [FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE, + [FeatureFlag.BulkDeviceApproval]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;