├── .busted ├── .editorconfig ├── .github └── workflows │ ├── lint.yml │ ├── sast.yml │ └── tests.yml ├── .gitignore ├── .luacheckrc ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── config.ld ├── docs ├── classes │ ├── AWS.html │ ├── ChainableTemporaryCredentials.html │ ├── CredentialProviderChain.html │ ├── Credentials.html │ ├── EC2MetadataCredentials.html │ ├── EnvironmentCredentials.html │ ├── RemoteCredentials.html │ ├── SharedFileCredentials.html │ └── TokenFileWebIdentityCredentials.html ├── index.html ├── ldoc.css ├── modules │ ├── resty.aws.config.html │ ├── resty.aws.service.rds.signer.html │ └── resty.aws.utils.html └── topics │ └── README.md.html ├── lua-resty-aws-dev-1.rockspec.template ├── spec ├── 01-generic │ ├── 01-config_spec.lua │ ├── 02-aws_spec.lua │ └── 03-service_spec.lua ├── 02-requests │ ├── 01-validate_spec.lua │ ├── 02-build_request_spec.lua │ ├── 03-execute_spec.lua │ ├── 04-sign_v4_spec.lua │ └── 05-presign_v4_spec.lua ├── 03-credentials │ ├── 01-Credentials_spec.lua │ ├── 02-EC2MetadataCredentials_spec.lua │ ├── 03-EnvironmentCredentials_spec.lua │ ├── 04-RemoteCredentials_spec.lua │ ├── 05-CredentialProviderChain_spec.lua │ ├── 06-ChainableTemporaryCredentials_spec.lua │ ├── 07-TokenFileWebIdentityCredentials_spec.lua │ └── 08-SharedFileCredentials_spec.lua ├── 04-services │ ├── 01-secret_manager.lua │ ├── 02-s3.lua │ ├── 03-s3_compat_api.lua │ ├── 04-rds-utils_spec.lua │ └── 05-sts_spec.lua ├── helpers.lua └── resty-runner.lua ├── src └── resty │ └── aws │ ├── config.lua │ ├── credentials │ ├── ChainableTemporaryCredentials.lua │ ├── CredentialProviderChain.lua │ ├── Credentials.lua │ ├── EC2MetadataCredentials.lua │ ├── EnvironmentCredentials.lua │ ├── README.md │ ├── RemoteCredentials.lua │ ├── SharedFileCredentials.lua │ └── TokenFileWebIdentityCredentials.lua │ ├── init.lua │ ├── request │ ├── build.lua │ ├── execute.lua │ ├── sign.lua │ ├── signatures │ │ ├── none.lua │ │ ├── presign.lua │ │ ├── utils.lua │ │ └── v4.lua │ └── validate.lua │ ├── service │ └── rds │ │ └── signer.lua │ └── utils.lua ├── test2.lua ├── update_api_files.sh └── upload.sh /.busted: -------------------------------------------------------------------------------- 1 | return { 2 | default = { 3 | lua = "spec/resty-runner.lua", 4 | verbose = true, 5 | coverage = false, 6 | output = "gtest", 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | charset = utf-8 8 | 9 | [*.lua] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [Makefile] 14 | indent_style = tab 15 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: {} 5 | workflow_dispatch: {} 6 | push: 7 | branches: 8 | - main 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: ${{ github.event_name == 'pull_request' }} 13 | 14 | jobs: 15 | lua-check: 16 | timeout-minutes: ${{ fromJSON(vars.GHA_DEFAULT_TIMEOUT) }} 17 | name: Lua Check 18 | runs-on: ubuntu-latest 19 | permissions: 20 | contents: read 21 | issues: read 22 | checks: write 23 | pull-requests: write 24 | if: (github.actor != 'dependabot[bot]') 25 | 26 | steps: 27 | - name: Checkout source code 28 | uses: actions/checkout@v3 29 | 30 | # Optional step to run on only changed files 31 | - name: Get changed files 32 | id: changed-files 33 | uses: kong/changed-files@4edd678ac3f81e2dc578756871e4d00c19191daf 34 | with: 35 | files: | 36 | **.lua 37 | 38 | - name: Lua Check 39 | if: steps.changed-files.outputs.any_changed == 'true' 40 | uses: Kong/public-shared-actions/code-check-actions/lua-lint@a98be0184f832cb24a9dd233f99074e8ba17b488 # v2.3.3 41 | with: 42 | additional_args: '--no-default-config --config .luacheckrc' 43 | files: ${{ steps.changed-files.outputs.all_changed_files }} 44 | action_fail: true 45 | -------------------------------------------------------------------------------- /.github/workflows/sast.yml: -------------------------------------------------------------------------------- 1 | name: SAST 2 | 3 | on: 4 | pull_request: {} 5 | push: 6 | branches: 7 | - master 8 | - main 9 | workflow_dispatch: {} 10 | 11 | 12 | jobs: 13 | semgrep: 14 | timeout-minutes: ${{ fromJSON(vars.GHA_DEFAULT_TIMEOUT) }} 15 | name: Semgrep SAST 16 | runs-on: ubuntu-latest 17 | permissions: 18 | # required for all workflows 19 | security-events: write 20 | # only required for workflows in private repositories 21 | actions: read 22 | contents: read 23 | 24 | if: (github.actor != 'dependabot[bot]') 25 | 26 | steps: 27 | - uses: actions/checkout@v3 28 | - uses: Kong/public-shared-actions/security-actions/semgrep@ac8939f0382827fbb43ce4e0028066a5ea4db01d # 4.1.2 29 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | LIBEXPAT_DOWNLOAD_URL: https://github.com/libexpat/libexpat/releases/download/R_2_5_0/expat-2.5.0.tar.gz 7 | 8 | jobs: 9 | 10 | tests: 11 | timeout-minutes: ${{ fromJSON(vars.GHA_DEFAULT_TIMEOUT) }} 12 | name: Busted Tests 13 | 14 | strategy: 15 | matrix: 16 | openresty_version: 17 | - 1.17.8.2 18 | - 1.19.9.1 19 | 20 | runs-on: ubuntu-latest 21 | container: 22 | image: openresty/openresty:${{ matrix.openresty_version }}-alpine-fat 23 | # --init runs tinit as PID 1 and prevents the 'WARNING: killing the child process' spam from the test suite 24 | options: --init 25 | 26 | steps: 27 | - uses: actions/checkout@v2 28 | - name: Install deps 29 | run: | 30 | apk add --no-cache curl perl bash wget git perl-dev libarchive-tools nodejs jq 31 | ln -s /usr/bin/bsdtar /usr/bin/tar 32 | 33 | - name: Build libexpat 34 | if: matrix.openresty_version == '1.17.8.2' 35 | run: | 36 | mkdir -p /tmp/expat 37 | curl -Ls $LIBEXPAT_DOWNLOAD_URL | tar -xz --strip-components=1 -f - -C /tmp/expat 38 | cd /tmp/expat && ./configure && make && make install 39 | 40 | - name: Install libexpat from package manager 41 | if: matrix.openresty_version == '1.19.9.1' 42 | run: | 43 | apk add --no-cache expat-dev 44 | 45 | - name: Cache 46 | uses: actions/cache@v4 47 | with: 48 | path: | 49 | ~/.cache 50 | key: ${{ runner.os }}-${{ matrix.openresty_version }}-cache 51 | 52 | - name: Install Busted 53 | run: | 54 | /usr/local/openresty/luajit/bin/luarocks install busted 55 | /usr/local/openresty/luajit/bin/luarocks install luatz 56 | /usr/local/openresty/luajit/bin/luarocks install luasocket 57 | 58 | - uses: actions/checkout@v2 59 | 60 | - name: Run tests 61 | run: | 62 | make dev 63 | /usr/local/openresty/luajit/bin/luarocks make 64 | busted 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Lua sources 2 | luac.out 3 | 4 | # luarocks build files 5 | *.rock 6 | *.zip 7 | *.tar.gz 8 | 9 | # Object files 10 | *.o 11 | *.os 12 | *.ko 13 | *.obj 14 | *.elf 15 | 16 | # Precompiled Headers 17 | *.gch 18 | *.pch 19 | 20 | # Libraries 21 | *.lib 22 | *.a 23 | *.la 24 | *.lo 25 | *.def 26 | *.exp 27 | 28 | # Shared objects (inc. Windows DLLs) 29 | *.dll 30 | *.so 31 | *.so.* 32 | *.dylib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | *.i*86 39 | *.x86_64 40 | *.hex 41 | 42 | # temp clone of AWS JS SDK 43 | delete-me 44 | 45 | # generated files 46 | *.rockspec 47 | src/resty/aws/raw-api 48 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | std = "ngx_lua" 2 | unused_args = false 3 | redefined = false 4 | max_line_length = false 5 | 6 | 7 | globals = { 8 | --"_KONG", 9 | --"kong", 10 | --"ngx.IS_CLI", 11 | } 12 | 13 | 14 | not_globals = { 15 | "string.len", 16 | "table.getn", 17 | } 18 | 19 | 20 | ignore = { 21 | --"6.", -- ignore whitespace warnings 22 | } 23 | 24 | 25 | exclude_files = { 26 | --"spec/fixtures/invalid-module.lua", 27 | --"spec-old-api/fixtures/invalid-module.lua", 28 | } 29 | 30 | 31 | --files["kong/plugins/ldap-auth/*.lua"] = { 32 | -- read_globals = { 33 | -- "bit.mod", 34 | -- "string.pack", 35 | -- "string.unpack", 36 | -- }, 37 | --} 38 | 39 | 40 | files["spec/**/*.lua"] = { 41 | std = "ngx_lua+busted", 42 | } 43 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: docs install dev clean test pack 2 | 3 | # specify VERSION as 'x.y.z' or 'dev' 4 | target_rock := lua-resty-aws-${VERSION}-1.src.rock 5 | 6 | install: 7 | if [ ! -d "src/resty/aws/raw-api" ]; then \ 8 | bash ./update_api_files.sh; \ 9 | fi 10 | luarocks make 11 | 12 | dev: 13 | bash ./update_api_files.sh 14 | 15 | test: 16 | luacheck . 17 | busted 18 | 19 | docs: 20 | mv docs/ldoc.css ./ 21 | -@rm -rf docs 22 | mkdir docs 23 | mv ldoc.css docs/ 24 | ldoc . 25 | 26 | clean: 27 | -@rm lua-resty-aws-dev-1.rockspec 28 | -@rm *.rock 29 | -@rm -rf delete-me 30 | -@rm -rf src/resty/aws/raw-api 31 | -@rm -rf ./docs; git checkout ./docs 32 | 33 | $(target_rock): 34 | [ -n "$$VERSION" ] || { echo VERSION not set; exit 1; } 35 | -@rm -rf /tmp/random_dir_2cs4f0tghRT 36 | mkdir /tmp/random_dir_2cs4f0tghRT 37 | cd /tmp/random_dir_2cs4f0tghRT; git clone https://github.com/kong/lua-resty-aws.git 38 | cd /tmp/random_dir_2cs4f0tghRT/lua-resty-aws; if [ ! "${VERSION}" = "dev" ]; then git checkout ${VERSION}; fi 39 | cd /tmp/random_dir_2cs4f0tghRT/lua-resty-aws; make dev 40 | cd /tmp/random_dir_2cs4f0tghRT; zip -r lua-resty-aws-${VERSION}-1.src.rock lua-resty-aws 41 | cd /tmp/random_dir_2cs4f0tghRT; cat lua-resty-aws/lua-resty-aws-dev-1.rockspec | sed "s/package_version = \"dev\"/package_version = \"${VERSION}\"/" > lua-resty-aws-${VERSION}-1.rockspec 42 | cd /tmp/random_dir_2cs4f0tghRT; zip -r lua-resty-aws-${VERSION}-1.src.rock lua-resty-aws-${VERSION}-1.rockspec 43 | mv /tmp/random_dir_2cs4f0tghRT/lua-resty-aws-${VERSION}-1.src.rock ./ 44 | -@rm lua-resty-aws-${VERSION}-1.rockspec 45 | mv /tmp/random_dir_2cs4f0tghRT/lua-resty-aws-${VERSION}-1.rockspec ./ 46 | 47 | pack: $(target_rock) 48 | 49 | upload: $(target_rock) 50 | [ -n "$$VERSION" ] || { echo VERSION not set; exit 1; } 51 | [ -n "$$APIKEY" ] || { echo APIKEY not set, should contain the LuaRocks api-key for uploads ; exit 1; } 52 | ./upload.sh ${VERSION} ${APIKEY} 53 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | At lua-resty-aws, we take security issues very seriously. If you believe you have found a security vulnerability in our project, we encourage you to disclose it responsibly. Please report any potential security vulnerabilities to us by sending an email to [vulnerability@konghq.com](mailto:vulnerability@konghq.com). 6 | 7 | ## How to Report 8 | 9 | 1. **Do not publicly disclose the vulnerability**: Please do not create a GitHub issue or post the vulnerability on public forums. Instead, contact us directly at [vulnerability@konghq.com](mailto:vulnerability@konghq.com). 10 | 1. **Provide detailed information**: When reporting a vulnerability, please include as much information as possible to help us understand and reproduce the issue. This may include: 11 | - Description of the vulnerability 12 | - Steps to reproduce the issue 13 | - Potential impact 14 | - Any relevant logs or screenshots 15 | 16 | ## What to Expect 17 | 18 | - **Acknowledgment**: We will acknowledge receipt of your vulnerability report within 48 hours. 19 | - **Investigation**: Our security team will investigate the report and will keep you informed of the progress. We aim to resolve critical vulnerabilities within 30 days of confirmation. 20 | - **Disclosure**: We prefer coordinated disclosure and will work with you to schedule the disclosure of the vulnerability in a way that minimizes the risk to users. 21 | 22 | ## Bug Bounty Program 23 | 24 | We encourage security researchers to participate in our bug bounty program as outlined on the [Kong Vulnerability Disclosure](https://konghq.com/compliance/bug-bounty) page. This program provides rewards for discovering and reporting security vulnerabilities in accordance with our disclosure guidelines. 25 | 26 | Thank you for helping to keep lua-resty-aws secure. 27 | 28 | For more information on our security policies and guidelines, please visit the [Kong Vulnerability Disclosure](https://konghq.com/compliance/bug-bounty) page. 29 | 30 | ## Contact 31 | 32 | For any questions or further assistance, please contact us at [vulnerability@konghq.com](mailto:vulnerability@konghq.com). 33 | -------------------------------------------------------------------------------- /config.ld: -------------------------------------------------------------------------------- 1 | project='lua-resty-aws' 2 | title='AWS SDK for OpenResty' 3 | description='AWS SDK to interact with AWS services' 4 | format='discount' 5 | file='./src/' 6 | dir='docs' 7 | readme='README.md' 8 | sort=true 9 | sort_modules=true 10 | all=false 11 | style='./docs/' 12 | -------------------------------------------------------------------------------- /docs/classes/AWS.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | AWS SDK for OpenResty 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 64 | 65 |
66 | 67 |

Class AWS

68 |

AWS class.

69 |

70 | 71 |

72 | 73 | 74 |

Methods

75 | 76 | 77 | 78 | 79 | 80 |
aws:new (config)Creates a new AWS instance.
81 | 82 |
83 |
84 | 85 | 86 |

Methods

87 | 88 |
89 |
90 | 91 | aws:new (config) 92 |
93 |
94 | Creates a new AWS instance. 95 | By default the instance will get a CredentialProviderChain set of 96 | credentials, which can be overridden.

97 | 98 |

Note that the AWS objects as well as the Service objects are expensive to 99 | create, so you might want to reuse them. 100 | 101 | 102 |

Parameters:

103 |
    104 |
  • config 105 | (optional) the config table to be copied into the instance as the global aws_instance.config 106 |
  • 107 |
108 | 109 | 110 | 111 | 112 |

Usage:

113 |
    114 |
  • -- in the "init" phase initialize the configuration
    115 | local _ = require("resty.aws.config").global
  • 116 |
  • -- In your code
    117 | local AWS = require("resty.aws")
    118 | local AWS_global_config = require("resty.aws.config").global
    119 | 
    120 | local config = { region = AWS_global_config.region }
    121 | 
    122 | local aws = AWS(config)
    123 | 
    124 | -- Override default "CredentialProviderChain" credentials.
    125 | -- This is optional, the defaults should work with AWS-IAM.
    126 | local my_creds = aws:Credentials {
    127 |   accessKeyId = "access",
    128 |   secretAccessKey = "secret",
    129 |   sessionToken = "token",
    130 | }
    131 | aws.config.credentials = my_creds
    132 | 
    133 | -- instantiate a service (optionally overriding the aws-instance config)
    134 | local sm = aws:SecretsManager {
    135 |   region = "us-east-2",
    136 | }
    137 | 
    138 | -- Invoke a method.
    139 | -- Note this only takes the parameter table, and NOT a callback as the
    140 | -- JS sdk requires. Instead this call will directly return the results.
    141 | local results, err = sm:getSecretValue {
    142 |   SecretId = "arn:aws:secretsmanager:us-east-2:238406704566:secret:test-HN1F1k",
    143 | }
  • 144 |
145 | 146 |
147 |
148 | 149 | 150 |
151 |
152 |
153 | generated by LDoc 1.5.0 154 | Last updated 2024-09-23 09:29:37 155 |
156 |
157 | 158 | 159 | -------------------------------------------------------------------------------- /docs/classes/ChainableTemporaryCredentials.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | AWS SDK for OpenResty 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 64 | 65 |
66 | 67 |

Class ChainableTemporaryCredentials

68 |

ChainableTemporaryCredentials class.

69 |

70 | 71 |

72 | 73 | 74 |

Functions

75 | 76 | 77 | 78 | 79 | 80 |
aws:ChainableTemporaryCredentials (opt)Constructor, inherits from Credentials.
81 | 82 |
83 |
84 | 85 | 86 |

Functions

87 | 88 |
89 |
90 | 91 | aws:ChainableTemporaryCredentials (opt) 92 |
93 |
94 | Constructor, inherits from Credentials. 95 | 96 | 97 |

Parameters:

98 |
    99 |
  • opt options table, additional fields to the Credentials class: 100 |
      101 |
    • params 102 | params table for the assumeRole function, or array of those 103 | tables in case of a chain of roles to assume. 104 |
    • 105 |
    • aws 106 | AWS instance, required when creating a chain. 107 |
    • 108 |
    • masterCredentials 109 | Credentials instance to use when assuming the 110 | role. Defaults to sts.config.credentials or aws.config.credentials in that 111 | order. 112 |
    • 113 |
    • sts 114 | the STS service instance to use for fetching the credentials. 115 | Defaults to a new instance created as aws:STS(). 116 |
    • 117 |
    118 |
119 | 120 | 121 | 122 | 123 |

Usage:

124 |
    125 |
    -- creating a chain of assumed roles
    126 | local aws = AWS()      -- provides the masterCredentials
    127 | local role1 = { ... }  -- parameters to assume role1, from the masterCredentials
    128 | local role2 = { ... }  -- parameters to assume role2, from the role1 credentials
    129 | local role3 = { ... }  -- parameters to assume role3, from the role2 credentials
    130 | 
    131 | local creds = aws:ChainableTemporaryCredentials {
    132 |     params = { role1, role2, role3 },
    133 |   }
    134 | 
    135 | -- Get credentials for role3
    136 | local success, id, key, token, expiretime = creds:get()
    137 | if not success then
    138 |   return nil, id
    139 | end
    140 |
141 | 142 |
143 |
144 | 145 | 146 |
147 |
148 |
149 | generated by LDoc 1.5.0 150 | Last updated 2024-09-23 09:29:37 151 |
152 |
153 | 154 | 155 | -------------------------------------------------------------------------------- /docs/classes/CredentialProviderChain.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | AWS SDK for OpenResty 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 64 | 65 |
66 | 67 |

Class CredentialProviderChain

68 |

CredentialProviderChain class.

69 |

70 | 71 |

72 | 73 | 74 |

Functions

75 | 76 | 77 | 78 | 79 | 80 |
aws:CredentialProviderChain (opt)Constructor, inherits from Credentials.
81 | 82 |
83 |
84 | 85 | 86 |

Functions

87 | 88 |
89 |
90 | 91 | aws:CredentialProviderChain (opt) 92 |
93 |
94 | 95 |

Constructor, inherits from Credentials.

96 | 97 |

The providers array defaults to the following list (in order, not all implemented):

98 | 99 |
    100 |
  1. EnvironmentCredentials; envPrefix = 'AWS'

  2. 101 |
  3. EnvironmentCredentials; envPrefix = 'AMAZON'

  4. 102 |
  5. SharedIniFileCredentials

  6. 103 |
  7. RemoteCredentials

  8. 104 |
  9. ProcessCredentials

  10. 105 |
  11. TokenFileWebIdentityCredentials

  12. 106 |
  13. EC2MetadataCredentials (only if AWS_EC2_METADATA_DISABLED hasn't been set to true)

  14. 107 |
108 | 109 | 110 | 111 | 112 |

Parameters:

113 |
    114 |
  • opt options table, additional fields to the Credentials class: 115 |
      116 |
    • providers 117 | array of Credentials objects or functions (functions must return a Credentials object) 118 |
    • 119 |
    120 |
121 | 122 | 123 | 124 | 125 | 126 |
127 |
128 | 129 | 130 |
131 |
132 |
133 | generated by LDoc 1.5.0 134 | Last updated 2024-09-23 09:29:37 135 |
136 |
137 | 138 | 139 | -------------------------------------------------------------------------------- /docs/classes/Credentials.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | AWS SDK for OpenResty 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 65 | 66 |
67 | 68 |

Class Credentials

69 |

Credentials class.

70 |

Manually sets credentials. 71 | Also the base class for all credential classes.

72 | 73 | 74 |

Functions

75 | 76 | 77 | 78 | 79 | 80 |
aws:Credentials (opt)Constructor.
81 |

Methods

82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 |
credentials:get ()Gets credentials, refreshes if required.
credentials:needsRefresh ()checks whether credentials have expired.
credentials:refresh ()updates credentials.
credentials:set (accessKeyId, secretAccessKey, sessionToken, expireTime)Sets credentials.
100 | 101 |
102 |
103 | 104 | 105 |

Functions

106 | 107 |
108 |
109 | 110 | aws:Credentials (opt) 111 |
112 |
113 | Constructor. 114 | 115 | 116 |

Parameters:

117 |
    118 |
  • opt options table 119 |
      120 |
    • expiryWindow 121 | number (default 15) of seconds before expiry to start refreshing 122 |
    • 123 |
    • accessKeyId 124 | (optional) only specify if you manually specify credentials 125 |
    • 126 |
    • secretAccessKey 127 | (optional) only specify if you manually specify credentials 128 |
    • 129 |
    • sessionToken 130 | (optional) only specify if you manually specify credentials 131 |
    • 132 |
    • expireTime 133 | (optional, number (epoch) or string (rfc3339)). This should 134 | not be specified. Default: If any of the 3 secrets are given; 10yrs, otherwise 0 135 | (forcing a refresh on the first call to get). 136 |
    • 137 |
    138 |
139 | 140 | 141 | 142 | 143 |

Usage:

144 |
    145 |
    local my_creds = aws:Credentials {
    146 |   accessKeyId = "access",
    147 |   secretAccessKey = "secret",
    148 |   sessionToken = "token",
    149 | }
    150 | 
    151 | local success, id, secret, token = my_creds:get()
    152 |
153 | 154 |
155 |
156 |

Methods

157 | 158 |
159 |
160 | 161 | credentials:get () 162 |
163 |
164 | Gets credentials, refreshes if required. 165 | Returns credentials, doesn't take a callback like AWS SDK.

166 | 167 |

When a refresh is executed, it will be done within a semaphore to prevent 168 | many simultaneous refreshes. 169 | 170 | 171 | 172 |

Returns:

173 |
    174 | 175 | success(true) + accessKeyId + secretAccessKey + sessionToken + expireTime or success(false) + error 176 |
177 | 178 | 179 | 180 | 181 |
182 |
183 | 184 | credentials:needsRefresh () 185 |
186 |
187 | checks whether credentials have expired. 188 | 189 | 190 | 191 |

Returns:

192 |
    193 | 194 | boolean 195 |
196 | 197 | 198 | 199 | 200 |
201 |
202 | 203 | credentials:refresh () 204 |
205 |
206 | updates credentials. 207 | override in subclasses, should call set to set the properties. 208 | 209 | 210 | 211 |

Returns:

212 |
    213 | 214 | success, or nil+err 215 |
216 | 217 | 218 | 219 | 220 |
221 |
222 | 223 | credentials:set (accessKeyId, secretAccessKey, sessionToken, expireTime) 224 |
225 |
226 | Sets credentials. 227 | additional to AWS SDK 228 | 229 | 230 |

Parameters:

231 |
    232 |
  • accessKeyId 233 | 234 | 235 | 236 |
  • 237 |
  • secretAccessKey 238 | 239 | 240 | 241 |
  • 242 |
  • sessionToken 243 | 244 | 245 | 246 |
  • 247 |
  • expireTime 248 | (optional) number (unix epoch based), or string (valid rfc 3339) 249 |
  • 250 |
251 | 252 |

Returns:

253 |
    254 | 255 | true 256 |
257 | 258 | 259 | 260 | 261 |
262 |
263 | 264 | 265 |
266 |
267 |
268 | generated by LDoc 1.5.0 269 | Last updated 2024-09-23 09:29:37 270 |
271 |
272 | 273 | 274 | -------------------------------------------------------------------------------- /docs/classes/EC2MetadataCredentials.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | AWS SDK for OpenResty 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 64 | 65 |
66 | 67 |

Class EC2MetadataCredentials

68 |

EC2MetadataCredentials class.

69 |

70 | 71 |

72 | 73 | 74 |

Functions

75 | 76 | 77 | 78 | 79 | 80 |
aws:EC2MetadataCredentials (opt)Constructor, inherits from Credentials.
81 | 82 |
83 |
84 | 85 | 86 |

Functions

87 | 88 |
89 |
90 | 91 | aws:EC2MetadataCredentials (opt) 92 |
93 |
94 | Constructor, inherits from Credentials. 95 | 96 | 97 |

Parameters:

98 |
    99 |
  • opt 100 | options table, no additional fields to the Credentials class. 101 |
  • 102 |
103 | 104 | 105 | 106 | 107 | 108 |
109 |
110 | 111 | 112 |
113 |
114 |
115 | generated by LDoc 1.5.0 116 | Last updated 2024-09-23 09:29:37 117 |
118 |
119 | 120 | 121 | -------------------------------------------------------------------------------- /docs/classes/EnvironmentCredentials.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | AWS SDK for OpenResty 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 64 | 65 |
66 | 67 |

Class EnvironmentCredentials

68 |

EnvironmentCredentials class.

69 |

70 | 71 |

72 | 73 | 74 |

Functions

75 | 76 | 77 | 78 | 79 | 80 |
aws:EnvironmentCredentials (opt)Constructor, inherits from Credentials.
81 | 82 |
83 |
84 | 85 | 86 |

Functions

87 | 88 |
89 |
90 | 91 | aws:EnvironmentCredentials (opt) 92 |
93 |
94 | Constructor, inherits from Credentials.

95 | 96 |

Note: this class will fetch the credentials upon instantiation. So it can be 97 | instantiated in the init phase where there is still access to the environment 98 | variables. The standard prefixes AWS and AMAZON are covered by the config 99 | module, so in case those are used, only the config module needs to be loaded 100 | in the init phase. 101 | 102 | 103 |

Parameters:

104 |
    105 |
  • opt options table, additional fields to the Credentials class: 106 |
      107 |
    • envPrefix 108 | prefix to use when looking for environment variables, defaults to "AWS". 109 |
    • 110 |
    111 |
112 | 113 | 114 | 115 | 116 | 117 |
118 |
119 | 120 | 121 |
122 |
123 |
124 | generated by LDoc 1.5.0 125 | Last updated 2024-09-23 09:29:37 126 |
127 |
128 | 129 | 130 | -------------------------------------------------------------------------------- /docs/classes/RemoteCredentials.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | AWS SDK for OpenResty 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 64 | 65 |
66 | 67 |

Class RemoteCredentials

68 |

RemoteCredentials class.

69 |

70 | 71 |

72 | 73 | 74 |

Functions

75 | 76 | 77 | 78 | 79 | 80 |
aws:RemoteCredentials (opt)Constructor, inherits from Credentials.
81 | 82 |
83 |
84 | 85 | 86 |

Functions

87 | 88 |
89 |
90 | 91 | aws:RemoteCredentials (opt) 92 |
93 |
94 | Constructor, inherits from Credentials. 95 | 96 | 97 |

Parameters:

98 |
    99 |
  • opt 100 | options table, no additional fields to the Credentials class. 101 |
  • 102 |
103 | 104 | 105 | 106 | 107 | 108 |
109 |
110 | 111 | 112 |
113 |
114 |
115 | generated by LDoc 1.5.0 116 | Last updated 2024-09-23 09:29:37 117 |
118 |
119 | 120 | 121 | -------------------------------------------------------------------------------- /docs/classes/SharedFileCredentials.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | AWS SDK for OpenResty 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 64 | 65 |
66 | 67 |

Class SharedFileCredentials

68 |

SharedFileCredentials class.

69 |

70 | 71 |

72 | 73 | 74 |

Functions

75 | 76 | 77 | 78 | 79 | 80 |
aws:SharedFileCredentials (opt)Constructor, inherits from Credentials.
81 | 82 |
83 |
84 | 85 | 86 |

Functions

87 | 88 |
89 |
90 | 91 | aws:SharedFileCredentials (opt) 92 |
93 |
94 | Constructor, inherits from Credentials. 95 | 96 | 97 |

Parameters:

98 |
    99 |
  • opt 100 | options table, additional fields to the Credentials class: 101 |
  • 102 |
103 | 104 | 105 | 106 | 107 | 108 |
109 |
110 | 111 | 112 |
113 |
114 |
115 | generated by LDoc 1.5.0 116 | Last updated 2024-09-23 09:29:37 117 |
118 |
119 | 120 | 121 | -------------------------------------------------------------------------------- /docs/classes/TokenFileWebIdentityCredentials.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | AWS SDK for OpenResty 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 64 | 65 |
66 | 67 |

Class TokenFileWebIdentityCredentials

68 |

TokenFileWebIdentityCredentials class.

69 |

70 | 71 |

72 | 73 | 74 |

Functions

75 | 76 | 77 | 78 | 79 | 80 |
aws:TokenFileWebIdentityCredentials (opts)Constructor, inherits from Credentials.
81 | 82 |
83 |
84 | 85 | 86 |

Functions

87 | 88 |
89 |
90 | 91 | aws:TokenFileWebIdentityCredentials (opts) 92 |
93 |
94 | Constructor, inherits from Credentials. 95 | 96 | 97 |

Parameters:

98 |
    99 |
  • opts options table, only listing additional fields to the Credentials class. 100 |
      101 |
    • token_file 102 | string 103 | filename of the token file 104 | (default AWS_WEB_IDENTITY_TOKEN_FILE env var) 105 |
    • 106 |
    • role_arn 107 | string 108 | arn of the role to assume 109 | (default AWS_ROLE_ARN env var) 110 |
    • 111 |
    • session_name 112 | string 113 | session name 114 | (default AWS_ROLE_SESSION_NAME env var or 'session@lua-resty-aws') 115 |
    • 116 |
    117 |
118 | 119 | 120 | 121 | 122 | 123 |
124 |
125 | 126 | 127 |
128 |
129 |
130 | generated by LDoc 1.5.0 131 | Last updated 2024-09-23 09:29:37 132 |
133 |
134 | 135 | 136 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | AWS SDK for OpenResty 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 57 | 58 |
59 | 60 | 61 |

AWS SDK to interact with AWS services

62 | 63 |

Modules

64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
resty.aws.configLoad AWS configuration.
resty.aws.service.rds.signerSigner class for RDS tokens for RDS DB access.
resty.aws.utilsAWS utility module.
78 |

Classes

79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 |
AWSAWS class.
ChainableTemporaryCredentialsChainableTemporaryCredentials class.
CredentialProviderChainCredentialProviderChain class.
CredentialsCredentials class.
EC2MetadataCredentialsEC2MetadataCredentials class.
EnvironmentCredentialsEnvironmentCredentials class.
RemoteCredentialsRemoteCredentials class.
SharedFileCredentialsSharedFileCredentials class.
TokenFileWebIdentityCredentialsTokenFileWebIdentityCredentials class.
117 |

Topics

118 | 119 | 120 | 121 | 122 | 123 |
README.md
124 | 125 |
126 |
127 |
128 | generated by LDoc 1.5.0 129 | Last updated 2024-09-23 09:29:37 130 |
131 |
132 | 133 | 134 | -------------------------------------------------------------------------------- /docs/ldoc.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #47555c; 3 | font-size: 16px; 4 | font-family: "Open Sans", sans-serif; 5 | margin: 0; 6 | background: #eff4ff; 7 | } 8 | 9 | a:link { color: #008fee; } 10 | a:visited { color: #008fee; } 11 | a:hover { color: #22a7ff; } 12 | 13 | h1 { font-size:26px; font-weight: normal; } 14 | h2 { font-size:22px; font-weight: normal; } 15 | h3 { font-size:18px; font-weight: normal; } 16 | h4 { font-size:16px; font-weight: bold; } 17 | 18 | hr { 19 | height: 1px; 20 | background: #c1cce4; 21 | border: 0px; 22 | margin: 15px 0; 23 | } 24 | 25 | code, tt { 26 | font-family: monospace; 27 | } 28 | span.parameter { 29 | font-family: monospace; 30 | font-weight: bold; 31 | color: rgb(99, 115, 131); 32 | } 33 | span.parameter:after { 34 | content:":"; 35 | } 36 | span.types:before { 37 | content:"("; 38 | } 39 | span.types:after { 40 | content:")"; 41 | } 42 | .type { 43 | font-weight: bold; font-style:italic 44 | } 45 | 46 | p.name { 47 | font-family: "Andale Mono", monospace; 48 | } 49 | 50 | #navigation { 51 | float: left; 52 | background-color: white; 53 | border-right: 1px solid #d3dbec; 54 | border-bottom: 1px solid #d3dbec; 55 | 56 | width: 14em; 57 | vertical-align: top; 58 | overflow: visible; 59 | } 60 | 61 | #navigation br { 62 | display: none; 63 | } 64 | 65 | #navigation h1 { 66 | background-color: white; 67 | border-bottom: 1px solid #d3dbec; 68 | padding: 15px; 69 | margin-top: 0px; 70 | margin-bottom: 0px; 71 | } 72 | 73 | #navigation h2 { 74 | font-size: 18px; 75 | background-color: white; 76 | border-bottom: 1px solid #d3dbec; 77 | padding-left: 15px; 78 | padding-right: 15px; 79 | padding-top: 10px; 80 | padding-bottom: 10px; 81 | margin-top: 30px; 82 | margin-bottom: 0px; 83 | } 84 | 85 | #content h1 { 86 | background-color: #2c3e67; 87 | color: white; 88 | padding: 15px; 89 | margin: 0px; 90 | } 91 | 92 | #content h2 { 93 | background-color: #6c7ea7; 94 | color: white; 95 | padding: 15px; 96 | padding-top: 15px; 97 | padding-bottom: 15px; 98 | margin-top: 0px; 99 | } 100 | 101 | #content h2 a { 102 | background-color: #6c7ea7; 103 | color: white; 104 | text-decoration: none; 105 | } 106 | 107 | #content h2 a:hover { 108 | text-decoration: underline; 109 | } 110 | 111 | #content h3 { 112 | font-style: italic; 113 | padding-top: 15px; 114 | padding-bottom: 4px; 115 | margin-right: 15px; 116 | margin-left: 15px; 117 | margin-bottom: 5px; 118 | border-bottom: solid 1px #bcd; 119 | } 120 | 121 | #content h4 { 122 | margin-right: 15px; 123 | margin-left: 15px; 124 | border-bottom: solid 1px #bcd; 125 | } 126 | 127 | #content pre { 128 | margin: 15px; 129 | } 130 | 131 | pre { 132 | background-color: rgb(50, 55, 68); 133 | color: white; 134 | border-radius: 3px; 135 | /* border: 1px solid #C0C0C0; /* silver */ 136 | padding: 15px; 137 | overflow: auto; 138 | font-family: "Andale Mono", monospace; 139 | } 140 | 141 | #content ul pre.example { 142 | margin-left: 0px; 143 | } 144 | 145 | table.index { 146 | /* border: 1px #00007f; */ 147 | } 148 | table.index td { text-align: left; vertical-align: top; } 149 | 150 | #navigation ul 151 | { 152 | font-size:1em; 153 | list-style-type: none; 154 | margin: 1px 1px 10px 1px; 155 | padding-left: 20px; 156 | } 157 | 158 | #navigation li { 159 | text-indent: -1em; 160 | display: block; 161 | margin: 3px 0px 0px 22px; 162 | } 163 | 164 | #navigation li li a { 165 | margin: 0px 3px 0px -1em; 166 | } 167 | 168 | #content { 169 | margin-left: 14em; 170 | } 171 | 172 | #content p { 173 | padding-left: 15px; 174 | padding-right: 15px; 175 | } 176 | 177 | #content table { 178 | padding-left: 15px; 179 | padding-right: 15px; 180 | background-color: white; 181 | } 182 | 183 | #content p, #content table, #content ol, #content ul, #content dl { 184 | max-width: 900px; 185 | } 186 | 187 | #about { 188 | padding: 15px; 189 | padding-left: 16em; 190 | background-color: white; 191 | border-top: 1px solid #d3dbec; 192 | border-bottom: 1px solid #d3dbec; 193 | } 194 | 195 | table.module_list, table.function_list { 196 | border-width: 1px; 197 | border-style: solid; 198 | border-color: #cccccc; 199 | border-collapse: collapse; 200 | margin: 15px; 201 | } 202 | table.module_list td, table.function_list td { 203 | border-width: 1px; 204 | padding-left: 10px; 205 | padding-right: 10px; 206 | padding-top: 5px; 207 | padding-bottom: 5px; 208 | border: solid 1px rgb(193, 204, 228); 209 | } 210 | table.module_list td.name, table.function_list td.name { 211 | background-color: white; min-width: 200px; border-right-width: 0px; 212 | } 213 | table.module_list td.summary, table.function_list td.summary { 214 | background-color: white; width: 100%; border-left-width: 0px; 215 | } 216 | 217 | dl.function { 218 | margin-right: 15px; 219 | margin-left: 15px; 220 | border-bottom: solid 1px rgb(193, 204, 228); 221 | border-left: solid 1px rgb(193, 204, 228); 222 | border-right: solid 1px rgb(193, 204, 228); 223 | background-color: white; 224 | } 225 | 226 | dl.function dt { 227 | color: rgb(99, 123, 188); 228 | font-family: monospace; 229 | border-top: solid 1px rgb(193, 204, 228); 230 | padding: 15px; 231 | } 232 | 233 | dl.function dd { 234 | margin-left: 15px; 235 | margin-right: 15px; 236 | margin-top: 5px; 237 | margin-bottom: 15px; 238 | } 239 | 240 | #content dl.function dd h3 { 241 | margin-top: 0px; 242 | margin-left: 0px; 243 | padding-left: 0px; 244 | font-size: 16px; 245 | color: rgb(128, 128, 128); 246 | border-bottom: solid 1px #def; 247 | } 248 | 249 | #content dl.function dd ul, #content dl.function dd ol { 250 | padding: 0px; 251 | padding-left: 15px; 252 | list-style-type: none; 253 | } 254 | 255 | ul.nowrap { 256 | overflow:auto; 257 | white-space:nowrap; 258 | } 259 | 260 | .section-description { 261 | padding-left: 15px; 262 | padding-right: 15px; 263 | } 264 | 265 | /* stop sublists from having initial vertical space */ 266 | ul ul { margin-top: 0px; } 267 | ol ul { margin-top: 0px; } 268 | ol ol { margin-top: 0px; } 269 | ul ol { margin-top: 0px; } 270 | 271 | /* make the target distinct; helps when we're navigating to a function */ 272 | a:target + * { 273 | background-color: #FF9; 274 | } 275 | 276 | 277 | /* styles for prettification of source */ 278 | pre .comment { color: #bbccaa; } 279 | pre .constant { color: #a8660d; } 280 | pre .escape { color: #844631; } 281 | pre .keyword { color: #ffc090; font-weight: bold; } 282 | pre .library { color: #0e7c6b; } 283 | pre .marker { color: #512b1e; background: #fedc56; font-weight: bold; } 284 | pre .string { color: #8080ff; } 285 | pre .number { color: #f8660d; } 286 | pre .operator { color: #2239a8; font-weight: bold; } 287 | pre .preprocessor, pre .prepro { color: #a33243; } 288 | pre .global { color: #c040c0; } 289 | pre .user-keyword { color: #800080; } 290 | pre .prompt { color: #558817; } 291 | pre .url { color: #272fc2; text-decoration: underline; } 292 | -------------------------------------------------------------------------------- /docs/modules/resty.aws.utils.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | AWS SDK for OpenResty 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 64 | 65 |
66 | 67 |

Module resty.aws.utils

68 |

AWS utility module.

69 |

Provides methods for detecting the AWS Region, as well as fetching metadata.

70 | 71 | 72 |

Functions

73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 |
Utils.getCurrentRegion ()Auto detects the current AWS region.
Utils.getECSTaskMetadata (subpath, version)Fetches ECS Task Metadata.
Utils.getIDMSMetadata (subpath, version)Fetches IDMS Metadata (EC2 and EKS).
87 | 88 |
89 |
90 | 91 | 92 |

Functions

93 | 94 |
95 |
96 | 97 | Utils.getCurrentRegion () 98 |
99 |
100 | Auto detects the current AWS region. 101 | It will try the following options (in order);

102 | 103 |
    104 |
  1. environment variable AWS_REGION
  2. 105 |
  3. environment variable AWS_DEFAULT_REGION
  4. 106 |
  5. ECS metadata V4 (parse region from "AvailabilityZone") if the environment 107 | variable ECS_CONTAINER_METADATA_URI_V4 is available
  6. 108 |
  7. ECS metadata V3 (parse region from "AvailabilityZone") if the environment 109 | variable ECS_CONTAINER_METADATA_URI is available
  8. 110 |
  9. IDMSv2 metadata (only if AWS_EC2_METADATA_DISABLED hasn't been set to true)
  10. 111 |
112 | 113 |

The IDMSv2 call makes a call to an IP endpoint, and hence could timeout 114 | (timeout is 5 seconds) if called on anything not being an EC2 or EKS instance.

115 | 116 |

Note: the result is cached so any consecutive calls will not perform any IO. 117 | 118 | 119 | 120 |

Returns:

121 |
    122 | 123 | region, or nil+err 124 |
125 | 126 | 127 | 128 | 129 |
130 |
131 | 132 | Utils.getECSTaskMetadata (subpath, version) 133 |
134 |
135 | Fetches ECS Task Metadata. Both for Fargate as well as EC2 based ECS. 136 | Support version 2, 3, and 4 (version 2 is NOT available on Fargate). 137 | V3 and V4 will return an error if no url is found in the related environment variable, V2 will make a request to 138 | the IP address and hence might timeout if ran on anything else than an EC2-based ECS container. 139 | 140 | 141 |

Parameters:

142 |
    143 |
  • subpath 144 | (optional) path to return data from (default "/metadata" for V2, nothing for V3+) 145 |
  • 146 |
  • version 147 | (optional) metadata version to get "V2", "V3", or "V4" (case insensitive, default "V4") 148 |
  • 149 |
150 | 151 |

Returns:

152 |
    153 | 154 | body & content-type (if json, the body will be decoded to a Lua table), or nil+err 155 |
156 | 157 | 158 | 159 | 160 |
161 |
162 | 163 | Utils.getIDMSMetadata (subpath, version) 164 |
165 |
166 | Fetches IDMS Metadata (EC2 and EKS). 167 | Will make a call to the IP address and hence might timeout if ran on anything 168 | else than an EC2-instance. 169 | Calling this function will make the calls, it will not honor the AWS_EC2_METADATA_DISABLED setting. 170 | 171 | 172 |

Parameters:

173 |
    174 |
  • subpath 175 | (optional) subpath to return data from, default /latest/meta-data/ 176 |
  • 177 |
  • version 178 | (optional) version of IDMS to use, either "V1" or "V2" (case insensitive, default "V2") 179 |
  • 180 |
181 | 182 |

Returns:

183 |
    184 | 185 | body & content-type (if json, the body will be decoded to a Lua table), or nil+err 186 |
187 | 188 | 189 | 190 | 191 |
192 |
193 | 194 | 195 |
196 |
197 |
198 | generated by LDoc 1.5.0 199 | Last updated 2024-09-23 09:29:37 200 |
201 |
202 | 203 | 204 | -------------------------------------------------------------------------------- /lua-resty-aws-dev-1.rockspec.template: -------------------------------------------------------------------------------- 1 | local package_name = "lua-resty-aws" 2 | local package_version = "dev" 3 | local rockspec_revision = "1" 4 | local github_account_name = "Kong" 5 | local github_repo_name = package_name 6 | local git_checkout = package_version == "dev" and "main" or package_version 7 | 8 | package = package_name 9 | version = package_version .. "-" .. rockspec_revision 10 | 11 | source = { 12 | url = "git://github.com/"..github_account_name.."/"..github_repo_name..".git", 13 | branch = git_checkout 14 | } 15 | 16 | description = { 17 | summary = "AWS SDK for OpenResty", 18 | detailed = [[ 19 | AWS SDK generated from the same data as the AWS JavaScript SDK. 20 | ]], 21 | license = "Apache 2.0", 22 | homepage = "https://"..github_account_name..".github.io/"..github_repo_name.."/topics/README.md.html" 23 | } 24 | 25 | dependencies = { 26 | "penlight ~> 1", 27 | "lua-resty-http >= 0.16", 28 | "lua-resty-luasocket ~> 1", 29 | "lua-resty-openssl >= 0.8.17", 30 | "luaexpat >= 1.5.1", 31 | } 32 | 33 | build = { 34 | type = "builtin", 35 | modules = { 36 | ["resty.aws.init"] = "src/resty/aws/init.lua", 37 | ["resty.aws.utils"] = "src/resty/aws/utils.lua", 38 | ["resty.aws.config"] = "src/resty/aws/config.lua", 39 | ["resty.aws.request.validate"] = "src/resty/aws/request/validate.lua", 40 | ["resty.aws.request.build"] = "src/resty/aws/request/build.lua", 41 | ["resty.aws.request.sign"] = "src/resty/aws/request/sign.lua", 42 | ["resty.aws.request.execute"] = "src/resty/aws/request/execute.lua", 43 | ["resty.aws.request.signatures.utils"] = "src/resty/aws/request/signatures/utils.lua", 44 | ["resty.aws.request.signatures.v4"] = "src/resty/aws/request/signatures/v4.lua", 45 | ["resty.aws.request.signatures.presign"] = "src/resty/aws/request/signatures/presign.lua", 46 | ["resty.aws.request.signatures.none"] = "src/resty/aws/request/signatures/none.lua", 47 | ["resty.aws.service.rds.signer"] = "src/resty/aws/service/rds/signer.lua", 48 | ["resty.aws.credentials.Credentials"] = "src/resty/aws/credentials/Credentials.lua", 49 | ["resty.aws.credentials.ChainableTemporaryCredentials"] = "src/resty/aws/credentials/ChainableTemporaryCredentials.lua", 50 | ["resty.aws.credentials.CredentialProviderChain"] = "src/resty/aws/credentials/CredentialProviderChain.lua", 51 | ["resty.aws.credentials.EC2MetadataCredentials"] = "src/resty/aws/credentials/EC2MetadataCredentials.lua", 52 | ["resty.aws.credentials.EnvironmentCredentials"] = "src/resty/aws/credentials/EnvironmentCredentials.lua", 53 | ["resty.aws.credentials.SharedFileCredentials"] = "src/resty/aws/credentials/SharedFileCredentials.lua", 54 | ["resty.aws.credentials.RemoteCredentials"] = "src/resty/aws/credentials/RemoteCredentials.lua", 55 | ["resty.aws.credentials.TokenFileWebIdentityCredentials"] = "src/resty/aws/credentials/TokenFileWebIdentityCredentials.lua", 56 | 57 | -- AWS SDK files 58 | -- Do not modify anything between the start and end markers, that part is generated 59 | ["resty.aws.raw-api.region_config_data"] = "src/resty/aws/raw-api/region_config_data.lua", 60 | ["resty.aws.raw-api.table_of_contents"] = "src/resty/aws/raw-api/table_of_contents.lua", 61 | --START-MARKER-- 62 | 63 | This will be replaced by the actual file list imported from the AWS SDK 64 | 65 | --END-MARKER-- 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /spec/01-generic/01-config_spec.lua: -------------------------------------------------------------------------------- 1 | local pl_path = require("pl.path") 2 | local pl_utils = require("pl.utils") 3 | local d = require("pl.text").dedent 4 | local restore = require "spec.helpers" 5 | 6 | 7 | describe("config loader", function() 8 | 9 | local config_info = d[[ 10 | [default] 11 | region=eu-central-1 12 | max_attempts=99 13 | 14 | [profile tieske] 15 | region=us-west-1 16 | ]] 17 | local config_filename = pl_path.tmpname() 18 | 19 | local config 20 | before_each(function() 21 | restore() 22 | restore.setenv("HOME") 23 | pl_utils.writefile(config_filename, config_info) 24 | end) 25 | 26 | after_each(function() 27 | restore() 28 | config = nil 29 | os.remove(config_filename) 30 | end) 31 | 32 | it("sets defaults", function() 33 | restore.setenv("AWS_CONFIG_FILE", config_filename) 34 | restore.setenv("AWS_DEFAULT_REGION", "eu-west-1") 35 | 36 | os.remove(config_filename) -- delete the file so we revert to defaults 37 | config = require "resty.aws.config" 38 | local conf = assert(config.get_config()) 39 | 40 | -- just calling out as separate test; picking default region 41 | assert.equal("eu-west-1", conf.region) 42 | 43 | -- all defaults 44 | assert.same({ 45 | AWS_DEFAULT_REGION = "eu-west-1", 46 | region = "eu-west-1", 47 | AWS_CONFIG_FILE = config_filename, 48 | AWS_EC2_METADATA_DISABLED = true, 49 | AWS_PROFILE = 'default', 50 | AWS_SHARED_CREDENTIALS_FILE = '~/.aws/credentials', 51 | cli_timestamp_format = 'iso8601', 52 | AWS_CLI_TIMESTAMP_FORMAT = 'iso8601', 53 | duration_seconds = 3600, 54 | AWS_DURATION_SECONDS = 3600, 55 | max_attempts = 5, 56 | AWS_MAX_ATTEMPTS = 5, 57 | parameter_validation = true, 58 | AWS_PARAMETER_VALIDATION = true, 59 | retry_mode = 'standard', 60 | AWS_RETRY_MODE = 'standard', 61 | sts_regional_endpoints = 'regional', 62 | AWS_STS_REGIONAL_ENDPOINTS = 'regional', 63 | }, conf) 64 | end) 65 | 66 | it("loads the configuration; default profile", function() 67 | restore.setenv("AWS_CONFIG_FILE", config_filename) 68 | config = require "resty.aws.config" 69 | local conf = assert(config.get_config()) 70 | 71 | -- from the config file; default profile 72 | assert.equal("eu-central-1", conf.region) 73 | assert.equal(99, conf.max_attempts) 74 | 75 | assert.same({ 76 | AWS_CONFIG_FILE = config_filename, 77 | AWS_EC2_METADATA_DISABLED = true, 78 | AWS_PROFILE = 'default', 79 | AWS_SHARED_CREDENTIALS_FILE = '~/.aws/credentials', 80 | cli_timestamp_format = 'iso8601', 81 | AWS_CLI_TIMESTAMP_FORMAT = 'iso8601', 82 | duration_seconds = 3600, 83 | AWS_DURATION_SECONDS = 3600, 84 | max_attempts = 99, 85 | AWS_MAX_ATTEMPTS = 5, 86 | region = "eu-central-1", 87 | parameter_validation = true, 88 | AWS_PARAMETER_VALIDATION = true, 89 | retry_mode = 'standard', 90 | AWS_RETRY_MODE = 'standard', 91 | sts_regional_endpoints = 'regional', 92 | AWS_STS_REGIONAL_ENDPOINTS = 'regional', 93 | }, conf) 94 | end) 95 | 96 | it("loads the configuration; 'tieske' profile", function() 97 | restore.setenv("AWS_CONFIG_FILE", config_filename) 98 | restore.setenv("AWS_PROFILE", "tieske") 99 | config = require "resty.aws.config" 100 | local conf = assert(config.get_config()) 101 | 102 | -- from the config file; profile 'tieske' 103 | assert.equal("us-west-1", conf.region) 104 | 105 | assert.same({ 106 | AWS_CONFIG_FILE = config_filename, 107 | AWS_EC2_METADATA_DISABLED = true, 108 | AWS_PROFILE = 'tieske', 109 | AWS_SHARED_CREDENTIALS_FILE = '~/.aws/credentials', 110 | cli_timestamp_format = 'iso8601', 111 | AWS_CLI_TIMESTAMP_FORMAT = 'iso8601', 112 | duration_seconds = 3600, 113 | AWS_DURATION_SECONDS = 3600, 114 | max_attempts = 5, 115 | AWS_MAX_ATTEMPTS = 5, 116 | region = "us-west-1", 117 | parameter_validation = true, 118 | AWS_PARAMETER_VALIDATION = true, 119 | retry_mode = 'standard', 120 | AWS_RETRY_MODE = 'standard', 121 | sts_regional_endpoints = 'regional', 122 | AWS_STS_REGIONAL_ENDPOINTS = 'regional', 123 | }, conf) 124 | end) 125 | 126 | it("global field returns the global configuration", function() 127 | config = require "resty.aws.config" 128 | local conf = config.global 129 | 130 | assert.same({ 131 | region = nil, -- detection should fail 132 | AWS_CONFIG_FILE = "~/.aws/config", 133 | AWS_EC2_METADATA_DISABLED = true, 134 | AWS_PROFILE = 'default', 135 | AWS_SHARED_CREDENTIALS_FILE = '~/.aws/credentials', 136 | cli_timestamp_format = 'iso8601', 137 | AWS_CLI_TIMESTAMP_FORMAT = 'iso8601', 138 | duration_seconds = 3600, 139 | AWS_DURATION_SECONDS = 3600, 140 | max_attempts = 5, 141 | AWS_MAX_ATTEMPTS = 5, 142 | parameter_validation = true, 143 | AWS_PARAMETER_VALIDATION = true, 144 | retry_mode = 'standard', 145 | AWS_RETRY_MODE = 'standard', 146 | sts_regional_endpoints = 'regional', 147 | AWS_STS_REGIONAL_ENDPOINTS = 'regional', 148 | }, conf) 149 | 150 | end) 151 | 152 | end) 153 | -------------------------------------------------------------------------------- /spec/01-generic/02-aws_spec.lua: -------------------------------------------------------------------------------- 1 | describe("AWS main instance", function() 2 | 3 | 4 | local AWS 5 | 6 | setup(function() 7 | package.loaded["resty.aws.request.execute"] = function () 8 | return { 9 | status = 200, 10 | reason = "OK", 11 | headers = {}, 12 | body = "" 13 | } 14 | end 15 | AWS = require "resty.aws" 16 | -- execute_request = require "resty.aws.request.execute" 17 | end) 18 | 19 | teardown(function() 20 | AWS = nil 21 | package.loaded["resty.aws"] = nil 22 | end) 23 | 24 | 25 | it("gets default config #only", function() 26 | local aws = AWS() 27 | assert.is.table(aws.config) 28 | assert.same({ 29 | apiVersion = "latest", 30 | credentials = aws:CredentialProviderChain(), 31 | }, aws.config) 32 | end) 33 | 34 | 35 | it("overrides default config", function() 36 | local aws = AWS({ 37 | region = "eu-central-1", 38 | apiVersion = "2020-09-29", 39 | }) 40 | assert.is.table(aws.config) 41 | assert.same({ 42 | region = "eu-central-1", 43 | apiVersion = "2020-09-29", 44 | credentials = require("resty.aws.credentials.CredentialProviderChain"):new({ aws = aws }), 45 | }, aws.config) 46 | end) 47 | 48 | 49 | it("allows custom config", function() 50 | local aws = AWS({ 51 | unknown_property = "hi!", 52 | }) 53 | assert.is.table(aws.config) 54 | assert.equal("hi!", aws.config.unknown_property) 55 | end) 56 | 57 | 58 | it("gets methods for services", function() 59 | local aws = AWS() 60 | assert.is.Function(aws.STS) 61 | end) 62 | 63 | 64 | it("gets methods for services, spaces removed from serviceId", function() 65 | local aws = AWS() 66 | assert.is.Function(aws.AppMesh) -- serviceId = "App Mesh" 67 | end) 68 | 69 | it("support sts regional endpoint inject and only inject once", function() 70 | local aws = AWS({ 71 | region = "eu-central-1", 72 | stsRegionalEndpoints = "regional", 73 | }) 74 | 75 | aws.config.credentials = aws:Credentials { 76 | accessKeyId = "test_id", 77 | secretAccessKey = "test_key", 78 | } 79 | 80 | assert.is.table(aws.config) 81 | local sts, _ = aws:STS() 82 | local _, _ = sts:assumeRole { 83 | RoleArn = "aws:arn::XXXXXXXXXXXXXXXXX:test123", 84 | RoleSessionName = "aws-test", 85 | } 86 | assert.same("https://sts.eu-central-1.amazonaws.com", sts.config.endpoint) 87 | 88 | local _, _ = sts:assumeRole { 89 | RoleArn = "aws:arn::XXXXXXXXXXXXXXXXX:test123", 90 | RoleSessionName = "aws-test", 91 | } 92 | assert.same("https://sts.eu-central-1.amazonaws.com", sts.config.endpoint) 93 | end) 94 | 95 | it("do not inject sts region info for sts vpc endpoint url", function() 96 | local aws = AWS({ 97 | region = "eu-central-1", 98 | stsRegionalEndpoints = "regional", 99 | }) 100 | 101 | aws.config.credentials = aws:Credentials { 102 | accessKeyId = "test_id", 103 | secretAccessKey = "test_key", 104 | } 105 | 106 | assert.is.table(aws.config) 107 | 108 | local regional_vpc_endpoint_url = "https://vpce-abcdefg-hijklmn-eu-central-1a.sts.eu-central-1.vpce.amazonaws.com" 109 | 110 | local sts, _ = aws:STS({ 111 | endpoint = regional_vpc_endpoint_url, 112 | }) 113 | local _, _ = sts:assumeRole { 114 | RoleArn = "aws:arn::XXXXXXXXXXXXXXXXX:test123", 115 | RoleSessionName = "aws-test", 116 | } 117 | 118 | assert.same(regional_vpc_endpoint_url, sts.config.endpoint) 119 | 120 | local _, _ = sts:assumeRole { 121 | RoleArn = "aws:arn::XXXXXXXXXXXXXXXXX:test123", 122 | RoleSessionName = "aws-test", 123 | } 124 | assert.same(regional_vpc_endpoint_url, sts.config.endpoint) 125 | end) 126 | 127 | 128 | end) 129 | -------------------------------------------------------------------------------- /spec/01-generic/03-service_spec.lua: -------------------------------------------------------------------------------- 1 | local tablex = require "pl.tablex" 2 | 3 | describe("service generator", function() 4 | 5 | 6 | local AWS 7 | 8 | setup(function() 9 | AWS = require "resty.aws" 10 | end) 11 | 12 | teardown(function() 13 | AWS = nil 14 | package.loaded["resty.aws"] = nil 15 | end) 16 | 17 | 18 | it("creates a service", function() 19 | local sts = AWS():STS() 20 | assert.is.table(sts) 21 | assert.is.table(sts.config) 22 | assert.is.table(sts.api) 23 | assert.equal("STS", sts.api.metadata.serviceId) 24 | assert.equal("v4", sts.api.metadata.signatureVersion) 25 | assert.equal("AWS Security Token Service", sts.api.metadata.serviceFullName) 26 | end) 27 | 28 | 29 | it("sets the parent aws instance", function() 30 | local aws = AWS() 31 | local sts = aws:STS() 32 | assert.equal(aws, sts.config.aws) 33 | end) 34 | 35 | 36 | it("creates a specific service version", function() 37 | -- App Mesh has 2 versions, test both to make sure we do 38 | -- not hit a default 39 | local mesh = assert(AWS():AppMesh({ apiVersion = "2019-01-25", region = "us-east-1" })) 40 | assert.equal("2019-01-25", mesh.config.apiVersion) 41 | 42 | local mesh = assert(AWS():AppMesh({ apiVersion = "2018-10-01", region = "us-east-1" })) 43 | assert.equal("2018-10-01", mesh.config.apiVersion) 44 | end) 45 | 46 | 47 | it("'latest' indicates the most recent service version", function() 48 | -- Find latest version from table of contents 49 | local list = require("resty.aws.raw-api.table_of_contents") 50 | local service_list = tablex.filter(list, function(v) return v:find("DynamoDB:") end) 51 | table.sort(service_list) 52 | local _, _, latest_version = service_list[#service_list]:match("^(.-)%:(.-)%-(%d%d%d%d%-%d%d%-%d%d)$") 53 | 54 | local dynamoDB = assert(AWS():DynamoDB({ apiVersion = "latest", region = "us-east-1" })) 55 | assert.equal(latest_version, dynamoDB.config.apiVersion) 56 | 57 | -- use latest version by default 58 | dynamoDB = assert(AWS():DynamoDB({ region = "us-east-1" })) 59 | assert.equal(latest_version, dynamoDB.config.apiVersion) 60 | end) 61 | 62 | 63 | it("creates methods for operations", function() 64 | local sts = AWS():STS() 65 | assert.is.Function(sts.assumeRole) 66 | end) 67 | 68 | 69 | it("generated operations validate input 1", function() 70 | local sts = assert(AWS():STS()) 71 | --print(require("pl.pretty").write(sts.config)) 72 | local ok, err = sts:assumeRole({ 73 | RoleSessionName = "just_a_name", 74 | }) 75 | assert.equal("STS:assumeRole() validation error: params.RoleArn is required but missing", err) 76 | assert.is_nil(ok) 77 | end) 78 | 79 | 80 | it("generated operations validate input 2", function() 81 | local sm = assert(AWS():SecretsManager({region = "us-east-1"})) 82 | --print(require("pl.pretty").write(sm.config)) 83 | local ok, err = sm:getSecretValue({ 84 | RoleSessionName = "just_a_name", 85 | }) 86 | assert.equal("SecretsManager:getSecretValue() validation error: params.SecretId is required but missing", err) 87 | assert.is_nil(ok) 88 | end) 89 | 90 | 91 | -- just for debugging, always fails 92 | --[[ 93 | it("creates a service", function() 94 | local sts = AWS().STS() 95 | assert.equal({}, sts.api.operations.assumeRole) 96 | end) 97 | --]] 98 | 99 | end) 100 | -------------------------------------------------------------------------------- /spec/02-requests/03-execute_spec.lua: -------------------------------------------------------------------------------- 1 | local restore = require "spec.helpers".restore 2 | local cjson = require "cjson" 3 | 4 | describe("request execution", function() 5 | local AWS, Credentials 6 | 7 | local mock_request_response = { 8 | ["s3.amazonaws.com"] = { 9 | ["/"] = { 10 | GET = { 11 | status = 200, 12 | headers = { 13 | ["x-amz-id-2"] = "test", 14 | ["x-amz-request-id"] = "test", 15 | ["Date"] = "test", 16 | ["Content-Type"] = "application/json", 17 | ["Server"] = "AmazonS3", 18 | }, 19 | body = [[{"ListAllMyBucketsResult":{"Buckets":[]}}]] 20 | } 21 | } 22 | } 23 | } 24 | 25 | setup(function() 26 | restore() 27 | local http = require "resty.luasocket.http" 28 | http.connect = function(...) return true end 29 | http.request = function(self, req) 30 | return { has_body = true, 31 | status = mock_request_response[req.headers.Host][req.path][req.method].status, 32 | headers = mock_request_response[req.headers.Host][req.path][req.method].headers, 33 | read_body = function() 34 | local resp = mock_request_response[req.headers.Host][req.path][req.method].body 35 | return resp 36 | end 37 | } 38 | end 39 | http.set_timeout = function(...) return true end 40 | http.set_keepalive = function(...) return true end 41 | http.close = function(...) return true end 42 | AWS = require "resty.aws" 43 | Credentials = require "resty.aws.credentials.Credentials" 44 | end) 45 | 46 | teardown(function() 47 | package.loaded["resty.luasocket.http"] = nil 48 | AWS = nil 49 | package.loaded["resty.aws"] = nil 50 | end) 51 | 52 | it("tls defaults to true", function () 53 | local config = { 54 | region = "us-east-1" 55 | } 56 | 57 | config.credentials = Credentials:new({ 58 | accessKeyId = "teqst_id", 59 | secretAccessKey = "test_key", 60 | }) 61 | 62 | local aws = AWS(config) 63 | aws.config.dry_run = true 64 | 65 | local s3 = aws:S3() 66 | 67 | assert.same(type(s3.getObject), "function") 68 | local request, err = s3:getObject({ 69 | Bucket = "test-bucket", 70 | Key = "test-key", 71 | }) 72 | 73 | assert.same(err, nil) 74 | assert.same(request.tls, true) 75 | end) 76 | 77 | it("support configuring tls false", function () 78 | local config = { 79 | region = "us-east-1" 80 | } 81 | 82 | config.credentials = Credentials:new({ 83 | accessKeyId = "teqst_id", 84 | secretAccessKey = "test_key", 85 | }) 86 | 87 | local aws = AWS(config) 88 | aws.config.tls = false 89 | aws.config.dry_run = true 90 | 91 | local s3 = aws:S3() 92 | 93 | assert.same(type(s3.getObject), "function") 94 | local request, err = s3:getObject({ 95 | Bucket = "test-bucket", 96 | Key = "test-key", 97 | }) 98 | 99 | assert.same(err, nil) 100 | assert.same(request.port, 80) 101 | assert.same(request.tls, false) 102 | end) 103 | 104 | it("support configuring ssl verify false", function () 105 | local config = { 106 | region = "us-east-1" 107 | } 108 | 109 | config.credentials = Credentials:new({ 110 | accessKeyId = "teqst_id", 111 | secretAccessKey = "test_key", 112 | }) 113 | 114 | local aws = AWS(config) 115 | aws.config.dry_run = true 116 | aws.config.ssl_verify = false 117 | 118 | local s3 = aws:S3() 119 | 120 | assert.same(type(s3.getObject), "function") 121 | local request, err = s3:getObject({ 122 | Bucket = "test-bucket", 123 | Key = "test-key", 124 | }) 125 | 126 | assert.same(err, nil) 127 | assert.same(request.ssl_verify, false) 128 | end) 129 | 130 | it("support configure timeout", function () 131 | local config = { 132 | region = "us-east-1" 133 | } 134 | 135 | config.credentials = Credentials:new({ 136 | accessKeyId = "teqst_id", 137 | secretAccessKey = "test_key", 138 | }) 139 | 140 | local aws = AWS(config) 141 | aws.config.dry_run = true 142 | aws.config.timeout = 123456000 143 | 144 | local s3 = aws:S3() 145 | 146 | assert.same(type(s3.getObject), "function") 147 | local request, err = s3:getObject({ 148 | Bucket = "test-bucket", 149 | Key = "test-key", 150 | }) 151 | 152 | assert.same(err, nil) 153 | assert.same(request.timeout, 123456000) 154 | end) 155 | 156 | it("support configure keepalive idle timeout", function () 157 | local config = { 158 | region = "us-east-1" 159 | } 160 | 161 | config.credentials = Credentials:new({ 162 | accessKeyId = "teqst_id", 163 | secretAccessKey = "test_key", 164 | }) 165 | 166 | local aws = AWS(config) 167 | aws.config.dry_run = true 168 | aws.config.keepalive_idle_timeout = 123456000 169 | 170 | local s3 = aws:S3() 171 | 172 | assert.same(type(s3.getObject), "function") 173 | local request, err = s3:getObject({ 174 | Bucket = "test-bucket", 175 | Key = "test-key", 176 | }) 177 | 178 | assert.same(err, nil) 179 | assert.same(request.keepalive_idle_timeout, 123456000) 180 | end) 181 | 182 | it("support set proxy options", function () 183 | local config = { 184 | region = "us-east-1" 185 | } 186 | 187 | config.credentials = Credentials:new({ 188 | accessKeyId = "teqst_id", 189 | secretAccessKey = "test_key", 190 | }) 191 | 192 | local proxy_opts = { 193 | http_proxy = 'http://test-http-proxy:1234', 194 | https_proxy = 'http://test-https-proxy:4321', 195 | no_proxy = '127.0.0.1,localhost' 196 | } 197 | 198 | local aws = AWS(config) 199 | aws.config.dry_run = true 200 | aws.config.http_proxy = proxy_opts.http_proxy 201 | aws.config.https_proxy = proxy_opts.https_proxy 202 | aws.config.no_proxy = proxy_opts.no_proxy 203 | 204 | local s3 = aws:S3() 205 | 206 | assert.same(type(s3.getObject), "function") 207 | local request, _ = s3:getObject({ 208 | Bucket = "test-bucket", 209 | Key = "test-key", 210 | }) 211 | 212 | assert.same(type(request.proxy_opts), "table") 213 | for k, v in pairs(proxy_opts) do 214 | assert.same(request.proxy_opts[k], v) 215 | end 216 | end) 217 | 218 | it("decoded json body should have array metatable", function () 219 | local config = { 220 | region = "us-east-1" 221 | } 222 | 223 | config.credentials = Credentials:new({ 224 | accessKeyId = "teqst_id", 225 | secretAccessKey = "test_key", 226 | }) 227 | 228 | local aws = AWS(config) 229 | 230 | local s3 = aws:S3() 231 | 232 | assert.same(type(s3.listBuckets), "function") 233 | local resp = s3:listBuckets() 234 | 235 | assert.is_not_nil(resp.body) 236 | assert.same([[{"ListAllMyBucketsResult":{"Buckets":[]}}]], cjson.encode(resp.body)) 237 | end) 238 | end) 239 | -------------------------------------------------------------------------------- /spec/02-requests/04-sign_v4_spec.lua: -------------------------------------------------------------------------------- 1 | pending("No tests for signing v4 yet", function() 2 | 3 | end) 4 | -------------------------------------------------------------------------------- /spec/02-requests/05-presign_v4_spec.lua: -------------------------------------------------------------------------------- 1 | setmetatable(_G, nil) 2 | 3 | -- -- hock request sending 4 | -- package.loaded["resty.aws.request.execute"] = function(...) 5 | -- return ... 6 | -- end 7 | 8 | local AWS = require("resty.aws") 9 | local AWS_global_config = require("resty.aws.config").global 10 | 11 | local presign = require("resty.aws.request.signatures.presign") 12 | 13 | local config = AWS_global_config 14 | local aws = AWS(config) 15 | 16 | 17 | aws.config.credentials = aws:Credentials { 18 | accessKeyId = "test_id", 19 | secretAccessKey = "test_key", 20 | } 21 | 22 | aws.config.region = "test_region" 23 | 24 | describe("Presign request", function() 25 | local presigned_request_data 26 | local origin_time 27 | 28 | setup(function() 29 | origin_time = ngx.time 30 | ngx.time = function () --luacheck: ignore 31 | return 1667543171 32 | end 33 | end) 34 | 35 | teardown(function () 36 | ngx.time = origin_time --luacheck: ignore 37 | end) 38 | 39 | before_each(function() 40 | local request_data = { 41 | method = "GET", 42 | scheme = "https", 43 | tls = true, 44 | host = "test_host", 45 | port = 443, 46 | path = "/", 47 | query = "Action=TestAction", 48 | headers = { 49 | ["Host"] = "test_host:443", 50 | }, 51 | } 52 | 53 | presigned_request_data = presign(aws.config, request_data, "test_service", "test_region", 900) 54 | end) 55 | 56 | after_each(function() 57 | presigned_request_data = nil 58 | end) 59 | 60 | it("should have correct signed request host header", function() 61 | assert.same(presigned_request_data.headers["Host"], "test_host:443") 62 | assert.same(presigned_request_data.host, "test_host") 63 | assert.same(presigned_request_data.port, 443) 64 | end) 65 | 66 | it("should have correct signed request path", function () 67 | assert.same(presigned_request_data.path, "/") 68 | end) 69 | 70 | it("should have correct signed query parameters", function () 71 | local query_params = {} 72 | for k, v in presigned_request_data.query:gmatch("([^&=]+)=?([^&]*)") do 73 | query_params[ngx.unescape_uri(k)] = ngx.unescape_uri(v) 74 | end 75 | assert.same(query_params["X-Amz-Algorithm"], "AWS4-HMAC-SHA256") 76 | assert.same(query_params["Action"], "TestAction") 77 | assert.same(query_params["X-Amz-Date"], "20221104T062611Z") 78 | assert.same(query_params["X-Amz-Expires"], "900") 79 | assert.same(query_params["X-Amz-SignedHeaders"], "host") 80 | assert.same(query_params["X-Amz-Credential"], "test_id/20221104/test_region/test_service/aws4_request") 81 | end) 82 | end) 83 | -------------------------------------------------------------------------------- /spec/03-credentials/01-Credentials_spec.lua: -------------------------------------------------------------------------------- 1 | local restore = require "spec.helpers" 2 | 3 | describe("Credentials base-class", function() 4 | 5 | local AWS, Credentials 6 | 7 | before_each(function() 8 | restore() 9 | local _ = require("resty.aws.config").global -- load config before anything else 10 | AWS = require "resty.aws" 11 | Credentials = require "resty.aws.credentials.Credentials" 12 | end) 13 | 14 | after_each(function() 15 | restore() 16 | end) 17 | 18 | 19 | 20 | describe("Class inheritance", function() 21 | local EnvironmentCredentials 22 | 23 | before_each(function() 24 | restore() 25 | restore.setenv("ABC_ACCESS_KEY_ID", "access") 26 | restore.setenv("ABC_SECRET_ACCESS_KEY", "secret") 27 | restore.setenv("ABC_SESSION_TOKEN", "token") 28 | local _ = require("resty.aws.config").global -- load config before anything else 29 | 30 | EnvironmentCredentials = require "resty.aws.credentials.EnvironmentCredentials" 31 | end) 32 | 33 | 34 | 35 | it("instance does not modify class", function() 36 | local cred = Credentials:new { expiryWindow = 20 } 37 | assert.equal(20, cred.expiryWindow) 38 | assert.Not.equal(20, Credentials.expiryWindow) 39 | end) 40 | 41 | 42 | it("sub-class instance does not modify class nor sub-class", function() 43 | local cred = EnvironmentCredentials:new { expiryWindow = 20, envPrefix = "ABC" } 44 | 45 | assert.equal(20, cred.expiryWindow) 46 | assert.Not.equal(20, Credentials.expiryWindow) 47 | assert.Not.equal(20, EnvironmentCredentials.expiryWindow) 48 | 49 | assert.equal("ABC", cred.envPrefix) 50 | assert.Not.equal("ABC", Credentials.envPrefix) 51 | assert.Not.equal("ABC", EnvironmentCredentials.envPrefix) 52 | 53 | cred:get() -- refreshes and sets values 54 | assert.is.equal("access", cred.accessKeyId) 55 | assert.is.Nil(Credentials.accessKeyId) 56 | assert.is.Nil(EnvironmentCredentials.accessKeyId) 57 | end) 58 | 59 | end) 60 | 61 | 62 | 63 | it("new() accepts credentials", function() 64 | local exp = ngx.now() + 60 65 | local cred = Credentials:new { 66 | accessKeyId = "access", 67 | secretAccessKey = "secret", 68 | sessionToken = "token", 69 | expireTime = exp 70 | } 71 | assert.is_false(cred:needsRefresh()) 72 | assert.same({true, "access", "secret", "token", exp}, {cred:get()}) 73 | end) 74 | 75 | 76 | it("instantiation from aws instance", function() 77 | local aws = AWS() 78 | local exp = ngx.now() + 60 79 | local cred = aws:Credentials({ 80 | accessKeyId = "access", 81 | secretAccessKey = "secret", 82 | sessionToken = "token", 83 | expireTime = exp 84 | }) 85 | assert.is_false(cred:needsRefresh()) 86 | assert.same({true, "access", "secret", "token", exp}, {cred:get()}) 87 | assert.equals(aws, cred.aws) 88 | end) 89 | 90 | 91 | it("new() expireTime defaults to 10 years if creds provided", function() 92 | local cred = Credentials:new { 93 | accessKeyId = "access", 94 | secretAccessKey = "secret", 95 | sessionToken = "token", 96 | } 97 | assert.is_false(cred:needsRefresh()) 98 | 99 | local get = {cred:get()} 100 | assert.is.near(ngx.now() + 10*365*24*60*60, 30, get[5]) -- max delta = 30 seconds 101 | 102 | get[5] = nil 103 | assert.same({true, "access", "secret", "token"}, get) 104 | end) 105 | 106 | 107 | it("new() only setting expireTime defaults to 0", function() 108 | local cred = Credentials:new { 109 | expireTime = ngx.now() + 60 110 | } 111 | assert.is_true(cred:needsRefresh()) 112 | end) 113 | 114 | 115 | it("needsRefresh()", function() 116 | local cred = Credentials:new() 117 | assert.is_true(cred:needsRefresh()) 118 | 119 | cred:set(1,2,3,ngx.now() + 60*60) 120 | assert.is_false(cred:needsRefresh()) 121 | end) 122 | 123 | 124 | it("needsRefresh() accounts for expiryWindow", function() 125 | local expWindow = 20 126 | local cred = Credentials:new { expiryWindow = expWindow } 127 | 128 | cred:set(1,2,3,ngx.now() + expWindow + 0.1) 129 | assert.is_false(cred:needsRefresh()) 130 | 131 | cred:set(1,2,3,ngx.now() + expWindow - 0.1) 132 | assert.is_true(cred:needsRefresh()) 133 | end) 134 | 135 | 136 | it("get() returns properties", function() 137 | local cred = Credentials:new() 138 | local exp = ngx.now() + 60 139 | cred:set(1,2,3,exp) 140 | 141 | assert.are.same({true, 1,2,3,exp}, {cred:get()}) 142 | end) 143 | 144 | 145 | it("get() invokes refresh() when expired", function() 146 | local cred = Credentials:new() 147 | 148 | stub(cred, "refresh") 149 | 150 | cred:get() 151 | assert.stub(cred.refresh).was.called() 152 | end) 153 | 154 | 155 | it("set() sets properties", function() 156 | local cred = Credentials:new() 157 | local exp = ngx.now() + 60 158 | cred:set(1,2,3,exp) 159 | 160 | assert.are.same({true,1,2,3,exp}, {cred:get()}) 161 | end) 162 | 163 | 164 | it("set() accepts rfc3339 dates for expireTime", function() 165 | local cred = Credentials:new() 166 | local exp = "2030-01-01T20:00:00Z" 167 | 168 | assert.has.no.error(function() 169 | cred:set(1,2,3,exp) 170 | end) 171 | 172 | local _, _, _, _, t = cred:get() 173 | assert.is.number(t) 174 | assert(ngx.now() < t) 175 | end) 176 | 177 | end) 178 | -------------------------------------------------------------------------------- /spec/03-credentials/02-EC2MetadataCredentials_spec.lua: -------------------------------------------------------------------------------- 1 | local json = require("cjson.safe").new() 2 | 3 | require "resty.aws.config" -- load before mocking the http lib 4 | 5 | -- Mock for HTTP client 6 | local response = {} -- override in tests 7 | 8 | describe("EC2MetadataCredentials ~ v2", function() 9 | local http = { 10 | new = function() 11 | return { 12 | connect = function() return true end, 13 | close = function() return true end, 14 | set_timeout = function() return true end, 15 | set_timeouts = function() return true end, 16 | request = function(self, opts) 17 | if opts.path == "/latest/meta-data/iam/security-credentials/" then 18 | return { -- the response for requesting the role name 19 | status = 200, 20 | read_body = function() return "the_role_name" end, 21 | } 22 | elseif opts.path == "/latest/meta-data/iam/security-credentials/the_role_name" then 23 | return { -- the response for the credentials for the role 24 | status = (response or {}).status or 200, 25 | read_body = function() return json.encode { 26 | AccessKeyId = (response or {}).AccessKeyId or "access", 27 | SecretAccessKey = (response or {}).SecretAccessKey or "secret", 28 | Token = (response or {}).Token or "token", 29 | Expiration = (response or {}).Expiration or "2030-01-01T20:00:00Z", 30 | } 31 | end, 32 | } 33 | elseif opts.path == "/latest/api/token" and opts.method == "PUT" then 34 | return { 35 | status = 200, 36 | read_body = function() return "the_token" end, 37 | } 38 | else 39 | error("bad test path provided??? " .. tostring(opts.path)) 40 | end 41 | end, 42 | } 43 | end, 44 | } 45 | 46 | local EC2MetadataCredentials 47 | 48 | setup(function() 49 | package.loaded["resty.luasocket.http"] = http 50 | end) 51 | 52 | teardown(function() 53 | package.loaded["resty.luasocket.http"] = nil 54 | end) 55 | 56 | before_each(function() 57 | package.loaded["resty.aws.credentials.EC2MetadataCredentials"] = nil 58 | EC2MetadataCredentials = require "resty.aws.credentials.EC2MetadataCredentials" 59 | end) 60 | 61 | 62 | 63 | it("fetches credentials", function() 64 | local cred = EC2MetadataCredentials:new() 65 | local success, key, secret, token = cred:get() 66 | assert.equal("access", key) 67 | assert.equal(true, success) 68 | assert.equal("secret", secret) 69 | assert.equal("token", token) 70 | end) 71 | 72 | end) 73 | 74 | describe("EC2MetadataCredentials ~ v1", function() 75 | local http = { 76 | new = function() 77 | return { 78 | connect = function() return true end, 79 | close = function() return true end, 80 | set_timeout = function() return true end, 81 | set_timeouts = function() return true end, 82 | request = function(self, opts) 83 | if opts.path == "/latest/meta-data/iam/security-credentials/" then 84 | return { -- the response for requesting the role name 85 | status = 200, 86 | read_body = function() return "the_role_name" end, 87 | } 88 | elseif opts.path == "/latest/meta-data/iam/security-credentials/the_role_name" then 89 | return { -- the response for the credentials for the role 90 | status = (response or {}).status or 200, 91 | read_body = function() return json.encode { 92 | AccessKeyId = (response or {}).AccessKeyId or "access", 93 | SecretAccessKey = (response or {}).SecretAccessKey or "secret", 94 | Token = (response or {}).Token or "token", 95 | Expiration = (response or {}).Expiration or "2030-01-01T20:00:00Z", 96 | } 97 | end, 98 | } 99 | elseif opts.path == "/latest/api/token" and opts.method == "PUT" then 100 | return { 101 | status = 400, 102 | read_body = function() return "fake" end, 103 | } 104 | else 105 | error("bad test path provided??? " .. tostring(opts.path)) 106 | end 107 | end, 108 | } 109 | end, 110 | } 111 | 112 | local EC2MetadataCredentials 113 | 114 | setup(function() 115 | package.loaded["resty.luasocket.http"] = http 116 | end) 117 | 118 | teardown(function() 119 | package.loaded["resty.luasocket.http"] = nil 120 | end) 121 | 122 | before_each(function() 123 | package.loaded["resty.aws.credentials.EC2MetadataCredentials"] = nil 124 | EC2MetadataCredentials = require "resty.aws.credentials.EC2MetadataCredentials" 125 | end) 126 | 127 | 128 | 129 | it("fetches credentials", function() 130 | local cred = EC2MetadataCredentials:new() 131 | local success, key, secret, token = cred:get() 132 | assert.equal("access", key) 133 | assert.equal(true, success) 134 | assert.equal("secret", secret) 135 | assert.equal("token", token) 136 | end) 137 | 138 | end) 139 | -------------------------------------------------------------------------------- /spec/03-credentials/03-EnvironmentCredentials_spec.lua: -------------------------------------------------------------------------------- 1 | local restore = require "spec.helpers" 2 | 3 | describe("EnvironmentCredentials", function() 4 | 5 | local EnvironmentCredentials 6 | 7 | before_each(function() 8 | restore() 9 | restore.setenv("ABC_ACCESS_KEY_ID", "access") 10 | restore.setenv("ABC_SECRET_ACCESS_KEY", "secret") 11 | restore.setenv("ABC_SESSION_TOKEN", "token") 12 | local _ = require("resty.aws.config").global -- load config before anything else 13 | 14 | EnvironmentCredentials = require "resty.aws.credentials.EnvironmentCredentials" 15 | end) 16 | 17 | after_each(function() 18 | restore() 19 | end) 20 | 21 | 22 | 23 | it("gets environment variables", function() 24 | local cred = EnvironmentCredentials:new { envPrefix = "ABC" } 25 | assert.is_false(cred:needsRefresh()) -- false; because we fetch upon instanciation 26 | 27 | local get = {cred:get()} 28 | assert.is.near(ngx.now() + 10*365*24*60*60, 30, get[5]) -- max delta = 30 seconds 29 | 30 | get[5] = nil 31 | assert.same({true, "access", "secret", "token"}, get) 32 | end) 33 | 34 | end) 35 | -------------------------------------------------------------------------------- /spec/03-credentials/04-RemoteCredentials_spec.lua: -------------------------------------------------------------------------------- 1 | local json = require("cjson.safe").new() 2 | local restore = require "spec.helpers" 3 | 4 | local old_pl_utils = require("pl.utils") 5 | 6 | -- Mock for HTTP client 7 | local response = {} -- override in tests 8 | local http_records = {} -- record requests for assertions 9 | local http = { 10 | new = function() 11 | return { 12 | connect = function() return true end, 13 | set_timeout = function() return true end, 14 | set_timeouts = function() return true end, 15 | request = function(self, opts) 16 | if opts.path == "/test/path" then 17 | table.insert(http_records, opts) 18 | return { -- the response for the credentials 19 | status = (response or {}).status or 200, 20 | headers = opts and opts.headers or {}, 21 | read_body = function() return json.encode { 22 | AccessKeyId = (response or {}).AccessKeyId or "access", 23 | SecretAccessKey = (response or {}).SecretAccessKey or "secret", 24 | Token = (response or {}).Token or "token", 25 | Expiration = (response or {}).Expiration or "2030-01-01T20:00:00Z", 26 | } 27 | end, 28 | } 29 | else 30 | error("bad test path provided??? " .. tostring(opts.path)) 31 | end 32 | end, 33 | } 34 | end, 35 | } 36 | 37 | 38 | describe("RemoteCredentials", function() 39 | 40 | local RemoteCredentials 41 | local pl_utils_readfile = old_pl_utils.readfile 42 | 43 | before_each(function() 44 | pl_utils_readfile = old_pl_utils.readfile 45 | old_pl_utils.readfile = function() 46 | return "testtokenabc123" 47 | end 48 | restore() 49 | restore.setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "https://localhost/test/path") 50 | 51 | local _ = require("resty.aws.config").global -- load config before mocking http client 52 | package.loaded["resty.luasocket.http"] = http 53 | 54 | RemoteCredentials = require "resty.aws.credentials.RemoteCredentials" 55 | end) 56 | 57 | after_each(function() 58 | old_pl_utils.readfile = pl_utils_readfile 59 | restore() 60 | end) 61 | 62 | 63 | it("fetches credentials", function() 64 | local cred = RemoteCredentials:new() 65 | local success, key, secret, token = cred:get() 66 | assert.equal(true, success) 67 | assert.equal("access", key) 68 | assert.equal("secret", secret) 69 | assert.equal("token", token) 70 | end) 71 | 72 | end) 73 | 74 | 75 | describe("RemoteCredentials with customized full URI", function () 76 | it("fetches credentials", function () 77 | local RemoteCredentials 78 | 79 | restore() 80 | restore.setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://localhost:12345/test/path") 81 | 82 | local _ = require("resty.aws.config").global -- load config before mocking http client 83 | package.loaded["resty.luasocket.http"] = http 84 | 85 | RemoteCredentials = require "resty.aws.credentials.RemoteCredentials" 86 | finally(function() 87 | restore() 88 | end) 89 | 90 | local cred = RemoteCredentials:new() 91 | local success, key, secret, token = cred:get() 92 | assert.equal(true, success) 93 | assert.equal("access", key) 94 | assert.equal("secret", secret) 95 | assert.equal("token", token) 96 | end) 97 | end) 98 | 99 | describe("RemoteCredentials with full URI and token file", function () 100 | local pl_utils_readfile 101 | before_each(function() 102 | pl_utils_readfile = old_pl_utils.readfile 103 | old_pl_utils.readfile = function() 104 | return "testtokenabc123" 105 | end 106 | end) 107 | after_each(function() 108 | old_pl_utils.readfile = pl_utils_readfile 109 | end) 110 | it("fetches credentials", function () 111 | local RemoteCredentials 112 | 113 | restore() 114 | restore.setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://localhost:12345/test/path") 115 | restore.setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", "/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token") 116 | 117 | local _ = require("resty.aws.config").global -- load config before mocking http client 118 | package.loaded["resty.luasocket.http"] = http 119 | 120 | RemoteCredentials = require "resty.aws.credentials.RemoteCredentials" 121 | finally(function() 122 | restore() 123 | end) 124 | 125 | local cred = RemoteCredentials:new() 126 | local success, key, secret, token = cred:get() 127 | assert.equal(true, success) 128 | assert.equal("access", key) 129 | assert.equal("secret", secret) 130 | assert.equal("token", token) 131 | 132 | assert.not_nil(http_records[#http_records].headers) 133 | assert.equal(http_records[#http_records].headers["Authorization"], "testtokenabc123") 134 | end) 135 | end) 136 | 137 | describe("RemoteCredentials with full URI and token and token file, file takes higher precedence", function () 138 | local pl_utils_readfile 139 | before_each(function() 140 | pl_utils_readfile = old_pl_utils.readfile 141 | old_pl_utils.readfile = function() 142 | return "testtokenabc123" 143 | end 144 | end) 145 | after_each(function() 146 | old_pl_utils.readfile = pl_utils_readfile 147 | end) 148 | it("fetches credentials", function () 149 | local RemoteCredentials 150 | 151 | restore() 152 | restore.setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://localhost:12345/test/path") 153 | restore.setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN", "testtoken") 154 | restore.setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", "/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token") 155 | 156 | local _ = require("resty.aws.config").global -- load config before mocking http client 157 | package.loaded["resty.luasocket.http"] = http 158 | 159 | RemoteCredentials = require "resty.aws.credentials.RemoteCredentials" 160 | finally(function() 161 | restore() 162 | end) 163 | 164 | local cred = RemoteCredentials:new() 165 | local success, key, secret, token = cred:get() 166 | assert.equal(true, success) 167 | assert.equal("access", key) 168 | assert.equal("secret", secret) 169 | assert.equal("token", token) 170 | 171 | assert.not_nil(http_records[#http_records].headers) 172 | assert.equal(http_records[#http_records].headers["Authorization"], "testtokenabc123") 173 | end) 174 | end) 175 | 176 | -------------------------------------------------------------------------------- /spec/03-credentials/05-CredentialProviderChain_spec.lua: -------------------------------------------------------------------------------- 1 | local restore = require "spec.helpers" 2 | 3 | describe("CredentialProviderChain", function() 4 | 5 | local CredentialProviderChain 6 | 7 | before_each(function() 8 | restore() 9 | restore.setenv("ABC_ACCESS_KEY_ID", "access-1") 10 | restore.setenv("ABC_SECRET_ACCESS_KEY", "secret-1") 11 | restore.setenv("ABC_SESSION_TOKEN", "token-1") 12 | local _ = require("resty.aws.config").global -- load config before anything else 13 | 14 | CredentialProviderChain = require "resty.aws.credentials.CredentialProviderChain" 15 | end) 16 | 17 | after_each(function() 18 | restore() 19 | end) 20 | 21 | 22 | 23 | it("gets environment variables which are first", function() 24 | local cred = CredentialProviderChain:new { 25 | providers = { 26 | require("resty.aws.credentials.EnvironmentCredentials"):new { envPrefix = "ABC" }, 27 | require("resty.aws.credentials.EnvironmentCredentials"):new { envPrefix = "AWS" }, 28 | require("resty.aws.credentials.Credentials"):new { 29 | accessKeyId = "access-2", 30 | secretAccessKey = "secret-2", 31 | sessionToken = "token-2", 32 | }, 33 | } 34 | } 35 | assert.is_true(cred:needsRefresh()) 36 | 37 | local get = {cred:get()} 38 | get[5] = nil -- drop the expireTime 39 | 40 | assert.same({true, "access-1", "secret-1", "token-1"}, get) 41 | end) 42 | 43 | 44 | it("gets plain credentials which are last", function() 45 | -- clear env vars such that the first 2 providers both fail 46 | restore.setenv("ABC_ACCESS_KEY_ID", nil) 47 | restore.setenv("ABC_SECRET_ACCESS_KEY", nil) 48 | restore.setenv("ABC_SESSION_TOKEN", nil) 49 | 50 | local cred = CredentialProviderChain:new { 51 | providers = { 52 | require("resty.aws.credentials.EnvironmentCredentials"):new { envPrefix = "ABC" }, 53 | require("resty.aws.credentials.EnvironmentCredentials"):new { envPrefix = "AWS" }, 54 | require("resty.aws.credentials.Credentials"):new { 55 | accessKeyId = "access-2", 56 | secretAccessKey = "secret-2", 57 | sessionToken = "token-2", 58 | }, 59 | } 60 | } 61 | assert.is_true(cred:needsRefresh()) 62 | 63 | local get = {cred:get()} 64 | get[5] = nil -- drop the expireTime 65 | 66 | assert.same({true, "access-2", "secret-2", "token-2"}, get) 67 | end) 68 | 69 | 70 | it("gets default providers if not specified", function() 71 | local cred = CredentialProviderChain:new() 72 | assert.is.not_nil(cred.providers[1]) 73 | end) 74 | 75 | end) 76 | -------------------------------------------------------------------------------- /spec/03-credentials/06-ChainableTemporaryCredentials_spec.lua: -------------------------------------------------------------------------------- 1 | 2 | describe("ChainableTemporaryCredentials", function() 3 | 4 | local AWS = require "resty.aws" 5 | local ChainableTemporaryCredentials = require "resty.aws.credentials.ChainableTemporaryCredentials" 6 | 7 | setup(function() 8 | --setup 9 | end) 10 | 11 | teardown(function() 12 | --teardown 13 | end) 14 | 15 | describe("new()", function() 16 | 17 | it("creates a new instance when providing AWS-instance", function() 18 | local aws = AWS() 19 | local params = {} 20 | local creds 21 | assert.has.no.error(function() 22 | creds = assert(ChainableTemporaryCredentials:new { 23 | params = params, 24 | aws = aws, 25 | }) 26 | end) 27 | assert.is_function(creds.sts.assumeRole) 28 | assert.are.equal(aws.config.credentials, creds.masterCredentials) 29 | assert.are.equal(params, creds.params) 30 | end) 31 | 32 | 33 | it("accepts params as a single entry array", function() 34 | local aws = AWS() 35 | local params = {} 36 | local creds 37 | assert.has.no.error(function() 38 | creds = assert(ChainableTemporaryCredentials:new { 39 | params = { params }, 40 | aws = aws, 41 | }) 42 | end) 43 | assert.is_function(creds.sts.assumeRole) 44 | assert.are.equal(aws.config.credentials, creds.masterCredentials) 45 | assert.are.equal(params, creds.params) 46 | end) 47 | 48 | 49 | it("creates a new instance when providing STS-instance", function() 50 | local sts = AWS():STS() 51 | local params = {} 52 | local creds 53 | assert.has.no.error(function() 54 | creds = assert(ChainableTemporaryCredentials:new { 55 | params = params, 56 | sts = sts, 57 | }) 58 | end) 59 | assert.is_function(creds.sts.assumeRole) 60 | assert.are.equal(sts.config.credentials, creds.masterCredentials) 61 | assert.are.equal(params, creds.params) 62 | end) 63 | 64 | 65 | it("creates chained credentials from params-array", function() 66 | local aws = AWS() 67 | local params_start, params_middle, params_final = {}, {}, {} 68 | local creds_final 69 | assert.has.no.error(function() 70 | creds_final = assert(ChainableTemporaryCredentials:new { 71 | params = { params_start, params_middle, params_final }, 72 | aws = aws, 73 | }) 74 | end) 75 | 76 | assert.are.equal(params_final, creds_final.params) 77 | 78 | local creds_middle = creds_final.masterCredentials 79 | assert.are.equal(params_middle, creds_middle.params) 80 | 81 | local creds_start = creds_middle.masterCredentials 82 | assert.are.equal(params_start, creds_start.params) 83 | 84 | assert.are.equal(aws.config.credentials, creds_start.masterCredentials) 85 | 86 | end) 87 | end) 88 | 89 | end) 90 | -------------------------------------------------------------------------------- /spec/03-credentials/07-TokenFileWebIdentityCredentials_spec.lua: -------------------------------------------------------------------------------- 1 | local restore = require "spec.helpers" 2 | 3 | describe("TokenFileWebIdentityCredentials", function() 4 | 5 | local TokenFileWebIdentityCredentials 6 | 7 | before_each(function() 8 | restore() 9 | restore.setenv("AWS_ROLE_ARN", "arn:abc123") 10 | restore.setenv("AWS_WEB_IDENTITY_TOKEN_FILE", "/some/file") 11 | local _ = require("resty.aws.config").global -- load config before anything else 12 | 13 | TokenFileWebIdentityCredentials = require "resty.aws.credentials.TokenFileWebIdentityCredentials" 14 | end) 15 | 16 | after_each(function() 17 | restore() 18 | end) 19 | 20 | 21 | 22 | it("gets the relevant environment variables", function() 23 | local cred = TokenFileWebIdentityCredentials:new() 24 | assert.is_true(cred:needsRefresh()) -- true; because we only get env vars 25 | 26 | assert.same("arn:abc123", cred.role_arn) 27 | assert.same("/some/file", cred.token_file) 28 | assert.same("session@lua-resty-aws", cred.session_name) 29 | end) 30 | 31 | it("options override defaults", function() 32 | local cred = TokenFileWebIdentityCredentials:new { 33 | token_file = "another/file", 34 | role_arn = "another arn", 35 | session_name = "i like sessions", 36 | } 37 | assert.same("another arn", cred.role_arn) 38 | assert.same("another/file", cred.token_file) 39 | assert.same("i like sessions", cred.session_name) 40 | end) 41 | 42 | end) 43 | -------------------------------------------------------------------------------- /spec/03-credentials/08-SharedFileCredentials_spec.lua: -------------------------------------------------------------------------------- 1 | local pl_path = require "pl.path" 2 | local pl_config = require "pl.config" 3 | local tbl_clear = require "table.clear" 4 | local restore = require "spec.helpers" 5 | 6 | local hooked_file = {} 7 | 8 | local origin_read = pl_config.read 9 | local origin_isfile = pl_path.isfile 10 | 11 | pl_config.read = function(name, ...) 12 | return hooked_file[name] or origin_read(name, ...) 13 | end 14 | 15 | pl_path.isfile = function(name) 16 | return hooked_file[name] and true or origin_isfile(name) 17 | end 18 | 19 | local function hook_config_file(name, content) 20 | hooked_file[name] = content 21 | end 22 | 23 | describe("SharedFileCredentials_spec", function() 24 | 25 | local SharedFileCredentials_spec 26 | 27 | before_each(function() 28 | -- make ci happy 29 | restore.setenv("HOME", "/home/ci-test") 30 | local _ = require("resty.aws.config").global -- load config before anything else 31 | 32 | SharedFileCredentials_spec = require "resty.aws.credentials.SharedFileCredentials" 33 | end) 34 | 35 | after_each(function() 36 | restore() 37 | tbl_clear(hooked_file) 38 | end) 39 | 40 | 41 | it("basical sanity", function() 42 | local cred = SharedFileCredentials_spec:new {} 43 | assert(cred:needsRefresh()) -- true; because we has no file 44 | end) 45 | 46 | it("gets from config", function() 47 | hook_config_file(pl_path.expanduser("~/.aws/config"), { 48 | default = { 49 | aws_access_key_id = "access", 50 | aws_secret_access_key = "secret", 51 | aws_session_token = "token", 52 | } 53 | }) 54 | local cred = SharedFileCredentials_spec:new {} 55 | assert.is_false(cred:needsRefresh()) -- false; because we fetch upon instanciation 56 | 57 | local get = {cred:get()} 58 | assert.is.near(ngx.now() + 10*365*24*60*60, 30, get[5]) -- max delta = 30 seconds 59 | 60 | get[5] = nil 61 | assert.same({true, "access", "secret", "token"}, get) 62 | end) 63 | 64 | it("gets from credentials", function() 65 | hook_config_file(pl_path.expanduser("~/.aws/credentials"), { 66 | default = { 67 | aws_access_key_id = "access", 68 | aws_secret_access_key = "secret", 69 | aws_session_token = "token", 70 | } 71 | }) 72 | 73 | local cred = SharedFileCredentials_spec:new {} 74 | assert.is_false(cred:needsRefresh()) -- false; because we fetch upon instanciation 75 | 76 | local get = {cred:get()} 77 | assert.is.near(ngx.now() + 10*365*24*60*60, 30, get[5]) -- max delta = 30 seconds 78 | 79 | get[5] = nil 80 | assert.same({true, "access", "secret", "token"}, get) 81 | end) 82 | 83 | end) 84 | -------------------------------------------------------------------------------- /spec/04-services/01-secret_manager.lua: -------------------------------------------------------------------------------- 1 | setmetatable(_G, nil) 2 | 3 | -- hook request sending 4 | package.loaded["resty.aws.request.execute"] = function(...) 5 | return ... 6 | end 7 | 8 | local AWS = require("resty.aws") 9 | local AWS_global_config = require("resty.aws.config").global 10 | 11 | 12 | local config = AWS_global_config 13 | config.tls = true 14 | local aws = AWS(config) 15 | 16 | 17 | aws.config.credentials = aws:Credentials { 18 | accessKeyId = "test_id", 19 | secretAccessKey = "test_key", 20 | } 21 | 22 | aws.config.region = "test_region" 23 | 24 | describe("Secret Manager service", function() 25 | local sm 26 | local origin_time 27 | 28 | setup(function() 29 | origin_time = ngx.time 30 | ngx.time = function () --luacheck: ignore 31 | return 1667543171 32 | end 33 | end) 34 | 35 | teardown(function () 36 | ngx.time = origin_time --luacheck: ignore 37 | end) 38 | 39 | before_each(function() 40 | sm = assert(aws:SecretsManager {}) 41 | end) 42 | 43 | after_each(function() 44 | end) 45 | 46 | local testcases = { 47 | -- API = { param, expected_result_aws, }, 48 | getSecretValue = { 49 | { 50 | SecretId = "test_id", 51 | VersionStage = "AWSCURRENT", 52 | }, 53 | { 54 | ['body'] = '{"VersionStage":"AWSCURRENT","SecretId":"test_id"}', 55 | ['headers'] = { 56 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/secretsmanager/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-date;x-amz-target, Signature=81618df993cf58510f22d95efb815d03a9bd3cfb7af0ef766e6980f7a99799ff', 57 | ['Content-Length'] = 50, 58 | ['Content-Type'] = 'application/x-amz-json-1.1', 59 | ['Host'] = 'secretsmanager.test_region.amazonaws.com', 60 | ['X-Amz-Date'] = '20221104T062611Z', 61 | ['X-Amz-Target'] = 'secretsmanager.GetSecretValue' 62 | }, 63 | ['host'] = 'secretsmanager.test_region.amazonaws.com', 64 | ['method'] = 'POST', 65 | ['path'] = '/', 66 | ['port'] = 443, 67 | ['query'] = {}, 68 | ['tls'] = true, 69 | }, 70 | }, 71 | } 72 | 73 | for api, test in pairs(testcases) do 74 | it("SecretsManager:" .. api, function() 75 | local param = test[1] 76 | local expected_result_aws = test[2] 77 | 78 | local result_aws = assert(sm[api](sm, param)) 79 | 80 | assert.same(expected_result_aws, result_aws) 81 | end) 82 | end 83 | end) 84 | -------------------------------------------------------------------------------- /spec/04-services/02-s3.lua: -------------------------------------------------------------------------------- 1 | setmetatable(_G, nil) 2 | 3 | -- hock request sending 4 | package.loaded["resty.aws.request.execute"] = function(...) 5 | return ... 6 | end 7 | 8 | local AWS = require("resty.aws") 9 | local AWS_global_config = require("resty.aws.config").global 10 | 11 | 12 | local config = AWS_global_config 13 | config.tls = true 14 | local aws = AWS(config) 15 | 16 | 17 | aws.config.credentials = aws:Credentials { 18 | accessKeyId = "test_id", 19 | secretAccessKey = "test_key", 20 | } 21 | 22 | aws.config.region = "test_region" 23 | 24 | describe("S3 service", function() 25 | local s3, s3_3rd 26 | local origin_time 27 | 28 | setup(function() 29 | origin_time = ngx.time 30 | ngx.time = function () --luacheck: ignore 31 | return 1667543171 32 | end 33 | end) 34 | 35 | teardown(function () 36 | ngx.time = origin_time --luacheck: ignore 37 | end) 38 | 39 | before_each(function() 40 | s3 = assert(aws:S3 {}) 41 | s3_3rd = assert(aws:S3 { 42 | scheme = "http", 43 | endpoint = "testendpoint.com", 44 | port = 443, 45 | tls = false, 46 | }) 47 | end) 48 | 49 | after_each(function() 50 | end) 51 | 52 | local testcases = { 53 | -- API = { param, expected_result_aws, expected_result_3rd_patry, }, 54 | putObject = { 55 | { 56 | Bucket = "testbucket", 57 | Key = "testkey", 58 | Body = "testbody", 59 | Metadata = { 60 | test = "test", 61 | } 62 | }, 63 | { 64 | body = 'testbody', 65 | headers = { 66 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/s3/aws4_request, SignedHeaders=content-length;host;x-amz-content-sha256;x-amz-date;x-amz-meta-test, Signature=57e7e544e5bce7cdf6321768d7577212a874a3504031fba4bb97ab2e5245532f', 67 | ['Content-Length'] = 8, 68 | ['Host'] = 'testbucket.s3.test_region.amazonaws.com', 69 | ['X-Amz-Content-Sha256'] = '2417e54e58ac3752d4d82355e13053e0b3d9601d09d4fd5027be26da405b8ccd', 70 | ['X-Amz-Date'] = '20221104T062611Z', 71 | ['X-Amz-Meta-Test'] = 'test', 72 | }, 73 | host = 'testbucket.s3.test_region.amazonaws.com', 74 | method = 'PUT', 75 | path = '/testkey', 76 | port = 443, 77 | query = {}, 78 | tls = true, 79 | }, 80 | { 81 | body = 'testbody', 82 | headers = { 83 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/s3/aws4_request, SignedHeaders=content-length;host;x-amz-content-sha256;x-amz-date;x-amz-meta-test, Signature=c821cc6d135ee1abe2efd235d7a8f699fbaa90e979584cc274f1ea1610679f86', 84 | ['Content-Length'] = 8, 85 | ['Host'] = 'testbucket.testendpoint.com', 86 | ['X-Amz-Content-Sha256'] = '2417e54e58ac3752d4d82355e13053e0b3d9601d09d4fd5027be26da405b8ccd', 87 | ['X-Amz-Date'] = '20221104T062611Z', 88 | ['X-Amz-Meta-Test'] = 'test', 89 | }, 90 | host = 'testbucket.testendpoint.com', 91 | method = 'PUT', 92 | path = '/testkey', 93 | port = 443, 94 | query = {}, 95 | tls = false, 96 | }, 97 | }, 98 | getObject = { 99 | { 100 | Bucket = "testbucket", 101 | Key = "testkey", 102 | }, 103 | { 104 | ['headers'] = { 105 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=a1cab7c5a3e2ec70af4acaed2dd5382842080af7dbe8f4416540cc99357b322b', 106 | ['Host'] = 'testbucket.s3.test_region.amazonaws.com', 107 | ['X-Amz-Content-Sha256'] = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', 108 | ['X-Amz-Date'] = '20221104T062611Z' 109 | }, 110 | ['host'] = 'testbucket.s3.test_region.amazonaws.com', 111 | ['method'] = 'GET', 112 | ['path'] = '/testkey', 113 | ['port'] = 443, 114 | ['query'] = {}, 115 | ['tls'] = true 116 | }, 117 | { 118 | ['headers'] = { 119 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=f0ba4ea255b0678c5e9339e44a976e3f6547bddfaf5dfe5a86403dc97d891010', 120 | ['Host'] = 'testbucket.testendpoint.com', 121 | ['X-Amz-Content-Sha256'] = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', 122 | ['X-Amz-Date'] = '20221104T062611Z' 123 | }, 124 | ['host'] = 'testbucket.testendpoint.com', 125 | ['method'] = 'GET', 126 | ['path'] = '/testkey', 127 | ['port'] = 443, 128 | ['query'] = {}, 129 | ['tls'] = false, 130 | } 131 | }, 132 | getBucketAcl = { 133 | { 134 | Bucket = "testbucket", 135 | }, 136 | { 137 | ['headers'] = { 138 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=69eb892e8f3cae5b3f777c31e4318d946ce0ebba97f8539a5064e1709d8477c6', 139 | ['Host'] = 'testbucket.s3.test_region.amazonaws.com', 140 | ['X-Amz-Content-Sha256'] = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', 141 | ['X-Amz-Date'] = '20221104T062611Z' 142 | }, 143 | ['host'] = 'testbucket.s3.test_region.amazonaws.com', 144 | ['method'] = 'GET', 145 | ['path'] = '', 146 | ['port'] = 443, 147 | ['query'] = { 148 | ['acl'] = '' 149 | }, 150 | ['tls'] = true, 151 | }, 152 | { 153 | ['headers'] = { 154 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=0aac6b456c28cd393ca06a779074c4797155338569fad6f3e95ea348406b16a9', 155 | ['Host'] = 'testbucket.testendpoint.com', 156 | ['X-Amz-Content-Sha256'] = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', 157 | ['X-Amz-Date'] = '20221104T062611Z' 158 | }, 159 | ['host'] = 'testbucket.testendpoint.com', 160 | ['method'] = 'GET', 161 | ['path'] = '', 162 | ['port'] = 443, 163 | ['query'] = { 164 | ['acl'] = '' 165 | }, 166 | ['tls'] = false, 167 | }, 168 | }, 169 | } 170 | 171 | 172 | for api, test in pairs(testcases) do 173 | it("s3:" .. api, function() 174 | local param = test[1] 175 | local expected_result_aws = test[2] 176 | local expected_result_3rd_patry = test[3] 177 | 178 | local result_aws = assert(s3[api](s3, param)) 179 | local result_3rd_patry = assert(s3_3rd[api](s3_3rd, param)) 180 | 181 | assert.same(expected_result_aws, result_aws) 182 | assert.same(expected_result_3rd_patry, result_3rd_patry) 183 | end) 184 | end 185 | end) 186 | -------------------------------------------------------------------------------- /spec/04-services/03-s3_compat_api.lua: -------------------------------------------------------------------------------- 1 | setmetatable(_G, nil) 2 | 3 | -- hock request sending 4 | package.loaded["resty.aws.request.execute"] = function(...) 5 | return ... 6 | end 7 | 8 | local AWS = require("resty.aws") 9 | local AWS_global_config = require("resty.aws.config").global 10 | 11 | 12 | local config = AWS_global_config 13 | config.tls = true 14 | -- old API format 15 | config.s3_bucket_in_path = true 16 | local aws = AWS(config) 17 | 18 | 19 | aws.config.credentials = aws:Credentials { 20 | accessKeyId = "test_id", 21 | secretAccessKey = "test_key", 22 | } 23 | 24 | aws.config.region = "test_region" 25 | 26 | describe("S3 service", function() 27 | local s3, s3_3rd 28 | local origin_time 29 | 30 | setup(function() 31 | origin_time = ngx.time 32 | ngx.time = function() --luacheck: ignore 33 | return 1667543171 34 | end 35 | end) 36 | 37 | teardown(function () 38 | ngx.time = origin_time --luacheck: ignore 39 | end) 40 | 41 | before_each(function() 42 | s3 = assert(aws:S3 {}) 43 | s3_3rd = assert(aws:S3 { 44 | scheme = "http", 45 | endpoint = "testendpoint.com", 46 | port = 443, 47 | tls = false, 48 | }) 49 | end) 50 | 51 | after_each(function() 52 | end) 53 | 54 | local testcases = { 55 | -- API = { param, expected_result_aws, expected_result_3rd_patry, }, 56 | putObject = { 57 | { 58 | Bucket = "testbucket", 59 | Key = "testkey", 60 | Body = "testbody", 61 | Metadata = { 62 | test = "test", 63 | } 64 | }, 65 | { 66 | body = 'testbody', 67 | headers = { 68 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/s3/aws4_request, SignedHeaders=content-length;host;x-amz-content-sha256;x-amz-date;x-amz-meta-test, Signature=5d3c4b53bfcecfba7b9c76637f64e832ad35af583d1098df6eabc1b98a5f4c4f', 69 | ['Content-Length'] = 8, 70 | ['Host'] = 's3.test_region.amazonaws.com', 71 | ['X-Amz-Content-Sha256'] = '2417e54e58ac3752d4d82355e13053e0b3d9601d09d4fd5027be26da405b8ccd', 72 | ['X-Amz-Date'] = '20221104T062611Z', 73 | ['X-Amz-Meta-Test'] = 'test', 74 | }, 75 | host = 's3.test_region.amazonaws.com', 76 | method = 'PUT', 77 | path = '/testbucket/testkey', 78 | port = 443, 79 | query = {}, 80 | tls = true, 81 | }, 82 | { 83 | body = 'testbody', 84 | headers = { 85 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/s3/aws4_request, SignedHeaders=content-length;host;x-amz-content-sha256;x-amz-date;x-amz-meta-test, Signature=3536e1dcf26a23b78a5345df54ef1f86c796f13251c7fdaa23d78f6d67aeb0be', 86 | ['Content-Length'] = 8, 87 | ['Host'] = 'testendpoint.com', 88 | ['X-Amz-Content-Sha256'] = '2417e54e58ac3752d4d82355e13053e0b3d9601d09d4fd5027be26da405b8ccd', 89 | ['X-Amz-Date'] = '20221104T062611Z', 90 | ['X-Amz-Meta-Test'] = 'test', 91 | }, 92 | host = 'testendpoint.com', 93 | method = 'PUT', 94 | path = '/testbucket/testkey', 95 | port = 443, 96 | query = {}, 97 | tls = false, 98 | }, 99 | }, 100 | getObject = { 101 | { 102 | Bucket = "testbucket", 103 | Key = "testkey", 104 | }, 105 | { 106 | ['headers'] = { 107 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=66fc85f53ba4ff3665e1c575c882216b6489442e1bc2822d73b2f43949cca0cf', 108 | ['Host'] = 's3.test_region.amazonaws.com', 109 | ['X-Amz-Content-Sha256'] = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', 110 | ['X-Amz-Date'] = '20221104T062611Z' 111 | }, 112 | ['host'] = 's3.test_region.amazonaws.com', 113 | ['method'] = 'GET', 114 | ['path'] = '/testbucket/testkey', 115 | ['port'] = 443, 116 | ['query'] = {}, 117 | ['tls'] = true 118 | }, 119 | { 120 | ['headers'] = { 121 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=67a06a5f1634a5e9598d576636ef7d6d77a6fe7f07a0e1d5f66df80a3c47e9f4', 122 | ['Host'] = 'testendpoint.com', 123 | ['X-Amz-Content-Sha256'] = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', 124 | ['X-Amz-Date'] = '20221104T062611Z' 125 | }, 126 | ['host'] = 'testendpoint.com', 127 | ['method'] = 'GET', 128 | ['path'] = '/testbucket/testkey', 129 | ['port'] = 443, 130 | ['query'] = {}, 131 | ['tls'] = false, 132 | } 133 | }, 134 | getBucketAcl = { 135 | { 136 | Bucket = "testbucket", 137 | }, 138 | { 139 | ['headers'] = { 140 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=19bf55ee1b8db1c5099c8e40cf96881479e47cb2e7f08d191f31733f0335d38d', 141 | ['Host'] = 's3.test_region.amazonaws.com', 142 | ['X-Amz-Content-Sha256'] = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', 143 | ['X-Amz-Date'] = '20221104T062611Z' 144 | }, 145 | ['host'] = 's3.test_region.amazonaws.com', 146 | ['method'] = 'GET', 147 | ['path'] = '/testbucket', 148 | ['port'] = 443, 149 | ['query'] = { 150 | ['acl'] = '' 151 | }, 152 | ['tls'] = true, 153 | }, 154 | { 155 | ['headers'] = { 156 | ['Authorization'] = 'AWS4-HMAC-SHA256 Credential=test_id/20221104/test_region/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=8439d7f57d8de5b9ecb6a578983b8e5fb6722ca375530753d4a6f498d2c4194e', 157 | ['Host'] = 'testendpoint.com', 158 | ['X-Amz-Content-Sha256'] = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', 159 | ['X-Amz-Date'] = '20221104T062611Z' 160 | }, 161 | ['host'] = 'testendpoint.com', 162 | ['method'] = 'GET', 163 | ['path'] = '/testbucket', 164 | ['port'] = 443, 165 | ['query'] = { 166 | ['acl'] = '' 167 | }, 168 | ['tls'] = false, 169 | }, 170 | }, 171 | } 172 | 173 | 174 | for api, test in pairs(testcases) do 175 | it("s3:" .. api, function() 176 | local param = test[1] 177 | local expected_result_aws = test[2] 178 | local expected_result_3rd_patry = test[3] 179 | 180 | local result_aws = assert(s3[api](s3, param)) 181 | local result_3rd_patry = assert(s3_3rd[api](s3_3rd, param)) 182 | 183 | assert.same(expected_result_aws, result_aws) 184 | assert.same(expected_result_3rd_patry, result_3rd_patry) 185 | end) 186 | end 187 | end) 188 | -------------------------------------------------------------------------------- /spec/04-services/04-rds-utils_spec.lua: -------------------------------------------------------------------------------- 1 | setmetatable(_G, nil) 2 | 3 | -- -- hock request sending 4 | -- package.loaded["resty.aws.request.execute"] = function(...) 5 | -- return ... 6 | -- end 7 | 8 | local AWS = require("resty.aws") 9 | local AWS_global_config = require("resty.aws.config").global 10 | 11 | local config = AWS_global_config 12 | local aws = AWS(config) 13 | 14 | aws.config.credentials = aws:Credentials { 15 | accessKeyId = "test_id", 16 | secretAccessKey = "test_key", 17 | } 18 | 19 | aws.config.region = "test_region" 20 | 21 | local DB_ENDPOINT = "test_database.test_cluster.us-east-1.rds.amazonaws.com" 22 | local DB_PORT = "443" 23 | local DB_REGION = "us-east-1" 24 | local DB_USER = "test_user" 25 | 26 | describe("RDS utils", function() 27 | local rds, signer 28 | local origin_time 29 | setup(function() 30 | origin_time = ngx.time 31 | ngx.time = function () --luacheck: ignore 32 | return 1667543171 33 | end 34 | end) 35 | 36 | teardown(function () 37 | ngx.time = origin_time --luacheck: ignore 38 | end) 39 | 40 | before_each(function() 41 | rds = aws:RDS() 42 | signer = rds:Signer { 43 | hostname = DB_ENDPOINT, 44 | port = DB_PORT, 45 | username = DB_USER, 46 | region = DB_REGION, -- override aws config 47 | } 48 | end) 49 | 50 | after_each(function() 51 | rds = nil 52 | signer = nil 53 | end) 54 | 55 | it("should generate expected IAM auth token with mock key", function() 56 | local auth_token, err = signer:getAuthToken() 57 | local expected_auth_token = "test_database.test_cluster.us-east-1.rds.amazonaws.com:443/?X-Amz-Signature=ff72d46f1937c1f5917f69d694929ca814b781619b8d730451c7ffef050059b0&Action=connect&DBUser=test_user&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=test_id%2F20221104%2Fus-east-1%2Frds-db%2Faws4_request&X-Amz-Date=20221104T062611Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host" 58 | assert.is_nil(err) 59 | assert.same(auth_token, expected_auth_token) 60 | end) 61 | 62 | it("should generate expected IAM auth token with mock temporary credential", function() 63 | signer.config.credentials = aws:Credentials { 64 | accessKeyId = "test_id2", 65 | secretAccessKey = "test_key2", 66 | sessionToken = "test_token2", 67 | } 68 | local auth_token, err = signer:getAuthToken() 69 | local expected_auth_token = "test_database.test_cluster.us-east-1.rds.amazonaws.com:443/?X-Amz-Signature=7fcb20a161bb493b405686590604bfb864f8ac68dea84b903cd551e93f850ac5&Action=connect&DBUser=test_user&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=test_id2%2F20221104%2Fus-east-1%2Frds-db%2Faws4_request&X-Amz-Date=20221104T062611Z&X-Amz-Expires=900&X-Amz-Security-Token=test_token2&X-Amz-SignedHeaders=host" 70 | assert.is_nil(err) 71 | assert.same(auth_token, expected_auth_token) 72 | end) 73 | end) 74 | -------------------------------------------------------------------------------- /spec/helpers.lua: -------------------------------------------------------------------------------- 1 | local ffi = require "ffi" 2 | 3 | 4 | local M = {} 5 | 6 | 7 | ffi.cdef [[ 8 | int setenv(const char *name, const char *value, int overwrite); 9 | int unsetenv(const char *name); 10 | ]] 11 | 12 | local function unsetenv(env) 13 | assert(type(env) == "string", "expected env name to be a string") 14 | return ffi.C.unsetenv(env) == 0 15 | end 16 | 17 | local function setenv(env, value) 18 | assert(type(env) == "string", "expected env name to be a string") 19 | if value == nil then 20 | return unsetenv(env) 21 | end 22 | assert(type(value) == "string", "expected value to be a string (or nil to clear)") 23 | return ffi.C.setenv(env, value, 1) == 0 24 | end 25 | 26 | local original_env_values = {} 27 | local nil_sentinel = {} 28 | local function backup_env(name) 29 | original_env_values[name] = original_env_values[name] or os.getenv(name) or nil_sentinel 30 | end 31 | 32 | 33 | -- sets an environment variable, set to nil to remove it 34 | function M.setenv(env, value) 35 | assert(type(env) == "string", "expected env name to be a string") 36 | assert(type(value) == "string" or value == nil, "expected value to be a string (or nil to clear)") 37 | backup_env(env) 38 | setenv(env, value) 39 | end 40 | 41 | 42 | -- unsets an environment variable 43 | function M.unsetenv(env) 44 | assert(type(env) == "string", "expected env name to be a string") 45 | backup_env(env) 46 | unsetenv(env) 47 | end 48 | 49 | 50 | -- gets an environment variable 51 | M.getenv = os.getenv -- for symetry; get/set/unset 52 | 53 | 54 | -- restores all env vars to original values and clears all loaded 'resty.aws' modules 55 | function M.restore() 56 | for name, value in pairs(original_env_values) do 57 | setenv(name, value ~= nil_sentinel and value or nil) 58 | end 59 | for name, mod in pairs(package.loaded) do 60 | if type(name) == "string" and (name:match("^resty%.aws$") or name:match("^resty%.aws%.")) then 61 | package.loaded[name] = nil 62 | end 63 | end 64 | collectgarbage() 65 | collectgarbage() 66 | 67 | -- disable EC2 metadata 68 | setenv("AWS_EC2_METADATA_DISABLED", "true") 69 | end 70 | 71 | return setmetatable(M, { 72 | __call = function(self, ...) 73 | return self.restore(...) 74 | end 75 | }) 76 | -------------------------------------------------------------------------------- /spec/resty-runner.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env resty 2 | 3 | -- script to run Busted tests using Openresty while setting some extra flags. 4 | -- 5 | -- This script should be specified as: 6 | -- busted --lua= 7 | -- 8 | -- Alternatively specify it in the `.busted` config file 9 | 10 | 11 | -- These flags are passed to `resty` by default, to allow for more connections 12 | -- and disable the Global variable write-guard. Override it by setting the 13 | -- environment variable `BUSTED_RESTY_FLAGS`. 14 | local RESTY_FLAGS=os.getenv("BUSTED_RESTY_FLAGS") or "-c 4096 -e 'setmetatable(_G, nil)'" 15 | 16 | -- rebuild the invoked commandline, while inserting extra resty-flags 17 | local cmd = { 18 | "exec", 19 | arg[-1], 20 | RESTY_FLAGS, 21 | } 22 | for i, param in ipairs(arg) do 23 | table.insert(cmd, "'" .. param .. "'") 24 | end 25 | 26 | local _, _, rc = os.execute(table.concat(cmd, " ")) 27 | os.exit(rc) 28 | -------------------------------------------------------------------------------- /src/resty/aws/credentials/ChainableTemporaryCredentials.lua: -------------------------------------------------------------------------------- 1 | --- ChainableTemporaryCredentials class. 2 | -- @classmod ChainableTemporaryCredentials 3 | 4 | local lom = require("lxp.lom") 5 | 6 | 7 | -- Create class 8 | local Super = require "resty.aws.credentials.Credentials" 9 | local ChainableTemporaryCredentials = setmetatable({}, Super) 10 | ChainableTemporaryCredentials.__index = ChainableTemporaryCredentials 11 | 12 | 13 | --- Constructor, inherits from `Credentials`. 14 | -- @function aws:ChainableTemporaryCredentials 15 | -- @param opt options table, additional fields to the `Credentials` class: 16 | -- @param opt.params params table for the `assumeRole` function, or array of those 17 | -- tables in case of a chain of roles to assume. 18 | -- @param opt.aws `AWS` instance, required when creating a chain. 19 | -- @param opt.masterCredentials `Credentials` instance to use when assuming the 20 | -- role. Defaults to `sts.config.credentials` or `aws.config.credentials` in that 21 | -- order. 22 | -- @param opt.sts the `STS` service instance to use for fetching the credentials. 23 | -- Defaults to a new instance created as `aws:STS()`. 24 | -- @usage -- creating a chain of assumed roles 25 | -- local aws = AWS() -- provides the masterCredentials 26 | -- local role1 = { ... } -- parameters to assume role1, from the masterCredentials 27 | -- local role2 = { ... } -- parameters to assume role2, from the role1 credentials 28 | -- local role3 = { ... } -- parameters to assume role3, from the role2 credentials 29 | -- 30 | -- local creds = aws:ChainableTemporaryCredentials { 31 | -- params = { role1, role2, role3 }, 32 | -- } 33 | -- 34 | -- -- Get credentials for role3 35 | -- local success, id, key, token, expiretime = creds:get() 36 | -- if not success then 37 | -- return nil, id 38 | -- end 39 | 40 | function ChainableTemporaryCredentials:new(opts) 41 | local self = Super:new(opts) -- override 'self' to be the new object/class 42 | setmetatable(self, ChainableTemporaryCredentials) 43 | 44 | opts = opts or {} 45 | 46 | assert(opts.tokenCodeFn == nil, "Option 'opts.tokenCodeFn' is not supported.") 47 | 48 | -- get the master credentials to use 49 | local mCredentials = opts.masterCredentials 50 | if not mCredentials and opts.sts then 51 | mCredentials = ((opts.sts or {}).config or {}).credentials 52 | end 53 | if not mCredentials and opts.aws then 54 | mCredentials = ((opts.aws or {}).config or {}).credentials 55 | end 56 | assert(type(mCredentials) == "table", "No master-credentials provided, either 'opts.masterCredentials', 'opts.aws', or 'opts.sts' options must be set") 57 | 58 | -- get array of params-tables 59 | local params = opts.params 60 | assert(type(params) == "table", "Expected 'opts.params' to be a parameter table/map or an array of parameter tables/maps") 61 | if not params[1] then 62 | -- not an array, so a parameter table/map, make it an array 63 | params = { params } 64 | end 65 | 66 | -- get the STS service instance to use 67 | local sts = opts.sts 68 | if sts then 69 | assert(#params == 1, "Cannot use 'opts.sts' to create a chain, only specify a single 'opts.params' entry, or specify 'opts.aws' instead of 'opts.sts'.") 70 | else 71 | if opts.aws then 72 | local err 73 | sts, err = opts.aws:STS() 74 | if not sts then 75 | error("failed to create STS instance: " .. tostring(err)) 76 | end 77 | end 78 | end 79 | assert(type(sts) == "table", "No STS service, either 'opts.sts' or 'opts.aws' option must be set") 80 | 81 | if #params == 1 then 82 | -- there is only 1 role to assume so that is us. 83 | -- self.aws = aws 84 | self.sts = sts 85 | self.params = params[1] 86 | self.masterCredentials = mCredentials 87 | else 88 | -- multiple roles to assume, so pick the last and create a sub-credential 89 | -- self.aws = aws 90 | self.sts = sts 91 | self.params = params[#params] 92 | params[#params] = nil 93 | self.masterCredentials = ChainableTemporaryCredentials:new { 94 | masterCredentials = mCredentials, 95 | params = params, 96 | aws = opts.aws, 97 | } 98 | end 99 | 100 | return self 101 | end 102 | 103 | 104 | -- updates credentials. 105 | -- @return success, or nil+err 106 | function ChainableTemporaryCredentials:refresh() 107 | local response, err = self.sts:assumeRole(self.params) 108 | if not response then 109 | return nil, "Request for token data failed: " .. tostring(err) 110 | end 111 | 112 | if response.status ~= 200 then 113 | return nil, ("request for token returned '%s': %s"):format(tostring(response.status), response.body) 114 | end 115 | 116 | if type(response.body) ~= "string" then 117 | return nil, "request for token returned invalid body: " .. err 118 | end 119 | 120 | local resp_body_lom, err = lom.parse(response.body) 121 | if not resp_body_lom then 122 | return nil, "failed to parse response body: " .. err 123 | end 124 | 125 | local cred_lom = lom.find_elem(lom.find_elem(resp_body_lom, "AssumeRoleResult"), "Credentials") 126 | 127 | local AccessKeyId = lom.find_elem(cred_lom, "AccessKeyId")[1] 128 | local SecretAccessKey = lom.find_elem(cred_lom, "SecretAccessKey")[1] 129 | local SessionToken = lom.find_elem(cred_lom, "SessionToken")[1] 130 | local Expiration = lom.find_elem(cred_lom, "Expiration")[1] 131 | 132 | self:set(AccessKeyId, SecretAccessKey, SessionToken, Expiration) 133 | 134 | return true 135 | end 136 | 137 | return ChainableTemporaryCredentials 138 | -------------------------------------------------------------------------------- /src/resty/aws/credentials/CredentialProviderChain.lua: -------------------------------------------------------------------------------- 1 | --- CredentialProviderChain class. 2 | -- @classmod CredentialProviderChain 3 | 4 | 5 | -- Create class 6 | local Super = require "resty.aws.credentials.Credentials" 7 | local CredentialProviderChain = setmetatable({}, Super) 8 | CredentialProviderChain.__index = CredentialProviderChain 9 | 10 | 11 | local aws_config = require("resty.aws.config") 12 | 13 | 14 | CredentialProviderChain.defaultProviders = {} 15 | 16 | 17 | local function initialize() 18 | -- while not everything is implemented this will load what we do have without 19 | -- failing on what is missing. Will auto pick up newly added classes afterwards. 20 | local function add_if_exists(name, opts) 21 | local ok, class = pcall(require, "resty.aws.credentials." .. name) 22 | if not ok then 23 | ngx.log(ngx.DEBUG, "AWS credential class '", name, "' not found or failed to load") 24 | return 25 | end 26 | -- instantiate and add 27 | local ok, instance = pcall(class.new, class, opts) 28 | if not ok then 29 | ngx.log(ngx.DEBUG, "AWS credential class '", name, "' failed to instantiate: ", instance) 30 | return 31 | end 32 | CredentialProviderChain.defaultProviders[#CredentialProviderChain.defaultProviders+1] = instance 33 | end 34 | 35 | -- add the defaults 36 | add_if_exists("EnvironmentCredentials", { envPrefix = 'AWS' }) 37 | add_if_exists("EnvironmentCredentials", { envPrefix = 'AMAZON' }) 38 | add_if_exists("SharedFileCredentials") 39 | add_if_exists("RemoteCredentials") -- since "ECSCredentials" doesn't exist? and for ECS RemoteCredentials is used??? 40 | add_if_exists("ProcessCredentials") 41 | add_if_exists("TokenFileWebIdentityCredentials") 42 | if aws_config.global.AWS_EC2_METADATA_DISABLED then 43 | ngx.log(ngx.DEBUG, "AWS_EC2_METADATA_DISABLED is set, skipping EC2MetadataCredentials provider") 44 | else 45 | add_if_exists("EC2MetadataCredentials") 46 | end 47 | 48 | initialize = nil 49 | end 50 | 51 | --- Constructor, inherits from `Credentials`. 52 | -- 53 | -- The `providers` array defaults to the following list (in order, not all implemented): 54 | -- 55 | -- 1. `EnvironmentCredentials`; envPrefix = 'AWS' 56 | -- 57 | -- 2. `EnvironmentCredentials`; envPrefix = 'AMAZON' 58 | -- 59 | -- 3. `SharedIniFileCredentials` 60 | -- 61 | -- 4. `RemoteCredentials` 62 | -- 63 | -- 5. `ProcessCredentials` 64 | -- 65 | -- 6. `TokenFileWebIdentityCredentials` 66 | -- 67 | -- 7. `EC2MetadataCredentials` (only if `AWS_EC2_METADATA_DISABLED` hasn't been set to `true`) 68 | -- 69 | -- @function aws:CredentialProviderChain 70 | -- @param opt options table, additional fields to the `Credentials` class: 71 | -- @param opt.providers array of `Credentials` objects or functions (functions must return a `Credentials` object) 72 | function CredentialProviderChain:new(opts) 73 | if initialize then 74 | initialize() 75 | end 76 | 77 | local self = Super:new(opts) -- override 'self' to be the new object/class 78 | setmetatable(self, CredentialProviderChain) 79 | 80 | opts = opts or {} 81 | 82 | self.providers = opts.providers 83 | if not self.providers then 84 | self.providers = {} 85 | for i, provider in ipairs(CredentialProviderChain.defaultProviders) do 86 | self.providers[i] = provider 87 | end 88 | end 89 | 90 | assert(type(self.providers) == "table", "expected opts.providers to be an array of 'Credentials' objects or functions returning Credentials") 91 | 92 | return self 93 | end 94 | 95 | -- updates credentials. 96 | -- @return true 97 | function CredentialProviderChain:refresh() 98 | for i, provider in ipairs(self.providers) do 99 | if type(provider) == "function" then 100 | -- lazily create Credential 101 | local p, err = provider() 102 | if not p then 103 | ngx.log(ngx.ERR, "failed to instantiate Credential from provider function: ", tostring(err)) 104 | else 105 | -- store succesful created credential, replacing the previous function 106 | self.providers[i] = p 107 | provider = p 108 | end 109 | end 110 | 111 | local success, accessKeyId, secretAccessKey, sessionToken, expireTime = provider:get() 112 | if not success then 113 | ngx.log(ngx.DEBUG, "Provider failed: ", accessKeyId) 114 | else 115 | -- success, store results and exit 116 | self:set(accessKeyId, secretAccessKey, sessionToken, expireTime) 117 | return true 118 | end 119 | end 120 | return nil, "none of the providers succeeded, no credentials available" 121 | end 122 | 123 | return CredentialProviderChain 124 | -------------------------------------------------------------------------------- /src/resty/aws/credentials/Credentials.lua: -------------------------------------------------------------------------------- 1 | --- Credentials class. 2 | -- Manually sets credentials. 3 | -- Also the base class for all credential classes. 4 | -- @classmod Credentials 5 | local parse_date = require("luatz").parse.rfc_3339 6 | local semaphore = require "ngx.semaphore" 7 | 8 | 9 | local SEMAPHORE_TIMEOUT = 30 -- semaphore timeout in seconds 10 | 11 | -- Executes a xpcall but returns hard-errors as Lua 'nil+err' result. 12 | -- Handles max of 10 return values. 13 | -- @param f function to execute 14 | -- @param ... parameters to pass to the function 15 | local function safe_call(f, ...) 16 | local ok, result, err, r3, r4, r5, r6, r7, r8, r9, r10 = xpcall(f, debug.traceback, ...) 17 | if ok then 18 | return result, err, r3, r4, r5, r6, r7, r8, r9, r10 19 | end 20 | return nil, result 21 | end 22 | 23 | 24 | local Credentials = {} 25 | Credentials.__index = Credentials 26 | 27 | --- Constructor. 28 | -- @function aws:Credentials 29 | -- @param opt options table 30 | -- @param opt.expiryWindow number (default 15) of seconds before expiry to start refreshing 31 | -- @param opt.accessKeyId (optional) only specify if you manually specify credentials 32 | -- @param opt.secretAccessKey (optional) only specify if you manually specify credentials 33 | -- @param opt.sessionToken (optional) only specify if you manually specify credentials 34 | -- @param opt.expireTime (optional, number (epoch) or string (rfc3339)). This should 35 | -- not be specified. Default: If any of the 3 secrets are given; 10yrs, otherwise 0 36 | -- (forcing a refresh on the first call to `get`). 37 | -- @usage 38 | -- local my_creds = aws:Credentials { 39 | -- accessKeyId = "access", 40 | -- secretAccessKey = "secret", 41 | -- sessionToken = "token", 42 | -- } 43 | -- 44 | -- local success, id, secret, token = my_creds:get() 45 | function Credentials:new(opts) 46 | local self = {} -- override 'self' to be the new object/class 47 | setmetatable(self, Credentials) 48 | 49 | opts = opts or {} 50 | if opts.aws then 51 | if getmetatable(opts.aws) ~= require("resty.aws") then 52 | error("'opts.aws' must be set to an AWS instance or nil") 53 | end 54 | self.aws = opts.aws 55 | end 56 | 57 | if opts.accessKeyId or opts.secretAccessKey or opts.sessionToken then 58 | -- credentials provided, if no expire given then use 10 yrs 59 | self:set(opts.accessKeyId, opts.secretAccessKey, opts.sessionToken, 60 | opts.expireTime or (ngx.now() + 10*365*24*60*60)) 61 | else 62 | self.accessKeyId = nil 63 | self.secretAccessKey = nil 64 | self.sessionToken = nil 65 | self.expireTime = 0 -- force refresh on next "get" 66 | end 67 | -- self.expired -- not implemented 68 | self.expiryWindow = opts.expiryWindow or 15 -- time in seconds befoer expireTime creds should be refreshed 69 | 70 | return self 71 | end 72 | 73 | --- checks whether credentials have expired. 74 | -- @return boolean 75 | function Credentials:needsRefresh() 76 | return (self.expireTime or 0) < (ngx.now() + self.expiryWindow) 77 | end 78 | 79 | --- Gets credentials, refreshes if required. 80 | -- Returns credentials, doesn't take a callback like AWS SDK. 81 | -- 82 | -- When a refresh is executed, it will be done within a semaphore to prevent 83 | -- many simultaneous refreshes. 84 | -- @return success(true) + accessKeyId + secretAccessKey + sessionToken + expireTime or success(false) + error 85 | function Credentials:get() 86 | while self:needsRefresh() do 87 | if self.semaphore then 88 | -- an update is in progress 89 | local ok, err = self.semaphore:wait(SEMAPHORE_TIMEOUT) 90 | if not ok then 91 | ngx.log(ngx.ERR, "[Credentials ", self.type, "] waiting for semaphore failed: ", err) 92 | return nil, "waiting for semaphore failed: " .. tostring(err) 93 | end 94 | else 95 | -- no update in progress 96 | local sema, err = semaphore.new() 97 | self.semaphore = sema 98 | if not sema then 99 | return nil, "create semaphore failed: " .. tostring(err) 100 | end 101 | 102 | local ok, err = safe_call(self.refresh, self) 103 | 104 | -- release all waiting threads 105 | self.semaphore = nil 106 | sema:post(math.abs(sema:count())+1) 107 | 108 | if not ok then return 109 | nil, err 110 | end 111 | break 112 | end 113 | end 114 | -- we always return a boolean successvalue, if we would rely on standard Lua 115 | -- "nil + err" behaviour, then if the accessKeyId happens to be 'nil' for some 116 | -- reason, we risk logging the secretAccessKey as the error message in some 117 | -- client code. 118 | return true, self.accessKeyId, self.secretAccessKey, self.sessionToken, self.expireTime 119 | end 120 | 121 | --- Sets credentials. 122 | -- additional to AWS SDK 123 | -- @param accessKeyId 124 | -- @param secretAccessKey 125 | -- @param sessionToken 126 | -- @param expireTime (optional) number (unix epoch based), or string (valid rfc 3339) 127 | -- @return true 128 | function Credentials:set(accessKeyId, secretAccessKey, sessionToken, expireTime) 129 | -- TODO: should we be parsing the token (if given) to get the expireTime? 130 | local expiration 131 | if type(expireTime) == "string" then 132 | expiration = parse_date(expireTime):timestamp() 133 | end 134 | if type(expireTime) == "number" then 135 | expiration = expireTime 136 | end 137 | if not expiration then 138 | error("expected expireTime to be a number (unix epoch based), or string (valid rfc 3339)", 2) 139 | end 140 | 141 | self.expireTime = expiration 142 | self.accessKeyId = accessKeyId 143 | self.secretAccessKey = secretAccessKey 144 | self.sessionToken = sessionToken 145 | return true 146 | end 147 | 148 | --- updates credentials. 149 | -- override in subclasses, should call `set` to set the properties. 150 | -- @return success, or nil+err 151 | function Credentials:refresh() 152 | error("Not implemented") 153 | end 154 | 155 | -- not implemented 156 | function Credentials:getPromise() 157 | error("Not implemented") 158 | end 159 | function Credentials:refreshPromise() 160 | error("Not implemented") 161 | end 162 | 163 | return Credentials 164 | -------------------------------------------------------------------------------- /src/resty/aws/credentials/EC2MetadataCredentials.lua: -------------------------------------------------------------------------------- 1 | --- EC2MetadataCredentials class. 2 | -- @classmod EC2MetadataCredentials 3 | 4 | local http = require "resty.luasocket.http" 5 | local json = require("cjson.safe").new() 6 | local log = ngx.log 7 | local DEBUG = ngx.DEBUG 8 | 9 | 10 | 11 | local METADATA_SERVICE_SCHEME = "http" 12 | local METADATA_SERVICE_PORT = 80 13 | local METADATA_SERVICE_REQUEST_TIMEOUT = 5000 -- milliseconds 14 | local METADATA_SERVICE_HOST = "169.254.169.254" 15 | local METADATA_SERVICE_TOKEN_TTL = 300 -- seconds 16 | 17 | 18 | 19 | -- Create class 20 | local Super = require "resty.aws.credentials.Credentials" 21 | local EC2MetadataCredentials = setmetatable({}, Super) 22 | EC2MetadataCredentials.__index = EC2MetadataCredentials 23 | 24 | 25 | --- Constructor, inherits from `Credentials`. 26 | -- @function aws:EC2MetadataCredentials 27 | -- @param opt options table, no additional fields to the `Credentials` class. 28 | function EC2MetadataCredentials:new(opts) 29 | local self = Super:new(opts) -- override 'self' to be the new object/class 30 | setmetatable(self, EC2MetadataCredentials) 31 | 32 | return self 33 | end 34 | 35 | -- updates credentials. 36 | -- @return success, or nil+err 37 | function EC2MetadataCredentials:refresh() 38 | local client = http.new() 39 | client:set_timeout(METADATA_SERVICE_REQUEST_TIMEOUT) 40 | 41 | local ok, err = client:connect { 42 | scheme = METADATA_SERVICE_SCHEME, 43 | host = METADATA_SERVICE_HOST, 44 | port = METADATA_SERVICE_PORT, 45 | } 46 | if not ok then 47 | return nil, "Could not connect to EC2 metadata service: " .. tostring(err) 48 | end 49 | 50 | local imds_token 51 | 52 | local token_request_res, err = client:request { 53 | method = "PUT", 54 | path = "/latest/api/token", 55 | headers = { 56 | ["X-aws-ec2-metadata-token-ttl-seconds"] = METADATA_SERVICE_TOKEN_TTL, 57 | }, 58 | } 59 | 60 | if not token_request_res then 61 | log(DEBUG, "Could not fetch token from EC2 metadata service: " .. tostring(err)) 62 | elseif token_request_res.status ~= 200 then 63 | log(DEBUG, "Failed to fetch token from EC2 metadata service: " .. tostring(err)) 64 | else 65 | imds_token = tostring(token_request_res:read_body()) 66 | end 67 | 68 | -- recycle the client, because the luasocket/ngx.socket compatibility is not 69 | -- solid enough to reuse the httrp client 70 | client:close() 71 | client = http.new() 72 | client:set_timeout(METADATA_SERVICE_REQUEST_TIMEOUT) 73 | 74 | local ok, err = client:connect { 75 | scheme = METADATA_SERVICE_SCHEME, 76 | host = METADATA_SERVICE_HOST, 77 | port = METADATA_SERVICE_PORT, 78 | } 79 | if not ok then 80 | return nil, "Could not connect to EC2 metadata service: " .. tostring(err) 81 | end 82 | 83 | local role_name_request_res, err = client:request { 84 | method = "GET", 85 | path = "/latest/meta-data/iam/security-credentials/", 86 | headers = { 87 | ["X-aws-ec2-metadata-token"] = imds_token, 88 | } 89 | } 90 | 91 | if not role_name_request_res then 92 | return nil, "Could not fetch role name from EC2 metadata service: " .. tostring(err) 93 | end 94 | 95 | if role_name_request_res.status ~= 200 then 96 | return nil, "Fetching role name from EC2 metadata service returned status code " .. 97 | role_name_request_res.status .. " with body: " .. role_name_request_res:read_body() 98 | end 99 | 100 | local iam_role_name = role_name_request_res:read_body() 101 | log(DEBUG, "Found EC2 IAM role on instance with name: ", iam_role_name) 102 | 103 | 104 | -- recycle the client, because the luasocket/ngx.socket compatibility is not 105 | -- solid enough to reuse the httrp client 106 | client:close() 107 | client = http.new() 108 | client:set_timeout(METADATA_SERVICE_REQUEST_TIMEOUT) 109 | 110 | 111 | local ok, err = client:connect { 112 | scheme = METADATA_SERVICE_SCHEME, 113 | host = METADATA_SERVICE_HOST, 114 | port = METADATA_SERVICE_PORT, 115 | } 116 | if not ok then 117 | return nil, "Could not connect to EC2 metadata service: " .. tostring(err) 118 | end 119 | 120 | local iam_security_token_request, err = client:request { 121 | method = "GET", 122 | path = "/latest/meta-data/iam/security-credentials/" .. iam_role_name, 123 | headers = { 124 | ["X-aws-ec2-metadata-token"] = imds_token, 125 | } 126 | } 127 | 128 | if not iam_security_token_request then 129 | return nil, "Failed to request EC2 IAM credentials for role " .. iam_role_name .. 130 | " Request returned error: " .. tostring(err) 131 | end 132 | 133 | if iam_security_token_request.status == 404 then 134 | return nil, "Unable to request EC2 IAM credentials for role " .. iam_role_name .. 135 | " Request returned status code 404." 136 | end 137 | 138 | if iam_security_token_request.status ~= 200 then 139 | return nil, "Unable to request EC2 IAM credentials for role" .. iam_role_name .. 140 | " Request returned status code " .. iam_security_token_request.status .. 141 | " " .. tostring(iam_security_token_request:read_body()) 142 | end 143 | 144 | local iam_security_token_request_body = iam_security_token_request:read_body() 145 | local iam_security_token_data = json.decode(iam_security_token_request_body) 146 | 147 | if not iam_security_token_data.AccessKeyId then 148 | return nil, "Unable to request EC2 IAM credentials for role " .. iam_role_name .. 149 | " Request returned unexpected result " .. iam_security_token_request_body 150 | end 151 | 152 | log(DEBUG, "Received temporary IAM credential from EC2 metadata service for role '", 153 | iam_role_name, "' with session token: ", 154 | string.sub(iam_security_token_data.Token, 1 , 10), 155 | "...") 156 | 157 | self:set(iam_security_token_data.AccessKeyId, 158 | iam_security_token_data.SecretAccessKey, 159 | iam_security_token_data.Token, 160 | iam_security_token_data.Expiration) 161 | 162 | return true 163 | end 164 | 165 | return EC2MetadataCredentials 166 | -------------------------------------------------------------------------------- /src/resty/aws/credentials/EnvironmentCredentials.lua: -------------------------------------------------------------------------------- 1 | --- EnvironmentCredentials class. 2 | -- @classmod EnvironmentCredentials 3 | 4 | 5 | local aws_config = require("resty.aws.config") 6 | 7 | 8 | -- Create class 9 | local Super = require "resty.aws.credentials.Credentials" 10 | local EnvironmentCredentials = setmetatable({}, Super) 11 | EnvironmentCredentials.__index = EnvironmentCredentials 12 | 13 | 14 | --- Constructor, inherits from `Credentials`. 15 | -- 16 | -- _Note_: this class will fetch the credentials upon instantiation. So it can be 17 | -- instantiated in the `init` phase where there is still access to the environment 18 | -- variables. The standard prefixes `AWS` and `AMAZON` are covered by the `config` 19 | -- module, so in case those are used, only the `config` module needs to be loaded 20 | -- in the `init` phase. 21 | -- @function aws:EnvironmentCredentials 22 | -- @param opt options table, additional fields to the `Credentials` class: 23 | -- @param opt.envPrefix prefix to use when looking for environment variables, defaults to "AWS". 24 | function EnvironmentCredentials:new(opts) 25 | local self = Super:new(opts) -- override 'self' to be the new object/class 26 | setmetatable(self, EnvironmentCredentials) 27 | 28 | opts = opts or {} 29 | self.envPrefix = opts.envPrefix or "AWS" 30 | 31 | self:get() -- force immediate refresh 32 | 33 | return self 34 | end 35 | 36 | -- updates credentials. 37 | -- @return success, or nil+err 38 | function EnvironmentCredentials:refresh() 39 | local global_config = aws_config.global 40 | 41 | local access = os.getenv(self.envPrefix .. "_ACCESS_KEY_ID") or global_config[self.envPrefix .. "_ACCESS_KEY_ID"] 42 | if not access then 43 | -- Note: nginx workers do not have access to env vars. initialize in init phase 44 | -- or enable access for any prefix other than "AWS" and "AMAZON" which are covered 45 | -- by the 'config' module. 46 | return nil, "Couldn't find " .. self.envPrefix .. "_ACCESS_KEY_ID env variable" 47 | end 48 | local secret = os.getenv(self.envPrefix .. "_SECRET_ACCESS_KEY") or global_config[self.envPrefix .. "_SECRET_ACCESS_KEY"] 49 | local token = os.getenv(self.envPrefix .. "_SESSION_TOKEN") or global_config[self.envPrefix .. "_SESSION_TOKEN"] 50 | local expire = ngx.now() + 10 * 365 * 24 * 60 * 60 -- static, so assume 10 year validity 51 | self:set(access, secret, token, expire) 52 | return true 53 | end 54 | 55 | return EnvironmentCredentials 56 | -------------------------------------------------------------------------------- /src/resty/aws/credentials/README.md: -------------------------------------------------------------------------------- 1 | # Credential classes 2 | 3 | see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Credentials.html 4 | 5 | ## Classes implemented 6 | 7 | Credentials 8 | CredentialProviderChain 9 | EC2MetadataCredentials 10 | EnvironmentCredentials 11 | RemoteCredentials 12 | TokenFileWebIdentityCredentials 13 | ChainableTemporaryCredentials --> to be tested 14 | 15 | ## Classes not yet implemented 16 | 17 | CognitoIdentityCredentials 18 | FileSystemCredentials 19 | ProcessCredentials 20 | SAMLCredentials 21 | SharedIniFileCredentials 22 | TemporaryCredentials (superseeded by ChainableTemporaryCredentials) 23 | WebIdentityCredentials 24 | -------------------------------------------------------------------------------- /src/resty/aws/credentials/RemoteCredentials.lua: -------------------------------------------------------------------------------- 1 | --- RemoteCredentials class. 2 | -- @classmod RemoteCredentials 3 | 4 | 5 | -- This code is reverse engineered from the original AWS sdk. Specifically: 6 | -- https://github.com/aws/aws-sdk-js/blob/c175cb2b89576f01c08ebf39b232584e4fa2c0e0/lib/credentials/remote_credentials.js 7 | 8 | local log = ngx.log 9 | local DEBUG = ngx.DEBUG 10 | 11 | local DEFAULT_SERVICE_REQUEST_TIMEOUT = 5000 12 | 13 | local url = require "socket.url" 14 | local http = require "resty.luasocket.http" 15 | local json = require "cjson" 16 | local readfile = require("pl.utils").readfile 17 | 18 | 19 | local FullUri 20 | local AuthToken 21 | local AuthTokenFile 22 | 23 | 24 | local function initialize() 25 | -- construct the URL 26 | local function makeset(t) 27 | for i = 1, #t do 28 | t[t[i]] = true 29 | end 30 | return t 31 | end 32 | 33 | local aws_config = require("resty.aws.config") 34 | 35 | local FULL_URI_UNRESTRICTED_PROTOCOLS = makeset { "https" } 36 | local FULL_URI_ALLOWED_PROTOCOLS = makeset { "http", "https" } 37 | local FULL_URI_ALLOWED_HOSTNAMES = makeset { "localhost", "127.0.0.1", "169.254.170.23" } 38 | local RELATIVE_URI_HOST = '169.254.170.2' 39 | 40 | local function getFullUri() 41 | if aws_config.global.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI then 42 | return 'http://' .. RELATIVE_URI_HOST .. aws_config.global.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI 43 | 44 | elseif aws_config.global.AWS_CONTAINER_CREDENTIALS_FULL_URI then 45 | local parsed_url = url.parse(aws_config.global.AWS_CONTAINER_CREDENTIALS_FULL_URI) 46 | 47 | if not FULL_URI_ALLOWED_PROTOCOLS[parsed_url.scheme] then 48 | return nil, 'Unsupported protocol, must be one of ' 49 | .. table.concat(FULL_URI_ALLOWED_PROTOCOLS, ',') .. '. Got: ' 50 | .. parsed_url.scheme 51 | end 52 | 53 | if (not FULL_URI_UNRESTRICTED_PROTOCOLS[parsed_url.scheme]) and 54 | (not FULL_URI_ALLOWED_HOSTNAMES[parsed_url.host]) then 55 | return nil, 'Unsupported hostname: AWS.RemoteCredentials only supports ' 56 | .. table.concat(FULL_URI_ALLOWED_HOSTNAMES, ',') .. ' for ' 57 | .. parsed_url.scheme .. '; ' .. parsed_url.scheme .. '://' 58 | .. parsed_url.host .. ' requested.' 59 | end 60 | 61 | return aws_config.global.AWS_CONTAINER_CREDENTIALS_FULL_URI 62 | 63 | else 64 | return nil, 'Environment variable AWS_CONTAINER_CREDENTIALS_RELATIVE_URI or ' 65 | .. 'AWS_CONTAINER_CREDENTIALS_FULL_URI must be set to use AWS.RemoteCredentials.' 66 | end 67 | end 68 | 69 | 70 | local err 71 | FullUri, err = getFullUri() 72 | if not FullUri then 73 | log(DEBUG, "Failed to construct RemoteCredentials url: ", err) 74 | 75 | else 76 | -- parse it and set a default port if omitted 77 | FullUri = url.parse(FullUri) 78 | FullUri.port = FullUri.port or 79 | ({ http = 80, https = 443 })[FullUri.scheme] 80 | end 81 | 82 | -- get auth token/file 83 | AuthToken = aws_config.global.AWS_CONTAINER_AUTHORIZATION_TOKEN 84 | AuthTokenFile = aws_config.global.AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE 85 | 86 | initialize = nil 87 | end 88 | 89 | 90 | -- Create class 91 | local Super = require "resty.aws.credentials.Credentials" 92 | local RemoteCredentials = setmetatable({}, Super) 93 | RemoteCredentials.__index = RemoteCredentials 94 | 95 | 96 | --- Constructor, inherits from `Credentials`. 97 | -- @function aws:RemoteCredentials 98 | -- @param opt options table, no additional fields to the `Credentials` class. 99 | function RemoteCredentials:new(opts) 100 | local self = Super:new(opts) -- override 'self' to be the new object/class 101 | setmetatable(self, RemoteCredentials) 102 | 103 | return self 104 | end 105 | 106 | -- updates credentials. 107 | -- @return success, or nil+err 108 | function RemoteCredentials:refresh() 109 | if initialize then 110 | initialize() 111 | end 112 | 113 | if not FullUri then 114 | return nil, "No URI environment variables found for RemoteCredentials" 115 | end 116 | 117 | 118 | local headers = {} 119 | 120 | if AuthToken then 121 | headers["Authorization"] = AuthToken 122 | end 123 | 124 | if AuthTokenFile then 125 | local token, err = readfile(AuthTokenFile) 126 | if not token then 127 | return nil, "Failed reading AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE: " .. err 128 | end 129 | 130 | headers["Authorization"] = token 131 | end 132 | 133 | local client = http.new() 134 | client:set_timeout(DEFAULT_SERVICE_REQUEST_TIMEOUT) 135 | 136 | local ok, err = client:connect { 137 | scheme = FullUri.scheme, 138 | host = FullUri.host, 139 | port = FullUri.port, 140 | } 141 | if not ok then 142 | return nil, "Could not connect to RemoteCredentials metadata service: " .. tostring(err) 143 | end 144 | 145 | local response, err = client:request { 146 | method = "GET", 147 | path = FullUri.path, 148 | headers = headers, 149 | } 150 | 151 | if not response then 152 | return nil, "Failed to request RemoteCredentials request returned error: " .. tostring(err) 153 | end 154 | 155 | if response.status ~= 200 then 156 | return nil, "Unable to request RemoteCredentials request returned status code " .. 157 | response.status .. " " .. tostring(response:read_body()) 158 | end 159 | 160 | local credentials = json.decode(response:read_body()) 161 | 162 | log(DEBUG, "Received temporary IAM credential from RemoteCredentials " .. 163 | "service with session token: ", credentials.Token) 164 | 165 | self:set(credentials.AccessKeyId, 166 | credentials.SecretAccessKey, 167 | credentials.Token, 168 | credentials.Expiration) 169 | return true 170 | end 171 | 172 | return RemoteCredentials 173 | -------------------------------------------------------------------------------- /src/resty/aws/credentials/SharedFileCredentials.lua: -------------------------------------------------------------------------------- 1 | --- SharedFileCredentials class. 2 | -- @classmod SharedFileCredentials 3 | 4 | 5 | -- Create class 6 | local Super = require "resty.aws.credentials.Credentials" 7 | local config = require "resty.aws.config" 8 | local SharedFileCredentials = setmetatable({}, Super) 9 | SharedFileCredentials.__index = SharedFileCredentials 10 | 11 | 12 | --- Constructor, inherits from `Credentials`. 13 | -- 14 | -- @function aws:SharedFileCredentials 15 | -- @param opt options table, additional fields to the `Credentials` class: 16 | function SharedFileCredentials:new(opts) 17 | local self = Super:new(opts) -- override 'self' to be the new object/class 18 | setmetatable(self, SharedFileCredentials) 19 | 20 | self:get() -- force immediate refresh 21 | 22 | return self 23 | end 24 | 25 | -- updates credentials. 26 | -- @return success, or nil+err 27 | function SharedFileCredentials:refresh() 28 | local cred = config.load_credentials() 29 | 30 | if not (cred.aws_access_key_id or cred.aws_session_token) then 31 | return false, "no credentials found" 32 | end 33 | 34 | local expire = ngx.now() + 10 * 365 * 24 * 60 * 60 -- static, so assume 10 year validity 35 | self:set(cred.aws_access_key_id, cred.aws_secret_access_key, cred.aws_session_token, expire) 36 | return true 37 | end 38 | 39 | return SharedFileCredentials 40 | -------------------------------------------------------------------------------- /src/resty/aws/credentials/TokenFileWebIdentityCredentials.lua: -------------------------------------------------------------------------------- 1 | --- TokenFileWebIdentityCredentials class. 2 | -- @classmod TokenFileWebIdentityCredentials 3 | 4 | local readfile = require("pl.utils").readfile 5 | local lom = require("lxp.lom") 6 | 7 | local aws_config = require("resty.aws.config") 8 | 9 | 10 | -- Create class 11 | local Super = require "resty.aws.credentials.Credentials" 12 | local TokenFileWebIdentityCredentials = setmetatable({}, Super) 13 | TokenFileWebIdentityCredentials.__index = TokenFileWebIdentityCredentials 14 | 15 | 16 | --- Constructor, inherits from `Credentials`. 17 | -- @function aws:TokenFileWebIdentityCredentials 18 | -- @tparam table opts options table, only listing additional fields to the `Credentials` class. 19 | -- @tparam[opt=AWS_WEB_IDENTITY_TOKEN_FILE env var] string opts.token_file filename of the token file 20 | -- @tparam[opt=AWS_ROLE_ARN env var] string opts.role_arn arn of the role to assume 21 | -- @tparam[opt=AWS_ROLE_SESSION_NAME env var or 'session@lua-resty-aws'] string opts.session_name session name 22 | function TokenFileWebIdentityCredentials:new(opts) 23 | local self = Super:new(opts) -- override 'self' to be the new object/class 24 | setmetatable(self, TokenFileWebIdentityCredentials) 25 | 26 | opts = opts or {} 27 | self.token_file = assert( 28 | opts.token_file or aws_config.global.AWS_WEB_IDENTITY_TOKEN_FILE, 29 | "either 'opts.token_file' or environment variable 'AWS_WEB_IDENTITY_TOKEN_FILE' must be set" 30 | ) 31 | self.role_arn = assert( 32 | opts.role_arn or aws_config.global.AWS_ROLE_ARN, 33 | "either 'opts.role_arn' or environment variable 'AWS_ROLE_ARN' must be set" 34 | ) 35 | self.session_name = opts.session_name or aws_config.global.AWS_ROLE_SESSION_NAME or "session@lua-resty-aws" 36 | 37 | return self 38 | end 39 | 40 | -- updates credentials. 41 | -- @return success, or nil+err 42 | function TokenFileWebIdentityCredentials:refresh() 43 | if not self.sts then 44 | -- instantiate on first use. Cannot do this in the constructor, since the 45 | -- constructor is called when instantiating an AWS instance (creating a loop). 46 | -- That's because this credentials class is part of the "CredentialProviderChain" 47 | local AWS = require "resty.aws" 48 | local aws = AWS { 49 | region = aws_config.global.region, 50 | stsRegionalEndpoints = aws_config.global.sts_regional_endpoints, 51 | } 52 | local sts, err = aws:STS() 53 | if not sts then 54 | error("failed to construct AWS.STS instance: " .. tostring(err)) 55 | end 56 | self.sts = sts 57 | end 58 | 59 | local token, err = readfile(self.token_file) 60 | if not token then 61 | return nil, "failed reading token file: " .. err 62 | end 63 | 64 | local response, err = self.sts:assumeRoleWithWebIdentity { 65 | RoleArn = self.role_arn, 66 | RoleSessionName = self.session_name, 67 | WebIdentityToken = token 68 | } 69 | 70 | if not response then 71 | return nil, "Request for token data failed: " .. tostring(err) 72 | end 73 | 74 | if response.status ~= 200 then 75 | return nil, ("request for token returned '%s': %s"):format(tostring(response.status), response.body) 76 | end 77 | 78 | if type(response.body) ~= "string" then 79 | return nil, "request for token returned invalid body: " .. err 80 | end 81 | 82 | local resp_body_lom, err = lom.parse(response.body) 83 | if not resp_body_lom then 84 | return nil, "failed to parse response body: " .. err 85 | end 86 | 87 | local cred_lom = lom.find_elem(lom.find_elem(resp_body_lom, "AssumeRoleWithWebIdentityResult"), "Credentials") 88 | 89 | local AccessKeyId = lom.find_elem(cred_lom, "AccessKeyId")[1] 90 | local SecretAccessKey = lom.find_elem(cred_lom, "SecretAccessKey")[1] 91 | local SessionToken = lom.find_elem(cred_lom, "SessionToken")[1] 92 | local Expiration = lom.find_elem(cred_lom, "Expiration")[1] 93 | 94 | self:set(AccessKeyId, SecretAccessKey, SessionToken, Expiration) 95 | 96 | return true 97 | end 98 | 99 | return TokenFileWebIdentityCredentials 100 | -------------------------------------------------------------------------------- /src/resty/aws/request/execute.lua: -------------------------------------------------------------------------------- 1 | local http = require "resty.luasocket.http" 2 | 3 | local json_safe = require("cjson.safe").new() 4 | json_safe.decode_array_with_array_mt(true) 5 | local json_decode = json_safe.decode 6 | 7 | -- TODO: retries and back-off: https://docs.aws.amazon.com/general/latest/gr/api-retries.html 8 | 9 | -- implement AWS api protocols. 10 | -- returns a response table; 11 | -- * status: status code 12 | -- * reason: status description 13 | -- * headers: table with response headers 14 | -- * body: string with the raw body 15 | -- 16 | -- Input parameters: 17 | -- * signed_request table 18 | local function execute_request(signed_request) 19 | 20 | local httpc = http.new() 21 | httpc:set_timeout(signed_request.timeout or 60000) 22 | 23 | local ok, err = httpc:connect { 24 | host = signed_request.host, 25 | port = signed_request.port, 26 | scheme = signed_request.tls and "https" or "http", 27 | ssl_server_name = signed_request.host, 28 | ssl_verify = signed_request.ssl_verify, 29 | proxy_opts = signed_request.proxy_opts, 30 | } 31 | if not ok then 32 | return nil, ("failed to connect to '%s://%s:%s': %s"):format( 33 | signed_request.tls and "https" or "http", 34 | tostring(signed_request.host), 35 | tostring(signed_request.port), 36 | tostring(err)) 37 | end 38 | 39 | local response, err = httpc:request({ 40 | path = signed_request.path, 41 | method = signed_request.method, 42 | headers = signed_request.headers, 43 | query = signed_request.query, 44 | body = signed_request.body, 45 | }) 46 | if not response then 47 | return nil, ("failed sending request to '%s:%s': %s"):format( 48 | tostring(signed_request.host), 49 | tostring(signed_request.port), 50 | tostring(err)) 51 | end 52 | 53 | 54 | local body do 55 | if response.has_body then 56 | body, err = response:read_body() 57 | if not body then 58 | return nil, ("failed reading response body from '%s:%s': %s"):format( 59 | tostring(signed_request.host), 60 | tostring(signed_request.port), 61 | tostring(err)) 62 | end 63 | end 64 | end 65 | 66 | if signed_request.keepalive_idle_timeout then 67 | httpc:set_keepalive(signed_request.keepalive_idle_timeout) 68 | else 69 | httpc:close() 70 | end 71 | 72 | local ct = response.headers["Content-Type"] 73 | if (ct and ct:lower():match("application/.*json")) then 74 | -- json body, let's decode 75 | local ok = json_decode(body) 76 | if ok then 77 | body = ok 78 | end 79 | end 80 | 81 | return { 82 | status = response.status, 83 | reason = response.reason, 84 | headers = response.headers, 85 | body = body 86 | } 87 | end 88 | 89 | 90 | return execute_request 91 | -------------------------------------------------------------------------------- /src/resty/aws/request/sign.lua: -------------------------------------------------------------------------------- 1 | 2 | -- table with signature functions, loaded on demand. Additional signatures can be 3 | -- implemented as modules. Typically the key would be "v4", "v3", etc. 4 | local signatures = setmetatable({}, { 5 | __index = function(self, key) 6 | -- if we do not have a specific signature version, then load it 7 | assert(type(key) == "string", "the signature type must be a string") 8 | if key == "s3" then 9 | key = "v4" 10 | end 11 | 12 | local ok, mod = pcall(require, "resty.aws.request.signatures." .. key) 13 | if not ok then 14 | return error("AWS signature version '"..key.."' does not exist or hasn't been implemented") 15 | end 16 | rawset(self, key, mod) 17 | return mod 18 | end 19 | }) 20 | 21 | 22 | return function(config, request) 23 | -- the 'nil' string is to ensure the __index method of 'signatures' throws the 24 | -- proper error message. 25 | return signatures[config.signatureVersion or "nil"](config, request) 26 | end 27 | -------------------------------------------------------------------------------- /src/resty/aws/request/signatures/none.lua: -------------------------------------------------------------------------------- 1 | -- signature module for "not signing" 2 | 3 | 4 | -- config to contain: 5 | -- config.endpoint: hostname to connect to 6 | -- 7 | -- tbl to contain: 8 | -- tbl.domain: optional, defaults to "amazon.com" 9 | -- tbl.region: amazon region identifier, eg. "us-east-1" 10 | -- tbl.service: amazon service targetted, eg. "lambda" 11 | -- tbl.method: GET/POST/etc 12 | -- tbl.path: path to invoke, defaults to 'canonicalURI' if given, or otherwise "/" 13 | -- tbl.query: string with the query parameters, defaults to 'canonical_querystring' 14 | -- tbl.canonical_querystring: if given will be used and override 'query' 15 | -- tbl.headers: table of headers for the request 16 | -- note: for headers "Host" and "Authorization"; they will be used if 17 | -- provided, and not be overridden by the generated ones 18 | -- tbl.body: string, defaults to "" 19 | -- tbl.tls: defaults to true (if nil) 20 | -- tbl.port: defaults to 443 or 80 depending on 'tls' 21 | -- tbl.global_endpoint: if true, then use "us-east-1" as signing region and different 22 | -- hostname template: see https://github.com/aws/aws-sdk-js/blob/ae07e498e77000e55da70b20996dc8fd2f8b3051/lib/region_config_data.json 23 | local function prepare_request(config, request_data) 24 | local host = request_data.host 25 | local port = request_data.port 26 | local timestamp = ngx.time() 27 | local req_date = os.date("!%Y%m%dT%H%M%SZ", timestamp) 28 | 29 | local timeout = config.timeout 30 | local keepalive_idle_timeout = config.keepalive_idle_timeout 31 | local tls = config.tls 32 | local ssl_verify = config.ssl_verify 33 | local proxy_opts = { 34 | http_proxy = config.http_proxy, 35 | https_proxy = config.https_proxy, 36 | no_proxy = config.no_proxy, 37 | } 38 | 39 | local headers = { 40 | ["X-Amz-Date"] = req_date, 41 | ["Host"] = host, 42 | } 43 | for k, v in pairs(request_data.headers or {}) do 44 | headers[k] = v 45 | end 46 | 47 | return { 48 | --url = url, -- "https://lambda.us-east-1.amazon.com:443/some/path?query1=val1" 49 | host = host, -- "lambda.us-east-1.amazon.com" 50 | port = port, -- 443 51 | timeout = timeout, -- 60000 52 | keepalive_idle_timeout = keepalive_idle_timeout, -- 60000 53 | tls = tls, -- true 54 | ssl_verify = ssl_verify, -- true 55 | proxy_opts = proxy_opts, -- table 56 | path = request_data.path, -- "/some/path" 57 | method = request_data.method, -- "GET" 58 | query = request_data.query, -- "query1=val1" 59 | headers = headers, -- table 60 | body = request_data.body, -- string 61 | --target = target, -- "/some/path?query1=val1" 62 | } 63 | end 64 | 65 | return prepare_request 66 | -------------------------------------------------------------------------------- /src/resty/aws/request/signatures/utils.lua: -------------------------------------------------------------------------------- 1 | -- AWS requests signing utils 2 | 3 | local resty_sha256 = require "resty.sha256" 4 | local openssl_hmac = require "resty.openssl.hmac" 5 | 6 | 7 | local CHAR_TO_HEX = {}; 8 | for i = 0, 255 do 9 | local char = string.char(i) 10 | local hex = string.format("%02x", i) 11 | CHAR_TO_HEX[char] = hex 12 | end 13 | 14 | 15 | local function hmac(secret, data) 16 | return openssl_hmac.new(secret, "sha256"):final(data) 17 | end 18 | 19 | 20 | local function hash(str) 21 | local sha256 = resty_sha256:new() 22 | sha256:update(str) 23 | return sha256:final() 24 | end 25 | 26 | 27 | local function hex_encode(str) -- From prosody's util.hex 28 | return (str:gsub(".", CHAR_TO_HEX)) 29 | end 30 | 31 | 32 | local function percent_encode(char) 33 | return string.format("%%%02X", string.byte(char)) 34 | end 35 | 36 | 37 | local function canonicalise_path(path) 38 | local segments = {} 39 | for segment in path:gmatch("/([^/]*)") do 40 | if segment == "" or segment == "." then 41 | segments = segments -- do nothing and avoid lint 42 | elseif segment == " .. " then 43 | -- intentionally discards components at top level 44 | segments[#segments] = nil 45 | else 46 | segments[#segments+1] = ngx.unescape_uri(segment):gsub("[^%w%-%._~]", 47 | percent_encode) 48 | end 49 | end 50 | local len = #segments 51 | if len == 0 then 52 | return "/" 53 | end 54 | -- If there was a slash on the end, keep it there. 55 | if path:sub(-1, -1) == "/" then 56 | len = len + 1 57 | segments[len] = "" 58 | end 59 | segments[0] = "" 60 | segments = table.concat(segments, "/", 0, len) 61 | return segments 62 | end 63 | 64 | 65 | local function canonicalise_query_string(query) 66 | local q = {} 67 | if type(query) == "string" then 68 | for key, val in query:gmatch("([^&=]+)=?([^&]*)") do 69 | key = ngx.unescape_uri(key):gsub("[^%w%-%._~]", percent_encode) 70 | val = ngx.unescape_uri(val):gsub("[^%w%-%._~]", percent_encode) 71 | q[#q+1] = key .. "=" .. val 72 | end 73 | 74 | elseif type(query) == "table" then 75 | for key, val in pairs(query) do 76 | key = ngx.unescape_uri(key):gsub("[^%w%-%._~]", percent_encode) 77 | val = ngx.unescape_uri(val):gsub("[^%w%-%._~]", percent_encode) 78 | q[#q+1] = key .. "=" .. val 79 | end 80 | 81 | else 82 | error("bad query type, expected string or table, got: ".. type(query)) 83 | end 84 | 85 | table.sort(q) 86 | return table.concat(q, "&") 87 | end 88 | 89 | 90 | local function add_args_to_query_string(query_args, query_string, sort) 91 | local q = {} 92 | if type(query_args) == "string" then 93 | for key, val in query_args:gmatch("([^&=]+)=?([^&]*)") do 94 | key = tostring(key):gsub("[^%w%-%._~]", percent_encode) 95 | val = tostring(val):gsub("[^%w%-%._~]", percent_encode) 96 | q[#q+1] = key .. "=" .. val 97 | end 98 | 99 | elseif type(query_args) == "table" then 100 | for key, val in pairs(query_args) do 101 | key = tostring(key):gsub("[^%w%-%._~]", percent_encode) 102 | val = tostring(val):gsub("[^%w%-%._~]", percent_encode) 103 | q[#q+1] = key .. "=" .. val 104 | end 105 | 106 | else 107 | error("bad query type, expected string or table, got: ".. type(query_args)) 108 | end 109 | 110 | for key, val in query_string:gmatch("([^&=]+)=?([^&]*)") do 111 | key = ngx.unescape_uri(key):gsub("[^%w%-%._~]", percent_encode) 112 | val = ngx.unescape_uri(val):gsub("[^%w%-%._~]", percent_encode) 113 | q[#q+1] = key .. "=" .. val 114 | end 115 | 116 | if sort then 117 | table.sort(q) 118 | end 119 | 120 | return table.concat(q, "&") 121 | end 122 | 123 | 124 | local function derive_signing_key(kSecret, date, region, service) 125 | -- TODO: add an LRU cache to cache the generated keys? 126 | local kDate = hmac("AWS4" .. kSecret, date) 127 | local kRegion = hmac(kDate, region) 128 | local kService = hmac(kRegion, service) 129 | local kSigning = hmac(kService, "aws4_request") 130 | return kSigning 131 | end 132 | 133 | 134 | return { 135 | hmac = hmac, 136 | hash = hash, 137 | hex_encode = hex_encode, 138 | percent_encode = percent_encode, 139 | canonicalise_path = canonicalise_path, 140 | canonicalise_query_string = canonicalise_query_string, 141 | derive_signing_key = derive_signing_key, 142 | add_args_to_query_string = add_args_to_query_string, 143 | } 144 | -------------------------------------------------------------------------------- /src/resty/aws/request/signatures/v4.lua: -------------------------------------------------------------------------------- 1 | -- Performs AWSv4 Signing 2 | -- http://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html 3 | 4 | local pl_string = require "pl.stringx" 5 | 6 | local utils = require "resty.aws.request.signatures.utils" 7 | local hmac = utils.hmac 8 | local hash = utils.hash 9 | local hex_encode = utils.hex_encode 10 | local canonicalise_path = utils.canonicalise_path 11 | local canonicalise_query_string = utils.canonicalise_query_string 12 | local derive_signing_key = utils.derive_signing_key 13 | 14 | 15 | local ALGORITHM = "AWS4-HMAC-SHA256" 16 | 17 | 18 | -- config to contain: 19 | -- config.endpoint: hostname to connect to 20 | -- config.credentials: the Credentials class to use 21 | -- 22 | -- tbl to contain: 23 | -- tbl.domain: optional, defaults to "amazon.com" 24 | -- tbl.region: amazon region identifier, eg. "us-east-1" 25 | -- tbl.service: amazon service targetted, eg. "lambda" 26 | -- tbl.method: GET/POST/etc 27 | -- tbl.path: path to invoke, defaults to 'canonicalURI' if given, or otherwise "/" 28 | -- tbl.canonicalURI: if given will be used and override 'path' 29 | -- tbl.query: string with the query parameters, defaults to 'canonical_querystring' 30 | -- tbl.canonical_querystring: if given will be used and override 'query' 31 | -- tbl.headers: table of headers for the request 32 | -- note: for headers "Host" and "Authorization"; they will be used if 33 | -- provided, and not be overridden by the generated ones 34 | -- tbl.body: string, defaults to "" 35 | -- tbl.timeout: number socket timeout (in ms), defaults to 60000 36 | -- tbl.keepalive_idle_timeout: number keepalive idle timeout (in ms), no keepalive if nil 37 | -- tbl.tls: defaults to true (if nil) 38 | -- tbl.ssl_verify: defaults to true (if nil) 39 | -- tbl.port: defaults to 443 or 80 depending on 'tls' 40 | -- tbl.timestamp: number defaults to 'ngx.time()'' 41 | -- tbl.global_endpoint: if true, then use "us-east-1" as signing region and different 42 | -- hostname template: see https://github.com/aws/aws-sdk-js/blob/ae07e498e77000e55da70b20996dc8fd2f8b3051/lib/region_config_data.json 43 | local function prepare_awsv4_request(config, request_data) 44 | local region = config.signingRegion or config.region 45 | local service = config.endpointPrefix or config.targetPrefix -- TODO: targetPrefix as fallback, correct??? 46 | local request_method = request_data.method -- TODO: should this get a fallback/default?? 47 | 48 | local canonicalURI = request_data.canonicalURI 49 | local path = request_data.path 50 | if path and not canonicalURI then 51 | canonicalURI = canonicalise_path(path) 52 | elseif canonicalURI == nil or canonicalURI == "" then 53 | canonicalURI = "/" 54 | end 55 | 56 | local canonical_querystring = request_data.canonical_querystring 57 | local query = request_data.query 58 | if query and not canonical_querystring then 59 | canonical_querystring = canonicalise_query_string(query) 60 | end 61 | 62 | local req_headers = request_data.headers 63 | local req_payload = request_data.body 64 | 65 | -- get credentials 66 | local access_key, secret_key, session_token do 67 | if not config.credentials then 68 | return nil, "cannot sign request without 'config.credentials'" 69 | end 70 | local success 71 | success, access_key, secret_key, session_token = config.credentials:get() 72 | if not success then 73 | return nil, "failed to get credentials: " .. tostring(access_key) 74 | end 75 | end 76 | 77 | local timeout = config.timeout 78 | local keepalive_idle_timeout = config.keepalive_idle_timeout 79 | local tls = config.tls 80 | local ssl_verify = config.ssl_verify 81 | local proxy_opts = { 82 | http_proxy = config.http_proxy, 83 | https_proxy = config.https_proxy, 84 | no_proxy = config.no_proxy, 85 | } 86 | 87 | local host = request_data.host 88 | local port = request_data.port 89 | local timestamp = ngx.time() 90 | local req_date = os.date("!%Y%m%dT%H%M%SZ", timestamp) 91 | local date = os.date("!%Y%m%d", timestamp) 92 | 93 | local headers = { 94 | ["X-Amz-Date"] = req_date, 95 | ["Host"] = host, 96 | ["X-Amz-Security-Token"] = session_token, 97 | } 98 | 99 | local S3 = config.signatureVersion == "s3" 100 | 101 | local hashed_payload = hex_encode(hash(req_payload or "")) 102 | 103 | -- Special handling of S3 104 | -- https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html#:~:text=Unsigned%20payload%20option 105 | if S3 then 106 | headers["X-Amz-Content-Sha256"] = hashed_payload 107 | end 108 | 109 | local add_auth_header = true 110 | for k, v in pairs(req_headers) do 111 | k = k:gsub("%f[^%z-]%w", string.upper) -- convert to standard header title case 112 | if k == "Authorization" then 113 | add_auth_header = false 114 | elseif v == false then -- don't allow a default value for this header 115 | v = nil 116 | end 117 | headers[k] = v 118 | end 119 | 120 | -- Task 1: Create a Canonical Request For Signature Version 4 121 | -- http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html 122 | local canonical_headers, signed_headers do 123 | -- We structure this code in a way so that we only have to sort once. 124 | canonical_headers, signed_headers = {}, {} 125 | local i = 0 126 | for name, value in pairs(headers) do 127 | if value then -- ignore headers with 'false', they are used to override defaults 128 | i = i + 1 129 | local name_lower = name:lower() 130 | signed_headers[i] = name_lower 131 | if canonical_headers[name_lower] ~= nil then 132 | return nil, "header collision" 133 | end 134 | canonical_headers[name_lower] = pl_string.strip(tostring(value)) 135 | end 136 | end 137 | table.sort(signed_headers) 138 | for j=1, i do 139 | local name = signed_headers[j] 140 | local value = canonical_headers[name] 141 | canonical_headers[j] = name .. ":" .. value .. "\n" 142 | end 143 | signed_headers = table.concat(signed_headers, ";", 1, i) 144 | canonical_headers = table.concat(canonical_headers, nil, 1, i) 145 | end 146 | 147 | local canonical_request = 148 | request_method .. '\n' .. 149 | canonicalURI .. '\n' .. 150 | (canonical_querystring or "") .. '\n' .. 151 | canonical_headers .. '\n' .. 152 | signed_headers .. '\n' .. 153 | hashed_payload 154 | 155 | local hashed_canonical_request = hex_encode(hash(canonical_request)) 156 | 157 | -- Task 2: Create a String to Sign for Signature Version 4 158 | -- http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html 159 | local credential_scope = date .. "/" .. region .. "/" .. service .. "/aws4_request" 160 | local string_to_sign = 161 | ALGORITHM .. '\n' .. 162 | req_date .. '\n' .. 163 | credential_scope .. '\n' .. 164 | hashed_canonical_request 165 | 166 | -- Task 3: Calculate the AWS Signature Version 4 167 | -- http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html 168 | local signing_key = derive_signing_key(secret_key, date, region, service) 169 | local signature = hex_encode(hmac(signing_key, string_to_sign)) 170 | 171 | -- Task 4: Add the Signing Information to the Request 172 | -- http://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html 173 | local authorization = ALGORITHM 174 | .. " Credential=" .. access_key .. "/" .. credential_scope 175 | .. ", SignedHeaders=" .. signed_headers 176 | .. ", Signature=" .. signature 177 | if add_auth_header then 178 | headers.Authorization = authorization 179 | end 180 | 181 | -- local target = path or canonicalURI 182 | -- if query or canonical_querystring then 183 | -- target = target .. "?" .. (query or canonical_querystring) 184 | -- end 185 | -- local scheme = tls and "https" or "http" 186 | -- local url = scheme .. "://" .. host_header .. target 187 | 188 | return { 189 | --url = url, -- "https://lambda.us-east-1.amazon.com:443/some/path?query1=val1" 190 | host = host, -- "lambda.us-east-1.amazon.com" 191 | port = port, -- 443 192 | timeout = timeout, -- 60000 193 | keepalive_idle_timeout = keepalive_idle_timeout, -- 60000 194 | tls = tls, -- true 195 | ssl_verify = ssl_verify, -- true 196 | proxy_opts = proxy_opts, -- table 197 | path = path or canonicalURI, -- "/some/path" 198 | method = request_method, -- "GET" 199 | query = query or canonical_querystring, -- "query1=val1" 200 | headers = headers, -- table 201 | body = req_payload, -- string 202 | --target = target, -- "/some/path?query1=val1" 203 | } 204 | end 205 | 206 | return prepare_awsv4_request 207 | -------------------------------------------------------------------------------- /src/resty/aws/service/rds/signer.lua: -------------------------------------------------------------------------------- 1 | --- Signer class for RDS tokens for RDS DB access. 2 | -- 3 | -- See [IAM database authentication for MariaDB, MySQL, and PostgreSQL](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html) 4 | -- for more information on using IAM database authentication with RDS. 5 | -- 6 | -- RDS services created will get a `Signer` method to create an instance. The `Signer` will 7 | -- inherit its configuration from the `AWS` instance (not from the RDS instance!). 8 | 9 | local httpc = require("resty.luasocket.http") 10 | local presign_awsv4_request = require("resty.aws.request.signatures.presign") 11 | 12 | local RDS_IAM_AUTH_EXPIRE_TIME = 15 * 60 13 | 14 | --- Return an authorization token used as the password for a RDS DB connection. 15 | -- The example shows how to use `getAuthToken` to create an authentication 16 | -- token for connecting to a PostgreSQL database in RDS. 17 | -- @name Signer:getAuthToken 18 | -- @tparam table opts configuration to use, to override the options inherited from the underlying `AWS` instance; 19 | -- @tparam string opts.region The AWS region 20 | -- @tparam string opts.hostname the DB hostname to connect to, eg. `"DB_INSTANCE.DB_CLUSTER.us-east-1.rds.amazonaws.com"` 21 | -- @tparam number opts.port the port for the DB connection 22 | -- @tparam string opts.username username of the account in the database to sign in with 23 | -- @tparam Credentials opts.credentials aws credentials 24 | -- @return token, err - Returns the token to use as the password for the DB connection, or nil and error if an error occurs 25 | -- @usage 26 | -- local pgmoon = require "pgmoon" 27 | -- local AWS = require("resty.aws") 28 | -- local AWS_global_config = require("resty.aws.config").global 29 | -- local aws = AWS { region = AWS_global_config.region } 30 | -- local rds = aws:RDS() 31 | -- 32 | -- 33 | -- local db_hostname = "DB_INSTANCE.DB_CLUSTER.us-east-1.rds.amazonaws.com" 34 | -- local db_port = 5432 35 | -- local db_name = "DB_NAME" 36 | -- 37 | -- local signer = rds:Signer { -- create a signer instance 38 | -- hostname = db_hostname, 39 | -- username = "db_user", 40 | -- port = db_port, 41 | -- region = nil, -- will be inherited from `aws` 42 | -- credentials = nil, -- will be inherited from `aws` 43 | -- } 44 | -- 45 | -- -- use the 'signer' to generate the token, whilst overriding some options 46 | -- local auth_token, err = signer:getAuthToken { 47 | -- username = "another_user" -- this overrides the earlier provided config above 48 | -- } 49 | -- 50 | -- if err then 51 | -- ngx.log(ngx.ERR, "Failed to build auth token: ", err) 52 | -- return 53 | -- end 54 | -- 55 | -- local pg = pgmoon.new({ 56 | -- host = db_hostname, 57 | -- port = db_port, 58 | -- database = db_name, 59 | -- user = "another_user", 60 | -- password = auth_token, 61 | -- ssl = true, 62 | -- }) 63 | -- 64 | -- local flag, err = pg:connect() 65 | -- if err then 66 | -- ngx.log(ngx.ERR, "Failed to connect to database: ", err) 67 | -- return 68 | -- end 69 | -- 70 | -- -- Test query 71 | -- assert(pg:query("select * from users where status = 'active' limit 20")) 72 | local function getAuthToken(self, opts) --endpoint, region, db_user) 73 | opts = setmetatable(opts or {}, { __index = self.config }) -- lookup missing params in inherited config 74 | 75 | local region = assert(opts.region, "parameter 'region' not set") 76 | local hostname = assert(opts.hostname, "parameter 'hostname' not set") 77 | local port = assert(opts.port, "parameter 'port' not set") 78 | local username = assert(opts.username, "parameter 'username' not set") 79 | 80 | local endpoint = hostname .. ":" .. port 81 | if endpoint:sub(1,7) ~= "http://" and endpoint:sub(1,8) ~= "https://" then 82 | endpoint = "https://" .. endpoint 83 | end 84 | 85 | local query_args = "Action=connect&DBUser=" .. username 86 | 87 | local canonical_request_url = endpoint .. "/?" .. query_args 88 | local scheme, host, port, path, query = unpack(httpc:parse_uri(canonical_request_url, false)) 89 | local req_data = { 90 | method = "GET", 91 | scheme = scheme, 92 | tls = scheme == "https", 93 | host = host, 94 | port = port, 95 | path = path, 96 | query = query, 97 | headers = { 98 | ["Host"] = host .. ":" .. port, 99 | }, 100 | } 101 | 102 | local presigned_request, err = presign_awsv4_request(self.config, req_data, opts.signingName, region, RDS_IAM_AUTH_EXPIRE_TIME) 103 | if err then 104 | return nil, err 105 | end 106 | 107 | return presigned_request.host .. ":" .. presigned_request.port .. presigned_request.path .. "?" .. presigned_request.query 108 | end 109 | 110 | 111 | -- signature: intended to be a method on the RDS service object, rds_instance == self in that case 112 | return function(rds_instance, config) 113 | local token_instance = { 114 | config = {}, 115 | getAuthToken = getAuthToken, -- injected method for token generation 116 | } 117 | 118 | -- first copy the inherited config elements NOTE: inherits from AWS, not the rds_instance!!! 119 | for k,v in pairs(rds_instance.aws.config) do 120 | token_instance.config[k] = v 121 | end 122 | 123 | -- service specifics 124 | -- see https://github.com/aws/aws-sdk-js/blob/9295e45fdcda93b62f8c1819e924cdb4fb378199/lib/rds/signer.js#L11-L15 125 | token_instance.config.signatureVersion = "v4" 126 | token_instance.config.signingName = "rds-db" 127 | 128 | -- then add/overwrite with provided config 129 | for k,v in pairs(config or {}) do 130 | token_instance.config[k] = v 131 | end 132 | 133 | return token_instance 134 | end 135 | -------------------------------------------------------------------------------- /test2.lua: -------------------------------------------------------------------------------- 1 | setmetatable(_G, nil) -- disable global warnings 2 | 3 | -- make sure we can use dev code 4 | package.path = "./src/?.lua;./src/?/init.lua;"..package.path 5 | 6 | -- quick debug dump function 7 | local dump = function(...) 8 | local t = { n = select("#", ...), ...} 9 | if t.n == 1 and type(t[1]) == "table" then t = t[1] end 10 | print(require("pl.pretty").write(t)) 11 | end 12 | 13 | 14 | 15 | 16 | 17 | local AWS = require("resty.aws") 18 | local aws = AWS() 19 | local secretsmanager = aws:SecretsManager { region = "us-east-2" } 20 | 21 | dump(secretsmanager:getSecretValue { 22 | SecretId = "arn:aws:secretsmanager:us-east-2:238406704566:secret:test2-HN1F1k", 23 | VersionStage = "AWSCURRENT", 24 | }) 25 | 26 | 27 | 28 | --[[ 29 | local secret = assert(credentials.fetch_secret(creds, { 30 | SecretId = "arn:aws:secretsmanager:us-east-2:238406704566:secret:test2-HN1F1k", 31 | })) 32 | 33 | dump(secret) 34 | 35 | local secret = assert(credentials.fetch_secret(creds, { 36 | SecretId = "arn:aws:secretsmanager:us-east-2:238406704566:secret:KEY_VALUE_test-IHwf2S", 37 | })) 38 | 39 | dump(secret) 40 | 41 | local secret = assert(credentials.fetch_secret(creds, { 42 | SecretId = "arn:aws:secretsmanager:us-east-2:238406704566:secret:test3_plain_text-PDPBwp", 43 | })) 44 | 45 | dump(secret) 46 | --]] 47 | 48 | --require "resty.credentials.aws.api" 49 | -------------------------------------------------------------------------------- /update_api_files.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This can be run from the Makefile. 4 | 5 | # script to update the AWS SDK from it's source repository (the JS sdk) 6 | # It will convert the service descriptions of the specified SDK version 7 | # (see SDK_VERSION_TAG) into Lua modules and generate a rockspec. 8 | 9 | SDK_VERSION_TAG=v2.751.0 10 | 11 | # ----------- nothing to customize below ----------- 12 | TARGET=./src/resty/aws/raw-api 13 | SOURCE=./delete-me 14 | TFILE=$(mktemp) 15 | set -e 16 | pushd "$(dirname "$(realpath "$0")")" > /dev/null 17 | 18 | 19 | # clone repo at requested version 20 | if [ -d $SOURCE ]; then 21 | echo "directory $SOURCE already exists, delete before updating" 22 | exit 1 23 | fi 24 | git clone --branch=$SDK_VERSION_TAG --depth=1 https://github.com/aws/aws-sdk-js.git $SOURCE 25 | 26 | 27 | # get a list of API files 28 | file_list=() 29 | pushd $SOURCE/apis/ > /dev/null 30 | for file_name in `ls -v *.normal.json` ; do 31 | file_list+=("${file_name%.normal.json}") 32 | done 33 | popd > /dev/null 34 | 35 | # remove existing files 36 | echo "removing: $TARGET" 37 | rm -rf "$TARGET" 38 | echo "creating: $TARGET" 39 | mkdir -p "$TARGET" 40 | 41 | # Create destination file in Lua format with hardcoded json in there 42 | echo "adding: $TARGET/_README.md" 43 | cat < $TARGET/_README.md 44 | # WARNING 45 | 46 | Everything in this directory is generated, do not modify, changes will be lost. 47 | 48 | To regenerate use the update script in the top directory of this repo. 49 | EOF 50 | 51 | 52 | # create TOC 53 | FILENAME=$TARGET/table_of_contents.lua 54 | echo "adding: $FILENAME" 55 | echo "return {" >> $FILENAME 56 | for f in "${file_list[@]}"; do 57 | source_file=$SOURCE/apis/$f.normal.json 58 | service_id=$(jq -r '.metadata.serviceId' $source_file | tr -d ' ') 59 | # replace . with - since . can't be in a Lua module name 60 | f=${f//./-} 61 | echo ' "'"$service_id:$f"'",' >> $FILENAME 62 | done 63 | echo "}" >> $FILENAME 64 | 65 | 66 | # copy region config file 67 | FILENAME=$TARGET/region_config_data.lua 68 | echo "adding: $FILENAME" 69 | echo 'local decode = require("cjson").new().decode' >> "$FILENAME" 70 | echo "return assert(decode([===[" >> "$FILENAME" 71 | cat $SOURCE/lib/region_config_data.json >> "$FILENAME" 72 | echo "" >> "$FILENAME" 73 | echo "]===]))" >> "$FILENAME" 74 | 75 | # Copy the individual API files 76 | for f in "${file_list[@]}"; do 77 | source_file=$SOURCE/apis/$f.normal.json 78 | # remove example keys from documentation to prevent security reports from being triggered 79 | jq 'walk( if (type == "object") and has("documentation") and (.documentation|contains("wJalrXUtnFEMI")) then del(.documentation) else . end )' "$source_file" >| "$TFILE" 80 | mv -f "$TFILE" "$source_file"; touch "$TFILE" 81 | # replace . with - since . can't be in a Lua module name 82 | target_file=$TARGET/${f//./-}.lua 83 | echo "adding: $target_file" 84 | echo 'local decode = require("cjson").new().decode' >> "$target_file" 85 | echo 'return assert(decode([===[' >> "$target_file" 86 | cat "$source_file" >> "$target_file" 87 | echo "" >> "$target_file" 88 | echo "]===]))" >> "$target_file" 89 | done 90 | 91 | # update the rockspec 92 | echo "writing rockspec file" 93 | rockspec=lua-resty-aws-dev-1.rockspec 94 | if [ -f $rockspec ]; then 95 | rm $rockspec 96 | fi 97 | 98 | echo "-- do not edit this file, it is generated and will be overwritten" >> $rockspec 99 | while IFS= read -r line; do 100 | echo "$line" >> $rockspec 101 | if [[ "$line" =~ "--START-MARKER--" ]]; then 102 | break 103 | fi 104 | done < lua-resty-aws-dev-1.rockspec.template 105 | 106 | for f in "${file_list[@]}"; do 107 | target_file=${f//./-} 108 | echo " [\"resty.aws.raw-api.$target_file\"] = \"src/resty/aws/raw-api/$target_file.lua\"," >> $rockspec 109 | done 110 | 111 | foundmarker=false 112 | while IFS= read -r line; do 113 | if [[ "$line" =~ "--END-MARKER--" ]]; then 114 | foundmarker=true 115 | fi 116 | if [[ $foundmarker == true ]]; then 117 | echo "$line" >> $rockspec 118 | fi 119 | done < lua-resty-aws-dev-1.rockspec.template 120 | 121 | rm -rf $SOURCE 122 | popd > /dev/null 123 | 124 | echo "Update complete" 125 | -------------------------------------------------------------------------------- /upload.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Use "make upload" to invoke this script 4 | 5 | ROCK_VERSION=$1-1 6 | LR_API_KEY=$2 7 | #LR_API_KEY=INfSIgkuArccxH9zq9M7enqackTiYtgRM6c9l6Y4 8 | 9 | 10 | ROCK_FILE=lua-resty-aws-$ROCK_VERSION.src.rock 11 | ROCKSPEC_FILE=lua-resty-aws-$ROCK_VERSION.rockspec 12 | 13 | if [ "$ROCK_VERSION" == "-1" ]; then 14 | echo "First argument (version) is missing." 15 | exit 1 16 | fi 17 | if [ "$LR_API_KEY" == "" ]; then 18 | echo "Second argument (LuaRocks api-key) is missing." 19 | exit 1 20 | fi 21 | if [ ! -f "$ROCKSPEC_FILE" ]; then 22 | echo "File '$ROCKSPEC_FILE' not found" 23 | exit 1 24 | fi 25 | if [ ! -f "$ROCK_FILE" ]; then 26 | echo "File '$ROCK_FILE' not found" 27 | exit 1 28 | fi 29 | 30 | echo "Uploading $ROCKSPEC_FILE..." 31 | curl -f -k -L --silent \ 32 | --user-agent "lua-resty-aws upload script via curl" \ 33 | --form "rockspec_file=@$ROCKSPEC_FILE" \ 34 | --connect-timeout 30 \ 35 | "https://luarocks.org/api/1/$LR_API_KEY/upload" \ 36 | -o "./upload1.json" 37 | 38 | LR_ROCK_VERSION_ID=$(jq .version.id < upload1.json) 39 | jq < upload1.json 40 | rm ./upload1.json 41 | echo "Rock ID: $LR_ROCK_VERSION_ID" 42 | 43 | echo "Uploading $ROCK_FILE..." 44 | curl -f -k -L --silent \ 45 | --user-agent "lua-resty-aws upload script via curl" \ 46 | --form "rock_file=@$ROCK_FILE" \ 47 | --connect-timeout 30 \ 48 | "https://luarocks.org/api/1/$LR_API_KEY/upload_rock/$LR_ROCK_VERSION_ID" \ 49 | -o "./upload2.json" 50 | 51 | jq < upload2.json 52 | rm ./upload2.json 53 | --------------------------------------------------------------------------------