stackport 0.1.7__tar.gz → 0.1.8__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.
- {stackport-0.1.7/stackport.egg-info → stackport-0.1.8}/PKG-INFO +4 -4
- {stackport-0.1.7 → stackport-0.1.8}/README.md +3 -3
- {stackport-0.1.7 → stackport-0.1.8}/backend/main.py +2 -1
- stackport-0.1.8/backend/routes/secretsmanager.py +112 -0
- {stackport-0.1.7 → stackport-0.1.8}/pyproject.toml +1 -1
- {stackport-0.1.7 → stackport-0.1.8/stackport.egg-info}/PKG-INFO +4 -4
- {stackport-0.1.7 → stackport-0.1.8}/stackport.egg-info/SOURCES.txt +6 -4
- stackport-0.1.8/tests/test_secretsmanager_routes.py +281 -0
- stackport-0.1.8/ui/dist/assets/index-BAMXqPoR.js +537 -0
- stackport-0.1.8/ui/dist/assets/index-CU1gzmix.css +1 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/index.html +2 -2
- stackport-0.1.7/ui/dist/assets/index-DI-V3ZCb.js +0 -527
- stackport-0.1.7/ui/dist/assets/index-DM3oKaVN.css +0 -1
- {stackport-0.1.7 → stackport-0.1.8}/LICENSE +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/MANIFEST.in +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/backend/__init__.py +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/backend/aws_client.py +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/backend/cache.py +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/backend/config.py +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/backend/routes/__init__.py +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/backend/routes/dynamodb.py +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/backend/routes/ec2.py +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/backend/routes/iam.py +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/backend/routes/lambda_svc.py +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/backend/routes/logs.py +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/backend/routes/resources.py +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/backend/routes/s3.py +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/backend/routes/sqs.py +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/backend/routes/stats.py +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/setup.cfg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/stackport.egg-info/dependency_links.txt +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/stackport.egg-info/entry_points.txt +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/stackport.egg-info/requires.txt +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/stackport.egg-info/top_level.txt +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/tests/test_cache.py +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/tests/test_client.py +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/tests/test_config.py +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/tests/test_dynamodb_routes.py +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/tests/test_ec2_routes.py +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/tests/test_iam_routes.py +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/tests/test_lambda_routes.py +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/tests/test_logs_routes.py +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/tests/test_registries.py +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/tests/test_routes.py +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/tests/test_sqs_routes.py +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/acm.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/apigateway.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/appsync.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/athena.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/cloudformation.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/cloudfront.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/cognito-idp.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/dynamodb.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/ec2.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/ecr.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/ecs.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/elasticache.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/elasticfilesystem.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/elasticloadbalancing.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/elasticmapreduce.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/events.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/firehose.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/glue.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/iam.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/kinesis.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/kms.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/lambda.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/logs.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/monitoring.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/rds.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/route53.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/s3.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/secretsmanager.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/ses.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/sns.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/sqs.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/ssm.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/stepfunctions.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/wafv2.svg +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/favicon.png +0 -0
- {stackport-0.1.7 → stackport-0.1.8}/ui/dist/favicon.svg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stackport
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: Universal AWS resource browser for local emulators
|
|
5
5
|
Author: Davi Reis Vieira
|
|
6
6
|
License: MIT
|
|
@@ -50,13 +50,13 @@ Dynamic: license-file
|
|
|
50
50
|
## Screenshots
|
|
51
51
|
|
|
52
52
|
**Dashboard** — Service overview with resource counts and health status
|
|
53
|
-

|
|
53
|
+

|
|
54
54
|
|
|
55
55
|
**DynamoDB Browser** — Generic resource table with search, pagination, and detail view
|
|
56
|
-

|
|
56
|
+

|
|
57
57
|
|
|
58
58
|
**S3 Browser** — File browser with folder navigation and object preview
|
|
59
|
-

|
|
59
|
+

|
|
60
60
|
|
|
61
61
|
## Features
|
|
62
62
|
|
|
@@ -18,13 +18,13 @@
|
|
|
18
18
|
## Screenshots
|
|
19
19
|
|
|
20
20
|
**Dashboard** — Service overview with resource counts and health status
|
|
21
|
-

|
|
21
|
+

|
|
22
22
|
|
|
23
23
|
**DynamoDB Browser** — Generic resource table with search, pagination, and detail view
|
|
24
|
-

|
|
24
|
+

|
|
25
25
|
|
|
26
26
|
**S3 Browser** — File browser with folder navigation and object preview
|
|
27
|
-

|
|
27
|
+

|
|
28
28
|
|
|
29
29
|
## Features
|
|
30
30
|
|
|
@@ -8,7 +8,7 @@ from fastapi.responses import FileResponse
|
|
|
8
8
|
from fastapi.staticfiles import StaticFiles
|
|
9
9
|
|
|
10
10
|
from backend.config import LOG_LEVEL, STACKPORT_PORT
|
|
11
|
-
from backend.routes import dynamodb, ec2, iam, lambda_svc, logs, resources, s3, sqs, stats
|
|
11
|
+
from backend.routes import dynamodb, ec2, iam, lambda_svc, logs, resources, s3, secretsmanager, sqs, stats
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class HealthcheckFilter(logging.Filter):
|
|
@@ -47,6 +47,7 @@ app.include_router(sqs.router, prefix="/api/sqs", tags=["sqs"])
|
|
|
47
47
|
app.include_router(iam.router, prefix="/api/iam", tags=["iam"])
|
|
48
48
|
app.include_router(ec2.router, prefix="/api/ec2", tags=["ec2"])
|
|
49
49
|
app.include_router(logs.router, prefix="/api/logs", tags=["logs"])
|
|
50
|
+
app.include_router(secretsmanager.router, prefix="/api/secretsmanager", tags=["secretsmanager"])
|
|
50
51
|
app.include_router(resources.router, prefix="/api")
|
|
51
52
|
|
|
52
53
|
# Serve UI static files — mount assets under /assets, SPA fallback for everything else
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Secrets Manager service-specific routes."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, HTTPException
|
|
7
|
+
|
|
8
|
+
from backend.aws_client import get_client
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _format_date(dt) -> str | None:
|
|
14
|
+
"""Format a datetime to ISO string, or return None."""
|
|
15
|
+
if dt is None:
|
|
16
|
+
return None
|
|
17
|
+
try:
|
|
18
|
+
return dt.isoformat()
|
|
19
|
+
except Exception:
|
|
20
|
+
return str(dt)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@router.get("/secrets")
|
|
24
|
+
def list_secrets() -> dict[str, Any]:
|
|
25
|
+
"""List all secrets with metadata."""
|
|
26
|
+
try:
|
|
27
|
+
client = get_client("secretsmanager")
|
|
28
|
+
paginator = client.get_paginator("list_secrets")
|
|
29
|
+
|
|
30
|
+
secrets = []
|
|
31
|
+
for page in paginator.paginate():
|
|
32
|
+
for secret in page.get("SecretList", []):
|
|
33
|
+
secrets.append(
|
|
34
|
+
{
|
|
35
|
+
"name": secret.get("Name"),
|
|
36
|
+
"arn": secret.get("ARN"),
|
|
37
|
+
"description": secret.get("Description", ""),
|
|
38
|
+
"createdDate": _format_date(secret.get("CreatedDate")),
|
|
39
|
+
"lastChangedDate": _format_date(
|
|
40
|
+
secret.get("LastChangedDate")
|
|
41
|
+
),
|
|
42
|
+
"lastAccessedDate": _format_date(
|
|
43
|
+
secret.get("LastAccessedDate")
|
|
44
|
+
),
|
|
45
|
+
"rotationEnabled": secret.get("RotationEnabled", False),
|
|
46
|
+
"tags": {
|
|
47
|
+
tag["Key"]: tag["Value"]
|
|
48
|
+
for tag in secret.get("Tags", [])
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return {"secrets": secrets}
|
|
54
|
+
except Exception as e:
|
|
55
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@router.get("/secrets/{secret_id:path}")
|
|
59
|
+
def get_secret_detail(secret_id: str) -> dict[str, Any]:
|
|
60
|
+
"""Get secret metadata and value."""
|
|
61
|
+
try:
|
|
62
|
+
client = get_client("secretsmanager")
|
|
63
|
+
|
|
64
|
+
# Get metadata
|
|
65
|
+
try:
|
|
66
|
+
meta = client.describe_secret(SecretId=secret_id)
|
|
67
|
+
except client.exceptions.ResourceNotFoundException:
|
|
68
|
+
raise HTTPException(
|
|
69
|
+
status_code=404, detail=f"Secret '{secret_id}' not found"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Get value
|
|
73
|
+
secret_value = None
|
|
74
|
+
secret_binary = None
|
|
75
|
+
version_id = None
|
|
76
|
+
version_stages = None
|
|
77
|
+
try:
|
|
78
|
+
value_resp = client.get_secret_value(SecretId=secret_id)
|
|
79
|
+
secret_value = value_resp.get("SecretString")
|
|
80
|
+
raw_binary = value_resp.get("SecretBinary")
|
|
81
|
+
if raw_binary is not None:
|
|
82
|
+
secret_binary = base64.b64encode(raw_binary).decode("utf-8")
|
|
83
|
+
version_id = value_resp.get("VersionId")
|
|
84
|
+
version_stages = value_resp.get("VersionStages")
|
|
85
|
+
except Exception:
|
|
86
|
+
# Value may not be retrievable (e.g., pending deletion)
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
"name": meta.get("Name"),
|
|
91
|
+
"arn": meta.get("ARN"),
|
|
92
|
+
"description": meta.get("Description", ""),
|
|
93
|
+
"createdDate": _format_date(meta.get("CreatedDate")),
|
|
94
|
+
"lastChangedDate": _format_date(meta.get("LastChangedDate")),
|
|
95
|
+
"lastAccessedDate": _format_date(meta.get("LastAccessedDate")),
|
|
96
|
+
"rotationEnabled": meta.get("RotationEnabled", False),
|
|
97
|
+
"rotationRules": meta.get("RotationRules"),
|
|
98
|
+
"rotationLambdaARN": meta.get("RotationLambdaARN"),
|
|
99
|
+
"deletedDate": _format_date(meta.get("DeletedDate")),
|
|
100
|
+
"tags": {
|
|
101
|
+
tag["Key"]: tag["Value"]
|
|
102
|
+
for tag in meta.get("Tags", [])
|
|
103
|
+
},
|
|
104
|
+
"versionId": version_id,
|
|
105
|
+
"versionStages": version_stages,
|
|
106
|
+
"secretValue": secret_value,
|
|
107
|
+
"secretBinary": secret_binary,
|
|
108
|
+
}
|
|
109
|
+
except HTTPException:
|
|
110
|
+
raise
|
|
111
|
+
except Exception as e:
|
|
112
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stackport
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: Universal AWS resource browser for local emulators
|
|
5
5
|
Author: Davi Reis Vieira
|
|
6
6
|
License: MIT
|
|
@@ -50,13 +50,13 @@ Dynamic: license-file
|
|
|
50
50
|
## Screenshots
|
|
51
51
|
|
|
52
52
|
**Dashboard** — Service overview with resource counts and health status
|
|
53
|
-

|
|
53
|
+

|
|
54
54
|
|
|
55
55
|
**DynamoDB Browser** — Generic resource table with search, pagination, and detail view
|
|
56
|
-

|
|
56
|
+

|
|
57
57
|
|
|
58
58
|
**S3 Browser** — File browser with folder navigation and object preview
|
|
59
|
-

|
|
59
|
+

|
|
60
60
|
|
|
61
61
|
## Features
|
|
62
62
|
|
|
@@ -10,8 +10,8 @@ backend/main.py
|
|
|
10
10
|
backend/../ui/dist/favicon.png
|
|
11
11
|
backend/../ui/dist/favicon.svg
|
|
12
12
|
backend/../ui/dist/index.html
|
|
13
|
-
backend/../ui/dist/assets/index-
|
|
14
|
-
backend/../ui/dist/assets/index-
|
|
13
|
+
backend/../ui/dist/assets/index-BAMXqPoR.js
|
|
14
|
+
backend/../ui/dist/assets/index-CU1gzmix.css
|
|
15
15
|
backend/../ui/dist/aws-icons/acm.svg
|
|
16
16
|
backend/../ui/dist/aws-icons/apigateway.svg
|
|
17
17
|
backend/../ui/dist/aws-icons/appsync.svg
|
|
@@ -54,6 +54,7 @@ backend/routes/lambda_svc.py
|
|
|
54
54
|
backend/routes/logs.py
|
|
55
55
|
backend/routes/resources.py
|
|
56
56
|
backend/routes/s3.py
|
|
57
|
+
backend/routes/secretsmanager.py
|
|
57
58
|
backend/routes/sqs.py
|
|
58
59
|
backend/routes/stats.py
|
|
59
60
|
stackport.egg-info/PKG-INFO
|
|
@@ -72,12 +73,13 @@ tests/test_lambda_routes.py
|
|
|
72
73
|
tests/test_logs_routes.py
|
|
73
74
|
tests/test_registries.py
|
|
74
75
|
tests/test_routes.py
|
|
76
|
+
tests/test_secretsmanager_routes.py
|
|
75
77
|
tests/test_sqs_routes.py
|
|
76
78
|
ui/dist/favicon.png
|
|
77
79
|
ui/dist/favicon.svg
|
|
78
80
|
ui/dist/index.html
|
|
79
|
-
ui/dist/assets/index-
|
|
80
|
-
ui/dist/assets/index-
|
|
81
|
+
ui/dist/assets/index-BAMXqPoR.js
|
|
82
|
+
ui/dist/assets/index-CU1gzmix.css
|
|
81
83
|
ui/dist/aws-icons/acm.svg
|
|
82
84
|
ui/dist/aws-icons/apigateway.svg
|
|
83
85
|
ui/dist/aws-icons/appsync.svg
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""Integration tests for Secrets Manager API routes."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
os.environ.setdefault("AWS_ENDPOINT_URL", "http://localhost:4566")
|
|
6
|
+
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from unittest.mock import MagicMock, patch
|
|
9
|
+
|
|
10
|
+
from fastapi.testclient import TestClient
|
|
11
|
+
|
|
12
|
+
from backend.main import app
|
|
13
|
+
|
|
14
|
+
client = TestClient(app)
|
|
15
|
+
|
|
16
|
+
CREATED = datetime(2025, 1, 15, 10, 30, 0, tzinfo=timezone.utc)
|
|
17
|
+
CHANGED = datetime(2025, 3, 20, 14, 0, 0, tzinfo=timezone.utc)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestListSecrets:
|
|
21
|
+
@patch("backend.routes.secretsmanager.get_client")
|
|
22
|
+
def test_list_secrets_empty(self, mock_get_client):
|
|
23
|
+
mock_sm = MagicMock()
|
|
24
|
+
mock_get_client.return_value = mock_sm
|
|
25
|
+
paginator = MagicMock()
|
|
26
|
+
mock_sm.get_paginator.return_value = paginator
|
|
27
|
+
paginator.paginate.return_value = [{"SecretList": []}]
|
|
28
|
+
|
|
29
|
+
resp = client.get("/api/secretsmanager/secrets")
|
|
30
|
+
assert resp.status_code == 200
|
|
31
|
+
data = resp.json()
|
|
32
|
+
assert data["secrets"] == []
|
|
33
|
+
|
|
34
|
+
@patch("backend.routes.secretsmanager.get_client")
|
|
35
|
+
def test_list_secrets_with_data(self, mock_get_client):
|
|
36
|
+
mock_sm = MagicMock()
|
|
37
|
+
mock_get_client.return_value = mock_sm
|
|
38
|
+
paginator = MagicMock()
|
|
39
|
+
mock_sm.get_paginator.return_value = paginator
|
|
40
|
+
paginator.paginate.return_value = [
|
|
41
|
+
{
|
|
42
|
+
"SecretList": [
|
|
43
|
+
{
|
|
44
|
+
"Name": "prod/db-password",
|
|
45
|
+
"ARN": "arn:aws:secretsmanager:us-east-1:000:secret:prod/db-password-abc",
|
|
46
|
+
"Description": "Production database password",
|
|
47
|
+
"CreatedDate": CREATED,
|
|
48
|
+
"LastChangedDate": CHANGED,
|
|
49
|
+
"LastAccessedDate": None,
|
|
50
|
+
"RotationEnabled": False,
|
|
51
|
+
"Tags": [
|
|
52
|
+
{"Key": "env", "Value": "prod"},
|
|
53
|
+
{"Key": "team", "Value": "backend"},
|
|
54
|
+
],
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
resp = client.get("/api/secretsmanager/secrets")
|
|
61
|
+
assert resp.status_code == 200
|
|
62
|
+
data = resp.json()
|
|
63
|
+
assert len(data["secrets"]) == 1
|
|
64
|
+
s = data["secrets"][0]
|
|
65
|
+
assert s["name"] == "prod/db-password"
|
|
66
|
+
assert s["arn"] == "arn:aws:secretsmanager:us-east-1:000:secret:prod/db-password-abc"
|
|
67
|
+
assert s["description"] == "Production database password"
|
|
68
|
+
assert s["createdDate"] == CREATED.isoformat()
|
|
69
|
+
assert s["lastChangedDate"] == CHANGED.isoformat()
|
|
70
|
+
assert s["lastAccessedDate"] is None
|
|
71
|
+
assert s["rotationEnabled"] is False
|
|
72
|
+
assert s["tags"] == {"env": "prod", "team": "backend"}
|
|
73
|
+
|
|
74
|
+
@patch("backend.routes.secretsmanager.get_client")
|
|
75
|
+
def test_list_secrets_no_tags(self, mock_get_client):
|
|
76
|
+
mock_sm = MagicMock()
|
|
77
|
+
mock_get_client.return_value = mock_sm
|
|
78
|
+
paginator = MagicMock()
|
|
79
|
+
mock_sm.get_paginator.return_value = paginator
|
|
80
|
+
paginator.paginate.return_value = [
|
|
81
|
+
{
|
|
82
|
+
"SecretList": [
|
|
83
|
+
{
|
|
84
|
+
"Name": "my-secret",
|
|
85
|
+
"ARN": "arn:aws:secretsmanager:us-east-1:000:secret:my-secret-xyz",
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
resp = client.get("/api/secretsmanager/secrets")
|
|
92
|
+
assert resp.status_code == 200
|
|
93
|
+
s = resp.json()["secrets"][0]
|
|
94
|
+
assert s["tags"] == {}
|
|
95
|
+
assert s["description"] == ""
|
|
96
|
+
assert s["rotationEnabled"] is False
|
|
97
|
+
|
|
98
|
+
@patch("backend.routes.secretsmanager.get_client")
|
|
99
|
+
def test_list_secrets_multiple_pages(self, mock_get_client):
|
|
100
|
+
mock_sm = MagicMock()
|
|
101
|
+
mock_get_client.return_value = mock_sm
|
|
102
|
+
paginator = MagicMock()
|
|
103
|
+
mock_sm.get_paginator.return_value = paginator
|
|
104
|
+
paginator.paginate.return_value = [
|
|
105
|
+
{"SecretList": [{"Name": "secret-1", "ARN": "arn:1"}]},
|
|
106
|
+
{"SecretList": [{"Name": "secret-2", "ARN": "arn:2"}]},
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
resp = client.get("/api/secretsmanager/secrets")
|
|
110
|
+
assert resp.status_code == 200
|
|
111
|
+
data = resp.json()
|
|
112
|
+
assert len(data["secrets"]) == 2
|
|
113
|
+
assert data["secrets"][0]["name"] == "secret-1"
|
|
114
|
+
assert data["secrets"][1]["name"] == "secret-2"
|
|
115
|
+
|
|
116
|
+
@patch("backend.routes.secretsmanager.get_client")
|
|
117
|
+
def test_list_secrets_rotation_enabled(self, mock_get_client):
|
|
118
|
+
mock_sm = MagicMock()
|
|
119
|
+
mock_get_client.return_value = mock_sm
|
|
120
|
+
paginator = MagicMock()
|
|
121
|
+
mock_sm.get_paginator.return_value = paginator
|
|
122
|
+
paginator.paginate.return_value = [
|
|
123
|
+
{
|
|
124
|
+
"SecretList": [
|
|
125
|
+
{
|
|
126
|
+
"Name": "rotated-secret",
|
|
127
|
+
"ARN": "arn:rotated",
|
|
128
|
+
"RotationEnabled": True,
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
resp = client.get("/api/secretsmanager/secrets")
|
|
135
|
+
assert resp.status_code == 200
|
|
136
|
+
assert resp.json()["secrets"][0]["rotationEnabled"] is True
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class TestGetSecretDetail:
|
|
140
|
+
@patch("backend.routes.secretsmanager.get_client")
|
|
141
|
+
def test_get_secret_with_string_value(self, mock_get_client):
|
|
142
|
+
mock_sm = MagicMock()
|
|
143
|
+
mock_get_client.return_value = mock_sm
|
|
144
|
+
mock_sm.describe_secret.return_value = {
|
|
145
|
+
"Name": "prod/db-password",
|
|
146
|
+
"ARN": "arn:aws:secretsmanager:us-east-1:000:secret:prod/db-password-abc",
|
|
147
|
+
"Description": "Production database password",
|
|
148
|
+
"CreatedDate": CREATED,
|
|
149
|
+
"LastChangedDate": CHANGED,
|
|
150
|
+
"LastAccessedDate": None,
|
|
151
|
+
"RotationEnabled": False,
|
|
152
|
+
"RotationRules": None,
|
|
153
|
+
"RotationLambdaARN": None,
|
|
154
|
+
"DeletedDate": None,
|
|
155
|
+
"Tags": [{"Key": "env", "Value": "prod"}],
|
|
156
|
+
}
|
|
157
|
+
mock_sm.get_secret_value.return_value = {
|
|
158
|
+
"SecretString": '{"username":"admin","password":"s3cret"}',
|
|
159
|
+
"VersionId": "ver-001",
|
|
160
|
+
"VersionStages": ["AWSCURRENT"],
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
resp = client.get("/api/secretsmanager/secrets/prod%2Fdb-password")
|
|
164
|
+
assert resp.status_code == 200
|
|
165
|
+
data = resp.json()
|
|
166
|
+
assert data["name"] == "prod/db-password"
|
|
167
|
+
assert data["description"] == "Production database password"
|
|
168
|
+
assert data["secretValue"] == '{"username":"admin","password":"s3cret"}'
|
|
169
|
+
assert data["secretBinary"] is None
|
|
170
|
+
assert data["versionId"] == "ver-001"
|
|
171
|
+
assert data["versionStages"] == ["AWSCURRENT"]
|
|
172
|
+
assert data["tags"] == {"env": "prod"}
|
|
173
|
+
assert data["createdDate"] == CREATED.isoformat()
|
|
174
|
+
assert data["rotationEnabled"] is False
|
|
175
|
+
|
|
176
|
+
@patch("backend.routes.secretsmanager.get_client")
|
|
177
|
+
def test_get_secret_with_binary_value(self, mock_get_client):
|
|
178
|
+
mock_sm = MagicMock()
|
|
179
|
+
mock_get_client.return_value = mock_sm
|
|
180
|
+
mock_sm.describe_secret.return_value = {
|
|
181
|
+
"Name": "binary-secret",
|
|
182
|
+
"ARN": "arn:binary",
|
|
183
|
+
"Tags": [],
|
|
184
|
+
}
|
|
185
|
+
raw_bytes = b"\x00\x01\x02\x03\xff"
|
|
186
|
+
mock_sm.get_secret_value.return_value = {
|
|
187
|
+
"SecretBinary": raw_bytes,
|
|
188
|
+
"VersionId": "ver-bin",
|
|
189
|
+
"VersionStages": ["AWSCURRENT"],
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
resp = client.get("/api/secretsmanager/secrets/binary-secret")
|
|
193
|
+
assert resp.status_code == 200
|
|
194
|
+
data = resp.json()
|
|
195
|
+
assert data["secretValue"] is None
|
|
196
|
+
import base64
|
|
197
|
+
assert data["secretBinary"] == base64.b64encode(raw_bytes).decode("utf-8")
|
|
198
|
+
assert data["versionId"] == "ver-bin"
|
|
199
|
+
|
|
200
|
+
@patch("backend.routes.secretsmanager.get_client")
|
|
201
|
+
def test_get_secret_not_found(self, mock_get_client):
|
|
202
|
+
mock_sm = MagicMock()
|
|
203
|
+
mock_get_client.return_value = mock_sm
|
|
204
|
+
mock_sm.exceptions.ResourceNotFoundException = type(
|
|
205
|
+
"ResourceNotFoundException", (Exception,), {}
|
|
206
|
+
)
|
|
207
|
+
mock_sm.describe_secret.side_effect = (
|
|
208
|
+
mock_sm.exceptions.ResourceNotFoundException()
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
resp = client.get("/api/secretsmanager/secrets/nonexistent")
|
|
212
|
+
assert resp.status_code == 404
|
|
213
|
+
|
|
214
|
+
@patch("backend.routes.secretsmanager.get_client")
|
|
215
|
+
def test_get_secret_value_not_retrievable(self, mock_get_client):
|
|
216
|
+
"""When get_secret_value fails (e.g. pending deletion), metadata is still returned."""
|
|
217
|
+
mock_sm = MagicMock()
|
|
218
|
+
mock_get_client.return_value = mock_sm
|
|
219
|
+
mock_sm.describe_secret.return_value = {
|
|
220
|
+
"Name": "deleted-secret",
|
|
221
|
+
"ARN": "arn:deleted",
|
|
222
|
+
"DeletedDate": CHANGED,
|
|
223
|
+
"Tags": [],
|
|
224
|
+
}
|
|
225
|
+
mock_sm.get_secret_value.side_effect = Exception("marked for deletion")
|
|
226
|
+
|
|
227
|
+
resp = client.get("/api/secretsmanager/secrets/deleted-secret")
|
|
228
|
+
assert resp.status_code == 200
|
|
229
|
+
data = resp.json()
|
|
230
|
+
assert data["name"] == "deleted-secret"
|
|
231
|
+
assert data["secretValue"] is None
|
|
232
|
+
assert data["secretBinary"] is None
|
|
233
|
+
assert data["versionId"] is None
|
|
234
|
+
assert data["versionStages"] is None
|
|
235
|
+
assert data["deletedDate"] == CHANGED.isoformat()
|
|
236
|
+
|
|
237
|
+
@patch("backend.routes.secretsmanager.get_client")
|
|
238
|
+
def test_get_secret_with_rotation(self, mock_get_client):
|
|
239
|
+
mock_sm = MagicMock()
|
|
240
|
+
mock_get_client.return_value = mock_sm
|
|
241
|
+
mock_sm.describe_secret.return_value = {
|
|
242
|
+
"Name": "rotated-secret",
|
|
243
|
+
"ARN": "arn:rotated",
|
|
244
|
+
"RotationEnabled": True,
|
|
245
|
+
"RotationRules": {"AutomaticallyAfterDays": 30},
|
|
246
|
+
"RotationLambdaARN": "arn:aws:lambda:us-east-1:000:function:rotate-fn",
|
|
247
|
+
"Tags": [],
|
|
248
|
+
}
|
|
249
|
+
mock_sm.get_secret_value.return_value = {
|
|
250
|
+
"SecretString": "rotated-value",
|
|
251
|
+
"VersionId": "ver-rot",
|
|
252
|
+
"VersionStages": ["AWSCURRENT"],
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
resp = client.get("/api/secretsmanager/secrets/rotated-secret")
|
|
256
|
+
assert resp.status_code == 200
|
|
257
|
+
data = resp.json()
|
|
258
|
+
assert data["rotationEnabled"] is True
|
|
259
|
+
assert data["rotationRules"] == {"AutomaticallyAfterDays": 30}
|
|
260
|
+
assert data["rotationLambdaARN"] == "arn:aws:lambda:us-east-1:000:function:rotate-fn"
|
|
261
|
+
assert data["secretValue"] == "rotated-value"
|
|
262
|
+
|
|
263
|
+
@patch("backend.routes.secretsmanager.get_client")
|
|
264
|
+
def test_get_secret_plain_text_value(self, mock_get_client):
|
|
265
|
+
mock_sm = MagicMock()
|
|
266
|
+
mock_get_client.return_value = mock_sm
|
|
267
|
+
mock_sm.describe_secret.return_value = {
|
|
268
|
+
"Name": "api-key",
|
|
269
|
+
"ARN": "arn:api-key",
|
|
270
|
+
"Tags": [],
|
|
271
|
+
}
|
|
272
|
+
mock_sm.get_secret_value.return_value = {
|
|
273
|
+
"SecretString": "sk-abc123def456",
|
|
274
|
+
"VersionId": "ver-plain",
|
|
275
|
+
"VersionStages": ["AWSCURRENT"],
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
resp = client.get("/api/secretsmanager/secrets/api-key")
|
|
279
|
+
assert resp.status_code == 200
|
|
280
|
+
data = resp.json()
|
|
281
|
+
assert data["secretValue"] == "sk-abc123def456"
|