mirror of
https://github.com/bitwarden/directory-connector
synced 2025-12-05 23:53:21 +00:00
[AC-3047] Refactor LoginCommand to only use organization api key login (#621)
* Add tests * Remove unused code from LoginCommand and refactor * Remove unused services * Remove unused npm deps * Install missing type-fest dep
This commit is contained in:
19
src/bwdc.ts
19
src/bwdc.ts
@@ -14,8 +14,6 @@ import { EnvironmentService } from "@/jslib/common/src/services/environment.serv
|
||||
import { KeyConnectorService } from "@/jslib/common/src/services/keyConnector.service";
|
||||
import { NoopMessagingService } from "@/jslib/common/src/services/noopMessaging.service";
|
||||
import { OrganizationService } from "@/jslib/common/src/services/organization.service";
|
||||
import { PasswordGenerationService } from "@/jslib/common/src/services/passwordGeneration.service";
|
||||
import { PolicyService } from "@/jslib/common/src/services/policy.service";
|
||||
import { TokenService } from "@/jslib/common/src/services/token.service";
|
||||
import { CliPlatformUtilsService } from "@/jslib/node/src/cli/services/cliPlatformUtils.service";
|
||||
import { ConsoleLogService } from "@/jslib/node/src/cli/services/consoleLog.service";
|
||||
@@ -39,6 +37,8 @@ const packageJson = require("../package.json");
|
||||
export class Main {
|
||||
dataFilePath: string;
|
||||
logService: ConsoleLogService;
|
||||
program: Program;
|
||||
|
||||
messagingService: NoopMessagingService;
|
||||
storageService: LowdbStorageService;
|
||||
secureStorageService: StorageServiceAbstraction;
|
||||
@@ -53,10 +53,7 @@ export class Main {
|
||||
cryptoFunctionService: NodeCryptoFunctionService;
|
||||
authService: AuthService;
|
||||
syncService: SyncService;
|
||||
passwordGenerationService: PasswordGenerationService;
|
||||
policyService: PolicyService;
|
||||
keyConnectorService: KeyConnectorService;
|
||||
program: Program;
|
||||
stateService: StateService;
|
||||
stateMigrationService: StateMigrationService;
|
||||
organizationService: OrganizationService;
|
||||
@@ -187,18 +184,6 @@ export class Main {
|
||||
this.stateService,
|
||||
);
|
||||
|
||||
this.policyService = new PolicyService(
|
||||
this.stateService,
|
||||
this.organizationService,
|
||||
this.apiService,
|
||||
);
|
||||
|
||||
this.passwordGenerationService = new PasswordGenerationService(
|
||||
this.cryptoService,
|
||||
this.policyService,
|
||||
this.stateService,
|
||||
);
|
||||
|
||||
this.program = new Program(this);
|
||||
}
|
||||
|
||||
|
||||
66
src/commands/login.command.spec.ts
Normal file
66
src/commands/login.command.spec.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
|
||||
import { AuthResult } from "@/jslib/common/src/models/domain/authResult";
|
||||
import { ApiLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
|
||||
|
||||
import { LoginCommand } from "./login.command";
|
||||
|
||||
const clientId = "test_client_id";
|
||||
const clientSecret = "test_client_secret";
|
||||
|
||||
// Mock responses from the inquirer prompt
|
||||
// This combines both prompt results into a single object which is returned both times
|
||||
jest.mock("inquirer", () => ({
|
||||
createPromptModule: () => () => ({
|
||||
clientId,
|
||||
clientSecret,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("LoginCommand", () => {
|
||||
let authService: MockProxy<AuthService>;
|
||||
|
||||
let loginCommand: LoginCommand;
|
||||
|
||||
beforeEach(() => {
|
||||
// reset env variables
|
||||
delete process.env.BW_CLIENTID;
|
||||
delete process.env.BW_CLIENTSECRET;
|
||||
|
||||
authService = mock();
|
||||
|
||||
loginCommand = new LoginCommand(authService);
|
||||
});
|
||||
|
||||
it("uses client id and secret stored in environment variables", async () => {
|
||||
process.env.BW_CLIENTID = clientId;
|
||||
process.env.BW_CLIENTSECRET = clientSecret;
|
||||
|
||||
authService.logIn.mockResolvedValue(new AuthResult()); // logging in with api key does not set any flag on the authResult
|
||||
|
||||
const result = await loginCommand.run();
|
||||
|
||||
expect(authService.logIn).toHaveBeenCalledWith(new ApiLogInCredentials(clientId, clientSecret));
|
||||
expect(result).toMatchObject({
|
||||
data: {
|
||||
title: "You are logged in!",
|
||||
},
|
||||
success: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses client id and secret prompted from the user", async () => {
|
||||
authService.logIn.mockResolvedValue(new AuthResult()); // logging in with api key does not set any flag on the authResult
|
||||
|
||||
const result = await loginCommand.run();
|
||||
|
||||
expect(authService.logIn).toHaveBeenCalledWith(new ApiLogInCredentials(clientId, clientSecret));
|
||||
expect(result).toMatchObject({
|
||||
data: {
|
||||
title: "You are logged in!",
|
||||
},
|
||||
success: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
88
src/commands/login.command.ts
Normal file
88
src/commands/login.command.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import * as inquirer from "inquirer";
|
||||
|
||||
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
|
||||
import { ApiLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
|
||||
import { Response } from "@/jslib/node/src/cli/models/response";
|
||||
import { MessageResponse } from "@/jslib/node/src/cli/models/response/messageResponse";
|
||||
|
||||
import { Utils } from "../../jslib/common/src/misc/utils";
|
||||
|
||||
export class LoginCommand {
|
||||
private canInteract: boolean;
|
||||
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
async run() {
|
||||
this.canInteract = process.env.BW_NOINTERACTION !== "true";
|
||||
|
||||
const { clientId, clientSecret } = await this.apiIdentifiers();
|
||||
|
||||
if (Utils.isNullOrWhitespace(clientId)) {
|
||||
return Response.error("Client ID is required.");
|
||||
}
|
||||
|
||||
if (Utils.isNullOrWhitespace(clientSecret)) {
|
||||
return Response.error("Client Secret is required.");
|
||||
}
|
||||
|
||||
try {
|
||||
await this.authService.logIn(new ApiLogInCredentials(clientId, clientSecret));
|
||||
|
||||
const res = new MessageResponse("You are logged in!", null);
|
||||
return Response.success(res);
|
||||
} catch (e) {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async apiClientId(): Promise<string> {
|
||||
let clientId: string = null;
|
||||
|
||||
const storedClientId: string = process.env.BW_CLIENTID;
|
||||
if (storedClientId == null) {
|
||||
if (this.canInteract) {
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||
output: process.stderr,
|
||||
})({
|
||||
type: "input",
|
||||
name: "clientId",
|
||||
message: "client_id:",
|
||||
});
|
||||
clientId = answer.clientId;
|
||||
} else {
|
||||
clientId = null;
|
||||
}
|
||||
} else {
|
||||
clientId = storedClientId;
|
||||
}
|
||||
|
||||
return clientId;
|
||||
}
|
||||
|
||||
private async apiClientSecret(): Promise<string> {
|
||||
let clientSecret: string = null;
|
||||
|
||||
const storedClientSecret = process.env.BW_CLIENTSECRET;
|
||||
if (this.canInteract && storedClientSecret == null) {
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||
output: process.stderr,
|
||||
})({
|
||||
type: "input",
|
||||
name: "clientSecret",
|
||||
message: "client_secret:",
|
||||
});
|
||||
clientSecret = answer.clientSecret;
|
||||
} else {
|
||||
clientSecret = storedClientSecret;
|
||||
}
|
||||
|
||||
return clientSecret;
|
||||
}
|
||||
|
||||
private async apiIdentifiers(): Promise<{ clientId: string; clientSecret: string }> {
|
||||
return {
|
||||
clientId: await this.apiClientId(),
|
||||
clientSecret: await this.apiClientSecret(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import { Command, OptionValues } from "commander";
|
||||
|
||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
||||
import { BaseProgram } from "@/jslib/node/src/cli/baseProgram";
|
||||
import { LoginCommand } from "@/jslib/node/src/cli/commands/login.command";
|
||||
import { LogoutCommand } from "@/jslib/node/src/cli/commands/logout.command";
|
||||
import { UpdateCommand } from "@/jslib/node/src/cli/commands/update.command";
|
||||
import { Response } from "@/jslib/node/src/cli/models/response";
|
||||
@@ -15,6 +14,7 @@ import { Main } from "./bwdc";
|
||||
import { ClearCacheCommand } from "./commands/clearCache.command";
|
||||
import { ConfigCommand } from "./commands/config.command";
|
||||
import { LastSyncCommand } from "./commands/lastSync.command";
|
||||
import { LoginCommand } from "./commands/login.command";
|
||||
import { SyncCommand } from "./commands/sync.command";
|
||||
import { TestCommand } from "./commands/test.command";
|
||||
|
||||
@@ -92,20 +92,7 @@ export class Program extends BaseProgram {
|
||||
})
|
||||
.action(async (clientId: string, clientSecret: string, options: OptionValues) => {
|
||||
await this.exitIfAuthed();
|
||||
const command = new LoginCommand(
|
||||
this.main.authService,
|
||||
this.main.apiService,
|
||||
this.main.i18nService,
|
||||
this.main.environmentService,
|
||||
this.main.passwordGenerationService,
|
||||
this.main.cryptoFunctionService,
|
||||
this.main.platformUtilsService,
|
||||
this.main.stateService,
|
||||
this.main.cryptoService,
|
||||
this.main.policyService,
|
||||
this.main.twoFactorService,
|
||||
"connector",
|
||||
);
|
||||
const command = new LoginCommand(this.main.authService);
|
||||
|
||||
if (!Utils.isNullOrWhitespace(clientId)) {
|
||||
process.env.BW_CLIENTID = clientId;
|
||||
@@ -114,8 +101,7 @@ export class Program extends BaseProgram {
|
||||
process.env.BW_CLIENTSECRET = clientSecret;
|
||||
}
|
||||
|
||||
options = Object.assign(options ?? {}, { apikey: true }); // force apikey use
|
||||
const response = await command.run(null, null, options);
|
||||
const response = await command.run();
|
||||
this.processResponse(response);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user