stackport 0.3.0__tar.gz → 0.3.1__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 (104) hide show
  1. {stackport-0.3.0/stackport.egg-info → stackport-0.3.1}/PKG-INFO +1 -1
  2. stackport-0.3.1/backend/routes/secretsmanager.py +256 -0
  3. stackport-0.3.1/backend/schemas/secretsmanager.py +33 -0
  4. {stackport-0.3.0 → stackport-0.3.1}/pyproject.toml +1 -1
  5. {stackport-0.3.0 → stackport-0.3.1/stackport.egg-info}/PKG-INFO +1 -1
  6. {stackport-0.3.0 → stackport-0.3.1}/stackport.egg-info/SOURCES.txt +5 -4
  7. {stackport-0.3.0 → stackport-0.3.1}/tests/test_secretsmanager_routes.py +227 -0
  8. stackport-0.3.1/ui/dist/assets/index-CAZXHF4B.js +639 -0
  9. stackport-0.3.1/ui/dist/assets/index-DLd-xBZq.css +1 -0
  10. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/index.html +2 -2
  11. stackport-0.3.0/backend/routes/secretsmanager.py +0 -113
  12. stackport-0.3.0/ui/dist/assets/index-Cda_0qu5.css +0 -1
  13. stackport-0.3.0/ui/dist/assets/index-tW-8e4eS.js +0 -629
  14. {stackport-0.3.0 → stackport-0.3.1}/LICENSE +0 -0
  15. {stackport-0.3.0 → stackport-0.3.1}/MANIFEST.in +0 -0
  16. {stackport-0.3.0 → stackport-0.3.1}/README.md +0 -0
  17. {stackport-0.3.0 → stackport-0.3.1}/backend/__init__.py +0 -0
  18. {stackport-0.3.0 → stackport-0.3.1}/backend/aws_client.py +0 -0
  19. {stackport-0.3.0 → stackport-0.3.1}/backend/cache.py +0 -0
  20. {stackport-0.3.0 → stackport-0.3.1}/backend/cli.py +0 -0
  21. {stackport-0.3.0 → stackport-0.3.1}/backend/config.py +0 -0
  22. {stackport-0.3.0 → stackport-0.3.1}/backend/endpoint_store.py +0 -0
  23. {stackport-0.3.0 → stackport-0.3.1}/backend/main.py +0 -0
  24. {stackport-0.3.0 → stackport-0.3.1}/backend/routes/__init__.py +0 -0
  25. {stackport-0.3.0 → stackport-0.3.1}/backend/routes/common.py +0 -0
  26. {stackport-0.3.0 → stackport-0.3.1}/backend/routes/dynamodb.py +0 -0
  27. {stackport-0.3.0 → stackport-0.3.1}/backend/routes/ec2.py +0 -0
  28. {stackport-0.3.0 → stackport-0.3.1}/backend/routes/endpoints.py +0 -0
  29. {stackport-0.3.0 → stackport-0.3.1}/backend/routes/iam.py +0 -0
  30. {stackport-0.3.0 → stackport-0.3.1}/backend/routes/lambda_svc.py +0 -0
  31. {stackport-0.3.0 → stackport-0.3.1}/backend/routes/logs.py +0 -0
  32. {stackport-0.3.0 → stackport-0.3.1}/backend/routes/resources.py +0 -0
  33. {stackport-0.3.0 → stackport-0.3.1}/backend/routes/s3.py +0 -0
  34. {stackport-0.3.0 → stackport-0.3.1}/backend/routes/sqs.py +0 -0
  35. {stackport-0.3.0 → stackport-0.3.1}/backend/routes/stats.py +0 -0
  36. {stackport-0.3.0 → stackport-0.3.1}/backend/routes/tags.py +0 -0
  37. {stackport-0.3.0 → stackport-0.3.1}/backend/schemas/__init__.py +0 -0
  38. {stackport-0.3.0 → stackport-0.3.1}/backend/schemas/dynamodb.py +0 -0
  39. {stackport-0.3.0 → stackport-0.3.1}/backend/schemas/endpoints.py +0 -0
  40. {stackport-0.3.0 → stackport-0.3.1}/backend/schemas/s3.py +0 -0
  41. {stackport-0.3.0 → stackport-0.3.1}/backend/schemas/sqs.py +0 -0
  42. {stackport-0.3.0 → stackport-0.3.1}/backend/schemas/tags.py +0 -0
  43. {stackport-0.3.0 → stackport-0.3.1}/backend/websocket.py +0 -0
  44. {stackport-0.3.0 → stackport-0.3.1}/setup.cfg +0 -0
  45. {stackport-0.3.0 → stackport-0.3.1}/stackport.egg-info/dependency_links.txt +0 -0
  46. {stackport-0.3.0 → stackport-0.3.1}/stackport.egg-info/entry_points.txt +0 -0
  47. {stackport-0.3.0 → stackport-0.3.1}/stackport.egg-info/requires.txt +0 -0
  48. {stackport-0.3.0 → stackport-0.3.1}/stackport.egg-info/top_level.txt +0 -0
  49. {stackport-0.3.0 → stackport-0.3.1}/tests/test_cache.py +0 -0
  50. {stackport-0.3.0 → stackport-0.3.1}/tests/test_cli.py +0 -0
  51. {stackport-0.3.0 → stackport-0.3.1}/tests/test_client.py +0 -0
  52. {stackport-0.3.0 → stackport-0.3.1}/tests/test_config.py +0 -0
  53. {stackport-0.3.0 → stackport-0.3.1}/tests/test_dynamodb_routes.py +0 -0
  54. {stackport-0.3.0 → stackport-0.3.1}/tests/test_ec2_routes.py +0 -0
  55. {stackport-0.3.0 → stackport-0.3.1}/tests/test_endpoint_store.py +0 -0
  56. {stackport-0.3.0 → stackport-0.3.1}/tests/test_endpoints.py +0 -0
  57. {stackport-0.3.0 → stackport-0.3.1}/tests/test_iam_routes.py +0 -0
  58. {stackport-0.3.0 → stackport-0.3.1}/tests/test_lambda_routes.py +0 -0
  59. {stackport-0.3.0 → stackport-0.3.1}/tests/test_logs_routes.py +0 -0
  60. {stackport-0.3.0 → stackport-0.3.1}/tests/test_multi_endpoint.py +0 -0
  61. {stackport-0.3.0 → stackport-0.3.1}/tests/test_readonly_middleware.py +0 -0
  62. {stackport-0.3.0 → stackport-0.3.1}/tests/test_registries.py +0 -0
  63. {stackport-0.3.0 → stackport-0.3.1}/tests/test_routes.py +0 -0
  64. {stackport-0.3.0 → stackport-0.3.1}/tests/test_s3_routes.py +0 -0
  65. {stackport-0.3.0 → stackport-0.3.1}/tests/test_s3_upload_limit_env.py +0 -0
  66. {stackport-0.3.0 → stackport-0.3.1}/tests/test_sqs_routes.py +0 -0
  67. {stackport-0.3.0 → stackport-0.3.1}/tests/test_tags_routes.py +0 -0
  68. {stackport-0.3.0 → stackport-0.3.1}/tests/test_websocket.py +0 -0
  69. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/acm.svg +0 -0
  70. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/apigateway.svg +0 -0
  71. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/appsync.svg +0 -0
  72. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/athena.svg +0 -0
  73. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/cloudformation.svg +0 -0
  74. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/cloudfront.svg +0 -0
  75. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/cognito-idp.svg +0 -0
  76. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/dynamodb.svg +0 -0
  77. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/ec2.svg +0 -0
  78. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/ecr.svg +0 -0
  79. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/ecs.svg +0 -0
  80. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/elasticache.svg +0 -0
  81. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/elasticfilesystem.svg +0 -0
  82. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/elasticloadbalancing.svg +0 -0
  83. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/elasticmapreduce.svg +0 -0
  84. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/events.svg +0 -0
  85. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/firehose.svg +0 -0
  86. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/glue.svg +0 -0
  87. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/iam.svg +0 -0
  88. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/kinesis.svg +0 -0
  89. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/kms.svg +0 -0
  90. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/lambda.svg +0 -0
  91. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/logs.svg +0 -0
  92. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/monitoring.svg +0 -0
  93. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/rds.svg +0 -0
  94. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/route53.svg +0 -0
  95. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/s3.svg +0 -0
  96. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/secretsmanager.svg +0 -0
  97. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/ses.svg +0 -0
  98. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/sns.svg +0 -0
  99. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/sqs.svg +0 -0
  100. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/ssm.svg +0 -0
  101. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/stepfunctions.svg +0 -0
  102. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/aws-icons/wafv2.svg +0 -0
  103. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/favicon.png +0 -0
  104. {stackport-0.3.0 → stackport-0.3.1}/ui/dist/favicon.svg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackport
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Universal AWS resource browser for local emulators
5
5
  Author: Davi Reis Vieira
6
6
  License: MIT
@@ -0,0 +1,256 @@
1
+ """Secrets Manager service-specific routes."""
2
+
3
+ import base64
4
+ from typing import Any
5
+
6
+ from fastapi import APIRouter, Depends, HTTPException, Query
7
+
8
+ from backend.aws_client import get_client
9
+ from backend.routes.common import EndpointInfo, get_endpoint_info
10
+ from backend.schemas.secretsmanager import (
11
+ CreateSecretBody,
12
+ UpdateSecretMetadataBody,
13
+ UpdateSecretValueBody,
14
+ )
15
+
16
+ router = APIRouter()
17
+
18
+
19
+ def _format_date(dt) -> str | None:
20
+ """Format a datetime to ISO string, or return None."""
21
+ if dt is None:
22
+ return None
23
+ try:
24
+ return dt.isoformat()
25
+ except Exception:
26
+ return str(dt)
27
+
28
+
29
+ @router.get("/secrets")
30
+ def list_secrets(ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
31
+ """List all secrets with metadata."""
32
+ try:
33
+ client = get_client("secretsmanager", **ep.client_kwargs())
34
+ paginator = client.get_paginator("list_secrets")
35
+
36
+ secrets = []
37
+ for page in paginator.paginate():
38
+ for secret in page.get("SecretList", []):
39
+ secrets.append(
40
+ {
41
+ "name": secret.get("Name"),
42
+ "arn": secret.get("ARN"),
43
+ "description": secret.get("Description", ""),
44
+ "createdDate": _format_date(secret.get("CreatedDate")),
45
+ "lastChangedDate": _format_date(
46
+ secret.get("LastChangedDate")
47
+ ),
48
+ "lastAccessedDate": _format_date(
49
+ secret.get("LastAccessedDate")
50
+ ),
51
+ "rotationEnabled": secret.get("RotationEnabled", False),
52
+ "tags": {
53
+ tag["Key"]: tag["Value"]
54
+ for tag in secret.get("Tags", [])
55
+ },
56
+ }
57
+ )
58
+
59
+ return {"secrets": secrets}
60
+ except Exception as e:
61
+ raise HTTPException(status_code=500, detail=str(e))
62
+
63
+
64
+ @router.get("/secrets/{secret_id:path}")
65
+ def get_secret_detail(secret_id: str, ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
66
+ """Get secret metadata and value."""
67
+ try:
68
+ client = get_client("secretsmanager", **ep.client_kwargs())
69
+
70
+ # Get metadata
71
+ try:
72
+ meta = client.describe_secret(SecretId=secret_id)
73
+ except client.exceptions.ResourceNotFoundException:
74
+ raise HTTPException(
75
+ status_code=404, detail=f"Secret '{secret_id}' not found"
76
+ )
77
+
78
+ # Get value
79
+ secret_value = None
80
+ secret_binary = None
81
+ version_id = None
82
+ version_stages = None
83
+ try:
84
+ value_resp = client.get_secret_value(SecretId=secret_id)
85
+ secret_value = value_resp.get("SecretString")
86
+ raw_binary = value_resp.get("SecretBinary")
87
+ if raw_binary is not None:
88
+ secret_binary = base64.b64encode(raw_binary).decode("utf-8")
89
+ version_id = value_resp.get("VersionId")
90
+ version_stages = value_resp.get("VersionStages")
91
+ except Exception:
92
+ # Value may not be retrievable (e.g., pending deletion)
93
+ pass
94
+
95
+ return {
96
+ "name": meta.get("Name"),
97
+ "arn": meta.get("ARN"),
98
+ "description": meta.get("Description", ""),
99
+ "createdDate": _format_date(meta.get("CreatedDate")),
100
+ "lastChangedDate": _format_date(meta.get("LastChangedDate")),
101
+ "lastAccessedDate": _format_date(meta.get("LastAccessedDate")),
102
+ "rotationEnabled": meta.get("RotationEnabled", False),
103
+ "rotationRules": meta.get("RotationRules"),
104
+ "rotationLambdaARN": meta.get("RotationLambdaARN"),
105
+ "deletedDate": _format_date(meta.get("DeletedDate")),
106
+ "tags": {
107
+ tag["Key"]: tag["Value"]
108
+ for tag in meta.get("Tags", [])
109
+ },
110
+ "versionId": version_id,
111
+ "versionStages": version_stages,
112
+ "secretValue": secret_value,
113
+ "secretBinary": secret_binary,
114
+ }
115
+ except HTTPException:
116
+ raise
117
+ except Exception as e:
118
+ raise HTTPException(status_code=500, detail=str(e))
119
+
120
+
121
+ @router.post("/secrets", status_code=201)
122
+ def create_secret(body: CreateSecretBody, ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
123
+ """Create a new secret."""
124
+ if not body.secret_string and not body.secret_binary:
125
+ raise HTTPException(status_code=400, detail="Must provide either secret_string or secret_binary")
126
+
127
+ try:
128
+ client = get_client("secretsmanager", **ep.client_kwargs())
129
+
130
+ kwargs: dict[str, Any] = {"Name": body.name}
131
+ if body.description:
132
+ kwargs["Description"] = body.description
133
+ if body.secret_string:
134
+ kwargs["SecretString"] = body.secret_string
135
+ if body.secret_binary:
136
+ kwargs["SecretBinary"] = base64.b64decode(body.secret_binary)
137
+ if body.tags:
138
+ kwargs["Tags"] = [{"Key": k, "Value": v} for k, v in body.tags.items()]
139
+
140
+ resp = client.create_secret(**kwargs)
141
+ return {
142
+ "name": resp["Name"],
143
+ "arn": resp["ARN"],
144
+ "versionId": resp.get("VersionId"),
145
+ }
146
+ except client.exceptions.ResourceExistsException:
147
+ raise HTTPException(status_code=409, detail=f"Secret '{body.name}' already exists")
148
+ except Exception as e:
149
+ raise HTTPException(status_code=500, detail=str(e))
150
+
151
+
152
+ @router.put("/secrets/{secret_id:path}/value")
153
+ def update_secret_value(secret_id: str, body: UpdateSecretValueBody, ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
154
+ """Update a secret's value."""
155
+ if not body.secret_string and not body.secret_binary:
156
+ raise HTTPException(status_code=400, detail="Must provide either secret_string or secret_binary")
157
+
158
+ try:
159
+ client = get_client("secretsmanager", **ep.client_kwargs())
160
+
161
+ kwargs: dict[str, Any] = {"SecretId": secret_id}
162
+ if body.secret_string:
163
+ kwargs["SecretString"] = body.secret_string
164
+ if body.secret_binary:
165
+ kwargs["SecretBinary"] = base64.b64decode(body.secret_binary)
166
+
167
+ resp = client.put_secret_value(**kwargs)
168
+ return {
169
+ "arn": resp["ARN"],
170
+ "name": resp["Name"],
171
+ "versionId": resp["VersionId"],
172
+ "versionStages": resp.get("VersionStages"),
173
+ }
174
+ except client.exceptions.ResourceNotFoundException:
175
+ raise HTTPException(status_code=404, detail=f"Secret '{secret_id}' not found")
176
+ except Exception as e:
177
+ raise HTTPException(status_code=500, detail=str(e))
178
+
179
+
180
+ @router.put("/secrets/{secret_id:path}/metadata")
181
+ def update_secret_metadata(secret_id: str, body: UpdateSecretMetadataBody, ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
182
+ """Update a secret's description and/or tags."""
183
+ try:
184
+ client = get_client("secretsmanager", **ep.client_kwargs())
185
+
186
+ # Update description
187
+ if body.description is not None:
188
+ client.update_secret(SecretId=secret_id, Description=body.description)
189
+
190
+ # Update tags (replace all)
191
+ if body.tags is not None:
192
+ arn = client.describe_secret(SecretId=secret_id)["ARN"]
193
+ # Remove all existing tags first
194
+ existing_tags_resp = client.describe_secret(SecretId=secret_id)
195
+ existing_tag_keys = [tag["Key"] for tag in existing_tags_resp.get("Tags", [])]
196
+ if existing_tag_keys:
197
+ client.untag_resource(SecretId=arn, TagKeys=existing_tag_keys)
198
+ # Add new tags
199
+ if body.tags:
200
+ client.tag_resource(
201
+ SecretId=arn,
202
+ Tags=[{"Key": k, "Value": v} for k, v in body.tags.items()],
203
+ )
204
+
205
+ return {"success": True, "message": "Metadata updated"}
206
+ except client.exceptions.ResourceNotFoundException:
207
+ raise HTTPException(status_code=404, detail=f"Secret '{secret_id}' not found")
208
+ except Exception as e:
209
+ raise HTTPException(status_code=500, detail=str(e))
210
+
211
+
212
+ @router.delete("/secrets/{secret_id:path}")
213
+ def delete_secret(
214
+ secret_id: str,
215
+ force: bool = Query(False, description="Skip recovery window (immediate deletion)"),
216
+ ep: EndpointInfo = Depends(get_endpoint_info),
217
+ ) -> dict[str, Any]:
218
+ """Delete a secret."""
219
+ try:
220
+ client = get_client("secretsmanager", **ep.client_kwargs())
221
+
222
+ kwargs: dict[str, Any] = {"SecretId": secret_id}
223
+ if force:
224
+ kwargs["ForceDeleteWithoutRecovery"] = True
225
+ else:
226
+ kwargs["RecoveryWindowInDays"] = 7
227
+
228
+ resp = client.delete_secret(**kwargs)
229
+ return {
230
+ "arn": resp["ARN"],
231
+ "name": resp["Name"],
232
+ "deletionDate": _format_date(resp.get("DeletionDate")),
233
+ }
234
+ except client.exceptions.ResourceNotFoundException:
235
+ raise HTTPException(status_code=404, detail=f"Secret '{secret_id}' not found")
236
+ except Exception as e:
237
+ raise HTTPException(status_code=500, detail=str(e))
238
+
239
+
240
+ @router.post("/secrets/{secret_id:path}/restore")
241
+ def restore_secret(secret_id: str, ep: EndpointInfo = Depends(get_endpoint_info)) -> dict[str, Any]:
242
+ """Restore a deleted secret."""
243
+ try:
244
+ client = get_client("secretsmanager", **ep.client_kwargs())
245
+ resp = client.restore_secret(SecretId=secret_id)
246
+ return {
247
+ "arn": resp["ARN"],
248
+ "name": resp["Name"],
249
+ }
250
+ except Exception as e:
251
+ error_code = getattr(e, "response", {}).get("Error", {}).get("Code", "")
252
+ if error_code == "ResourceNotFoundException":
253
+ raise HTTPException(status_code=404, detail=f"Secret '{secret_id}' not found")
254
+ elif error_code == "InvalidRequestException":
255
+ raise HTTPException(status_code=400, detail=str(e))
256
+ raise HTTPException(status_code=500, detail=str(e))
@@ -0,0 +1,33 @@
1
+ """Pydantic schemas for Secrets Manager API requests."""
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+
6
+ class CreateSecretBody(BaseModel):
7
+ """Request body for creating a new secret."""
8
+
9
+ model_config = ConfigDict(populate_by_name=True)
10
+
11
+ name: str = Field(..., description="Secret name")
12
+ description: str | None = Field(None, description="Secret description")
13
+ secret_string: str | None = Field(None, alias="secretString", description="Secret value as string")
14
+ secret_binary: str | None = Field(None, alias="secretBinary", description="Secret value as base64-encoded binary")
15
+ tags: dict[str, str] = Field(default_factory=dict, description="Resource tags")
16
+
17
+
18
+ class UpdateSecretValueBody(BaseModel):
19
+ """Request body for updating a secret's value."""
20
+
21
+ model_config = ConfigDict(populate_by_name=True)
22
+
23
+ secret_string: str | None = Field(None, alias="secretString", description="New secret value as string")
24
+ secret_binary: str | None = Field(None, alias="secretBinary", description="New secret value as base64-encoded binary")
25
+
26
+
27
+ class UpdateSecretMetadataBody(BaseModel):
28
+ """Request body for updating a secret's metadata."""
29
+
30
+ model_config = ConfigDict(populate_by_name=True)
31
+
32
+ description: str | None = Field(None, description="New description")
33
+ tags: dict[str, str] | None = Field(None, description="New tags (replaces existing)")
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stackport"
3
- version = "0.3.0"
3
+ version = "0.3.1"
4
4
  description = "Universal AWS resource browser for local emulators"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackport
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Universal AWS resource browser for local emulators
5
5
  Author: Davi Reis Vieira
6
6
  License: MIT
@@ -13,8 +13,8 @@ backend/websocket.py
13
13
  backend/../ui/dist/favicon.png
14
14
  backend/../ui/dist/favicon.svg
15
15
  backend/../ui/dist/index.html
16
- backend/../ui/dist/assets/index-Cda_0qu5.css
17
- backend/../ui/dist/assets/index-tW-8e4eS.js
16
+ backend/../ui/dist/assets/index-CAZXHF4B.js
17
+ backend/../ui/dist/assets/index-DLd-xBZq.css
18
18
  backend/../ui/dist/aws-icons/acm.svg
19
19
  backend/../ui/dist/aws-icons/apigateway.svg
20
20
  backend/../ui/dist/aws-icons/appsync.svg
@@ -67,6 +67,7 @@ backend/schemas/__init__.py
67
67
  backend/schemas/dynamodb.py
68
68
  backend/schemas/endpoints.py
69
69
  backend/schemas/s3.py
70
+ backend/schemas/secretsmanager.py
70
71
  backend/schemas/sqs.py
71
72
  backend/schemas/tags.py
72
73
  stackport.egg-info/PKG-INFO
@@ -99,8 +100,8 @@ tests/test_websocket.py
99
100
  ui/dist/favicon.png
100
101
  ui/dist/favicon.svg
101
102
  ui/dist/index.html
102
- ui/dist/assets/index-Cda_0qu5.css
103
- ui/dist/assets/index-tW-8e4eS.js
103
+ ui/dist/assets/index-CAZXHF4B.js
104
+ ui/dist/assets/index-DLd-xBZq.css
104
105
  ui/dist/aws-icons/acm.svg
105
106
  ui/dist/aws-icons/apigateway.svg
106
107
  ui/dist/aws-icons/appsync.svg
@@ -279,3 +279,230 @@ class TestGetSecretDetail:
279
279
  assert resp.status_code == 200
280
280
  data = resp.json()
281
281
  assert data["secretValue"] == "sk-abc123def456"
282
+
283
+
284
+ class TestCreateSecret:
285
+ @patch("backend.routes.secretsmanager.get_client")
286
+ def test_create_secret_with_string(self, mock_get_client):
287
+ mock_sm = MagicMock()
288
+ mock_get_client.return_value = mock_sm
289
+ mock_sm.create_secret.return_value = {
290
+ "Name": "test-secret",
291
+ "ARN": "arn:aws:secretsmanager:us-east-1:000:secret:test-secret-abc",
292
+ "VersionId": "v1",
293
+ }
294
+
295
+ resp = client.post(
296
+ "/api/secretsmanager/secrets",
297
+ json={
298
+ "name": "test-secret",
299
+ "description": "Test secret",
300
+ "secret_string": "my-secret-value",
301
+ "tags": {"env": "test"},
302
+ },
303
+ )
304
+ assert resp.status_code == 201
305
+ data = resp.json()
306
+ assert data["name"] == "test-secret"
307
+ assert data["arn"] == "arn:aws:secretsmanager:us-east-1:000:secret:test-secret-abc"
308
+ assert data["versionId"] == "v1"
309
+
310
+ mock_sm.create_secret.assert_called_once()
311
+ call_kwargs = mock_sm.create_secret.call_args[1]
312
+ assert call_kwargs["Name"] == "test-secret"
313
+ assert call_kwargs["Description"] == "Test secret"
314
+ assert call_kwargs["SecretString"] == "my-secret-value"
315
+ assert call_kwargs["Tags"] == [{"Key": "env", "Value": "test"}]
316
+
317
+ @patch("backend.routes.secretsmanager.get_client")
318
+ def test_create_secret_with_binary(self, mock_get_client):
319
+ mock_sm = MagicMock()
320
+ mock_get_client.return_value = mock_sm
321
+ mock_sm.create_secret.return_value = {
322
+ "Name": "binary-secret",
323
+ "ARN": "arn:binary",
324
+ "VersionId": "v1",
325
+ }
326
+
327
+ import base64
328
+ binary_data = base64.b64encode(b"\x00\x01\x02").decode("utf-8")
329
+
330
+ resp = client.post(
331
+ "/api/secretsmanager/secrets",
332
+ json={"name": "binary-secret", "secret_binary": binary_data},
333
+ )
334
+ assert resp.status_code == 201
335
+
336
+ call_kwargs = mock_sm.create_secret.call_args[1]
337
+ assert call_kwargs["SecretBinary"] == b"\x00\x01\x02"
338
+
339
+ @patch("backend.routes.secretsmanager.get_client")
340
+ def test_create_secret_duplicate_name(self, mock_get_client):
341
+ mock_sm = MagicMock()
342
+ mock_get_client.return_value = mock_sm
343
+ mock_sm.exceptions.ResourceExistsException = type(
344
+ "ResourceExistsException", (Exception,), {}
345
+ )
346
+ mock_sm.create_secret.side_effect = mock_sm.exceptions.ResourceExistsException()
347
+
348
+ resp = client.post(
349
+ "/api/secretsmanager/secrets",
350
+ json={"name": "existing", "secret_string": "value"},
351
+ )
352
+ assert resp.status_code == 409
353
+ assert "already exists" in resp.json()["detail"]
354
+
355
+ @patch("backend.routes.secretsmanager.get_client")
356
+ def test_create_secret_no_value(self, mock_get_client):
357
+ resp = client.post(
358
+ "/api/secretsmanager/secrets",
359
+ json={"name": "test"},
360
+ )
361
+ assert resp.status_code == 400
362
+ assert "secret_string or secret_binary" in resp.json()["detail"]
363
+
364
+
365
+ class TestUpdateSecretValue:
366
+ @patch("backend.routes.secretsmanager.get_client")
367
+ def test_update_secret_value(self, mock_get_client):
368
+ mock_sm = MagicMock()
369
+ mock_get_client.return_value = mock_sm
370
+ mock_sm.put_secret_value.return_value = {
371
+ "ARN": "arn:test",
372
+ "Name": "test-secret",
373
+ "VersionId": "v2",
374
+ "VersionStages": ["AWSCURRENT"],
375
+ }
376
+
377
+ resp = client.put(
378
+ "/api/secretsmanager/secrets/test-secret/value",
379
+ json={"secret_string": "new-value"},
380
+ )
381
+ assert resp.status_code == 200
382
+ data = resp.json()
383
+ assert data["name"] == "test-secret"
384
+ assert data["versionId"] == "v2"
385
+
386
+ mock_sm.put_secret_value.assert_called_once()
387
+ call_kwargs = mock_sm.put_secret_value.call_args[1]
388
+ assert call_kwargs["SecretId"] == "test-secret"
389
+ assert call_kwargs["SecretString"] == "new-value"
390
+
391
+ @patch("backend.routes.secretsmanager.get_client")
392
+ def test_update_secret_value_not_found(self, mock_get_client):
393
+ mock_sm = MagicMock()
394
+ mock_get_client.return_value = mock_sm
395
+ mock_sm.exceptions.ResourceNotFoundException = type(
396
+ "ResourceNotFoundException", (Exception,), {}
397
+ )
398
+ mock_sm.put_secret_value.side_effect = (
399
+ mock_sm.exceptions.ResourceNotFoundException()
400
+ )
401
+
402
+ resp = client.put(
403
+ "/api/secretsmanager/secrets/nonexistent/value",
404
+ json={"secret_string": "value"},
405
+ )
406
+ assert resp.status_code == 404
407
+
408
+
409
+ class TestUpdateSecretMetadata:
410
+ @patch("backend.routes.secretsmanager.get_client")
411
+ def test_update_description(self, mock_get_client):
412
+ mock_sm = MagicMock()
413
+ mock_get_client.return_value = mock_sm
414
+
415
+ resp = client.put(
416
+ "/api/secretsmanager/secrets/test-secret/metadata",
417
+ json={"description": "New description"},
418
+ )
419
+ assert resp.status_code == 200
420
+ mock_sm.update_secret.assert_called_once_with(
421
+ SecretId="test-secret", Description="New description"
422
+ )
423
+
424
+ @patch("backend.routes.secretsmanager.get_client")
425
+ def test_update_tags(self, mock_get_client):
426
+ mock_sm = MagicMock()
427
+ mock_get_client.return_value = mock_sm
428
+ mock_sm.describe_secret.return_value = {
429
+ "ARN": "arn:test",
430
+ "Tags": [{"Key": "old", "Value": "tag"}],
431
+ }
432
+
433
+ resp = client.put(
434
+ "/api/secretsmanager/secrets/test-secret/metadata",
435
+ json={"tags": {"new": "tag"}},
436
+ )
437
+ assert resp.status_code == 200
438
+ mock_sm.untag_resource.assert_called_once()
439
+ mock_sm.tag_resource.assert_called_once()
440
+
441
+
442
+ class TestDeleteSecret:
443
+ @patch("backend.routes.secretsmanager.get_client")
444
+ def test_delete_secret_with_recovery(self, mock_get_client):
445
+ mock_sm = MagicMock()
446
+ mock_get_client.return_value = mock_sm
447
+ mock_sm.delete_secret.return_value = {
448
+ "ARN": "arn:test",
449
+ "Name": "test-secret",
450
+ "DeletionDate": CHANGED,
451
+ }
452
+
453
+ resp = client.delete("/api/secretsmanager/secrets/test-secret")
454
+ assert resp.status_code == 200
455
+ data = resp.json()
456
+ assert data["name"] == "test-secret"
457
+ assert data["deletionDate"] == CHANGED.isoformat()
458
+
459
+ call_kwargs = mock_sm.delete_secret.call_args[1]
460
+ assert call_kwargs["RecoveryWindowInDays"] == 7
461
+ assert "ForceDeleteWithoutRecovery" not in call_kwargs
462
+
463
+ @patch("backend.routes.secretsmanager.get_client")
464
+ def test_delete_secret_force(self, mock_get_client):
465
+ mock_sm = MagicMock()
466
+ mock_get_client.return_value = mock_sm
467
+ mock_sm.delete_secret.return_value = {
468
+ "ARN": "arn:test",
469
+ "Name": "test-secret",
470
+ "DeletionDate": CHANGED,
471
+ }
472
+
473
+ resp = client.delete("/api/secretsmanager/secrets/test-secret?force=true")
474
+ assert resp.status_code == 200
475
+
476
+ call_kwargs = mock_sm.delete_secret.call_args[1]
477
+ assert call_kwargs["ForceDeleteWithoutRecovery"] is True
478
+ assert "RecoveryWindowInDays" not in call_kwargs
479
+
480
+
481
+ class TestRestoreSecret:
482
+ @patch("backend.routes.secretsmanager.get_client")
483
+ def test_restore_secret(self, mock_get_client):
484
+ mock_sm = MagicMock()
485
+ mock_get_client.return_value = mock_sm
486
+ mock_sm.restore_secret.return_value = {
487
+ "ARN": "arn:test",
488
+ "Name": "test-secret",
489
+ }
490
+
491
+ resp = client.post("/api/secretsmanager/secrets/test-secret/restore")
492
+ assert resp.status_code == 200
493
+ data = resp.json()
494
+ assert data["name"] == "test-secret"
495
+
496
+ @patch("backend.routes.secretsmanager.get_client")
497
+ def test_restore_secret_not_deleted(self, mock_get_client):
498
+ mock_sm = MagicMock()
499
+ mock_get_client.return_value = mock_sm
500
+
501
+ error = Exception("Invalid request")
502
+ error.response = {
503
+ "Error": {"Code": "InvalidRequestException", "Message": "Secret not in deleted state"}
504
+ }
505
+ mock_sm.restore_secret.side_effect = error
506
+
507
+ resp = client.post("/api/secretsmanager/secrets/test-secret/restore")
508
+ assert resp.status_code == 400