apx-aws-lambda 0.3.0__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apx_aws_lambda-0.3.2/PKG-INFO +246 -0
- apx_aws_lambda-0.3.2/README.md +226 -0
- apx_aws_lambda-0.3.2/apx_aws_lambda/cli.py +139 -0
- {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda/lambda_.py +50 -13
- {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda/overlays/cloud/aws/lambda/{{cookiecutter.project_slug}}/template.yaml +6 -0
- {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda/plugin.py +2 -2
- apx_aws_lambda-0.3.2/apx_aws_lambda/proxy.py +128 -0
- {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda/shell.py +23 -1
- apx_aws_lambda-0.3.2/apx_aws_lambda.egg-info/PKG-INFO +246 -0
- {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda.egg-info/SOURCES.txt +2 -0
- {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda.egg-info/requires.txt +2 -0
- {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/pyproject.toml +6 -2
- apx_aws_lambda-0.3.2/tests/test_proxy.py +168 -0
- {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/tests/test_targets.py +13 -0
- apx_aws_lambda-0.3.0/PKG-INFO +0 -93
- apx_aws_lambda-0.3.0/README.md +0 -75
- apx_aws_lambda-0.3.0/apx_aws_lambda/cli.py +0 -65
- apx_aws_lambda-0.3.0/apx_aws_lambda.egg-info/PKG-INFO +0 -93
- {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/LICENSE +0 -0
- {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda/__init__.py +0 -0
- {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda/overlays/cloud/aws/lambda/cookiecutter.json +0 -0
- {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda/overlays/cloud/aws/lambda/hooks/post_gen_project.py +0 -0
- {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda/overlays/cloud/aws/lambda/{{cookiecutter.project_slug}}/.github/workflows/deploy.yml +0 -0
- {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda/overlays/cloud/aws/lambda/{{cookiecutter.project_slug}}/DEPLOY.md +0 -0
- {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda/overlays/cloud/aws/lambda/{{cookiecutter.project_slug}}/_lang/python/Makefile +0 -0
- {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda/overlays/cloud/aws/lambda/{{cookiecutter.project_slug}}/_lang/python/lambda_handler.py +0 -0
- {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda/overlays/cloud/aws/lambda/{{cookiecutter.project_slug}}/requirements/policy.json +0 -0
- {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda/overlays/cloud/aws/lambda/{{cookiecutter.project_slug}}/requirements/trust.json +0 -0
- {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda/overlays/cloud/aws/lambda/{{cookiecutter.project_slug}}/samconfig.toml +0 -0
- {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda/overlays/index.json +0 -0
- {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda/tools.py +0 -0
- {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda.egg-info/dependency_links.txt +0 -0
- {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda.egg-info/entry_points.txt +0 -0
- {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda.egg-info/top_level.txt +0 -0
- {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/setup.cfg +0 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: apx-aws-lambda
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: AWS Lambda for Action Platform: the aws/lambda deploy target (SAM) with its overlay, and tools to read stacks and functions.
|
|
5
|
+
Author-email: Action Platform <cloud@actionplatform.io>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: action-platform>=0.17
|
|
11
|
+
Requires-Dist: awscli>=1.34
|
|
12
|
+
Requires-Dist: aws-sam-cli>=1.120
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
15
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
16
|
+
Requires-Dist: action-platform[mcp]; extra == "dev"
|
|
17
|
+
Requires-Dist: boto3>=1.34; extra == "dev"
|
|
18
|
+
Requires-Dist: cryptography>=42; extra == "dev"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# apx-aws-lambda
|
|
22
|
+
|
|
23
|
+
AWS Lambda for [Action Platform](https://github.com/actionplatform/action-platform): the `aws/lambda` deploy target (SAM), its overlay, read-only tools and commands, and a **deploy proxy** for your AWS account so that no AWS key ever lives on the platform, on a machine or in a repository. `apx-` is the prefix every Action Platform extension carries.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
action-platform plugin install aws-lambda
|
|
27
|
+
action-platform cloud set aws/lambda # overlay files come from this plugin
|
|
28
|
+
action-platform deploy --dry-run # preflight: credentials, sam validate
|
|
29
|
+
action-platform deploy # sam build + sam deploy --config-env <stage>
|
|
30
|
+
action-platform diagnose
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Contents
|
|
34
|
+
|
|
35
|
+
- [What the plugin does](#what-the-plugin-does)
|
|
36
|
+
- [Credentials without keys](#credentials-without-keys)
|
|
37
|
+
- [The deploy proxy (recommended)](#the-deploy-proxy-recommended)
|
|
38
|
+
- [A role of your own (OIDC)](#a-role-of-your-own-oidc)
|
|
39
|
+
- [The AWS CLI's own credentials](#the-aws-clis-own-credentials)
|
|
40
|
+
- [Where the deploy runs](#where-the-deploy-runs)
|
|
41
|
+
- [The proxy in detail](#the-proxy-in-detail)
|
|
42
|
+
- [Tools and commands](#tools-and-commands)
|
|
43
|
+
- [Requirements](#requirements)
|
|
44
|
+
- [Development](#development)
|
|
45
|
+
|
|
46
|
+
## What the plugin does
|
|
47
|
+
|
|
48
|
+
| | |
|
|
49
|
+
|---|---|
|
|
50
|
+
| Overlay | `template.yaml`, `samconfig.toml`, `lambda_handler.py`, `Makefile`, `requirements/` (IAM examples), `.github/workflows/deploy.yml` (optional) |
|
|
51
|
+
| Deploy | `sam build` + `sam deploy --config-env <stage>` (`dev` → `default`, `prod` → `prod`) |
|
|
52
|
+
| Rollback | CloudFormation `rollback-stack` — previous stack state |
|
|
53
|
+
| Diagnose | stack status and the HTTP API url |
|
|
54
|
+
| Destroy | `sam delete` |
|
|
55
|
+
| Tools | `aws_lambda_stacks`, `aws_lambda_functions` |
|
|
56
|
+
| Commands | `action-platform aws-lambda stacks\|functions`, `action-platform aws-lambda proxy …` |
|
|
57
|
+
|
|
58
|
+
`[deploy]` in `platform.toml`:
|
|
59
|
+
|
|
60
|
+
```toml
|
|
61
|
+
[deploy]
|
|
62
|
+
target = "aws/lambda"
|
|
63
|
+
region = "us-east-1" # optional; samconfig.toml / AWS_REGION otherwise
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Deploying itself goes through the core's `deploy` / `rollback` / `diagnose`, which drive the target.
|
|
67
|
+
|
|
68
|
+
## Credentials without keys
|
|
69
|
+
|
|
70
|
+
The target looks for credentials in this order:
|
|
71
|
+
|
|
72
|
+
| Under `[deploy]` | How | Who decides what the app may do |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `proxy_url` + `app` | the deploy proxy in your account exchanges a platform token for the app's deploy-role credentials | the proxy's grants, in your account |
|
|
75
|
+
| `role_arn` | `sts assume-role-with-web-identity` with a platform token | the role's trust and permission policies |
|
|
76
|
+
| neither | the AWS CLI's own chain: SSO, profile, instance role | whatever that identity may do |
|
|
77
|
+
|
|
78
|
+
### The deploy proxy (recommended)
|
|
79
|
+
|
|
80
|
+
`proxy/` is a small SAM application you install **once per AWS account**. It decides who may deploy which app and hands out 15–60 minute credentials of that app's deploy role. It holds no platform secret — it verifies the platform's OIDC tokens against `https://<platform>/.well-known/jwks.json` — and the platform holds no AWS key.
|
|
81
|
+
|
|
82
|
+
**1. Install the proxy** — with your own AWS credentials, this one time (`aws sso login` or a profile; not the root account):
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
git clone https://github.com/actionplatform/apx-aws-lambda
|
|
86
|
+
cd apx-aws-lambda
|
|
87
|
+
./proxy/deploy.sh https://platform.example.com acme # issuer url, organization slug [region] [stack name]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The script runs `sam build` + `sam deploy`, prints the stack's `ProxyUrl` and calls `/health`.
|
|
91
|
+
|
|
92
|
+
**2. Register the app** — from a machine logged in to the platform (`action-platform login`) as someone with `org.manage`:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
P=https://xxxx.lambda-url.us-east-1.on.aws
|
|
96
|
+
|
|
97
|
+
action-platform aws-lambda proxy create $P acme/shop/orders --region us-east-1
|
|
98
|
+
action-platform aws-lambda proxy grant $P acme/shop/orders org:acme:project:shop:app:orders org:acme
|
|
99
|
+
action-platform aws-lambda proxy show $P acme/shop/orders
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`create` makes the app's two roles and grants deploy to the app itself; `grant` replaces the list of subject prefixes that may deploy — `org:acme` means anyone in the organization, `org:acme:project:shop` any app of the project, the full subject only deploys the platform runs for that app.
|
|
103
|
+
|
|
104
|
+
**3. Point the repository at it:**
|
|
105
|
+
|
|
106
|
+
```toml
|
|
107
|
+
[deploy]
|
|
108
|
+
target = "aws/lambda"
|
|
109
|
+
proxy_url = "https://xxxx.lambda-url.us-east-1.on.aws"
|
|
110
|
+
app = "acme/shop/orders"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`stack_name` in `samconfig.toml` must start with `ap-acme-shop-orders` — the deploy role reaches those stacks only, and the target refuses anything else before touching AWS.
|
|
114
|
+
|
|
115
|
+
**4.** `action-platform deploy`.
|
|
116
|
+
|
|
117
|
+
### A role of your own (OIDC)
|
|
118
|
+
|
|
119
|
+
Without the proxy, register the platform as an identity provider once per account and write the trust policy yourself:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
aws iam create-open-id-connect-provider --url https://platform.example.com --client-id-list sts.amazonaws.com
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"Version": "2012-10-17",
|
|
128
|
+
"Statement": [{
|
|
129
|
+
"Effect": "Allow",
|
|
130
|
+
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/platform.example.com" },
|
|
131
|
+
"Action": "sts:AssumeRoleWithWebIdentity",
|
|
132
|
+
"Condition": {
|
|
133
|
+
"StringEquals": { "platform.example.com:aud": "sts.amazonaws.com" },
|
|
134
|
+
"StringLike": { "platform.example.com:sub": "org:acme:project:shop:app:orders" }
|
|
135
|
+
}
|
|
136
|
+
}]
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`requirements/policy.json` from the overlay is a starting permission policy. Then:
|
|
141
|
+
|
|
142
|
+
```toml
|
|
143
|
+
[deploy]
|
|
144
|
+
target = "aws/lambda"
|
|
145
|
+
role_arn = "arn:aws:iam::123456789012:role/shop-deploy"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`sub` is `org:<org>:project:<project>:app:<app>` for a deploy the platform runs; `org:<org>` plus an `actor` claim for `action-platform deploy` from a logged-in machine (`StringLike` with `org:acme:*` covers both).
|
|
149
|
+
|
|
150
|
+
### The AWS CLI's own credentials
|
|
151
|
+
|
|
152
|
+
With neither `proxy_url` nor `role_arn`, `aws` and `sam` use what the machine has: `aws sso login`, a profile, an instance role. Still no key in a file when you use SSO — but nothing ties the identity to one app.
|
|
153
|
+
|
|
154
|
+
## Where the deploy runs
|
|
155
|
+
|
|
156
|
+
Independent of how credentials are obtained:
|
|
157
|
+
|
|
158
|
+
| Where | Token for the proxy / role comes from |
|
|
159
|
+
|---|---|
|
|
160
|
+
| the platform's worker (a deploy job) | signed by the platform for that app: `sub = org:…:project:…:app:…`, no scopes |
|
|
161
|
+
| a logged-in machine (`action-platform deploy`) | `POST /api/v1/identity/token` with the user's login: `sub = org:<org>`, `actor`, `scopes` |
|
|
162
|
+
| the overlay's `.github/workflows/deploy.yml` | GitHub's own OIDC — optional, unrelated to the proxy; delete the workflow when the platform deploys |
|
|
163
|
+
|
|
164
|
+
## The proxy in detail
|
|
165
|
+
|
|
166
|
+
### What the stack creates
|
|
167
|
+
|
|
168
|
+
| Resource | Purpose |
|
|
169
|
+
|---|---|
|
|
170
|
+
| Lambda + Function URL | the API (`python3.13`, standard library only — no compiled dependency, so `sam build` needs no pip) |
|
|
171
|
+
| DynamoDB table | one row per app: region, grants |
|
|
172
|
+
| `ActionPlatformBoundary` managed policy | the cap on every execution role the proxy creates |
|
|
173
|
+
| the function's role | may assume `role/action-platform/ap-deploy-*`, manage roles under `/action-platform/`, and create `ap-exec-*` roles only with the boundary attached |
|
|
174
|
+
|
|
175
|
+
Parameters: `IssuerUrl` (the platform's public url), `Organization` (one proxy serves one organization), `Version`.
|
|
176
|
+
|
|
177
|
+
### What it creates per app
|
|
178
|
+
|
|
179
|
+
| Role | Trust | Policy |
|
|
180
|
+
|---|---|---|
|
|
181
|
+
| `ap-deploy-<org>-<project>-<app>` | the proxy's function role | CloudFormation on stacks `ap-<org>-<project>-<app>*`; Lambda, API Gateway and logs with the same prefix; `iam:PassRole` on the execution role; SAM's managed bucket |
|
|
182
|
+
| `ap-exec-<org>-<project>-<app>` | `lambda.amazonaws.com` | `AWSLambdaBasicExecutionRole` + the boundary |
|
|
183
|
+
|
|
184
|
+
Both live under `/action-platform/`, carry tags `action-platform:app` and `action-platform:prefix`, and are idempotent: `create` again syncs trust policies, tags and policies without duplicating anything. `delete` removes both and the grants.
|
|
185
|
+
|
|
186
|
+
### The boundary
|
|
187
|
+
|
|
188
|
+
`ActionPlatformBoundary` is one policy for every app, scoped per app by the role's `action-platform:prefix` tag used as a policy variable in the resources:
|
|
189
|
+
|
|
190
|
+
- logs: `/aws/lambda/<prefix>*`
|
|
191
|
+
- S3 buckets, DynamoDB tables, SQS queues, SNS topics, Lambda functions: `<prefix>-*`
|
|
192
|
+
- Secrets Manager secrets and SSM parameters: `<prefix>/*`
|
|
193
|
+
- X-Ray tracing
|
|
194
|
+
|
|
195
|
+
An app's own `template.yaml` still declares what its function needs; the boundary caps it. Edit the boundary in `proxy/template.yaml` (and redeploy) to widen or narrow it for the whole account.
|
|
196
|
+
|
|
197
|
+
### The API
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
GET /health version, issuer, organization, boundary, account
|
|
201
|
+
POST /apps/{org}/{project}/{app} create both roles; body {"region": "…"} org.manage
|
|
202
|
+
GET /apps/{org}/{project}/{app} roles and grants org.manage
|
|
203
|
+
DELETE /apps/{org}/{project}/{app} delete roles and grants org.manage
|
|
204
|
+
PUT /apps/{org}/{project}/{app}/grants {"subjects": ["org:acme", …]} org.manage
|
|
205
|
+
POST /apps/{org}/{project}/{app}/credentials {"duration": 900..3600} → temporary credentials a grant
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Every call except `/health` carries `Authorization: Bearer <platform token>` with `aud` = the proxy url. The proxy checks the signature against the issuer's JWKS, `iss`, `aud`, expiry, and that `organization` is the one it serves. Admin calls need `org.manage` in the token's `scopes`; deploy-job tokens carry no scopes, so a compromised worker cannot widen a grant. `credentials` needs the token's `sub` to start with one of the app's granted subject prefixes.
|
|
209
|
+
|
|
210
|
+
Errors are `{"error": "…"}`: 400 (input), 401 (token), 403 (organization, scope or grant), 404 (app not created).
|
|
211
|
+
|
|
212
|
+
The response of `credentials` also carries `stack_prefix` and `execution_role`; the target passes the latter as the overlay's `ExecutionRoleArn` parameter so `sam deploy` needs no `iam:CreateRole`.
|
|
213
|
+
|
|
214
|
+
### Upgrading
|
|
215
|
+
|
|
216
|
+
`/health` reports the proxy version; the plugin refuses a proxy older than the `MIN_PROXY` it was built for. Run `proxy/deploy.sh` again from a newer checkout — CloudFormation updates the stack in place, grants stay.
|
|
217
|
+
|
|
218
|
+
## Tools and commands
|
|
219
|
+
|
|
220
|
+
| | |
|
|
221
|
+
|---|---|
|
|
222
|
+
| `aws_lambda_stacks` / `action-platform aws-lambda stacks [prefix]` | CloudFormation stacks |
|
|
223
|
+
| `aws_lambda_functions` / `action-platform aws-lambda functions [prefix]` | Lambda functions |
|
|
224
|
+
| `action-platform aws-lambda proxy health <url>` | what the proxy serves |
|
|
225
|
+
| `action-platform aws-lambda proxy create <url> <org/project/app> [--region]` | the app's roles (org.manage) |
|
|
226
|
+
| `action-platform aws-lambda proxy show <url> <org/project/app>` | roles and grants |
|
|
227
|
+
| `action-platform aws-lambda proxy grant <url> <org/project/app> <subject>…` | who may deploy (org.manage) |
|
|
228
|
+
| `action-platform aws-lambda proxy delete <url> <org/project/app>` | remove the app from AWS IAM (org.manage) |
|
|
229
|
+
|
|
230
|
+
The `proxy` commands take the token from the CLI's login (`action-platform login`).
|
|
231
|
+
|
|
232
|
+
## Requirements
|
|
233
|
+
|
|
234
|
+
- `aws` and `sam` — on PATH when present; otherwise the `awscli` and `aws-sam-cli` packages the plugin depends on run as `python -m`, which is how the hosted platform deploys without the CLIs in its image.
|
|
235
|
+
- `AWS_REGION` or `region` under `[deploy]`.
|
|
236
|
+
- Network: `*.amazonaws.com`, and the proxy url when set.
|
|
237
|
+
- Installing the proxy: `sam`, `aws`, credentials for the account, Python 3.13 on PATH for `sam build` (or `sam build --use-container`).
|
|
238
|
+
|
|
239
|
+
## Development
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
pip install -e ".[dev]"
|
|
243
|
+
pytest
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
The tests fake `aws`, `sam`, IAM, STS and DynamoDB; nothing reaches AWS. `proxy/tests/test_oidc.py` signs a token with `cryptography` and verifies it with the proxy's standard-library RS256.
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# apx-aws-lambda
|
|
2
|
+
|
|
3
|
+
AWS Lambda for [Action Platform](https://github.com/actionplatform/action-platform): the `aws/lambda` deploy target (SAM), its overlay, read-only tools and commands, and a **deploy proxy** for your AWS account so that no AWS key ever lives on the platform, on a machine or in a repository. `apx-` is the prefix every Action Platform extension carries.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
action-platform plugin install aws-lambda
|
|
7
|
+
action-platform cloud set aws/lambda # overlay files come from this plugin
|
|
8
|
+
action-platform deploy --dry-run # preflight: credentials, sam validate
|
|
9
|
+
action-platform deploy # sam build + sam deploy --config-env <stage>
|
|
10
|
+
action-platform diagnose
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Contents
|
|
14
|
+
|
|
15
|
+
- [What the plugin does](#what-the-plugin-does)
|
|
16
|
+
- [Credentials without keys](#credentials-without-keys)
|
|
17
|
+
- [The deploy proxy (recommended)](#the-deploy-proxy-recommended)
|
|
18
|
+
- [A role of your own (OIDC)](#a-role-of-your-own-oidc)
|
|
19
|
+
- [The AWS CLI's own credentials](#the-aws-clis-own-credentials)
|
|
20
|
+
- [Where the deploy runs](#where-the-deploy-runs)
|
|
21
|
+
- [The proxy in detail](#the-proxy-in-detail)
|
|
22
|
+
- [Tools and commands](#tools-and-commands)
|
|
23
|
+
- [Requirements](#requirements)
|
|
24
|
+
- [Development](#development)
|
|
25
|
+
|
|
26
|
+
## What the plugin does
|
|
27
|
+
|
|
28
|
+
| | |
|
|
29
|
+
|---|---|
|
|
30
|
+
| Overlay | `template.yaml`, `samconfig.toml`, `lambda_handler.py`, `Makefile`, `requirements/` (IAM examples), `.github/workflows/deploy.yml` (optional) |
|
|
31
|
+
| Deploy | `sam build` + `sam deploy --config-env <stage>` (`dev` → `default`, `prod` → `prod`) |
|
|
32
|
+
| Rollback | CloudFormation `rollback-stack` — previous stack state |
|
|
33
|
+
| Diagnose | stack status and the HTTP API url |
|
|
34
|
+
| Destroy | `sam delete` |
|
|
35
|
+
| Tools | `aws_lambda_stacks`, `aws_lambda_functions` |
|
|
36
|
+
| Commands | `action-platform aws-lambda stacks\|functions`, `action-platform aws-lambda proxy …` |
|
|
37
|
+
|
|
38
|
+
`[deploy]` in `platform.toml`:
|
|
39
|
+
|
|
40
|
+
```toml
|
|
41
|
+
[deploy]
|
|
42
|
+
target = "aws/lambda"
|
|
43
|
+
region = "us-east-1" # optional; samconfig.toml / AWS_REGION otherwise
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Deploying itself goes through the core's `deploy` / `rollback` / `diagnose`, which drive the target.
|
|
47
|
+
|
|
48
|
+
## Credentials without keys
|
|
49
|
+
|
|
50
|
+
The target looks for credentials in this order:
|
|
51
|
+
|
|
52
|
+
| Under `[deploy]` | How | Who decides what the app may do |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `proxy_url` + `app` | the deploy proxy in your account exchanges a platform token for the app's deploy-role credentials | the proxy's grants, in your account |
|
|
55
|
+
| `role_arn` | `sts assume-role-with-web-identity` with a platform token | the role's trust and permission policies |
|
|
56
|
+
| neither | the AWS CLI's own chain: SSO, profile, instance role | whatever that identity may do |
|
|
57
|
+
|
|
58
|
+
### The deploy proxy (recommended)
|
|
59
|
+
|
|
60
|
+
`proxy/` is a small SAM application you install **once per AWS account**. It decides who may deploy which app and hands out 15–60 minute credentials of that app's deploy role. It holds no platform secret — it verifies the platform's OIDC tokens against `https://<platform>/.well-known/jwks.json` — and the platform holds no AWS key.
|
|
61
|
+
|
|
62
|
+
**1. Install the proxy** — with your own AWS credentials, this one time (`aws sso login` or a profile; not the root account):
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone https://github.com/actionplatform/apx-aws-lambda
|
|
66
|
+
cd apx-aws-lambda
|
|
67
|
+
./proxy/deploy.sh https://platform.example.com acme # issuer url, organization slug [region] [stack name]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The script runs `sam build` + `sam deploy`, prints the stack's `ProxyUrl` and calls `/health`.
|
|
71
|
+
|
|
72
|
+
**2. Register the app** — from a machine logged in to the platform (`action-platform login`) as someone with `org.manage`:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
P=https://xxxx.lambda-url.us-east-1.on.aws
|
|
76
|
+
|
|
77
|
+
action-platform aws-lambda proxy create $P acme/shop/orders --region us-east-1
|
|
78
|
+
action-platform aws-lambda proxy grant $P acme/shop/orders org:acme:project:shop:app:orders org:acme
|
|
79
|
+
action-platform aws-lambda proxy show $P acme/shop/orders
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`create` makes the app's two roles and grants deploy to the app itself; `grant` replaces the list of subject prefixes that may deploy — `org:acme` means anyone in the organization, `org:acme:project:shop` any app of the project, the full subject only deploys the platform runs for that app.
|
|
83
|
+
|
|
84
|
+
**3. Point the repository at it:**
|
|
85
|
+
|
|
86
|
+
```toml
|
|
87
|
+
[deploy]
|
|
88
|
+
target = "aws/lambda"
|
|
89
|
+
proxy_url = "https://xxxx.lambda-url.us-east-1.on.aws"
|
|
90
|
+
app = "acme/shop/orders"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`stack_name` in `samconfig.toml` must start with `ap-acme-shop-orders` — the deploy role reaches those stacks only, and the target refuses anything else before touching AWS.
|
|
94
|
+
|
|
95
|
+
**4.** `action-platform deploy`.
|
|
96
|
+
|
|
97
|
+
### A role of your own (OIDC)
|
|
98
|
+
|
|
99
|
+
Without the proxy, register the platform as an identity provider once per account and write the trust policy yourself:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
aws iam create-open-id-connect-provider --url https://platform.example.com --client-id-list sts.amazonaws.com
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"Version": "2012-10-17",
|
|
108
|
+
"Statement": [{
|
|
109
|
+
"Effect": "Allow",
|
|
110
|
+
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/platform.example.com" },
|
|
111
|
+
"Action": "sts:AssumeRoleWithWebIdentity",
|
|
112
|
+
"Condition": {
|
|
113
|
+
"StringEquals": { "platform.example.com:aud": "sts.amazonaws.com" },
|
|
114
|
+
"StringLike": { "platform.example.com:sub": "org:acme:project:shop:app:orders" }
|
|
115
|
+
}
|
|
116
|
+
}]
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`requirements/policy.json` from the overlay is a starting permission policy. Then:
|
|
121
|
+
|
|
122
|
+
```toml
|
|
123
|
+
[deploy]
|
|
124
|
+
target = "aws/lambda"
|
|
125
|
+
role_arn = "arn:aws:iam::123456789012:role/shop-deploy"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`sub` is `org:<org>:project:<project>:app:<app>` for a deploy the platform runs; `org:<org>` plus an `actor` claim for `action-platform deploy` from a logged-in machine (`StringLike` with `org:acme:*` covers both).
|
|
129
|
+
|
|
130
|
+
### The AWS CLI's own credentials
|
|
131
|
+
|
|
132
|
+
With neither `proxy_url` nor `role_arn`, `aws` and `sam` use what the machine has: `aws sso login`, a profile, an instance role. Still no key in a file when you use SSO — but nothing ties the identity to one app.
|
|
133
|
+
|
|
134
|
+
## Where the deploy runs
|
|
135
|
+
|
|
136
|
+
Independent of how credentials are obtained:
|
|
137
|
+
|
|
138
|
+
| Where | Token for the proxy / role comes from |
|
|
139
|
+
|---|---|
|
|
140
|
+
| the platform's worker (a deploy job) | signed by the platform for that app: `sub = org:…:project:…:app:…`, no scopes |
|
|
141
|
+
| a logged-in machine (`action-platform deploy`) | `POST /api/v1/identity/token` with the user's login: `sub = org:<org>`, `actor`, `scopes` |
|
|
142
|
+
| the overlay's `.github/workflows/deploy.yml` | GitHub's own OIDC — optional, unrelated to the proxy; delete the workflow when the platform deploys |
|
|
143
|
+
|
|
144
|
+
## The proxy in detail
|
|
145
|
+
|
|
146
|
+
### What the stack creates
|
|
147
|
+
|
|
148
|
+
| Resource | Purpose |
|
|
149
|
+
|---|---|
|
|
150
|
+
| Lambda + Function URL | the API (`python3.13`, standard library only — no compiled dependency, so `sam build` needs no pip) |
|
|
151
|
+
| DynamoDB table | one row per app: region, grants |
|
|
152
|
+
| `ActionPlatformBoundary` managed policy | the cap on every execution role the proxy creates |
|
|
153
|
+
| the function's role | may assume `role/action-platform/ap-deploy-*`, manage roles under `/action-platform/`, and create `ap-exec-*` roles only with the boundary attached |
|
|
154
|
+
|
|
155
|
+
Parameters: `IssuerUrl` (the platform's public url), `Organization` (one proxy serves one organization), `Version`.
|
|
156
|
+
|
|
157
|
+
### What it creates per app
|
|
158
|
+
|
|
159
|
+
| Role | Trust | Policy |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| `ap-deploy-<org>-<project>-<app>` | the proxy's function role | CloudFormation on stacks `ap-<org>-<project>-<app>*`; Lambda, API Gateway and logs with the same prefix; `iam:PassRole` on the execution role; SAM's managed bucket |
|
|
162
|
+
| `ap-exec-<org>-<project>-<app>` | `lambda.amazonaws.com` | `AWSLambdaBasicExecutionRole` + the boundary |
|
|
163
|
+
|
|
164
|
+
Both live under `/action-platform/`, carry tags `action-platform:app` and `action-platform:prefix`, and are idempotent: `create` again syncs trust policies, tags and policies without duplicating anything. `delete` removes both and the grants.
|
|
165
|
+
|
|
166
|
+
### The boundary
|
|
167
|
+
|
|
168
|
+
`ActionPlatformBoundary` is one policy for every app, scoped per app by the role's `action-platform:prefix` tag used as a policy variable in the resources:
|
|
169
|
+
|
|
170
|
+
- logs: `/aws/lambda/<prefix>*`
|
|
171
|
+
- S3 buckets, DynamoDB tables, SQS queues, SNS topics, Lambda functions: `<prefix>-*`
|
|
172
|
+
- Secrets Manager secrets and SSM parameters: `<prefix>/*`
|
|
173
|
+
- X-Ray tracing
|
|
174
|
+
|
|
175
|
+
An app's own `template.yaml` still declares what its function needs; the boundary caps it. Edit the boundary in `proxy/template.yaml` (and redeploy) to widen or narrow it for the whole account.
|
|
176
|
+
|
|
177
|
+
### The API
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
GET /health version, issuer, organization, boundary, account
|
|
181
|
+
POST /apps/{org}/{project}/{app} create both roles; body {"region": "…"} org.manage
|
|
182
|
+
GET /apps/{org}/{project}/{app} roles and grants org.manage
|
|
183
|
+
DELETE /apps/{org}/{project}/{app} delete roles and grants org.manage
|
|
184
|
+
PUT /apps/{org}/{project}/{app}/grants {"subjects": ["org:acme", …]} org.manage
|
|
185
|
+
POST /apps/{org}/{project}/{app}/credentials {"duration": 900..3600} → temporary credentials a grant
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Every call except `/health` carries `Authorization: Bearer <platform token>` with `aud` = the proxy url. The proxy checks the signature against the issuer's JWKS, `iss`, `aud`, expiry, and that `organization` is the one it serves. Admin calls need `org.manage` in the token's `scopes`; deploy-job tokens carry no scopes, so a compromised worker cannot widen a grant. `credentials` needs the token's `sub` to start with one of the app's granted subject prefixes.
|
|
189
|
+
|
|
190
|
+
Errors are `{"error": "…"}`: 400 (input), 401 (token), 403 (organization, scope or grant), 404 (app not created).
|
|
191
|
+
|
|
192
|
+
The response of `credentials` also carries `stack_prefix` and `execution_role`; the target passes the latter as the overlay's `ExecutionRoleArn` parameter so `sam deploy` needs no `iam:CreateRole`.
|
|
193
|
+
|
|
194
|
+
### Upgrading
|
|
195
|
+
|
|
196
|
+
`/health` reports the proxy version; the plugin refuses a proxy older than the `MIN_PROXY` it was built for. Run `proxy/deploy.sh` again from a newer checkout — CloudFormation updates the stack in place, grants stay.
|
|
197
|
+
|
|
198
|
+
## Tools and commands
|
|
199
|
+
|
|
200
|
+
| | |
|
|
201
|
+
|---|---|
|
|
202
|
+
| `aws_lambda_stacks` / `action-platform aws-lambda stacks [prefix]` | CloudFormation stacks |
|
|
203
|
+
| `aws_lambda_functions` / `action-platform aws-lambda functions [prefix]` | Lambda functions |
|
|
204
|
+
| `action-platform aws-lambda proxy health <url>` | what the proxy serves |
|
|
205
|
+
| `action-platform aws-lambda proxy create <url> <org/project/app> [--region]` | the app's roles (org.manage) |
|
|
206
|
+
| `action-platform aws-lambda proxy show <url> <org/project/app>` | roles and grants |
|
|
207
|
+
| `action-platform aws-lambda proxy grant <url> <org/project/app> <subject>…` | who may deploy (org.manage) |
|
|
208
|
+
| `action-platform aws-lambda proxy delete <url> <org/project/app>` | remove the app from AWS IAM (org.manage) |
|
|
209
|
+
|
|
210
|
+
The `proxy` commands take the token from the CLI's login (`action-platform login`).
|
|
211
|
+
|
|
212
|
+
## Requirements
|
|
213
|
+
|
|
214
|
+
- `aws` and `sam` — on PATH when present; otherwise the `awscli` and `aws-sam-cli` packages the plugin depends on run as `python -m`, which is how the hosted platform deploys without the CLIs in its image.
|
|
215
|
+
- `AWS_REGION` or `region` under `[deploy]`.
|
|
216
|
+
- Network: `*.amazonaws.com`, and the proxy url when set.
|
|
217
|
+
- Installing the proxy: `sam`, `aws`, credentials for the account, Python 3.13 on PATH for `sam build` (or `sam build --use-container`).
|
|
218
|
+
|
|
219
|
+
## Development
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
pip install -e ".[dev]"
|
|
223
|
+
pytest
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The tests fake `aws`, `sam`, IAM, STS and DynamoDB; nothing reaches AWS. `proxy/tests/test_oidc.py` signs a token with `cryptography` and verifies it with the proxy's standard-library RS256.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""`action-platform aws-lambda …`: the same reads as the tools, from the terminal."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from action_platform.core.exception import ActionPlatformError
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from apx_aws_lambda import shell
|
|
11
|
+
from apx_aws_lambda.proxy import ProxyClient
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(
|
|
14
|
+
help="AWS Lambda: stacks, functions, the deploy proxy.", no_args_is_help=True
|
|
15
|
+
)
|
|
16
|
+
proxy_app = typer.Typer(
|
|
17
|
+
help="The deploy proxy in your account: apps and grants.", no_args_is_help=True
|
|
18
|
+
)
|
|
19
|
+
app.add_typer(proxy_app, name="proxy")
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _table(columns: list[str], rows: list[list[str]]) -> None:
|
|
24
|
+
table = Table(box=None, pad_edge=False)
|
|
25
|
+
|
|
26
|
+
for c in columns:
|
|
27
|
+
table.add_column(c)
|
|
28
|
+
|
|
29
|
+
for r in rows:
|
|
30
|
+
table.add_row(*r)
|
|
31
|
+
|
|
32
|
+
console.print(table)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.command("stacks")
|
|
36
|
+
def stacks(
|
|
37
|
+
prefix: str = typer.Argument(""), region: str | None = typer.Option(None)
|
|
38
|
+
) -> None:
|
|
39
|
+
"""CloudFormation stacks."""
|
|
40
|
+
data = shell.aws(
|
|
41
|
+
"cloudformation",
|
|
42
|
+
"list-stacks",
|
|
43
|
+
"--stack-status-filter",
|
|
44
|
+
"CREATE_COMPLETE",
|
|
45
|
+
"UPDATE_COMPLETE",
|
|
46
|
+
"ROLLBACK_COMPLETE",
|
|
47
|
+
"UPDATE_ROLLBACK_COMPLETE",
|
|
48
|
+
region=region,
|
|
49
|
+
)
|
|
50
|
+
_table(
|
|
51
|
+
["stack", "status", "updated"],
|
|
52
|
+
[
|
|
53
|
+
[s["StackName"], s["StackStatus"], s.get("LastUpdatedTime", "")]
|
|
54
|
+
for s in data.get("StackSummaries", [])
|
|
55
|
+
if s["StackName"].startswith(prefix)
|
|
56
|
+
],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@app.command("functions")
|
|
61
|
+
def functions(
|
|
62
|
+
prefix: str = typer.Argument(""), region: str | None = typer.Option(None)
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Lambda functions."""
|
|
65
|
+
data = shell.aws("lambda", "list-functions", region=region)
|
|
66
|
+
_table(
|
|
67
|
+
["function", "runtime", "memory"],
|
|
68
|
+
[
|
|
69
|
+
[f["FunctionName"], f.get("Runtime", ""), str(f.get("MemorySize", ""))]
|
|
70
|
+
for f in data.get("Functions", [])
|
|
71
|
+
if f["FunctionName"].startswith(prefix)
|
|
72
|
+
],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _client(proxy: str, app_path: str) -> ProxyClient:
|
|
77
|
+
try:
|
|
78
|
+
return ProxyClient(proxy, app_path)
|
|
79
|
+
except ActionPlatformError as e:
|
|
80
|
+
raise typer.BadParameter(str(e)) from e
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _show(data: dict) -> None:
|
|
84
|
+
for key in ("app", "region", "stack_prefix", "deploy_role", "execution_role"):
|
|
85
|
+
if data.get(key):
|
|
86
|
+
console.print(f"[bold]{key}[/bold] {data[key]}")
|
|
87
|
+
|
|
88
|
+
console.print(
|
|
89
|
+
"[bold]subjects[/bold] " + ", ".join(data.get("subjects") or []) or "-"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@proxy_app.command("health")
|
|
94
|
+
def proxy_health(proxy: str = typer.Argument(..., help="Proxy url")) -> None:
|
|
95
|
+
"""Version, issuer and organization the proxy serves."""
|
|
96
|
+
for key, value in ProxyClient(proxy, "a/b/c").health().items():
|
|
97
|
+
console.print(f"[bold]{key}[/bold] {value}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@proxy_app.command("create")
|
|
101
|
+
def proxy_create(
|
|
102
|
+
proxy: str = typer.Argument(..., help="Proxy url"),
|
|
103
|
+
app_path: str = typer.Argument(..., metavar="ORG/PROJECT/APP"),
|
|
104
|
+
region: str | None = typer.Option(None, help="Region the app deploys to"),
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Create the app's deploy and execution roles (needs org.manage)."""
|
|
107
|
+
_show(_client(proxy, app_path).create(region))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@proxy_app.command("show")
|
|
111
|
+
def proxy_show(
|
|
112
|
+
proxy: str = typer.Argument(..., help="Proxy url"),
|
|
113
|
+
app_path: str = typer.Argument(..., metavar="ORG/PROJECT/APP"),
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Roles and grants of an app."""
|
|
116
|
+
_show(_client(proxy, app_path).show())
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@proxy_app.command("grant")
|
|
120
|
+
def proxy_grant(
|
|
121
|
+
proxy: str = typer.Argument(..., help="Proxy url"),
|
|
122
|
+
app_path: str = typer.Argument(..., metavar="ORG/PROJECT/APP"),
|
|
123
|
+
subjects: list[str] = typer.Argument(
|
|
124
|
+
...,
|
|
125
|
+
help="Subject prefixes that may deploy: org:<org>, org:<org>:project:<p>, the app's own",
|
|
126
|
+
),
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Replace who may deploy the app (needs org.manage)."""
|
|
129
|
+
_show(_client(proxy, app_path).grant(subjects))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@proxy_app.command("delete")
|
|
133
|
+
def proxy_delete(
|
|
134
|
+
proxy: str = typer.Argument(..., help="Proxy url"),
|
|
135
|
+
app_path: str = typer.Argument(..., metavar="ORG/PROJECT/APP"),
|
|
136
|
+
) -> None:
|
|
137
|
+
"""Delete the app's roles and grants (needs org.manage)."""
|
|
138
|
+
_client(proxy, app_path).delete()
|
|
139
|
+
console.print(f"deleted {app_path}")
|