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.
Files changed (35) hide show
  1. apx_aws_lambda-0.3.2/PKG-INFO +246 -0
  2. apx_aws_lambda-0.3.2/README.md +226 -0
  3. apx_aws_lambda-0.3.2/apx_aws_lambda/cli.py +139 -0
  4. {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda/lambda_.py +50 -13
  5. {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
  6. {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda/plugin.py +2 -2
  7. apx_aws_lambda-0.3.2/apx_aws_lambda/proxy.py +128 -0
  8. {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda/shell.py +23 -1
  9. apx_aws_lambda-0.3.2/apx_aws_lambda.egg-info/PKG-INFO +246 -0
  10. {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda.egg-info/SOURCES.txt +2 -0
  11. {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda.egg-info/requires.txt +2 -0
  12. {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/pyproject.toml +6 -2
  13. apx_aws_lambda-0.3.2/tests/test_proxy.py +168 -0
  14. {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/tests/test_targets.py +13 -0
  15. apx_aws_lambda-0.3.0/PKG-INFO +0 -93
  16. apx_aws_lambda-0.3.0/README.md +0 -75
  17. apx_aws_lambda-0.3.0/apx_aws_lambda/cli.py +0 -65
  18. apx_aws_lambda-0.3.0/apx_aws_lambda.egg-info/PKG-INFO +0 -93
  19. {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/LICENSE +0 -0
  20. {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda/__init__.py +0 -0
  21. {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda/overlays/cloud/aws/lambda/cookiecutter.json +0 -0
  22. {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
  23. {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
  24. {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
  25. {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
  26. {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
  27. {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
  28. {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
  29. {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
  30. {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda/overlays/index.json +0 -0
  31. {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda/tools.py +0 -0
  32. {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda.egg-info/dependency_links.txt +0 -0
  33. {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda.egg-info/entry_points.txt +0 -0
  34. {apx_aws_lambda-0.3.0 → apx_aws_lambda-0.3.2}/apx_aws_lambda.egg-info/top_level.txt +0 -0
  35. {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}")