1
0
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:
Thomas Rittson
2024-09-19 07:50:40 +10:00
committed by GitHub
parent 3d9465917d
commit 9dc497dd13
12 changed files with 217 additions and 1445 deletions

View File

@@ -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);
}

View 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,
});
});
});

View 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(),
};
}
}

View File

@@ -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);
});