1
0
mirror of https://github.com/bitwarden/cli synced 2026-01-03 16:53:14 +00:00

normalize options parsing and error messages

This commit is contained in:
Kyle Spearrin
2021-10-06 16:23:57 -04:00
parent ffe774f81f
commit 6233a5d1cd
7 changed files with 167 additions and 88 deletions

View File

@@ -1,5 +1,3 @@
import * as program from 'commander';
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
@@ -12,35 +10,36 @@ import { Utils } from 'jslib-common/misc/utils';
export class ConfirmCommand {
constructor(private apiService: ApiService, private cryptoService: CryptoService) { }
async run(object: string, id: string, cmd: program.Command | any): Promise<Response> {
async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> {
if (id != null) {
id = id.toLowerCase();
}
const normalizedOptions = this.normalizeOptions(cmdOptions);
switch (object.toLowerCase()) {
case 'org-member':
return await this.confirmOrganizationMember(id, cmd);
return await this.confirmOrganizationMember(id, normalizedOptions);
default:
return Response.badRequest('Unknown object.');
}
}
private async confirmOrganizationMember(id: string, options: program.OptionValues) {
if (options.organizationid == null || options.organizationid === '') {
return Response.badRequest('--organizationid <organizationid> required.');
private async confirmOrganizationMember(id: string, options: Options) {
if (options.organizationId == null || options.organizationId === '') {
return Response.badRequest('`organizationid` option is required.');
}
if (!Utils.isGuid(id)) {
return Response.error('`' + id + '` is not a GUID.');
return Response.badRequest('`' + id + '` is not a GUID.');
}
if (!Utils.isGuid(options.organizationid)) {
return Response.error('`' + options.organizationid + '` is not a GUID.');
if (!Utils.isGuid(options.organizationId)) {
return Response.badRequest('`' + options.organizationId + '` is not a GUID.');
}
try {
const orgKey = await this.cryptoService.getOrgKey(options.organizationid);
const orgKey = await this.cryptoService.getOrgKey(options.organizationId);
if (orgKey == null) {
throw new Error('No encryption key for this organization.');
}
const orgUser = await this.apiService.getOrganizationUser(options.organizationid, id);
const orgUser = await this.apiService.getOrganizationUser(options.organizationId, id);
if (orgUser == null) {
throw new Error('Member id does not exist for this organization.');
}
@@ -49,10 +48,20 @@ export class ConfirmCommand {
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer);
const req = new OrganizationUserConfirmRequest();
req.key = key.encryptedString;
await this.apiService.postOrganizationUserConfirm(options.organizationid, id, req);
await this.apiService.postOrganizationUserConfirm(options.organizationId, id, req);
return Response.success();
} catch (e) {
return Response.error(e);
}
}
private normalizeOptions(passedOptions: Record<string, any>): Options {
const typedOptions = new Options();
typedOptions.organizationId = passedOptions.organizationid || passedOptions.organizationId;
return typedOptions;
}
}
class Options {
organizationId: string;
}

View File

@@ -1,4 +1,3 @@
import * as program from 'commander';
import * as fs from 'fs';
import * as path from 'path';
@@ -32,9 +31,10 @@ export class CreateCommand {
private userService: UserService, private cryptoService: CryptoService,
private apiService: ApiService) { }
async run(object: string, requestJson: any, cmd: program.Command | any,
async run(object: string, requestJson: any, cmdOptions: Record<string, any>,
additionalData: any = null): Promise<Response> {
let req: any = null;
if (object !== 'attachment') {
if (requestJson == null || requestJson === '') {
requestJson = await CliUtils.readStdin();
@@ -56,15 +56,16 @@ export class CreateCommand {
}
}
const normalizedOptions = this.normalizeOptions(cmdOptions);
switch (object.toLowerCase()) {
case 'item':
return await this.createCipher(req);
case 'attachment':
return await this.createAttachment(cmd, additionalData);
return await this.createAttachment(normalizedOptions, additionalData);
case 'folder':
return await this.createFolder(req);
case 'org-collection':
return await this.createOrganizationCollection(req, cmd);
return await this.createOrganizationCollection(req, normalizedOptions);
default:
return Response.badRequest('Unknown object.');
}
@@ -83,9 +84,9 @@ export class CreateCommand {
}
}
private async createAttachment(options: program.OptionValues, additionalData: any) {
if (options.itemid == null || options.itemid === '') {
return Response.badRequest('--itemid <itemid> required.');
private async createAttachment(options: Options, additionalData: any) {
if (options.itemId == null || options.itemId === '') {
return Response.badRequest('`itemid` option is required.');
}
let fileBuf: Buffer = null;
let fileName: string = null;
@@ -94,7 +95,7 @@ export class CreateCommand {
fileName = additionalData.fileName;
} else {
if (options.file == null || options.file === '') {
return Response.badRequest('--file <file> required.');
return Response.badRequest('`file` option is required.');
}
const filePath = path.resolve(options.file);
if (!fs.existsSync(options.file)) {
@@ -111,7 +112,7 @@ export class CreateCommand {
return Response.badRequest('File name not provided.');
}
const itemId = options.itemid.toLowerCase();
const itemId = options.itemId.toLowerCase();
const cipher = await this.cipherService.get(itemId);
if (cipher == null) {
return Response.notFound();
@@ -151,15 +152,15 @@ export class CreateCommand {
}
}
private async createOrganizationCollection(req: OrganizationCollectionRequest, options: program.OptionValues) {
if (options.organizationid == null || options.organizationid === '') {
return Response.badRequest('--organizationid <organizationid> required.');
private async createOrganizationCollection(req: OrganizationCollectionRequest, options: Options) {
if (options.organizationId == null || options.organizationId === '') {
return Response.badRequest('`organizationid` option is required.');
}
if (!Utils.isGuid(options.organizationid)) {
return Response.error('`' + options.organizationid + '` is not a GUID.');
if (!Utils.isGuid(options.organizationId)) {
return Response.badRequest('`' + options.organizationId + '` is not a GUID.');
}
if (options.organizationid !== req.organizationId) {
return Response.error('--organizationid <organizationid> does not match request object.');
if (options.organizationId !== req.organizationId) {
return Response.badRequest('`organizationid` option does not match request object.');
}
try {
const orgKey = await this.cryptoService.getOrgKey(req.organizationId);
@@ -182,4 +183,18 @@ export class CreateCommand {
return Response.error(e);
}
}
private normalizeOptions(passedOptions: Record<string, any>): Options {
const typedOptions = new Options();
typedOptions.organizationId = passedOptions.organizationid || passedOptions.organizationId;
typedOptions.itemId = passedOptions.itemid || passedOptions.itemId;
typedOptions.file = passedOptions.file;
return typedOptions;
}
}
class Options {
itemId: string;
organizationId: string;
file: string;
}

View File

@@ -1,5 +1,3 @@
import * as program from 'commander';
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CipherService } from 'jslib-common/abstractions/cipher.service';
import { FolderService } from 'jslib-common/abstractions/folder.service';
@@ -9,30 +7,33 @@ import { Response } from 'jslib-node/cli/models/response';
import { Utils } from 'jslib-common/misc/utils';
import { CliUtils } from '../utils';
export class DeleteCommand {
constructor(private cipherService: CipherService, private folderService: FolderService,
private userService: UserService, private apiService: ApiService) { }
async run(object: string, id: string, cmd: program.Command | any): Promise<Response> {
async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> {
if (id != null) {
id = id.toLowerCase();
}
const normalizedOptions = this.normalizeOptions(cmdOptions);
switch (object.toLowerCase()) {
case 'item':
return await this.deleteCipher(id, cmd);
return await this.deleteCipher(id, normalizedOptions);
case 'attachment':
return await this.deleteAttachment(id, cmd);
return await this.deleteAttachment(id, normalizedOptions);
case 'folder':
return await this.deleteFolder(id);
case 'org-collection':
return await this.deleteOrganizationCollection(id, cmd);
return await this.deleteOrganizationCollection(id, normalizedOptions);
default:
return Response.badRequest('Unknown object.');
}
}
private async deleteCipher(id: string, options: program.OptionValues) {
private async deleteCipher(id: string, options: Options) {
const cipher = await this.cipherService.get(id);
if (cipher == null) {
return Response.notFound();
@@ -50,12 +51,12 @@ export class DeleteCommand {
}
}
private async deleteAttachment(id: string, options: program.OptionValues) {
if (options.itemid == null || options.itemid === '') {
return Response.badRequest('--itemid <itemid> required.');
private async deleteAttachment(id: string, options: Options) {
if (options.itemId == null || options.itemId === '') {
return Response.badRequest('`itemid` option is required.');
}
const itemId = options.itemid.toLowerCase();
const itemId = options.itemId.toLowerCase();
const cipher = await this.cipherService.get(itemId);
if (cipher == null) {
return Response.notFound();
@@ -96,21 +97,35 @@ export class DeleteCommand {
}
}
private async deleteOrganizationCollection(id: string, options: program.OptionValues) {
if (options.organizationid == null || options.organizationid === '') {
return Response.badRequest('--organizationid <organizationid> required.');
private async deleteOrganizationCollection(id: string, options: Options) {
if (options.organizationId == null || options.organizationId === '') {
return Response.badRequest('`organizationid` option is required.');
}
if (!Utils.isGuid(id)) {
return Response.error('`' + id + '` is not a GUID.');
return Response.badRequest('`' + id + '` is not a GUID.');
}
if (!Utils.isGuid(options.organizationid)) {
return Response.error('`' + options.organizationid + '` is not a GUID.');
if (!Utils.isGuid(options.organizationId)) {
return Response.badRequest('`' + options.organizationId + '` is not a GUID.');
}
try {
await this.apiService.deleteCollection(options.organizationid, id);
await this.apiService.deleteCollection(options.organizationId, id);
return Response.success();
} catch (e) {
return Response.error(e);
}
}
private normalizeOptions(passedOptions: Record<string, any>): Options {
const typedOptions = new Options();
typedOptions.organizationId = passedOptions.organizationid || passedOptions.organizationId;
typedOptions.itemId = passedOptions.itemid || passedOptions.itemId;
typedOptions.permanent = CliUtils.convertBooleanOption(passedOptions.permanent);
return typedOptions;
}
}
class Options {
itemId: string;
organizationId: string;
permanent: boolean;
}

View File

@@ -28,7 +28,7 @@ export class EditCommand {
constructor(private cipherService: CipherService, private folderService: FolderService,
private cryptoService: CryptoService, private apiService: ApiService) { }
async run(object: string, id: string, requestJson: any, cmd: program.Command | any): Promise<Response> {
async run(object: string, id: string, requestJson: any, cmdOptions: Record<string, any>): Promise<Response> {
if (requestJson == null || requestJson === '') {
requestJson = await CliUtils.readStdin();
}
@@ -53,6 +53,7 @@ export class EditCommand {
id = id.toLowerCase();
}
const normalizedOptions = this.normalizeOptions(cmdOptions);
switch (object.toLowerCase()) {
case 'item':
return await this.editCipher(id, req);
@@ -61,7 +62,7 @@ export class EditCommand {
case 'folder':
return await this.editFolder(id, req);
case 'org-collection':
return await this.editOrganizationCollection(id, req, cmd);
return await this.editOrganizationCollection(id, req, normalizedOptions);
default:
return Response.badRequest('Unknown object.');
}
@@ -75,7 +76,7 @@ export class EditCommand {
let cipherView = await cipher.decrypt();
if (cipherView.isDeleted) {
return Response.badRequest('You may not edit a deleted cipher. Use restore item <id> command first.');
return Response.badRequest('You may not edit a deleted item. Use the restore command first.');
}
cipherView = Cipher.toView(req, cipherView);
const encCipher = await this.cipherService.encrypt(cipherView);
@@ -96,7 +97,7 @@ export class EditCommand {
return Response.notFound();
}
if (cipher.organizationId == null) {
return Response.badRequest('Item does not belong to an organization. Consider sharing it first.');
return Response.badRequest('Item does not belong to an organization. Consider moving it first.');
}
cipher.collectionIds = req;
@@ -131,18 +132,18 @@ export class EditCommand {
}
}
private async editOrganizationCollection(id: string, req: OrganizationCollectionRequest, options: program.OptionValues) {
if (options.organizationid == null || options.organizationid === '') {
return Response.badRequest('--organizationid <organizationid> required.');
private async editOrganizationCollection(id: string, req: OrganizationCollectionRequest, options: Options) {
if (options.organizationId == null || options.organizationId === '') {
return Response.badRequest('`organizationid` option is required.');
}
if (!Utils.isGuid(id)) {
return Response.error('`' + id + '` is not a GUID.');
return Response.badRequest('`' + id + '` is not a GUID.');
}
if (!Utils.isGuid(options.organizationid)) {
return Response.error('`' + options.organizationid + '` is not a GUID.');
if (!Utils.isGuid(options.organizationId)) {
return Response.badRequest('`' + options.organizationId + '` is not a GUID.');
}
if (options.organizationid !== req.organizationId) {
return Response.error('--organizationid <organizationid> does not match request object.');
if (options.organizationId !== req.organizationId) {
return Response.badRequest('`organizationid` option does not match request object.');
}
try {
const orgKey = await this.cryptoService.getOrgKey(req.organizationId);
@@ -165,4 +166,14 @@ export class EditCommand {
return Response.error(e);
}
}
private normalizeOptions(passedOptions: Record<string, any>): Options {
const typedOptions = new Options();
typedOptions.organizationId = passedOptions.organizationid || passedOptions.organizationId;
return typedOptions;
}
}
class Options {
organizationId: string;
}

View File

@@ -1,5 +1,3 @@
import * as program from 'commander';
import { Response } from 'jslib-node/cli/models/response';
import { StringResponse } from 'jslib-node/cli/models/response/stringResponse';

View File

@@ -1,43 +1,70 @@
import * as program from 'commander';
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service';
import { Response } from 'jslib-node/cli/models/response';
import { StringResponse } from 'jslib-node/cli/models/response/stringResponse';
import { CliUtils } from '../utils';
export class GenerateCommand {
constructor(private passwordGenerationService: PasswordGenerationService) { }
async run(cmdOptions: program.OptionValues | any): Promise<Response> {
async run(cmdOptions: Record<string, any>): Promise<Response> {
const normalizedOptions = this.normalizeOptions(cmdOptions);
const options = {
uppercase: cmdOptions.uppercase || false,
lowercase: cmdOptions.lowercase || false,
number: cmdOptions.number || false,
special: cmdOptions.special || false,
length: cmdOptions.length || 14,
type: cmdOptions.passphrase ? 'passphrase' : 'password',
wordSeparator: cmdOptions.separator == null ? '-' : cmdOptions.separator,
numWords: cmdOptions.words || 3,
uppercase: normalizedOptions.uppercase,
lowercase: normalizedOptions.lowercase,
number: normalizedOptions.number,
special: normalizedOptions.special,
length: normalizedOptions.length,
type: normalizedOptions.type,
wordSeparator: normalizedOptions.separator,
numWords: normalizedOptions.words,
};
if (!options.uppercase && !options.lowercase && !options.special && !options.number) {
options.lowercase = true;
options.uppercase = true;
options.number = true;
}
if (options.length < 5) {
options.length = 5;
}
if (options.numWords < 3) {
options.numWords = 3;
}
if (options.wordSeparator === 'space') {
options.wordSeparator = ' ';
} else if (options.wordSeparator != null && options.wordSeparator.length > 1) {
options.wordSeparator = options.wordSeparator[0];
}
const enforcedOptions = await this.passwordGenerationService.enforcePasswordGeneratorPoliciesOnOptions(options);
const password = await this.passwordGenerationService.generatePassword(enforcedOptions[0]);
const res = new StringResponse(password);
return Response.success(res);
}
private normalizeOptions(passedOptions: Record<string, any>): Options {
const typedOptions = new Options();
typedOptions.uppercase = CliUtils.convertBooleanOption(passedOptions.uppercase);
typedOptions.lowercase = CliUtils.convertBooleanOption(passedOptions.lowercase);
typedOptions.number = CliUtils.convertBooleanOption(passedOptions.number);
typedOptions.special = CliUtils.convertBooleanOption(passedOptions.special);
typedOptions.length = passedOptions.length != null ? parseInt(passedOptions.length, null) : 14;
typedOptions.type = passedOptions.passphrase ? 'passphrase' : 'password';
typedOptions.separator = passedOptions.separator == null ? '-' : passedOptions.separator + '';
typedOptions.words = passedOptions.words != null ? parseInt(passedOptions.words, null) : 3;
if (!typedOptions.uppercase && !typedOptions.lowercase && !typedOptions.special && !typedOptions.number) {
typedOptions.lowercase = true;
typedOptions.uppercase = true;
typedOptions.number = true;
}
if (typedOptions.length < 5) {
typedOptions.length = 5;
}
if (typedOptions.words < 3) {
typedOptions.words = 3;
}
if (typedOptions.separator === 'space') {
typedOptions.separator = ' ';
} else if (typedOptions.separator != null && typedOptions.separator.length > 1) {
typedOptions.separator = typedOptions.separator[0];
}
return typedOptions;
}
}
class Options {
uppercase: boolean;
lowercase: boolean;
number: boolean;
special: boolean;
length: number;
type: 'passphrase' | 'password';
separator: string;
words: number;
}

View File

@@ -170,4 +170,8 @@ export class CliUtils {
return false;
});
}
static convertBooleanOption(optionValue: any) {
return optionValue || optionValue === '' ? true : false;
}
}