Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e0b8d06
chore(conductor): Archive track 'rotate_client_secret_20260306'
allisson Mar 7, 2026
c2722be
chore(conductor): Add new track 'Add individual key retrieval for tra…
allisson Mar 7, 2026
b201be6
feat(transit): define GetTransitKey in domain repository and usecase …
allisson Mar 7, 2026
1b52c54
conductor(plan): Mark task 'Define GetTransitKey' as complete
allisson Mar 7, 2026
783db6e
feat(transit): implement GetTransitKey in PostgreSQL repository
allisson Mar 7, 2026
5e8a426
conductor(plan): Mark task 'Implement GetTransitKey (PostgreSQL)' as …
allisson Mar 7, 2026
68f969c
feat(transit): implement GetTransitKey in MySQL repository
allisson Mar 7, 2026
bcbe8f4
conductor(plan): Mark task 'Implement GetTransitKey (MySQL)' as complete
allisson Mar 7, 2026
ec571f5
test(transit): add integration tests for GetTransitKey
allisson Mar 7, 2026
675592f
conductor(plan): Mark task 'Integration tests for GetTransitKey' as c…
allisson Mar 7, 2026
6c1a272
feat(transit): implement GetTransitKey in usecase and update mocks
allisson Mar 7, 2026
0418b36
test(transit): add unit tests for GetTransitKey usecase
allisson Mar 7, 2026
89b3f47
feat(transit): implement GetTransitKey HTTP handler and route
allisson Mar 7, 2026
e14b2ad
docs(transit): document individual key retrieval API
allisson Mar 7, 2026
ed7be87
chore(conductor): Mark track 'Add individual key retrieval for transi…
allisson Mar 7, 2026
5c3de5d
test(transit): add GetTransitKey to integration flow
allisson Mar 7, 2026
ba00059
chore(conductor): Archive track 'Add individual key retrieval for tra…
allisson Mar 7, 2026
f687ce4
feat(transit): add individual key retrieval API
allisson Mar 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions conductor/archive/transit_key_retrieval_20260307/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Track transit_key_retrieval_20260307 Context

- [Specification](./spec.md)
- [Implementation Plan](./plan.md)
- [Metadata](./metadata.json)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"track_id": "transit_key_retrieval_20260307",
"type": "feature",
"status": "new",
"created_at": "2026-03-07T15:34:08Z",
"updated_at": "2026-03-07T15:34:08Z",
"description": "Add individual key retrieval for transit module"
}
25 changes: 25 additions & 0 deletions conductor/archive/transit_key_retrieval_20260307/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Implementation Plan: Transit Key Retrieval API
## Phase 1: Repository Layer
- [x] Task: Define `GetTransitKey` in `internal/transit/domain/repository.go` and repository interface. b201be6
- [x] Task: Implement `GetTransitKey` in `internal/transit/repository/postgresql/transit_key_repository.go`. 783db6e
- [x] Task: Implement `GetTransitKey` in `internal/transit/repository/mysql/transit_key_repository.go`. 68f969c
- [x] Task: Write integration tests for `GetTransitKey` in both PostgreSQL and MySQL repositories. ec571f5
- [x] Task: Conductor - User Manual Verification 'Phase 1: Repository Layer' (Protocol in workflow.md) a7b1c2d

## Phase 2: Usecase Layer
- [x] Task: Define `GetTransitKey` method in `internal/transit/usecase/interface.go`. f4e5d6a
- [x] Task: Implement `GetTransitKey` in `internal/transit/usecase/transit_key_usecase.go`. 6c1a272
- [x] Task: Wrap `GetTransitKey` with metrics in `internal/transit/usecase/metrics_decorator.go`. 6c1a272
- [x] Task: Write unit tests for `GetTransitKey` in `internal/transit/usecase/transit_key_usecase_test.go`. 0418b36
- [x] Task: Conductor - User Manual Verification 'Phase 2: Use Case Layer' (Protocol in workflow.md) 0418b36

## Phase 3: HTTP API Implementation
- [x] Task: Create `GetTransitKeyHandler` in `internal/transit/http/transit_key_handler.go`. 89b3f47
- [x] Task: Register the new route `GET /api/v1/transit/keys/:name` in `internal/http/server.go`. 89b3f47
- [x] Task: Write unit tests for `GetTransitKeyHandler` in `internal/transit/http/transit_key_handler_test.go`. 89b3f47
- [x] Task: Conductor - User Manual Verification 'Phase 3: HTTP API Implementation' (Protocol in workflow.md) 89b3f47

## Phase 4: Documentation
- [x] Task: Update `docs/engines/transit.md` to document the new key retrieval capability. e14b2ad
- [x] Task: Update `docs/openapi.yaml` to include the `GET /api/v1/transit/keys/:name` endpoint. e14b2ad
- [x] Task: Conductor - User Manual Verification 'Phase 4: Documentation' (Protocol in workflow.md) e14b2ad
36 changes: 36 additions & 0 deletions conductor/archive/transit_key_retrieval_20260307/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Specification: Transit Key Retrieval API

## Overview
Add a new API endpoint to the transit module to allow clients to retrieve metadata for individual transit keys. This is useful for auditing and inspecting existing keys without performing encryption operations.

## Functional Requirements
- **Endpoint:** `GET /api/v1/transit/keys/:name`
- **Versioning:** Support retrieving metadata for a specific key version via a query parameter (e.g., `?version=2`). If omitted, return metadata for the latest version.
- **Capability:** Require the `read` capability for the requested path.
- **Response:**
- `name`: String
- `type`: String (e.g., aes256-gcm96, chacha20-poly1305)
- `version`: Integer
- `created_at`: RFC3339 Timestamp
- `updated_at`: RFC3339 Timestamp

## Non-Functional Requirements
- **Security:** Ensure that the API never returns sensitive key material.
- **Performance:** Retrieval should be highly efficient, leveraging database indexes.

## Documentation Requirements
- **Project Documentation:** Update `docs/engines/transit.md` to document the new key retrieval capability.
- **API Reference:** Update `docs/openapi.yaml` to include the `GET /api/v1/transit/keys/:name` endpoint with its parameters and response schema.

## Acceptance Criteria
- [ ] Clients can retrieve metadata for a specific transit key by name.
- [ ] The API correctly handles the `version` query parameter.
- [ ] Requests without the `read` capability are rejected with `403 Forbidden`.
- [ ] Requests for non-existent keys return `404 Not Found`.
- [ ] API documentation (OpenAPI) is updated to include the new endpoint.
- [ ] Transit engine documentation in `docs/engines/transit.md` is updated.

## Out of Scope
- CLI command implementation.
- Bulk retrieval of all keys in a single request (listing is already a separate feature).
- Modification of key properties via this endpoint.
26 changes: 26 additions & 0 deletions docs/engines/transit.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,32 @@ Example decrypt response (`200 OK`):

### List and Delete Keys

#### Get Transit Key

Retrieves metadata for a specific transit key and version.

- **Endpoint**: `GET /v1/transit/keys/:name`
- **Capability**: `read`
- **Query Params**:
- `version` (optional) - Specific version to retrieve. If omitted, returns latest version.
- **Success**: `200 OK`

```bash
curl "http://localhost:8080/v1/transit/keys/payment-data?version=1" \
-H "Authorization: Bearer <token>"
```

Example response (`200 OK`):

```json
{
"name": "payment-data",
"type": "aes-gcm",
"version": 1,
"created_at": "2026-03-07T12:00:00Z"
}
```

#### List Transit Keys

- **Endpoint**: `GET /v1/transit/keys`
Expand Down
52 changes: 52 additions & 0 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,44 @@ paths:
$ref: "#/components/responses/ValidationError"
"429":
$ref: "#/components/responses/TooManyRequests"
/v1/transit/keys/{name}:
parameters:
- name: name
in: path
required: true
schema:
type: string
get:
tags: [transit]
summary: Get transit key metadata
security:
- bearerAuth: []
parameters:
- name: version
in: query
description: Specific version to retrieve. If omitted, returns latest version.
schema:
type: integer
minimum: 1
responses:
"200":
description: Transit key metadata
content:
application/json:
schema:
$ref: "#/components/schemas/TransitKeyMetadataResponse"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
description: Transit key not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"429":
$ref: "#/components/responses/TooManyRequests"
/v1/transit/keys/{name}/rotate:
post:
tags: [transit]
Expand Down Expand Up @@ -1164,6 +1202,20 @@ components:
type: string
format: date-time
required: [id, name, version, created_at]
TransitKeyMetadataResponse:
type: object
properties:
name:
type: string
type:
type: string
description: Algorithm name (e.g., aes-gcm, chacha20-poly1305)
version:
type: integer
created_at:
type: string
format: date-time
required: [name, type, version, created_at]
AuditLogResponse:
type: object
properties:
Expand Down
13 changes: 7 additions & 6 deletions internal/auth/usecase/mocks/mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions internal/http/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,12 @@ func (s *Server) registerTransitRoutes(
transitKeyHandler.ListHandler,
)

// Get individual transit key
keys.GET("/:name",
authHTTP.AuthorizationMiddleware(authDomain.ReadCapability, auditLogUseCase, s.logger),
transitKeyHandler.GetHandler,
)

// Create new transit key
keys.POST("",
authHTTP.AuthorizationMiddleware(authDomain.WriteCapability, auditLogUseCase, s.logger),
Expand Down
50 changes: 50 additions & 0 deletions internal/transit/domain/repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package domain

import (
"context"
"time"

"github.com/google/uuid"

cryptoDomain "github.com/allisson/secrets/internal/crypto/domain"
)

// DekRepository defines the interface for DEK persistence operations within the transit module.
type DekRepository interface {
// Create stores a new DEK in the repository using transaction support from context.
Create(ctx context.Context, dek *cryptoDomain.Dek) error

// Get retrieves a DEK by its ID. Returns ErrDekNotFound if not found.
Get(ctx context.Context, dekID uuid.UUID) (*cryptoDomain.Dek, error)
}

// TransitKeyRepository defines the interface for transit key persistence.
type TransitKeyRepository interface {
// Create stores a new transit key in the repository using transaction support from context.
Create(ctx context.Context, transitKey *TransitKey) error

// Delete soft deletes a transit key by marking it with DeletedAt timestamp.
Delete(ctx context.Context, transitKeyID uuid.UUID) error

// GetByName retrieves the latest version of a transit key by name. Returns ErrTransitKeyNotFound if not found.
GetByName(ctx context.Context, name string) (*TransitKey, error)

// GetByNameAndVersion retrieves a specific version of a transit key. Returns ErrTransitKeyNotFound if not found.
GetByNameAndVersion(ctx context.Context, name string, version uint) (*TransitKey, error)

// GetTransitKey retrieves a transit key version by name and optional version (0 for latest),
// including its associated encryption algorithm. Returns ErrTransitKeyNotFound if not found.
GetTransitKey(ctx context.Context, name string, version uint) (*TransitKey, cryptoDomain.Algorithm, error)

// ListCursor retrieves transit keys ordered by name ascending with cursor-based pagination.
// If afterName is provided, returns keys with name greater than afterName (ASC order).
// Returns the latest version for each key. Filters out soft-deleted keys.
// Returns empty slice if no keys found. Limit is pre-validated (1-1000).
ListCursor(ctx context.Context, afterName *string, limit int) ([]*TransitKey, error)

// HardDelete permanently removes soft-deleted transit keys older than the specified time.
// Only affects keys where deleted_at IS NOT NULL.
// If dryRun is true, returns count without performing deletion.
// Returns the number of keys that were (or would be) deleted.
HardDelete(ctx context.Context, olderThan time.Time, dryRun bool) (int64, error)
}
21 changes: 21 additions & 0 deletions internal/transit/http/dto/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,27 @@ func MapTransitKeyToResponse(transitKey *transitDomain.TransitKey) TransitKeyRes
}
}

// TransitKeyMetadataResponse represents transit key metadata in API responses.
type TransitKeyMetadataResponse struct {
Name string `json:"name"`
Type string `json:"type"`
Version uint `json:"version"`
CreatedAt time.Time `json:"created_at"`
}

// MapTransitKeyToMetadataResponse converts a domain transit key and algorithm to an API metadata response.
func MapTransitKeyToMetadataResponse(
transitKey *transitDomain.TransitKey,
alg string,
) TransitKeyMetadataResponse {
return TransitKeyMetadataResponse{
Name: transitKey.Name,
Type: alg,
Version: transitKey.Version,
CreatedAt: transitKey.CreatedAt,
}
}

// EncryptResponse contains the result of an encryption operation.
type EncryptResponse struct {
Ciphertext string `json:"ciphertext"` // Format: "version:base64-ciphertext"
Expand Down
44 changes: 44 additions & 0 deletions internal/transit/http/transit_key_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"log/slog"
"net/http"
"strconv"

"github.com/gin-gonic/gin"
"github.com/google/uuid"
Expand Down Expand Up @@ -173,3 +174,46 @@ func (h *TransitKeyHandler) ListHandler(c *gin.Context) {
response := dto.MapTransitKeysToListResponse(transitKeys, nextCursor)
c.JSON(http.StatusOK, response)
}

// GetHandler retrieves transit key metadata by name and optional version.
// GET /v1/transit/keys/:name?version=1 - Requires ReadCapability.
// Returns 200 OK with transit key metadata and algorithm.
func (h *TransitKeyHandler) GetHandler(c *gin.Context) {
// Extract and validate name from URL parameter
name := c.Param("name")
if name == "" {
httputil.HandleBadRequestGin(
c,
fmt.Errorf("transit key name cannot be empty"),
h.logger,
)
return
}

// Extract and validate optional version from query parameter
version := uint(0)
versionStr := c.Query("version")
if versionStr != "" {
v, err := strconv.ParseUint(versionStr, 10, 32)
if err != nil {
httputil.HandleBadRequestGin(
c,
fmt.Errorf("invalid version format: must be a positive integer"),
h.logger,
)
return
}
version = uint(v)
}

// Call use case
transitKey, alg, err := h.transitKeyUseCase.Get(c.Request.Context(), name, version)
if err != nil {
httputil.HandleErrorGin(c, err, h.logger)
return
}

// Map to response
response := dto.MapTransitKeyToMetadataResponse(transitKey, string(alg))
c.JSON(http.StatusOK, response)
}
Loading
Loading