Skip to content

Commit

Permalink
feat: provide input to optionally mask output docker password (#491)
Browse files Browse the repository at this point in the history
This will allow the user to specify an optional input to mask
the Docker password from being leaked in workflow logs via either
explicitly printing or running a job in debug mode. Since many
users rely on the output, the default behaviour has been false to
ensure this is not a breaking change.

This also re-arranges the inputs throughout the code and in the GH
action spec to be in alphabetical order as a styling improvement.

Signed-off-by: [email protected] <[email protected]>
  • Loading branch information
therealdwright committed Aug 8, 2023
1 parent 57f4ffc commit 98f33d2
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 32 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,28 @@ Logs in the local Docker client to one or more Amazon ECR Private registries or
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
```
#### Login to Amazon ECR Private, then build and push a Docker image masking the password:
> [!WARNING]
> Setting mask-password to true will prevent the password GitHub output from being shared between separate jobs.
```yaml
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
with:
mask-password: 'true'

- name: Build, tag, and push docker image to Amazon ECR
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
REPOSITORY: my-ecr-repo
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $REGISTRY/$REPOSITORY:$IMAGE_TAG .
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
```
#### Login to Amazon ECR Public, then build and push a Docker image:
```yaml
- name: Login to Amazon ECR Public
Expand Down
27 changes: 17 additions & 10 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,37 @@ branding:
icon: 'cloud'
color: 'orange'
inputs:
registries:
http-proxy:
description: >-
A comma-delimited list of AWS account IDs that are associated with the ECR Private registries.
If you do not specify a registry, the default ECR Private registry is assumed.
If 'public' is given as input to 'registry-type', this input is ignored.
Proxy to use for the AWS SDK agent.
required: false
skip-logout:
mask-password:
description: >-
Whether to skip explicit logout of the registries during post-job cleanup.
Exists for backward compatibility on self-hosted runners.
Not recommended.
Mask the docker password to prevent it being printed to logs to std-out. This will prevent the
password GitHub output from being shared between separate jobs.
Options: ['true', 'false']
required: false
default: 'false'
registries:
description: >-
A comma-delimited list of AWS account IDs that are associated with the ECR Private registries.
If you do not specify a registry, the default ECR Private registry is assumed.
If 'public' is given as input to 'registry-type', this input is ignored.
required: false
registry-type:
description: >-
Which ECR registry type to log into.
Options: [private, public]
required: false
default: private
http-proxy:
skip-logout:
description: >-
Proxy to use for the AWS SDK agent.
Whether to skip explicit logout of the registries during post-job cleanup.
Exists for backward compatibility on self-hosted runners.
Not recommended.
Options: ['true', 'false']
required: false
default: 'false'
outputs:
registry:
description: >-
Expand Down
11 changes: 7 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ const ECR_LOGIN_GITHUB_ACTION_USER_AGENT = 'amazon-ecr-login-for-github-actions'
const ECR_PUBLIC_REGISTRY_URI = 'public.ecr.aws';

const INPUTS = {
skipLogout: 'skip-logout',
httpProxy: 'http-proxy',
maskPassword: 'mask-password',
registries: 'registries',
registryType: 'registry-type',
httpProxy: 'http-proxy'
skipLogout: 'skip-logout'
};

const OUTPUTS = {
Expand Down Expand Up @@ -104,10 +105,11 @@ function replaceSpecialCharacters(registryUri) {

async function run() {
// Get inputs
const skipLogout = core.getInput(INPUTS.skipLogout, { required: false }).toLowerCase() === 'true';
const httpProxy = core.getInput(INPUTS.httpProxy, { required: false });
const maskPassword = core.getInput(INPUTS.maskPassword, { required: false }).toLowerCase() === 'true';
const registries = core.getInput(INPUTS.registries, { required: false });
const registryType = core.getInput(INPUTS.registryType, { required: false }).toLowerCase() || REGISTRY_TYPES.private;
const httpProxy = core.getInput(INPUTS.httpProxy, { required: false });
const skipLogout = core.getInput(INPUTS.skipLogout, { required: false }).toLowerCase() === 'true';

const registryUriState = [];

Expand Down Expand Up @@ -169,6 +171,7 @@ async function run() {

// Output docker username and password
const secretSuffix = replaceSpecialCharacters(registryUri);
if (maskPassword) core.setSecret(creds[1]);
core.setOutput(`${OUTPUTS.dockerUsername}_${secretSuffix}`, creds[0]);
core.setOutput(`${OUTPUTS.dockerPassword}_${secretSuffix}`, creds[1]);

Expand Down
76 changes: 58 additions & 18 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,19 @@ function mockGetInput(requestResponse) {
}

const ECR_DEFAULT_INPUTS = {
'http-proxy': '',
'mask-password': '',
'registries': '',
'skip-logout': '',
'registry-type': '',
'http-proxy': ''
'skip-logout': ''
};

const ECR_PUBLIC_DEFAULT_INPUTS = {
'http-proxy': '',
'mask-password': '',
'registries': '',
'skip-logout': '',
'registry-type': 'public',
'http-proxy': ''
'skip-logout': ''
};

const defaultAuthToken = {
Expand Down Expand Up @@ -76,9 +78,10 @@ describe('Login to ECR', () => {

test('gets auth token from ECR and logins the Docker client for each provided registry', async () => {
const mockInputs = {
'mask-password': '',
'registries': '123456789012,111111111111',
'skip-logout': '',
'registry-type': ''
'registry-type': '',
'skip-logout': ''
};
core.getInput = jest.fn().mockImplementation(mockGetInput(mockInputs));
ecrMock.on(GetAuthorizationTokenCommand).resolves({
Expand Down Expand Up @@ -116,9 +119,10 @@ describe('Login to ECR', () => {

test('outputs the registry ID if a single registry is provided in the input', async () => {
const mockInputs = {
'mask-password': '',
'registries': '111111111111',
'skip-logout': '',
'registry-type': ''
'registry-type': '',
'skip-logout': ''
};
core.getInput = jest.fn().mockImplementation(mockGetInput(mockInputs));
ecrMock.on(GetAuthorizationTokenCommand).resolves({
Expand Down Expand Up @@ -160,9 +164,10 @@ describe('Login to ECR', () => {

test('logged-in registries are saved as state even if the action fails', async () => {
const mockInputs = {
'mask-password': '',
'registries': '123456789012,111111111111',
'skip-logout': '',
'registry-type': ''
'registry-type': '',
'skip-logout': ''
};
core.getInput = jest.fn().mockImplementation(mockGetInput(mockInputs));
ecrMock.on(GetAuthorizationTokenCommand).resolves({
Expand Down Expand Up @@ -261,9 +266,10 @@ describe('Login to ECR', () => {
test('skips logout when specified and logging into default registry', async () => {
ecrMock.on(GetAuthorizationTokenCommand).resolves(defaultOutputToken);
const mockInputs = {
'mask-password': '',
'registries': '',
'skip-logout': 'true',
'registry-type': ''
'registry-type': '',
'skip-logout': 'true'
};
core.getInput = jest.fn().mockImplementation(mockGetInput(mockInputs));

Expand All @@ -280,9 +286,10 @@ describe('Login to ECR', () => {

test('skips logout when specified and logging into multiple registries', async () => {
const mockInputs = {
'mask-password': '',
'registries': '123456789012,111111111111',
'skip-logout': 'true',
'registry-type': ''
'registry-type': '',
'skip-logout': 'true'
};
core.getInput = jest.fn().mockImplementation(mockGetInput(mockInputs));
ecrMock.on(GetAuthorizationTokenCommand).resolves({
Expand Down Expand Up @@ -314,9 +321,10 @@ describe('Login to ECR', () => {

test('sets the Actions outputs to the docker credentials', async () => {
const mockInputs = {
'mask-password': '',
'registries': '123456789012,111111111111',
'skip-logout': 'true',
'registry-type': ''
'registry-type': '',
'skip-logout': 'true'
};
core.getInput = jest.fn().mockImplementation(mockGetInput(mockInputs));
ecrMock.on(GetAuthorizationTokenCommand).resolves({
Expand All @@ -341,6 +349,37 @@ describe('Login to ECR', () => {
expect(core.setOutput).toHaveBeenNthCalledWith(4, 'docker_password_111111111111_dkr_ecr_aws_region_1_amazonaws_com', 'bar');
});

test('sets the Actions outputs to the docker credentials with masked password', async () => {
const mockInputs = {
'mask-password': 'true',
'registries': '123456789012,111111111111',
'registry-type': '',
'skip-logout': 'true'
};
core.getInput = jest.fn().mockImplementation(mockGetInput(mockInputs));
ecrMock.on(GetAuthorizationTokenCommand).resolves({
authorizationData: [
{
authorizationToken: Buffer.from('hello:world').toString('base64'),
proxyEndpoint: 'https://123456789012.dkr.ecr.aws-region-1.amazonaws.com'
},
{
authorizationToken: Buffer.from('foo:bar').toString('base64'),
proxyEndpoint: 'https://111111111111.dkr.ecr.aws-region-1.amazonaws.com'
}
]
});

await run();

expect(core.setOutput).toHaveBeenCalledTimes(4);
expect(core.setSecret).toHaveBeenCalledTimes(2);
expect(core.setOutput).toHaveBeenNthCalledWith(1, 'docker_username_123456789012_dkr_ecr_aws_region_1_amazonaws_com', 'hello');
expect(core.setOutput).toHaveBeenNthCalledWith(2, 'docker_password_123456789012_dkr_ecr_aws_region_1_amazonaws_com', 'world');
expect(core.setOutput).toHaveBeenNthCalledWith(3, 'docker_username_111111111111_dkr_ecr_aws_region_1_amazonaws_com', 'foo');
expect(core.setOutput).toHaveBeenNthCalledWith(4, 'docker_password_111111111111_dkr_ecr_aws_region_1_amazonaws_com', 'bar');
});

describe('proxy settings', () => {
afterEach(() => {
process.env = {};
Expand Down Expand Up @@ -392,9 +431,10 @@ describe('Login to ECR Public', () => {
describe('inputs and outputs', () => {
test('error is caught by core.setFailed for invalid registry-type input', async () => {
const mockInputs = {
'mask-password': '',
'registries': '',
'skip-logout': '',
'registry-type': 'invalid'
'registry-type': 'invalid',
'skip-logout': ''
};
core.getInput = jest.fn().mockImplementation(mockGetInput(mockInputs));
ecrPublicMock.on(GetAuthorizationTokenCommandPublic).resolves(defaultAuthToken);
Expand Down

0 comments on commit 98f33d2

Please sign in to comment.