Skip to content

Commit

Permalink
Add action input to choose between bw cloud regions (#102)
Browse files Browse the repository at this point in the history
* Add action input to choose between  bw cloud regions

Signed-off-by: Kim Oliver Drechsel <[email protected]>

* Fix region check

Signed-off-by: Kim Oliver Drechsel <[email protected]>

* Remove default values from inputs identity_url and api_url

* Adjust script

Signed-off-by: Kim Oliver Drechsel <[email protected]>

* Adjust exception

Signed-off-by: Kim Oliver Drechsel <[email protected]>

* Add documentation on how the cloud_region setting affects the identity or api url.

* Adjust code to use base_url if specified

Signed-off-by: Kim Oliver Drechsel <[email protected]>

* Adjust mock test

Signed-off-by: Kim Oliver Drechsel <[email protected]>

* Adjust README.md

Signed-off-by: Kim Oliver Drechsel <[email protected]>

* Adjust code for cloud-based url

Signed-off-by: Kim Oliver Drechsel <[email protected]>

* Adjust code

Signed-off-by: Kim Oliver Drechsel <[email protected]>

* fix input reference in error

Signed-off-by: Kim Oliver Drechsel <[email protected]>

* Declare variable customUrls as boolean with default false

Co-authored-by: Maciej Zieniuk <[email protected]>

* fix linting

Signed-off-by: Kim Oliver Drechsel <[email protected]>

* remove default value for non-required base_url

Signed-off-by: Kim Oliver Drechsel <[email protected]>

* fix missing cloudRegion variable

Signed-off-by: Kim Oliver Drechsel <[email protected]>

* adjust error messages

Signed-off-by: Kim Oliver Drechsel <[email protected]>

* add cloudRegion tests for wrong inputs

Signed-off-by: Kim Oliver Drechsel <[email protected]>

* fix: re-add default value for base_url input

Signed-off-by: Kim Oliver Drechsel <[email protected]>

* fix: add default value for identity_url and api_url input

Signed-off-by: Kim Oliver Drechsel <[email protected]>

* Update __tests__/main.test.ts

Co-authored-by: Maciej Zieniuk <[email protected]>

* test: adjust tests

Signed-off-by: Kim Oliver Drechsel <[email protected]>

* dist package update

* fixing and expanding tests

---------

Signed-off-by: Kim Oliver Drechsel <[email protected]>
Signed-off-by: Kim Oliver Drechsel <[email protected]>
Co-authored-by: cd-bitwarden <[email protected]>
Co-authored-by: Maciej Zieniuk <[email protected]>
Co-authored-by: Maciej Zieniuk <[email protected]>
  • Loading branch information
4 people authored Aug 21, 2024
1 parent d94f0a0 commit 6a0f396
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 61 deletions.
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ To use the action, add a step to your GitHub workflow using the following syntax
00000000-0000-0000-0000-000000000000 > TEST_EXAMPLE
```

- `cloud_region`

(Optional) For usage with the cloud-hosted services on either https://vault.bitwarden.com or https://vault.bitwarden.eu

The default value will use `us`, which is the region for https://vault.bitwarden.com

To use https://vault.bitwarden.eu, set the value to `eu`

- `base_url`

(Optional) For self-hosted bitwarden instances provide your https://your.domain.com
Expand All @@ -61,13 +69,13 @@ To use the action, add a step to your GitHub workflow using the following syntax

(Optional) For self-hosted bitwarden instances provide your https://your.domain.com/identity

The default value will use https://identity.bitwarden.com
Depending on the `cloud_region` setting, the default value will use https://identity.bitwarden.com for `us` (default) or https://identity.bitwarden.eu for `eu`.

- `api_url`

(Optional) For self-hosted bitwarden instances provide your https://your.domain.com/api

The default value will use https://api.bitwarden.com
Depending on the `cloud_region` setting, the default value will use https://api.bitwarden.com for `us` (default) or https://api.bitwarden.eu for `eu`.

## Examples

Expand Down Expand Up @@ -95,6 +103,7 @@ TEST_EXAMPLE_2: SECRET_VALUE_FOR_bdbb16bc-0b9b-472e-99fa-af4101309076
uses: bitwarden/sm-action@v1
with:
access_token: ${{ secrets.ACCESS_TOKEN }}
cloud_region: eu
secrets: |
00000000-0000-0000-0000-000000000000 > TEST_EXAMPLE
- name: Use Secret
Expand Down
163 changes: 116 additions & 47 deletions __tests__/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,32 @@ import {
import { randomUUID } from "crypto";

const INVALID_URL = "INVALID_URL";
const INVALID_CLOUD_REGION = "INVALID_CLOUD_REGION";
const TEST_ACCESS_TOKEN = randomUUID().toString();
const TEST_SECRETS = [`\t${randomUUID().toString()} > TEST_VALUE`];
const DEFAULT_BASE_URL = "";
const DEFAULT_IDENTITY_URL = "https://identity.bitwarden.com";
const DEFAULT_API_URL = "https://api.bitwarden.com";
const DEFAULT_IDENTITY_URL = "";
const DEFAULT_API_URL = "";
const DEFAULT_CLOUD_REGION = "us";

const OPTIONAL_TEST_INPUTS = {
baseUrl: DEFAULT_BASE_URL,
identityUrl: DEFAULT_IDENTITY_URL,
apiUrl: DEFAULT_API_URL,
cloudRegion: DEFAULT_CLOUD_REGION,
} as OptionalInputs;

const TEST_INPUTS = {
accessToken: TEST_ACCESS_TOKEN,
secrets: TEST_SECRETS,
...OPTIONAL_TEST_INPUTS,
} as Inputs;

const EXPECTED_DEFAULT_IDENTITY_URL = "https://identity.bitwarden.com";
const EXPECTED_DEFAULT_API_URL = "https://api.bitwarden.com";

// Mock the GitHub Actions core library
let warningMock: jest.SpyInstance;
let errorMock: jest.SpyInstance;
let getInputMock: jest.SpyInstance;
let getMultilineInputMock: jest.SpyInstance;
Expand Down Expand Up @@ -46,6 +65,7 @@ describe("action", () => {
beforeEach(() => {
jest.clearAllMocks();

warningMock = jest.spyOn(core, "warning").mockImplementation();
errorMock = jest.spyOn(core, "error").mockImplementation();
getInputMock = jest.spyOn(core, "getInput").mockImplementation();
getMultilineInputMock = jest.spyOn(core, "getMultilineInput").mockImplementation();
Expand All @@ -65,28 +85,54 @@ describe("action", () => {
],
[
"base_url",
{ accessToken: TEST_ACCESS_TOKEN, secrets: TEST_SECRETS, baseUrl: INVALID_URL } as Inputs,
{
...TEST_INPUTS,
baseUrl: INVALID_URL,
} as Inputs,
"input provided for base_url not in expected format",
],
[
"identity_url",
"identity_url with missing api_url",
{
accessToken: TEST_ACCESS_TOKEN,
secrets: TEST_SECRETS,
...TEST_INPUTS,
identityUrl: INVALID_URL,
} as Inputs,
"input provided for identity_url not in expected format",
"if using custom Urls, both identity_url and api_url need to be set.",
],
[
"api_url with missing identity_url",
{
...TEST_INPUTS,
apiUrl: INVALID_URL,
} as Inputs,
"if using custom Urls, both identity_url and api_url need to be set.",
],
[
"api_url",
{
accessToken: TEST_ACCESS_TOKEN,
secrets: TEST_SECRETS,
identityUrl: DEFAULT_IDENTITY_URL,
...TEST_INPUTS,
identityUrl: "https://identity.example.com",
apiUrl: INVALID_URL,
} as Inputs,
"input provided for api_url not in expected format",
],
[
"identity_url",
{
...TEST_INPUTS,
identityUrl: INVALID_URL,
apiUrl: "https://api.example.com",
} as Inputs,
"input provided for identity_url not in expected format",
],
[
"cloud_region",
{
...TEST_INPUTS,
cloudRegion: INVALID_CLOUD_REGION,
} as Inputs,
"input provided for cloud_region is not in the expected format",
],
])("readActionInputs: invalid input %s", async (_, inputs: Inputs, errorMessage) => {
mockInputs(inputs);

Expand All @@ -101,6 +147,7 @@ describe("action", () => {
secrets: ["UUID : ENV_VAR_NAME"],
identityUrl: DEFAULT_IDENTITY_URL,
apiUrl: DEFAULT_API_URL,
cloudRegion: DEFAULT_CLOUD_REGION,
} as Inputs);

await main.run();
Expand All @@ -116,6 +163,7 @@ describe("action", () => {
secrets: TEST_SECRETS,
identityUrl: DEFAULT_IDENTITY_URL,
apiUrl: DEFAULT_API_URL,
cloudRegion: DEFAULT_CLOUD_REGION,
} as Inputs);
loginWithAccessToken.mockReturnValue(
Promise.resolve(<ResponseForAPIKeyLoginResponse>{
Expand All @@ -135,6 +183,7 @@ describe("action", () => {
secrets: TEST_SECRETS,
identityUrl: DEFAULT_IDENTITY_URL,
apiUrl: DEFAULT_API_URL,
cloudRegion: DEFAULT_CLOUD_REGION,
} as Inputs);
mockSecretsGetByIdResponse({
success: false,
Expand All @@ -155,74 +204,59 @@ describe("action", () => {
expect(setSecretMock).not.toHaveBeenCalled();
expect(exportVariableMock).not.toHaveBeenCalled();
expect(setOutputMock).not.toHaveBeenCalled();
expect(warningMock).not.toHaveBeenCalled();
}
});

describe("default inputs", () => {
describe("optional inputs", () => {
it.each([
[
"no optional inputs",
{
baseUrl: DEFAULT_BASE_URL,
identityUrl: DEFAULT_IDENTITY_URL,
apiUrl: DEFAULT_API_URL,
...OPTIONAL_TEST_INPUTS,
} as OptionalInputs,
{
identityUrl: DEFAULT_IDENTITY_URL,
apiUrl: DEFAULT_API_URL,
identityUrl: EXPECTED_DEFAULT_IDENTITY_URL,
apiUrl: EXPECTED_DEFAULT_API_URL,
} as ClientSettings,
],
[
"base_url provided",
{
...OPTIONAL_TEST_INPUTS,
baseUrl: "https://bitwarden.example.com",
identityUrl: DEFAULT_IDENTITY_URL,
apiUrl: DEFAULT_API_URL,
} as OptionalInputs,
{
identityUrl: "https://bitwarden.example.com/identity",
apiUrl: "https://bitwarden.example.com/api",
} as ClientSettings,
],
[
"identity_url provided",
{
baseUrl: DEFAULT_BASE_URL,
identityUrl: "https://identity.bitwarden.example.com",
apiUrl: DEFAULT_API_URL,
} as OptionalInputs,
"api_url and identity_url provided",
{
...OPTIONAL_TEST_INPUTS,
identityUrl: "https://identity.bitwarden.example.com",
apiUrl: DEFAULT_API_URL,
} as ClientSettings,
],
[
"api_url provided",
{
baseUrl: DEFAULT_BASE_URL,
identityUrl: DEFAULT_IDENTITY_URL,
apiUrl: "https://api.bitwarden.example.com",
} as OptionalInputs,
{
identityUrl: DEFAULT_IDENTITY_URL,
identityUrl: "https://identity.bitwarden.example.com",
apiUrl: "https://api.bitwarden.example.com",
} as ClientSettings,
],
[
"api_url and identity_url provided",
"cloud_region provided",
{
baseUrl: DEFAULT_BASE_URL,
identityUrl: "https://identity.bitwarden.example.com",
apiUrl: "https://api.bitwarden.example.com",
...OPTIONAL_TEST_INPUTS,
cloudRegion: "eu",
} as OptionalInputs,
{
identityUrl: "https://identity.bitwarden.example.com",
apiUrl: "https://api.bitwarden.example.com",
identityUrl: "https://identity.bitwarden.eu",
apiUrl: "https://api.bitwarden.eu",
} as ClientSettings,
],
])("%s", async (_, optionalInputs: OptionalInputs, expectedClientSettings: ClientSettings) => {
mockInputs({
accessToken: TEST_ACCESS_TOKEN,
...TEST_INPUTS,
secrets: [] as string[],
...optionalInputs,
} as Inputs);
Expand All @@ -247,16 +281,51 @@ describe("action", () => {
} as ClientSettings,
LogLevel.Info,
]);
expect(warningMock).not.toHaveBeenCalled();
});

it("all self-hosted url inputs provided at once", async () => {
mockInputs({
...TEST_INPUTS,
secrets: [] as string[],
baseUrl: "https://bitwarden.example.com",
identityUrl: "https://identity.bitwarden.example.com",
apiUrl: "https://api.bitwarden.example.com",
} as Inputs);
mockSecretsGetByIdResponse({
success: true,
data: {
data: [],
},
});

await main.run();

expect(setFailedMock).not.toHaveBeenCalled();
expect(errorMock).not.toHaveBeenCalled();
expect(setSecretMock).not.toHaveBeenCalled();
expect(bitwardenClientMock).toHaveBeenCalledTimes(1);
expect(bitwardenClientMock).toHaveBeenCalledWith([
{
deviceType: DeviceType.SDK,
userAgent: "bitwarden/sm-action",
identityUrl: "https://bitwarden.example.com/identity",
apiUrl: "https://bitwarden.example.com/api",
} as ClientSettings,
LogLevel.Info,
]);
expect(warningMock).toHaveBeenCalledTimes(1);
expect(warningMock).toHaveBeenCalledWith(
"both base_url and api_url/identity_url are set, but only one of the two options should be set. In this case, base_url is used.",
);
});
});

describe("sets secrets", () => {
it("no secrets", async () => {
mockInputs({
accessToken: TEST_ACCESS_TOKEN,
...TEST_INPUTS,
secrets: [] as string[],
identityUrl: DEFAULT_IDENTITY_URL,
apiUrl: DEFAULT_API_URL,
} as Inputs);
mockSecretsGetByIdResponse({
success: true,
Expand Down Expand Up @@ -289,13 +358,11 @@ describe("action", () => {
];

mockInputs({
accessToken: TEST_ACCESS_TOKEN,
...TEST_INPUTS,
secrets: [
`${secretsResponse[0].id} > TEST_ENV_VAR_1`,
`${secretsResponse[1].id} > TEST_ENV_VAR_2`,
],
identityUrl: DEFAULT_IDENTITY_URL,
apiUrl: DEFAULT_API_URL,
} as Inputs);
mockSecretsGetByIdResponse({
success: true,
Expand Down Expand Up @@ -346,6 +413,7 @@ type OptionalInputs = {
baseUrl?: string;
identityUrl?: string;
apiUrl?: string;
cloudRegion?: string;
};

type Inputs = {
Expand All @@ -354,7 +422,7 @@ type Inputs = {
} & OptionalInputs;

function mockInputs(inputs: Inputs) {
const { accessToken, secrets, baseUrl, identityUrl, apiUrl } = inputs;
const { accessToken, secrets, baseUrl, identityUrl, apiUrl, cloudRegion } = inputs;

getInputMock.mockImplementation(
(name: string, options?: { required?: boolean }): string | undefined => {
Expand All @@ -363,6 +431,7 @@ function mockInputs(inputs: Inputs) {
base_url: baseUrl,
identity_url: identityUrl,
api_url: apiUrl,
cloud_region: cloudRegion,
}[name];

// Error from core.getInput is thrown if the input is required and not supplied
Expand Down
8 changes: 6 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,22 @@ inputs:
secrets:
description: "One or more secret Ids to retrieve and the corresponding GitHub environment variable name to set"
required: true
cloud_region:
description: "(Optional) The Bitwarden server region to use if cloud-hosted service is used. Either 'us' or 'eu'"
required: false
default: "us"
base_url:
description: "(Optional) For self-hosted bitwarden instances provide your https://your.domain.com"
required: false
default: ""
identity_url:
description: "(Optional) For self-hosted bitwarden instances provide your https://your.domain.com/identity"
required: false
default: "https://identity.bitwarden.com"
default: ""
api_url:
description: "(Optional) For self-hosted bitwarden instances provide your https://your.domain.com/api"
required: false
default: "https://api.bitwarden.com"
default: ""
runs:
using: "node20"
main: "dist/index.js"
Loading

0 comments on commit 6a0f396

Please sign in to comment.