From 68c98d0d1d82ed46853d524d4007fe5ecd9b1937 Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Mon, 23 Mar 2026 08:40:31 -0300 Subject: [PATCH] release: v0.28.0 and configurable tokenization batch limit This commit prepares the v0.28.0 release and adds a configurable limit for batch tokenization operations. - Update version to v0.28.0 and update CHANGELOG.md with release notes. - Add TOKENIZATION_BATCH_LIMIT configuration (default: 100). - Enforce batch limit in TokenizationHandler for tokenize/detokenize. - Update docs/openapi.yaml with v0.28.0 changes and new parameters. - Archive related conductor tracks (batch_limit_config and release_v0.28.0). V0.28.0 Key Changes: - Batch tokenization limit configuration. - Individual transit/tokenization key retrieval APIs. - Atomic batch tokenize/detokenize endpoints. - Audit log filtering by client_id. - Configurable Metrics Server timeouts and DB connection settings. - Client secret rotation with token revocation. - Strict capability validation and key deletion by name. - Rate limiter resource leak fix. --- .env.example | 4 ++ CHANGELOG.md | 21 ++++++++++ cmd/app/main.go | 2 +- .../batch_limit_config_20260321/index.md | 5 +++ .../batch_limit_config_20260321/metadata.json | 8 ++++ .../batch_limit_config_20260321/plan.md | 27 +++++++++++++ .../batch_limit_config_20260321/spec.md | 23 +++++++++++ .../archive/release_v0.28.0_20260323/index.md | 5 +++ .../release_v0.28.0_20260323/metadata.json | 8 ++++ .../archive/release_v0.28.0_20260323/plan.md | 33 ++++++++++++++++ .../archive/release_v0.28.0_20260323/spec.md | 32 +++++++++++++++ conductor/tech-stack.md | 2 + conductor/tracks.md | 3 ++ docs/configuration.md | 9 ++++- docs/engines/tokenization.md | 4 +- docs/openapi.yaml | 38 ++++++++++++++++++ internal/app/di_tokenization.go | 6 ++- internal/config/config.go | 10 +++++ internal/config/config_test.go | 39 +++++++++++++++++++ internal/config/metrics_server_config_test.go | 1 + internal/tokenization/http/dto/request.go | 8 ++-- .../tokenization/http/dto/request_test.go | 20 +++++----- .../tokenization/http/tokenization_handler.go | 7 +++- .../http/tokenization_handler_test.go | 35 ++++++++++++++++- 24 files changed, 329 insertions(+), 21 deletions(-) create mode 100644 conductor/archive/batch_limit_config_20260321/index.md create mode 100644 conductor/archive/batch_limit_config_20260321/metadata.json create mode 100644 conductor/archive/batch_limit_config_20260321/plan.md create mode 100644 conductor/archive/batch_limit_config_20260321/spec.md create mode 100644 conductor/archive/release_v0.28.0_20260323/index.md create mode 100644 conductor/archive/release_v0.28.0_20260323/metadata.json create mode 100644 conductor/archive/release_v0.28.0_20260323/plan.md create mode 100644 conductor/archive/release_v0.28.0_20260323/spec.md diff --git a/.env.example b/.env.example index 037cbf6..09b2b40 100644 --- a/.env.example +++ b/.env.example @@ -66,6 +66,10 @@ LOCKOUT_DURATION_MINUTES=30 # Maximum size allowed for a secret value in bytes (default: 524288 = 512 KB) SECRET_VALUE_SIZE_LIMIT_BYTES=524288 +# Tokenization batch limit +# Maximum number of items in a batch tokenization request (default: 100) +TOKENIZATION_BATCH_LIMIT=100 + # CORS configuration # ⚠️ SECURITY WARNING: CORS is disabled by default for server-to-server API diff --git a/CHANGELOG.md b/CHANGELOG.md index f264bd1..b5f0098 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.28.0] - 2026-03-23 + +### Added +- Added configurable batch limit for tokenize and detokenize operations via `BATCH_LIMIT_TOKENIZATION` environment variable. +- Added individual transit key retrieval API (`GET /v1/transit/keys/{name}`) (#115). +- Added atomic batch tokenize and detokenize endpoints (`POST /v1/tokenization/tokenize/batch`, `POST /v1/tokenization/detokenize/batch`) (#119). +- Added individual tokenization key retrieval API by name (`GET /v1/tokenization/keys/{name}`) (#116). +- Added audit log filtering by `client_id` for `GET /v1/audit/logs` (#118). +- Added configurable Metrics Server timeouts (`METRICS_SERVER_READ_TIMEOUT`, `METRICS_SERVER_WRITE_TIMEOUT`, `METRICS_SERVER_IDLE_TIMEOUT`) (#114). +- Added database connection max idle time configuration (`DB_CONN_MAX_IDLE_TIME`) (#113). +- Added client secret rotation with automatic token revocation. +- Added strict capability validation for policies (#111). + +### Changed +- Updated Transit Engine to support key deletion by name instead of UUID (#120). +- Updated Tokenization Engine to support key deletion by name (#117). + +### Fixed +- Fixed rate limiter goroutine lifecycle and resource leaks (#112). + ## [0.27.0] - 2026-03-06 ### Added @@ -441,6 +461,7 @@ If you are using `sslmode=disable` (PostgreSQL) or `tls=false` (MySQL) in produc - Security model documentation - Architecture documentation +[0.28.0]: https://github.com/allisson/secrets/compare/v0.27.0...v0.28.0 [0.27.0]: https://github.com/allisson/secrets/compare/v0.26.0...v0.27.0 [0.26.0]: https://github.com/allisson/secrets/compare/v0.25.0...v0.26.0 [0.25.0]: https://github.com/allisson/secrets/compare/v0.24.0...v0.25.0 diff --git a/cmd/app/main.go b/cmd/app/main.go index 8465db1..10399e4 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -12,7 +12,7 @@ import ( // Build-time version information (injected via ldflags during build). var ( - version = "v0.27.0" // Semantic version with "v" prefix (e.g., "v0.12.0") + version = "v0.28.0" // Semantic version with "v" prefix (e.g., "v0.12.0") buildDate = "unknown" // ISO 8601 build timestamp commitSHA = "unknown" // Git commit SHA ) diff --git a/conductor/archive/batch_limit_config_20260321/index.md b/conductor/archive/batch_limit_config_20260321/index.md new file mode 100644 index 0000000..7b0b8d9 --- /dev/null +++ b/conductor/archive/batch_limit_config_20260321/index.md @@ -0,0 +1,5 @@ +# Track batch_limit_config_20260321 Context + +- [Specification](./spec.md) +- [Implementation Plan](./plan.md) +- [Metadata](./metadata.json) diff --git a/conductor/archive/batch_limit_config_20260321/metadata.json b/conductor/archive/batch_limit_config_20260321/metadata.json new file mode 100644 index 0000000..0c3f48d --- /dev/null +++ b/conductor/archive/batch_limit_config_20260321/metadata.json @@ -0,0 +1,8 @@ +{ + "track_id": "batch_limit_config_20260321", + "type": "chore", + "status": "new", + "created_at": "2026-03-21T10:00:00Z", + "updated_at": "2026-03-21T10:00:00Z", + "description": "The TokenizeBatchRequest.Validate method uses a hardcoded value for the batch limit; this value must come from the global configuration." +} diff --git a/conductor/archive/batch_limit_config_20260321/plan.md b/conductor/archive/batch_limit_config_20260321/plan.md new file mode 100644 index 0000000..aac59a1 --- /dev/null +++ b/conductor/archive/batch_limit_config_20260321/plan.md @@ -0,0 +1,27 @@ +# Implementation Plan: Configurable Batch Limit for TokenizeBatchRequest + +## Phase 1: Configuration [checkpoint: e43e7cc] +- [x] Task: Add TokenizationBatchLimit to Config struct and DefaultTokenizationBatchLimit constant in internal/config/config.go. +- [x] Task: Update `config.Load()` to load `TOKENIZATION_BATCH_LIMIT` from environment variables in `internal/config/config.go`. +- [x] Task: Update `config.Validate()` to include validation for `TokenizationBatchLimit` in `internal/config/config.go`. +- [x] Task: Update `.env.example` to include `TOKENIZATION_BATCH_LIMIT=100`. +- [x] Task: Conductor - User Manual Verification 'Configuration' (Protocol in workflow.md) e43e7cc + +## Phase 2: DTO Updates [checkpoint: 86d62c6] +- [x] Task: Update `TokenizeBatchRequest.Validate` and `DetokenizeBatchRequest.Validate` in `internal/tokenization/http/dto/request.go` to accept `limit int`. +- [x] Task: Update validation rules to use `validation.Length(1, limit).Error(fmt.Sprintf("batch size exceeds limit of %d", limit))`. +- [x] Task: Update all tests calling these `Validate()` methods in `internal/tokenization/http/dto/request_test.go`. +- [x] Task: Conductor - User Manual Verification 'DTO Updates' (Protocol in workflow.md) 86d62c6 + +## Phase 3: Handler and DI Updates [checkpoint: 86d62c6] +- [x] Task: Update `TokenizationHandler` struct in `internal/tokenization/http/tokenization_handler.go` to include `batchLimit int`. +- [x] Task: Update `NewTokenizationHandler` in `internal/tokenization/http/tokenization_handler.go` to accept `batchLimit int`. +- [x] Task: Update `TokenizeBatchHandler` and `DetokenizeBatchHandler` in `internal/tokenization/http/tokenization_handler.go` to call `Validate(h.batchLimit)`. +- [x] Task: Update `initTokenizationHandler` in `internal/app/di_tokenization.go` to pass `c.config.TokenizationBatchLimit` to `NewTokenizationHandler`. +- [x] Task: Update `TokenizationHandler` tests in `internal/tokenization/http/tokenization_handler_test.go` to pass the batch limit to `NewTokenizationHandler`. +- [x] Task: Conductor - User Manual Verification 'Handler and DI Updates' (Protocol in workflow.md) 86d62c6 + +## Phase 4: Documentation +- [x] Task: Update `docs/configuration.md` with `TOKENIZATION_BATCH_LIMIT`. +- [x] Task: Update `docs/engines/tokenization.md` with information about the batch limit. +- [x] Task: Conductor - User Manual Verification 'Documentation' (Protocol in workflow.md) diff --git a/conductor/archive/batch_limit_config_20260321/spec.md b/conductor/archive/batch_limit_config_20260321/spec.md new file mode 100644 index 0000000..9a4adb5 --- /dev/null +++ b/conductor/archive/batch_limit_config_20260321/spec.md @@ -0,0 +1,23 @@ +# Specification: Configurable Batch Limit for TokenizeBatchRequest + +## Overview +The `TokenizeBatchRequest.Validate` method currently uses a hardcoded value of 100 for the batch limit. This track aims to make this limit configurable via the global configuration using the `TOKENIZATION_BATCH_LIMIT` key, defaulting to 100 if not specified. + +## Functional Requirements +- **Configuration Integration**: Add `TOKENIZATION_BATCH_LIMIT` to the global configuration structure and support its initialization from environment variables. +- **Dynamic Validation**: Update the `TokenizeBatchRequest.Validate` method to use the configured batch limit instead of the hardcoded value. +- **Error Message Update**: Ensure the error message returned when the limit is exceeded is `batch size exceeds limit of %d`, where `%d` is the current limit. +- **Update Documentation**: Update relevant documentation (e.g., `docs/configuration.md`, `docs/engines/tokenization.md`) to reflect the new configuration option. +- **Update `.env.example`**: Add `TOKENIZATION_BATCH_LIMIT` with the default value of 100 to the `.env.example` file. + +## Acceptance Criteria +- [ ] `TOKENIZATION_BATCH_LIMIT` is successfully added to the configuration and can be set via an environment variable. +- [ ] The `TokenizeBatchRequest.Validate` method correctly uses the value from the configuration. +- [ ] If `TOKENIZATION_BATCH_LIMIT` is not set, the system defaults to a limit of 100. +- [ ] When a batch exceeds the limit, the error message correctly includes the configured limit value. +- [ ] Documentation (`docs/configuration.md`, `docs/engines/tokenization.md`) is updated. +- [ ] `.env.example` is updated. + +## Out of Scope +- Modifying the core tokenization or batch processing logic. +- Adding limits to other batch operations beyond `TokenizeBatchRequest`. diff --git a/conductor/archive/release_v0.28.0_20260323/index.md b/conductor/archive/release_v0.28.0_20260323/index.md new file mode 100644 index 0000000..ef0ae1a --- /dev/null +++ b/conductor/archive/release_v0.28.0_20260323/index.md @@ -0,0 +1,5 @@ +# Track release_v0.28.0_20260323 Context + +- [Specification](./spec.md) +- [Implementation Plan](./plan.md) +- [Metadata](./metadata.json) diff --git a/conductor/archive/release_v0.28.0_20260323/metadata.json b/conductor/archive/release_v0.28.0_20260323/metadata.json new file mode 100644 index 0000000..b9a48d7 --- /dev/null +++ b/conductor/archive/release_v0.28.0_20260323/metadata.json @@ -0,0 +1,8 @@ +{ + "track_id": "release_v0.28.0_20260323", + "type": "chore", + "status": "new", + "created_at": "2026-03-23T12:00:00Z", + "updated_at": "2026-03-23T12:00:00Z", + "description": "Prepare to the next version v0.28.0" +} diff --git a/conductor/archive/release_v0.28.0_20260323/plan.md b/conductor/archive/release_v0.28.0_20260323/plan.md new file mode 100644 index 0000000..03cc38b --- /dev/null +++ b/conductor/archive/release_v0.28.0_20260323/plan.md @@ -0,0 +1,33 @@ +# Implementation Plan: v0.28.0 Release Preparation + +## Phase 1: Version Update & Changelog [checkpoint: 1052e1b] +- [x] Task: Update `version` to `v0.28.0` in `cmd/app/main.go` 165ae80 +- [x] Task: Add v0.28.0 entry to `CHANGELOG.md` with all key changes: 61fdeda + - **New Feature:** Configurable batch limit for tokenize and detokenize operations. + - **Transit Engine:** Key deletion by name instead of UUID (#120). + - **Transit Engine:** Individual key retrieval API (#115). + - **Tokenization Engine:** Atomic batch tokenize and detokenize endpoints (#119). + - **Tokenization Engine:** Delete tokenization keys by name (#117). + - **Tokenization Engine:** Individual key retrieval API by name (#116). + - **Audit Logs:** Implement audit log filtering by `client_id` (#118). + - **Configuration:** Make Metrics Server timeouts configurable (#114). + - **Database:** Expose DB connection max idle time configuration (#113). + - **Auth:** Client secret rotation with automatic token revocation. + - **Auth:** Fix rate limiter goroutine lifecycle and resource leaks (#112). + - **Auth:** Implement strict capability validation for policies (#111). +- [x] Task: Conductor - User Manual Verification 'Phase 1: Version Update & Changelog' (Protocol in workflow.md) 1052e1b + +## Phase 2: Documentation & OpenAPI Sync [checkpoint: a01f409] +- [x] Task: Run `make docs-lint` and address any issues. +- [x] Task: Audit `docs/openapi.yaml` and update it with new endpoints: cce88e6 + - `/api/v1/tokenization/tokenize/batch` (POST) + - `/api/v1/tokenization/detokenize/batch` (POST) + - `/api/v1/tokenization/keys/{name}` (GET) + - `/api/v1/transit/keys/{name}` (GET) + - Verify audit log filtering params for `/api/v1/audit/logs`. +- [x] Task: Conductor - User Manual Verification 'Phase 2: Documentation & OpenAPI Sync' (Protocol in workflow.md) a01f409 + +## Phase 3: Final Verification [checkpoint: ec975f9] +- [x] Task: Run full test suite using `make test-all`. 60b5e6f +- [x] Task: Perform a final sanity check of the CHANGELOG and CLI version output. 60b5e6f +- [x] Task: Conductor - User Manual Verification 'Phase 3: Final Verification' (Protocol in workflow.md) ec975f9 diff --git a/conductor/archive/release_v0.28.0_20260323/spec.md b/conductor/archive/release_v0.28.0_20260323/spec.md new file mode 100644 index 0000000..eb0e5b1 --- /dev/null +++ b/conductor/archive/release_v0.28.0_20260323/spec.md @@ -0,0 +1,32 @@ +# Track Specification: v0.28.0 Release Preparation + +**Overview:** +Prepare for the release of version v0.28.0. This includes updating the version number, generating the changelog for the significant features and fixes since v0.27.0, and performing final documentation/API audits. + +**Functional Requirements:** +- **Update Version:** Update `version` in `cmd/app/main.go` and ensure all relevant locations reflect `v0.28.0`. +- **Update Changelog:** Generate and append release notes to `CHANGELOG.md` based on commits since `v0.27.0`. +- **Key Changes for v0.28.0:** + - **New Feature:** Configurable batch limit for tokenize and detokenize operations. + - **Transit Engine:** Key deletion by name instead of UUID (#120). + - **Transit Engine:** Individual key retrieval API (#115). + - **Tokenization Engine:** Atomic batch tokenize and detokenize endpoints (#119). + - **Tokenization Engine:** Delete tokenization keys by name (#117). + - **Tokenization Engine:** Individual key retrieval API by name (#116). + - **Audit Logs:** Implement audit log filtering by `client_id` (#118). + - **Configuration:** Make Metrics Server timeouts configurable (#114). + - **Database:** Expose DB connection max idle time configuration (#113). + - **Auth:** Client secret rotation with automatic token revocation. + - **Auth:** Fix rate limiter goroutine lifecycle and resource leaks (#112). + - **Auth:** Implement strict capability validation for policies (#111). +- **Documentation/OpenAPI Audit:** Run `make docs-lint` and ensure `docs/openapi.yaml` matches the current API state. + +**Acceptance Criteria:** +- `cmd/app/main.go` reports version `v0.28.0`. +- `CHANGELOG.md` contains a comprehensive entry for v0.28.0 with all key changes correctly categorized. +- `make docs-lint` passes without errors. +- `docs/openapi.yaml` reflects the new batch tokenization, filtering, and key retrieval endpoints. + +**Out of Scope:** +- Functional code changes (other than version bump). +- Deployment/Infrastructure changes. diff --git a/conductor/tech-stack.md b/conductor/tech-stack.md index 144e07f..398565f 100644 --- a/conductor/tech-stack.md +++ b/conductor/tech-stack.md @@ -8,6 +8,7 @@ ## Data Persistence - **PostgreSQL:** Primary relational database for production environments. Supported via `lib/pq`. - **MySQL:** Alternative relational database support for broader infrastructure compatibility. Supported via `go-sql-driver/mysql`. +- **Connection Management:** Configurable connection pool settings including max open/idle connections, lifetime, and idle time for optimized resource usage. - **Migrations:** [golang-migrate/migrate](https://github.com/golang-migrate/migrate) - Versioned database migrations for both PostgreSQL and MySQL. ## Cryptography & Security @@ -16,6 +17,7 @@ - **Configurable Metrics Timeouts:** Environment-controlled Read, Write, and Idle timeouts for the Prometheus metrics server to prevent resource exhaustion. - **Request Body Size Limiting:** Middleware to prevent DoS attacks from large payloads. - **Rate Limiting:** Per-client and per-IP rate limiting middleware for DoS protection and API abuse prevention. +- **Tokenization Batch Limit:** Configurable limit for batch tokenization operations to ensure predictable performance and resource usage. - **Secret Value Size Limiting:** Global limit on individual secret values to ensure predictable storage and memory usage. - **Strict Capability Validation:** Centralized domain helpers for validating policy capabilities (`read`, `write`, `delete`, `encrypt`, `decrypt`, `rotate`) in CLI and API layers. - **Secret Path Validation:** Strict naming rules for secret paths (alphanumeric, -, _, /) to ensure consistency and security. diff --git a/conductor/tracks.md b/conductor/tracks.md index 22d3d64..bae9481 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -1,3 +1,6 @@ # Project Tracks This file tracks all major tracks for the project. Each track has its own detailed plan in its respective folder. + +--- + diff --git a/docs/configuration.md b/docs/configuration.md index 172226a..ba06fb7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -53,6 +53,9 @@ LOCKOUT_DURATION_MINUTES=30 # Secret value size limit SECRET_VALUE_SIZE_LIMIT_BYTES=524288 + +# Tokenization batch limit +TOKENIZATION_BATCH_LIMIT=100 ``` ## Database configuration @@ -170,7 +173,11 @@ Logging level. Supported values: `debug`, `info`, `warn`, `error`, `fatal`, `pan Maximum size allowed for a secret value in bytes (default: `524288` - 512 KB). -This limit is enforced before encryption and storage to prevent denial-of-service attacks using large secret payloads. If exceeded, the API returns a `413 Payload Too Large` error. +### TOKENIZATION_BATCH_LIMIT + +Maximum number of items allowed in a single batch tokenization or detokenization request (default: `100`). + +This limit is enforced to prevent resource exhaustion from excessively large batch operations. If exceeded, the API returns a `422 Unprocessable Entity` error. ## Master key configuration diff --git a/docs/engines/tokenization.md b/docs/engines/tokenization.md index a2c05f1..9cd5c07 100644 --- a/docs/engines/tokenization.md +++ b/docs/engines/tokenization.md @@ -79,7 +79,7 @@ Example response (`201 Created`): - **Endpoint**: `POST /v1/tokenization/keys/:name/tokenize-batch` - **Capability**: `encrypt` - **Body**: `items` (array of objects with `plaintext`, `metadata`, `ttl`). -- **Limit**: Maximum 100 items per batch. +- **Limit**: Maximum items per batch is configurable via `TOKENIZATION_BATCH_LIMIT` (default 100). Generates tokens for multiple plaintext values in a single atomic operation. If any item fails (e.g., invalid format), the entire batch is rejected. @@ -117,7 +117,7 @@ Example response (`200 OK`): - **Endpoint**: `POST /v1/tokenization/detokenize-batch` - **Capability**: `decrypt` - **Body**: `{"tokens": ["string", "string"]}` -- **Limit**: Maximum 100 tokens per batch. +- **Limit**: Maximum tokens per batch is configurable via `TOKENIZATION_BATCH_LIMIT` (default 100). Retrieves original plaintext values for multiple tokens in a single atomic operation. diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 2753afa..db1496d 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -29,6 +29,44 @@ paths: status: type: string example: healthy + /ready: + get: + tags: [auth] + summary: Readiness check + description: Comprehensive check that includes database connectivity. + responses: + "200": + description: Ready + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: ready + components: + type: object + properties: + database: + type: string + example: ok + "503": + description: Not ready + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: not_ready + components: + type: object + properties: + database: + type: string + example: error /v1/token: post: tags: [auth] diff --git a/internal/app/di_tokenization.go b/internal/app/di_tokenization.go index 8aa1b07..e1b7652 100644 --- a/internal/app/di_tokenization.go +++ b/internal/app/di_tokenization.go @@ -334,5 +334,9 @@ func (c *Container) initTokenizationHandler( logger := c.Logger() - return tokenizationHTTP.NewTokenizationHandler(tokenizationUseCase, logger), nil + return tokenizationHTTP.NewTokenizationHandler( + tokenizationUseCase, + c.config.TokenizationBatchLimit, + logger, + ), nil } diff --git a/internal/config/config.go b/internal/config/config.go index 58b9eda..1d1af19 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -47,6 +47,7 @@ const ( DefaultLockoutDuration = 30 // minutes DefaultMaxRequestBodySize = 1048576 DefaultSecretValueSizeLimit = 524288 + DefaultTokenizationBatchLimit = 100 ) // Config holds all application configuration. @@ -128,6 +129,8 @@ type Config struct { MaxRequestBodySize int64 // SecretValueSizeLimitBytes is the maximum size of a secret value in bytes. SecretValueSizeLimitBytes int + // TokenizationBatchLimit is the maximum number of items in a batch tokenization request. + TokenizationBatchLimit int } // Validate checks if the configuration is valid. @@ -208,6 +211,7 @@ func (c *Config) Validate() error { ), validation.Field(&c.MaxRequestBodySize, validation.Required, validation.Min(int64(1))), validation.Field(&c.SecretValueSizeLimitBytes, validation.Required, validation.Min(1)), + validation.Field(&c.TokenizationBatchLimit, validation.Required, validation.Min(1)), ) } @@ -323,6 +327,12 @@ func Load() (*Config, error) { "SECRET_VALUE_SIZE_LIMIT_BYTES", DefaultSecretValueSizeLimit, ), + + // Tokenization Batch Limit + TokenizationBatchLimit: env.GetInt( + "TOKENIZATION_BATCH_LIMIT", + DefaultTokenizationBatchLimit, + ), } // Validate configuration diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 79f8a18..f6c35ba 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -36,6 +36,7 @@ func TestConfig_Validate(t *testing.T) { RateLimitTokenRequestsPerSec: 5, MaxRequestBodySize: 1048576, SecretValueSizeLimitBytes: 524288, + TokenizationBatchLimit: 100, }, wantErr: false, }, @@ -58,6 +59,26 @@ func TestConfig_Validate(t *testing.T) { }, wantErr: true, }, + { + name: "invalid tokenization batch limit - zero", + cfg: &Config{ + DBDriver: "postgres", + DBConnectionString: "postgres://localhost", + ServerPort: 8080, + MetricsPort: 8081, + LogLevel: "info", + ServerReadTimeout: 15 * time.Second, + ServerWriteTimeout: 15 * time.Second, + ServerIdleTimeout: 60 * time.Second, + MetricsServerReadTimeout: 15 * time.Second, + MetricsServerWriteTimeout: 15 * time.Second, + MetricsServerIdleTimeout: 60 * time.Second, + MaxRequestBodySize: 1048576, + SecretValueSizeLimitBytes: 524288, + TokenizationBatchLimit: 0, + }, + wantErr: true, + }, { name: "invalid db driver", cfg: &Config{ @@ -206,6 +227,7 @@ func TestConfig_Validate(t *testing.T) { MetricsServerIdleTimeout: 60 * time.Second, MaxRequestBodySize: 1048576, SecretValueSizeLimitBytes: 524288, + TokenizationBatchLimit: 100, KMSProvider: "localsecrets", KMSKeyURI: "base64key://smGbjm71Nxd1Ig5FS0wj9SlbzAIrnolCz9bQQ6uAhl4=", }, @@ -227,6 +249,7 @@ func TestConfig_Validate(t *testing.T) { MetricsServerIdleTimeout: 60 * time.Second, MaxRequestBodySize: 1048576, SecretValueSizeLimitBytes: 524288, + TokenizationBatchLimit: 100, KMSProvider: "gcpkms", KMSKeyURI: "gcpkms://projects/my-project/locations/global/keyRings/my-keyring/cryptoKeys/my-key", }, @@ -248,6 +271,7 @@ func TestConfig_Validate(t *testing.T) { MetricsServerIdleTimeout: 60 * time.Second, MaxRequestBodySize: 1048576, SecretValueSizeLimitBytes: 524288, + TokenizationBatchLimit: 100, KMSProvider: "awskms", KMSKeyURI: "awskms://arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012?region=us-east-1", }, @@ -269,6 +293,7 @@ func TestConfig_Validate(t *testing.T) { MetricsServerIdleTimeout: 60 * time.Second, MaxRequestBodySize: 1048576, SecretValueSizeLimitBytes: 524288, + TokenizationBatchLimit: 100, KMSProvider: "azurekeyvault", KMSKeyURI: "azurekeyvault://myvault.vault.azure.net/keys/mykey", }, @@ -290,6 +315,7 @@ func TestConfig_Validate(t *testing.T) { MetricsServerIdleTimeout: 60 * time.Second, MaxRequestBodySize: 1048576, SecretValueSizeLimitBytes: 524288, + TokenizationBatchLimit: 100, KMSProvider: "hashivault", KMSKeyURI: "hashivault://mykey", }, @@ -311,6 +337,7 @@ func TestConfig_Validate(t *testing.T) { MetricsServerIdleTimeout: 60 * time.Second, MaxRequestBodySize: 1048576, SecretValueSizeLimitBytes: 524288, + TokenizationBatchLimit: 100, }, wantErr: false, }, @@ -330,6 +357,7 @@ func TestConfig_Validate(t *testing.T) { MetricsServerIdleTimeout: 1 * time.Second, MaxRequestBodySize: 1048576, SecretValueSizeLimitBytes: 524288, + TokenizationBatchLimit: 100, }, wantErr: false, }, @@ -349,6 +377,7 @@ func TestConfig_Validate(t *testing.T) { MetricsServerIdleTimeout: 300 * time.Second, MaxRequestBodySize: 1048576, SecretValueSizeLimitBytes: 524288, + TokenizationBatchLimit: 100, }, wantErr: false, }, @@ -434,6 +463,15 @@ func TestLoad(t *testing.T) { validate func(t *testing.T, cfg *Config) expectError bool }{ + { + name: "load custom tokenization batch limit", + envVars: map[string]string{ + "TOKENIZATION_BATCH_LIMIT": "50", + }, + validate: func(t *testing.T, cfg *Config) { + assert.Equal(t, 50, cfg.TokenizationBatchLimit) + }, + }, { name: "load default configuration", envVars: map[string]string{}, @@ -467,6 +505,7 @@ func TestLoad(t *testing.T) { assert.Equal(t, 60*time.Second, cfg.MetricsServerIdleTimeout) assert.Equal(t, int64(1048576), cfg.MaxRequestBodySize) assert.Equal(t, 524288, cfg.SecretValueSizeLimitBytes) + assert.Equal(t, 100, cfg.TokenizationBatchLimit) }, }, { diff --git a/internal/config/metrics_server_config_test.go b/internal/config/metrics_server_config_test.go index c68321a..49bcfd2 100644 --- a/internal/config/metrics_server_config_test.go +++ b/internal/config/metrics_server_config_test.go @@ -19,6 +19,7 @@ func TestMetricsServerConfig_Validate(t *testing.T) { ServerIdleTimeout: 60 * time.Second, MaxRequestBodySize: 1048576, SecretValueSizeLimitBytes: 524288, + TokenizationBatchLimit: 100, MetricsServerReadTimeout: 15 * time.Second, MetricsServerWriteTimeout: 15 * time.Second, MetricsServerIdleTimeout: 60 * time.Second, diff --git a/internal/tokenization/http/dto/request.go b/internal/tokenization/http/dto/request.go index 99ab06a..2a643a6 100644 --- a/internal/tokenization/http/dto/request.go +++ b/internal/tokenization/http/dto/request.go @@ -90,11 +90,11 @@ type TokenizeBatchRequest struct { } // Validate checks if the tokenize batch request is valid. -func (r *TokenizeBatchRequest) Validate() error { +func (r *TokenizeBatchRequest) Validate(limit int) error { return validation.ValidateStruct(r, validation.Field(&r.Items, validation.Required, - validation.Length(1, 100), + validation.Length(1, limit).Error(fmt.Sprintf("batch size exceeds limit of %d", limit)), ), ) } @@ -120,11 +120,11 @@ type DetokenizeBatchRequest struct { } // Validate checks if the detokenize batch request is valid. -func (r *DetokenizeBatchRequest) Validate() error { +func (r *DetokenizeBatchRequest) Validate(limit int) error { return validation.ValidateStruct(r, validation.Field(&r.Tokens, validation.Required, - validation.Length(1, 100), + validation.Length(1, limit).Error(fmt.Sprintf("batch size exceeds limit of %d", limit)), validation.Each(validation.Required, customValidation.NotBlank), ), ) diff --git a/internal/tokenization/http/dto/request_test.go b/internal/tokenization/http/dto/request_test.go index 52cf0e6..01d78a9 100644 --- a/internal/tokenization/http/dto/request_test.go +++ b/internal/tokenization/http/dto/request_test.go @@ -486,7 +486,7 @@ func TestTokenizeBatchRequest_Validate(t *testing.T) { {Plaintext: "V29ybGQ="}, }, } - err := req.Validate() + err := req.Validate(100) assert.NoError(t, err) }) @@ -494,16 +494,17 @@ func TestTokenizeBatchRequest_Validate(t *testing.T) { req := TokenizeBatchRequest{ Items: []TokenizeRequest{}, } - err := req.Validate() + err := req.Validate(100) assert.Error(t, err) }) t.Run("Error_TooManyItems", func(t *testing.T) { req := TokenizeBatchRequest{ - Items: make([]TokenizeRequest, 101), + Items: make([]TokenizeRequest, 11), } - err := req.Validate() + err := req.Validate(10) assert.Error(t, err) + assert.Equal(t, "items: batch size exceeds limit of 10.", err.Error()) }) } @@ -512,7 +513,7 @@ func TestDetokenizeBatchRequest_Validate(t *testing.T) { req := DetokenizeBatchRequest{ Tokens: []string{"t1", "t2"}, } - err := req.Validate() + err := req.Validate(100) assert.NoError(t, err) }) @@ -520,23 +521,24 @@ func TestDetokenizeBatchRequest_Validate(t *testing.T) { req := DetokenizeBatchRequest{ Tokens: []string{}, } - err := req.Validate() + err := req.Validate(100) assert.Error(t, err) }) t.Run("Error_TooManyTokens", func(t *testing.T) { req := DetokenizeBatchRequest{ - Tokens: make([]string, 101), + Tokens: make([]string, 11), } - err := req.Validate() + err := req.Validate(10) assert.Error(t, err) + assert.Equal(t, "tokens: batch size exceeds limit of 10.", err.Error()) }) t.Run("Error_BlankTokenInBatch", func(t *testing.T) { req := DetokenizeBatchRequest{ Tokens: []string{"t1", " "}, } - err := req.Validate() + err := req.Validate(100) assert.Error(t, err) }) } diff --git a/internal/tokenization/http/tokenization_handler.go b/internal/tokenization/http/tokenization_handler.go index 2b35e34..377a052 100644 --- a/internal/tokenization/http/tokenization_handler.go +++ b/internal/tokenization/http/tokenization_handler.go @@ -21,16 +21,19 @@ import ( // Coordinates tokenize, detokenize, validate, and revoke operations with TokenizationUseCase. type TokenizationHandler struct { tokenizationUseCase tokenizationUseCase.TokenizationUseCase + batchLimit int logger *slog.Logger } // NewTokenizationHandler creates a new tokenization handler with required dependencies. func NewTokenizationHandler( tokenizationUseCase tokenizationUseCase.TokenizationUseCase, + batchLimit int, logger *slog.Logger, ) *TokenizationHandler { return &TokenizationHandler{ tokenizationUseCase: tokenizationUseCase, + batchLimit: batchLimit, logger: logger, } } @@ -111,7 +114,7 @@ func (h *TokenizationHandler) TokenizeBatchHandler(c *gin.Context) { } // Validate request - if err := req.Validate(); err != nil { + if err := req.Validate(h.batchLimit); err != nil { httputil.HandleValidationErrorGin(c, customValidation.WrapValidationError(err), h.logger) return } @@ -233,7 +236,7 @@ func (h *TokenizationHandler) DetokenizeBatchHandler(c *gin.Context) { } // Validate request - if err := req.Validate(); err != nil { + if err := req.Validate(h.batchLimit); err != nil { httputil.HandleValidationErrorGin(c, customValidation.WrapValidationError(err), h.logger) return } diff --git a/internal/tokenization/http/tokenization_handler_test.go b/internal/tokenization/http/tokenization_handler_test.go index 0520bdb..0eab515 100644 --- a/internal/tokenization/http/tokenization_handler_test.go +++ b/internal/tokenization/http/tokenization_handler_test.go @@ -30,7 +30,7 @@ func setupTestTokenizationHandler(t *testing.T) (*TokenizationHandler, *mocks.Mo mockTokenizationUseCase := mocks.NewMockTokenizationUseCase(t) logger := slog.New(slog.NewTextHandler(io.Discard, nil)) - handler := NewTokenizationHandler(mockTokenizationUseCase, logger) + handler := NewTokenizationHandler(mockTokenizationUseCase, 100, logger) return handler, mockTokenizationUseCase } @@ -489,6 +489,23 @@ func TestTokenizationHandler_TokenizeBatchHandler(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) }) + + t.Run("Error_BatchLimitExceeded", func(t *testing.T) { + handler, _ := setupTestTokenizationHandler(t) + handler.batchLimit = 5 + + request := dto.TokenizeBatchRequest{ + Items: make([]dto.TokenizeRequest, 6), + } + + c, w := createTestContext(http.MethodPost, "/v1/tokenization/keys/test-key/tokenize-batch", request) + c.Params = gin.Params{{Key: "name", Value: "test-key"}} + + handler.TokenizeBatchHandler(c) + + assert.Equal(t, http.StatusUnprocessableEntity, w.Code) + assert.Contains(t, w.Body.String(), "batch size exceeds limit of 5") + }) } func TestTokenizationHandler_DetokenizeBatchHandler(t *testing.T) { @@ -534,6 +551,22 @@ func TestTokenizationHandler_DetokenizeBatchHandler(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) }) + + t.Run("Error_BatchLimitExceeded", func(t *testing.T) { + handler, _ := setupTestTokenizationHandler(t) + handler.batchLimit = 5 + + request := dto.DetokenizeBatchRequest{ + Tokens: make([]string, 6), + } + + c, w := createTestContext(http.MethodPost, "/v1/tokenization/detokenize-batch", request) + + handler.DetokenizeBatchHandler(c) + + assert.Equal(t, http.StatusUnprocessableEntity, w.Code) + assert.Contains(t, w.Body.String(), "batch size exceeds limit of 5") + }) } func TestTokenizationHandler_RevokeHandler(t *testing.T) {