Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d08e057
chore(conductor): Add new track 'Add Individual Key Retrieval Endpoin…
allisson Mar 7, 2026
2fe4a7a
feat(tokenization): Add GetByName to TokenizationKeyUseCase interface
allisson Mar 7, 2026
cc268cc
conductor(plan): Mark task 'Add GetByName to TokenizationKeyUseCase i…
allisson Mar 7, 2026
3ae4bf7
feat(tokenization): Implement GetByName in TokenizationKeyUseCase
allisson Mar 7, 2026
d906fd8
conductor(plan): Mark task 'Implement GetByName in tokenizationKeyUse…
allisson Mar 7, 2026
7385039
conductor(checkpoint): Checkpoint end of Phase 1
allisson Mar 7, 2026
53faa43
conductor(plan): Mark phase 'Phase 1: Domain and Use Case Layer' as c…
allisson Mar 7, 2026
7e55e0d
feat(tokenization): Add GetByNameHandler to TokenizationKeyHandler
allisson Mar 7, 2026
dfdb689
conductor(plan): Mark task 'Add GetByNameHandler to TokenizationKeyHa…
allisson Mar 7, 2026
b8170b6
feat(tokenization): Register GetByName route for tokenization keys
allisson Mar 7, 2026
5047107
conductor(plan): Mark task 'Register the new route GET /v1/tokenizati…
allisson Mar 7, 2026
1f25be5
conductor(checkpoint): Checkpoint end of Phase 2
allisson Mar 7, 2026
acdacd6
conductor(plan): Mark phase 'Phase 2: HTTP Layer' as complete
allisson Mar 7, 2026
d506864
test(tokenization): Add GetByName integration test
allisson Mar 7, 2026
46f9545
conductor(plan): Mark task 'Update integration tests' as complete
allisson Mar 7, 2026
cca1f42
docs(tokenization): Document GetByName endpoint for tokenization keys
allisson Mar 7, 2026
fafc479
conductor(plan): Mark task 'Update project documentation' as complete
allisson Mar 7, 2026
17dc1eb
docs(openapi): Add GetByName endpoint to OpenAPI specification
allisson Mar 7, 2026
046ad84
conductor(plan): Mark task 'Update OpenAPI specification' as complete
allisson Mar 7, 2026
8ab39bd
conductor(checkpoint): Checkpoint end of Phase 3
allisson Mar 7, 2026
cbccfb0
conductor(plan): Mark phase 'Phase 3: Integration and Documentation' …
allisson Mar 7, 2026
2ffb18a
chore(conductor): Mark track 'Add Individual Key Retrieval Endpoint f…
allisson Mar 7, 2026
4b8f631
chore(conductor): Archive track 'Add Individual Key Retrieval Endpoin…
allisson Mar 7, 2026
39370d2
feat(tokenization): add individual key retrieval API by name
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Track tokenization_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": "tokenization_key_retrieval_20260307",
"type": "feature",
"status": "new",
"created_at": "2026-03-07T14:45:00Z",
"updated_at": "2026-03-07T14:45:00Z",
"description": "Add Individual Key Retrieval Endpoint for tokenization module (by name)"
}
25 changes: 25 additions & 0 deletions conductor/archive/tokenization_key_retrieval_20260307/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Implementation Plan: Tokenization Key Retrieval by Name

This plan outlines the steps to add a new endpoint `GET /v1/tokenization/keys/:name` to retrieve a single tokenization key by its name.

## Phase 1: Domain and Use Case Layer [checkpoint: 7385039]
Add the `GetByName` functionality to the use case layer.

- [x] Task: Add `GetByName` to `TokenizationKeyUseCase` interface. 2fe4a7a
- [x] Task: Implement `GetByName` in `tokenizationKeyUseCase` struct. 3ae4bf7
- [x] Task: Conductor - User Manual Verification 'Phase 1: Domain and Use Case Layer' (Protocol in workflow.md) d906fd8

## Phase 2: HTTP Layer [checkpoint: 1f25be5]
Expose the new functionality through a REST endpoint.

- [x] Task: Add `GetByNameHandler` to `TokenizationKeyHandler`. 7e55e0d
- [x] Task: Register the new route `GET /v1/tokenization/keys/:name`. b8170b6
- [x] Task: Conductor - User Manual Verification 'Phase 2: HTTP Layer' (Protocol in workflow.md) 5047107

## Phase 3: Integration and Documentation [checkpoint: 8ab39bd]
Ensure end-to-end functionality and update documentation.

- [x] Task: Update integration tests in `test/integration/tokenization_flow_test.go`. d506864
- [x] Task: Update project documentation `docs/engines/tokenization.md`. cca1f42
- [x] Task: Update OpenAPI specification `docs/openapi.yaml`. 17dc1eb
- [x] Task: Conductor - User Manual Verification 'Phase 3: Integration and Documentation' (Protocol in workflow.md) 046ad84
35 changes: 35 additions & 0 deletions conductor/archive/tokenization_key_retrieval_20260307/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Specification: Tokenization Key Retrieval by Name

## Overview
Currently, the tokenization module supports listing keys and rotating keys, but it lacks a direct endpoint to retrieve a single key's metadata by its name. This track adds a new `GET` endpoint to the tokenization key API to allow efficient lookup of individual keys.

## Functional Requirements
- **Endpoint:** `GET /v1/tokenization/keys/:name`
- **Capability:** `tokenization:read`
- **Input:** `name` (string) as a path parameter.
- **Output:** Returns the latest version of the tokenization key metadata.
- **Status Codes:**
- `200 OK`: Key found. Returns key metadata (ID, Name, Version, FormatType, IsDeterministic, CreatedAt).
- `404 Not Found`: Key with the given name does not exist or has been soft-deleted.
- `401 Unauthorized`: Missing or invalid authentication token.
- `403 Forbidden`: Authenticated client lacks `tokenization:read` capability.

## Non-Functional Requirements
- **Consistency:** The response format must match the existing `TokenizationKeyResponse` DTO.
- **Performance:** Direct lookup by name should be efficient (indexed in the database).

## Acceptance Criteria
- [ ] A new method `GetByName` is added to `TokenizationKeyUseCase`.
- [ ] A new handler `GetByNameHandler` is added to `TokenizationKeyHandler`.
- [ ] The route `GET /v1/tokenization/keys/:name` is registered in the application.
- [ ] The endpoint requires the `tokenization:read` capability.
- [ ] Unit tests for the use case and handler are implemented.
- [ ] Integration tests verify the end-to-end functionality (MySQL and PostgreSQL).
- [ ] Updated integration tests in `test/integration/tokenization_flow_test.go`.
- [ ] Updated documentation: `docs/engines/tokenization.md`
- [ ] Updated OpenAPI documentation: `docs/openapi.yaml`

## Out of Scope
- Retrieving specific versions of a key (only the latest version is returned).
- Retrieving soft-deleted keys.
- Modifying or deleting keys via this endpoint.
1 change: 0 additions & 1 deletion conductor/tracks.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,3 @@
This file tracks all major tracks for the project. Each track has its own detailed plan in its respective folder.

---

41 changes: 32 additions & 9 deletions docs/engines/tokenization.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ All endpoints require `Authorization: Bearer <token>`.
- **Body**: `name`, `format_type` (`uuid`, `numeric`, `luhn-preserving`, `alphanumeric`), `is_deterministic`, `algorithm`.

```bash
curl -X POST http://localhost:8080/v1/tokenization/keys
-H "Authorization: Bearer <token>"
-H "Content-Type: application/json"
curl -X POST http://localhost:8080/v1/tokenization/keys \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "payment-cards",
"format_type": "luhn-preserving",
Expand All @@ -52,9 +52,9 @@ curl -X POST http://localhost:8080/v1/tokenization/keys
- **Body**: `plaintext` (base64), `metadata` (optional object), `ttl` (optional seconds).

```bash
curl -X POST http://localhost:8080/v1/tokenization/keys/payment-cards/tokenize
-H "Authorization: Bearer <token>"
-H "Content-Type: application/json"
curl -X POST http://localhost:8080/v1/tokenization/keys/payment-cards/tokenize \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"plaintext": "NDUzMjAxNTExMjgzMDM2Ng==",
"metadata": { "last_four": "0366" }
Expand Down Expand Up @@ -125,7 +125,8 @@ Example response (`200 OK`):
{
"id": "0194f4c1-82de-7f9a-c2b3-9def1a7bc5d8",
"name": "customer-ids",
"algorithm": "uuid_v7",
"format_type": "uuid",
"algorithm": "aes-gcm",
"is_deterministic": true,
"version": 2,
"created_at": "2026-02-27T20:10:00Z",
Expand All @@ -134,7 +135,8 @@ Example response (`200 OK`):
{
"id": "0194f4d3-a5bc-7e2f-d8a1-4bef2c9ad7e1",
"name": "payment-tokens",
"algorithm": "luhn_preserving",
"format_type": "luhn-preserving",
"algorithm": "chacha20-poly1305",
"is_deterministic": false,
"version": 1,
"created_at": "2026-02-27T21:45:00Z",
Expand All @@ -147,9 +149,30 @@ Example response (`200 OK`):

**Note**: The `next_cursor` field is only present when there are more pages available.

#### Get Tokenization Key by Name

- **Endpoint**: `GET /v1/tokenization/keys/:name`
- **Capability**: `read`
- **Success**: `200 OK`

Example response (`200 OK`):

```json
{
"id": "0194f4c1-82de-7f9a-c2b3-9def1a7bc5d8",
"name": "customer-ids",
"format_type": "uuid",
"algorithm": "aes-gcm",
"is_deterministic": true,
"version": 2,
"created_at": "2026-02-27T20:10:00Z",
"updated_at": "2026-02-28T10:30:00Z"
}
```

#### Delete Tokenization Key

- **Endpoint**: `DELETE /v1/tokenization/keys/:name`
- **Endpoint**: `DELETE /v1/tokenization/keys/:id`
- **Capability**: `delete`
- **Success**: `204 No Content`

Expand Down
31 changes: 31 additions & 0 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,37 @@ paths:
$ref: "#/components/responses/ValidationError"
"429":
$ref: "#/components/responses/TooManyRequests"
/v1/tokenization/keys/{name}:
parameters:
- name: name
in: path
required: true
schema:
type: string
get:
tags: [tokenization]
summary: Get tokenization key details
security:
- bearerAuth: []
responses:
"200":
description: Tokenization key details
content:
application/json:
schema:
$ref: "#/components/schemas/TokenizationKeyResponse"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
description: Tokenization key not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"429":
$ref: "#/components/responses/TooManyRequests"
/v1/tokenization/keys/{name}/rotate:
post:
tags: [tokenization]
Expand Down
6 changes: 6 additions & 0 deletions internal/http/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,12 @@ func (s *Server) registerTokenizationRoutes(
tokenizationKeyHandler.ListHandler,
)

// Get individual tokenization key
keys.GET("/:name",
authHTTP.AuthorizationMiddleware(authDomain.ReadCapability, auditLogUseCase, s.logger),
tokenizationKeyHandler.GetByNameHandler,
)

// Create new tokenization key
keys.POST("",
authHTTP.AuthorizationMiddleware(authDomain.WriteCapability, auditLogUseCase, s.logger),
Expand Down
25 changes: 25 additions & 0 deletions internal/tokenization/http/tokenization_key_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,31 @@ func (h *TokenizationKeyHandler) DeleteHandler(c *gin.Context) {
c.Data(http.StatusNoContent, "application/json", nil)
}

// GetByNameHandler retrieves a single tokenization key by its name.
// GET /v1/tokenization/keys/:name - Requires ReadCapability.
// Returns 200 OK with key details.
func (h *TokenizationKeyHandler) GetByNameHandler(c *gin.Context) {
// Get key name from URL parameter
keyName := c.Param("name")
if keyName == "" {
httputil.HandleBadRequestGin(c,
fmt.Errorf("key name is required in URL path"),
h.logger)
return
}

// Call use case
key, err := h.keyUseCase.GetByName(c.Request.Context(), keyName)
if err != nil {
httputil.HandleErrorGin(c, err, h.logger)
return
}

// Map to response
response := dto.MapTokenizationKeyToResponse(key)
c.JSON(http.StatusOK, response)
}

// ListHandler retrieves tokenization keys with cursor-based pagination support.
// GET /v1/tokenization/keys?after_name=key-name&limit=50 - Requires ReadCapability.
// Returns 200 OK with paginated tokenization key list ordered by name ascending.
Expand Down
88 changes: 88 additions & 0 deletions internal/tokenization/http/tokenization_key_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -470,3 +470,91 @@ func TestTokenizationKeyHandler_ListHandler(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code)
})
}

func TestTokenizationKeyHandler_GetByNameHandler(t *testing.T) {
t.Run("Success_GetKeyByName", func(t *testing.T) {
handler, mockUseCase := setupTestKeyHandler(t)

keyID := uuid.Must(uuid.NewV7())
expectedKey := &tokenizationDomain.TokenizationKey{
ID: keyID,
Name: "test-key",
Version: 1,
FormatType: tokenizationDomain.FormatUUID,
IsDeterministic: false,
DekID: uuid.Must(uuid.NewV7()),
CreatedAt: time.Now().UTC(),
}

mockUseCase.EXPECT().
GetByName(mock.Anything, "test-key").
Return(expectedKey, nil).
Once()

c, w := createTestContext(http.MethodGet, "/v1/tokenization/keys/test-key", nil)
c.Params = gin.Params{{Key: "name", Value: "test-key"}}

handler.GetByNameHandler(c)

assert.Equal(t, http.StatusOK, w.Code)

var response dto.TokenizationKeyResponse
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, keyID.String(), response.ID)
assert.Equal(t, "test-key", response.Name)
assert.Equal(t, uint(1), response.Version)
assert.Equal(t, "uuid", response.FormatType)
assert.False(t, response.IsDeterministic)
})

t.Run("Error_MissingKeyNameInURL", func(t *testing.T) {
handler, _ := setupTestKeyHandler(t)

c, w := createTestContext(http.MethodGet, "/v1/tokenization/keys/", nil)
c.Params = gin.Params{{Key: "name", Value: ""}}

handler.GetByNameHandler(c)

assert.Equal(t, http.StatusBadRequest, w.Code)

var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "bad_request", response["error"])
})

t.Run("Error_KeyNotFound", func(t *testing.T) {
handler, mockUseCase := setupTestKeyHandler(t)

mockUseCase.EXPECT().
GetByName(mock.Anything, "nonexistent-key").
Return(nil, tokenizationDomain.ErrTokenizationKeyNotFound).
Once()

c, w := createTestContext(http.MethodGet, "/v1/tokenization/keys/nonexistent-key", nil)
c.Params = gin.Params{{Key: "name", Value: "nonexistent-key"}}

handler.GetByNameHandler(c)

assert.Equal(t, http.StatusNotFound, w.Code)
})

t.Run("Error_UseCaseError", func(t *testing.T) {
handler, mockUseCase := setupTestKeyHandler(t)

dbError := errors.New("database error")

mockUseCase.EXPECT().
GetByName(mock.Anything, "test-key").
Return(nil, dbError).
Once()

c, w := createTestContext(http.MethodGet, "/v1/tokenization/keys/test-key", nil)
c.Params = gin.Params{{Key: "name", Value: "test-key"}}

handler.GetByNameHandler(c)

assert.Equal(t, http.StatusInternalServerError, w.Code)
})
}
4 changes: 4 additions & 0 deletions internal/tokenization/usecase/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ type TokenizationKeyUseCase interface {
// Delete soft deletes a tokenization key and all its versions by key ID.
Delete(ctx context.Context, keyID uuid.UUID) error

// GetByName retrieves a single tokenization key by its name.
// Returns the latest version for the key. Filters out soft-deleted keys.
GetByName(ctx context.Context, name string) (*tokenizationDomain.TokenizationKey, error)

// ListCursor retrieves tokenization 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.
Expand Down
Loading
Loading