Skip to content

Commit 6148030

Browse files
committed
add tests and github action workflow
1 parent b0203a4 commit 6148030

File tree

6 files changed

+350
-1
lines changed

6 files changed

+350
-1
lines changed

.github/workflows/vault.yml

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: LocalStack Vault Extension Tests
2+
3+
on:
4+
push:
5+
paths:
6+
- vault/**
7+
branches:
8+
- main
9+
pull_request:
10+
paths:
11+
- .github/workflows/vault.yml
12+
- vault/**
13+
workflow_dispatch:
14+
15+
env:
16+
LOCALSTACK_DISABLE_EVENTS: "1"
17+
LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }}
18+
19+
jobs:
20+
integration-tests:
21+
name: Run Integration Tests
22+
runs-on: ubuntu-latest
23+
timeout-minutes: 15
24+
steps:
25+
- name: Checkout
26+
uses: actions/checkout@v4
27+
28+
- name: Setup Vault
29+
uses: eLco/setup-vault@v1
30+
31+
- name: Setup Python
32+
uses: actions/setup-python@v5
33+
with:
34+
python-version: "3.11"
35+
36+
- name: Setup LocalStack and extension
37+
run: |
38+
cd vault
39+
40+
# Pull Docker images in parallel
41+
docker pull localstack/localstack-pro &
42+
docker pull hashicorp/vault:latest &
43+
docker pull public.ecr.aws/lambda/python:3.11 &
44+
pip install localstack awscli-local[ver1]
45+
46+
# Install and build extension
47+
make install
48+
make lint
49+
make dist
50+
localstack extensions -v install file://$(ls ./dist/localstack_vault-*.tar.gz)
51+
52+
# Wait for Docker pulls to complete
53+
wait
54+
55+
# Start LocalStack with extension
56+
DEBUG=1 EXTENSION_DEV_MODE=1 localstack start -d
57+
localstack wait
58+
59+
- name: Run pytest tests
60+
run: |
61+
cd vault
62+
make test
63+
64+
- name: Run sample app (Lambda + Vault Extension)
65+
run: |
66+
cd vault
67+
make sample-app
68+
69+
- name: Print logs
70+
if: always()
71+
run: |
72+
localstack logs
73+
localstack stop

vault/.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,21 @@ build/
44
dist/
55
.eggs/
66
*egg-info/
7+
__pycache__/
8+
.pytest_cache/
9+
10+
# Terraform
11+
.terraform/
12+
.terraform.lock.hcl
13+
terraform.tfstate*
14+
*.tfplan
715

816
# OS
917
.DS_Store
1018

19+
# Temp files
20+
*.zip
21+
/tmp/
22+
1123
# Ignored Directories
1224
sample-app-terraform/

vault/Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ format: ## Run ruff to format the whole codebase
3838
lint: ## Run ruff to lint the codebase
3939
$(VENV_RUN); python -m ruff check --output-format=full .
4040

41+
test: ## Run pytest tests (requires LocalStack running)
42+
$(VENV_RUN); python -m pytest tests/ -v
43+
4144
sample-app: ## Deploy sample app with Lambda + Vault
4245
@echo "Setting up Vault secrets..."
4346
sample-app-extension/bin/setup-vault.sh
@@ -49,4 +52,4 @@ sample-app: ## Deploy sample app with Lambda + Vault
4952
clean-dist: clean
5053
rm -rf dist/
5154

52-
.PHONY: clean clean-dist dist install publish usage venv format lint sample-app
55+
.PHONY: clean clean-dist dist install publish usage venv format lint test sample-app

vault/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ dev = [
3030
"jsonpatch",
3131
"localstack",
3232
"pytest",
33+
"requests",
3334
"rolo",
3435
"ruff",
3536
"twisted",

vault/tests/__init__.py

Whitespace-only changes.

vault/tests/test_extension.py

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import base64
2+
3+
import boto3
4+
import requests
5+
from localstack.utils.strings import short_uid
6+
7+
8+
# Vault connection details
9+
VAULT_ADDR = "http://vault.localhost.localstack.cloud:4566"
10+
VAULT_TOKEN = "root"
11+
LOCALSTACK_ENDPOINT = "http://localhost:4566"
12+
13+
14+
def vault_request(method, path, data=None, token=VAULT_TOKEN):
15+
"""Make a request to Vault API."""
16+
url = f"{VAULT_ADDR}/v1/{path}"
17+
headers = {"X-Vault-Token": token}
18+
if data:
19+
headers["Content-Type"] = "application/json"
20+
return requests.request(method, url, headers=headers, json=data)
21+
return requests.request(method, url, headers=headers)
22+
23+
24+
def test_vault_health():
25+
"""Test that Vault is running and healthy."""
26+
response = requests.get(f"{VAULT_ADDR}/v1/sys/health")
27+
assert response.status_code == 200
28+
29+
data = response.json()
30+
assert data["initialized"] is True
31+
assert data["sealed"] is False
32+
33+
34+
def test_vault_auth_with_token():
35+
"""Test authentication with root token."""
36+
response = vault_request("GET", "auth/token/lookup-self")
37+
assert response.status_code == 200
38+
39+
data = response.json()
40+
assert "data" in data
41+
assert data["data"]["id"] == VAULT_TOKEN
42+
43+
44+
def test_kv_secrets_engine():
45+
"""Test KV v2 secrets engine operations."""
46+
secret_path = f"myapp/config-{short_uid()}"
47+
48+
# Write a secret
49+
secret_data = {
50+
"data": {
51+
"api_key": "test-api-key-123",
52+
"db_password": "supersecret",
53+
}
54+
}
55+
response = vault_request("POST", f"secret/data/{secret_path}", secret_data)
56+
assert response.status_code == 200
57+
58+
# Read the secret back
59+
response = vault_request("GET", f"secret/data/{secret_path}")
60+
assert response.status_code == 200
61+
62+
data = response.json()
63+
assert data["data"]["data"]["api_key"] == "test-api-key-123"
64+
assert data["data"]["data"]["db_password"] == "supersecret"
65+
66+
# Delete the secret
67+
response = vault_request("DELETE", f"secret/data/{secret_path}")
68+
assert response.status_code == 204
69+
70+
71+
def test_kv_list_secrets():
72+
"""Test listing secrets in KV engine."""
73+
# Create a few secrets
74+
for i in range(3):
75+
secret_data = {"data": {"value": f"secret-{i}"}}
76+
vault_request("POST", f"secret/data/list-test/item-{i}", secret_data)
77+
78+
# List secrets
79+
response = vault_request("LIST", "secret/metadata/list-test")
80+
assert response.status_code == 200
81+
82+
data = response.json()
83+
keys = data["data"]["keys"]
84+
assert len(keys) == 3
85+
assert "item-0" in keys
86+
assert "item-1" in keys
87+
assert "item-2" in keys
88+
89+
# Cleanup
90+
for i in range(3):
91+
vault_request("DELETE", f"secret/metadata/list-test/item-{i}")
92+
93+
94+
def test_transit_engine():
95+
"""Test Transit secrets engine for encryption."""
96+
key_name = f"test-key-{short_uid()}"
97+
98+
# Create an encryption key
99+
response = vault_request("POST", f"transit/keys/{key_name}")
100+
assert response.status_code in (200, 204) # Vault may return either
101+
102+
# Encrypt some data
103+
plaintext = "Hello, Vault!"
104+
plaintext_b64 = base64.b64encode(plaintext.encode()).decode()
105+
106+
response = vault_request(
107+
"POST",
108+
f"transit/encrypt/{key_name}",
109+
{"plaintext": plaintext_b64},
110+
)
111+
assert response.status_code == 200
112+
ciphertext = response.json()["data"]["ciphertext"]
113+
assert ciphertext.startswith("vault:v1:")
114+
115+
# Decrypt the data
116+
response = vault_request(
117+
"POST",
118+
f"transit/decrypt/{key_name}",
119+
{"ciphertext": ciphertext},
120+
)
121+
assert response.status_code == 200
122+
decrypted_b64 = response.json()["data"]["plaintext"]
123+
decrypted = base64.b64decode(decrypted_b64).decode()
124+
assert decrypted == plaintext
125+
126+
# Delete the key
127+
vault_request("POST", f"transit/keys/{key_name}/config", {"deletion_allowed": True})
128+
vault_request("DELETE", f"transit/keys/{key_name}")
129+
130+
131+
def test_aws_auth_method_enabled():
132+
"""Test that AWS auth method is enabled and configured."""
133+
response = vault_request("GET", "sys/auth")
134+
assert response.status_code == 200
135+
136+
data = response.json()
137+
assert "aws/" in data["data"]
138+
assert data["data"]["aws/"]["type"] == "aws"
139+
140+
141+
def test_default_lambda_role_exists():
142+
"""Test that the default Lambda IAM auth role exists or can be created."""
143+
response = vault_request("GET", "auth/aws/role/default-lambda-role")
144+
145+
# If role doesn't exist (e.g., after Terraform testing), create it
146+
if response.status_code == 404:
147+
role_config = {
148+
"auth_type": "iam",
149+
"bound_iam_principal_arn": ["arn:aws:iam::000000000000:role/*"],
150+
"token_policies": ["default-lambda-policy"],
151+
"resolve_aws_unique_ids": False,
152+
}
153+
create_response = vault_request(
154+
"POST", "auth/aws/role/default-lambda-role", role_config
155+
)
156+
assert create_response.status_code == 204
157+
response = vault_request("GET", "auth/aws/role/default-lambda-role")
158+
159+
assert response.status_code == 200
160+
data = response.json()
161+
assert data["data"]["auth_type"] == "iam"
162+
assert data["data"]["resolve_aws_unique_ids"] is False
163+
164+
165+
def test_default_lambda_policy_exists():
166+
"""Test that the default Lambda policy exists with correct permissions."""
167+
response = vault_request("GET", "sys/policies/acl/default-lambda-policy")
168+
assert response.status_code == 200
169+
170+
data = response.json()
171+
policy = data["data"]["policy"]
172+
assert "secret/*" in policy
173+
assert "transit/*" in policy
174+
175+
176+
def test_mixed_vault_and_aws_traffic():
177+
"""
178+
Test that Vault HTTP traffic and AWS API traffic work together.
179+
180+
This verifies that the Vault extension properly proxies Vault requests
181+
while not interfering with regular AWS API requests.
182+
"""
183+
# Test Vault API
184+
response = vault_request("GET", "sys/health")
185+
assert response.status_code == 200
186+
assert response.json()["sealed"] is False
187+
188+
# Test AWS S3 API
189+
s3_client = boto3.client(
190+
"s3",
191+
endpoint_url=LOCALSTACK_ENDPOINT,
192+
aws_access_key_id="test",
193+
aws_secret_access_key="test",
194+
region_name="us-east-1",
195+
)
196+
197+
bucket_name = f"vault-test-bucket-{short_uid()}"
198+
s3_client.create_bucket(Bucket=bucket_name)
199+
200+
buckets = s3_client.list_buckets()
201+
bucket_names = [b["Name"] for b in buckets["Buckets"]]
202+
assert bucket_name in bucket_names
203+
204+
# Cleanup
205+
s3_client.delete_bucket(Bucket=bucket_name)
206+
207+
# Test AWS STS API (used by Vault for IAM auth)
208+
sts_client = boto3.client(
209+
"sts",
210+
endpoint_url=LOCALSTACK_ENDPOINT,
211+
aws_access_key_id="test",
212+
aws_secret_access_key="test",
213+
region_name="us-east-1",
214+
)
215+
216+
identity = sts_client.get_caller_identity()
217+
assert "Account" in identity
218+
assert "Arn" in identity
219+
220+
# Verify Vault still works after AWS calls
221+
response = vault_request("GET", "auth/token/lookup-self")
222+
assert response.status_code == 200
223+
224+
225+
def test_create_custom_policy_and_role():
226+
"""Test creating custom Vault policies and IAM auth roles."""
227+
policy_name = f"custom-policy-{short_uid()}"
228+
role_name = f"custom-role-{short_uid()}"
229+
230+
# Create a custom policy
231+
policy_hcl = """
232+
path "secret/data/custom/*" {
233+
capabilities = ["read"]
234+
}
235+
"""
236+
response = vault_request(
237+
"PUT", f"sys/policies/acl/{policy_name}", {"policy": policy_hcl}
238+
)
239+
assert response.status_code == 204
240+
241+
# Create a custom IAM auth role
242+
role_config = {
243+
"auth_type": "iam",
244+
"bound_iam_principal_arn": [
245+
"arn:aws:iam::000000000000:role/custom-lambda-role"
246+
],
247+
"token_policies": [policy_name],
248+
"resolve_aws_unique_ids": False,
249+
}
250+
response = vault_request("POST", f"auth/aws/role/{role_name}", role_config)
251+
assert response.status_code == 204
252+
253+
# Verify the role was created
254+
response = vault_request("GET", f"auth/aws/role/{role_name}")
255+
assert response.status_code == 200
256+
assert policy_name in response.json()["data"]["token_policies"]
257+
258+
# Cleanup
259+
vault_request("DELETE", f"auth/aws/role/{role_name}")
260+
vault_request("DELETE", f"sys/policies/acl/{policy_name}")

0 commit comments

Comments
 (0)