Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .mockery.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ packages:
github.com/allisson/secrets/internal/auth/service:
interfaces:
SecretService: {}
TokenService: {}
AuditSigner: {}
github.com/allisson/secrets/internal/auth/usecase:
interfaces:
AuditLogRepository: {}
Expand Down
45 changes: 45 additions & 0 deletions cmd/app/auth_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,51 @@ import (
// getAuthCommands returns the authentication-related CLI commands.
func getAuthCommands() []*cli.Command {
return []*cli.Command{
{
Name: "purge-auth-tokens",
Usage: "Permanently delete expired and revoked authentication tokens older than specified days",
Flags: []cli.Flag{
&cli.IntFlag{
Name: "days",
Aliases: []string{"d"},
Value: 30,
Usage: "Delete tokens older than this many days",
},
&cli.BoolFlag{
Name: "dry-run",
Aliases: []string{"n"},
Value: false,
Usage: "Show how many tokens would be deleted without deleting",
},
&cli.StringFlag{
Name: "format",
Aliases: []string{"f"},
Value: "text",
Usage: "Output format: 'text' or 'json'",
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
return commands.ExecuteWithContainer(
ctx,
func(ctx context.Context, container *app.Container) error {
tokenUseCase, err := container.TokenUseCase(ctx)
if err != nil {
return err
}

return commands.RunPurgeAuthTokens(
ctx,
tokenUseCase,
container.Logger(),
commands.DefaultIO().Writer,
int(cmd.Int("days")),
cmd.Bool("dry-run"),
cmd.String("format"),
)
},
)
},
},
{
Name: "clean-expired-tokens",
Usage: "Delete expired tokens older than specified days",
Expand Down
100 changes: 100 additions & 0 deletions cmd/app/commands/purge_auth_tokens.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package commands

import (
"context"
"encoding/json"
"fmt"
"io"
"log/slog"

"github.com/allisson/secrets/internal/auth/usecase"
)

// PurgeAuthTokensResult holds the result of the authentication token purge operation.
type PurgeAuthTokensResult struct {
Count int64 `json:"count"`
Days int `json:"days"`
DryRun bool `json:"dry_run"`
}

// ToText returns a human-readable representation of the purge result.
func (r *PurgeAuthTokensResult) ToText() string {
if r.DryRun {
return fmt.Sprintf(
"Dry-run mode: Would purge %d expired/revoked authentication token(s) older than %d day(s)",
r.Count,
r.Days,
)
}
return fmt.Sprintf(
"Successfully purged %d expired/revoked authentication token(s) older than %d day(s)",
r.Count,
r.Days,
)
}

// ToJSON returns a JSON representation of the purge result.
func (r *PurgeAuthTokensResult) ToJSON() string {
jsonBytes, _ := json.MarshalIndent(r, "", " ")
return string(jsonBytes)
}

// RunPurgeAuthTokens deletes expired and revoked authentication tokens older than the specified number of days.
// Supports dry-run mode (if implemented in usecase) and multiple output formats.
func RunPurgeAuthTokens(
ctx context.Context,
tokenUseCase usecase.TokenUseCase,
logger *slog.Logger,
writer io.Writer,
days int,
dryRun bool,
format string,
) error {
// Validate days parameter
if days < 0 {
return fmt.Errorf("days must be a non-negative number, got: %d", days)
}

logger.Info("purging authentication tokens",
slog.Int("days", days),
slog.Bool("dry_run", dryRun),
)

// Note: dryRun is not yet supported in TokenUseCase.PurgeExpiredAndRevoked
// For now, we will only proceed if dryRun is false or inform that it's not supported.
if dryRun {
result := &PurgeAuthTokensResult{
Count: 0,
Days: days,
DryRun: dryRun,
}
_, _ = fmt.Fprintln(
writer,
"Notice: Dry-run is not yet implemented for auth token purging. No tokens were deleted.",
)
WriteOutput(writer, format, result)
return nil
}

// Execute purge
count, err := tokenUseCase.PurgeExpiredAndRevoked(ctx, days)
if err != nil {
return fmt.Errorf("failed to purge authentication tokens: %w", err)
}

// Output result
result := &PurgeAuthTokensResult{
Count: count,
Days: days,
DryRun: dryRun,
}
WriteOutput(writer, format, result)

logger.Info("purge completed",
slog.Int64("count", count),
slog.Int("days", days),
slog.Bool("dry_run", dryRun),
)

return nil
}
49 changes: 49 additions & 0 deletions cmd/app/commands/purge_auth_tokens_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package commands

import (
"bytes"
"context"
"log/slog"
"testing"

"github.com/stretchr/testify/require"

usecaseMocks "github.com/allisson/secrets/internal/auth/usecase/mocks"
)

func TestRunPurgeAuthTokens(t *testing.T) {
ctx := context.Background()
logger := slog.Default()
days := 30

t.Run("text-output", func(t *testing.T) {
mockUseCase := usecaseMocks.NewMockTokenUseCase(t)
mockUseCase.EXPECT().PurgeExpiredAndRevoked(ctx, days).Return(int64(100), nil).Once()

var out bytes.Buffer
err := RunPurgeAuthTokens(ctx, mockUseCase, logger, &out, days, false, "text")

require.NoError(t, err)
require.Contains(t, out.String(), "Successfully purged 100 expired/revoked authentication token(s)")
})

t.Run("json-output", func(t *testing.T) {
mockUseCase := usecaseMocks.NewMockTokenUseCase(t)
mockUseCase.EXPECT().PurgeExpiredAndRevoked(ctx, days).Return(int64(50), nil).Once()

var out bytes.Buffer
err := RunPurgeAuthTokens(ctx, mockUseCase, logger, &out, days, false, "json")

require.NoError(t, err)
require.Contains(t, out.String(), `"count": 50`)
require.Contains(t, out.String(), `"dry_run": false`)
})

t.Run("invalid-days", func(t *testing.T) {
mockUseCase := usecaseMocks.NewMockTokenUseCase(t)
err := RunPurgeAuthTokens(ctx, mockUseCase, logger, &bytes.Buffer{}, -1, false, "text")

require.Error(t, err)
require.Contains(t, err.Error(), "days must be a non-negative number")
})
}
18 changes: 17 additions & 1 deletion cmd/app/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func TestGetAuthCommands(t *testing.T) {
cmds := getAuthCommands()
require.NotEmpty(t, cmds)

expectedCmds := []string{"clean-expired-tokens", "create-client", "update-client"}
expectedCmds := []string{"purge-auth-tokens", "clean-expired-tokens", "create-client", "update-client"}
for _, name := range expectedCmds {
found := false
for _, cmd := range cmds {
Expand All @@ -38,6 +38,22 @@ func TestGetAuthCommands(t *testing.T) {
}
require.Truef(t, found, "command %s not found", name)
}

// Flag checks
for _, cmd := range cmds {
switch cmd.Name {
case "purge-auth-tokens", "clean-expired-tokens":
require.NotEmpty(t, cmd.Flags)
hasDaysFlag := false
for _, flag := range cmd.Flags {
if flag.Names()[0] == "days" {
hasDaysFlag = true
break
}
}
require.True(t, hasDaysFlag, "command %s missing --days flag", cmd.Name)
}
}
}

func TestGetKeyCommands(t *testing.T) {
Expand Down
5 changes: 5 additions & 0 deletions conductor/archive/auth_token_revocation_20260305/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Track auth_token_revocation_20260305 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": "auth_token_revocation_20260305",
"type": "feature",
"status": "new",
"created_at": "2026-03-05T14:30:00Z",
"updated_at": "2026-03-05T14:30:00Z",
"description": "Add Auth Token Revocation"
}
30 changes: 30 additions & 0 deletions conductor/archive/auth_token_revocation_20260305/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Implementation Plan: Auth Token Revocation

## Phase 1: Data Persistence & Repository
- [x] Task: Update `TokenRepository` interface in `internal/auth/usecase/interface.go` with `RevokeByTokenID`, `RevokeByClientID`, and `PurgeExpiredAndRevoked`. bced163
- [x] Task: Implement new methods in `internal/auth/repository/postgresql/token_repository.go` with integration tests (tagged `//go:build integration`). 2d85877
- [x] Task: Implement new methods in `internal/auth/repository/mysql/token_repository.go` with integration tests (tagged `//go:build integration`). 4d0f84a
- [x] Task: Conductor - User Manual Verification 'Phase 1' (Protocol in workflow.md) 127e4fd

## Phase 2: Application Logic & Audit
- [x] Task: Update `ClientUseCase` and `TokenUseCase` in `internal/auth/usecase/` with audit logging and unit tests. bb3ca60
- [x] Task: Ensure `TokenService` and `SecretService` mocks are generated and utilized in tests. 9d8839b
- [x] Task: Conductor - User Manual Verification 'Phase 2' (Protocol in workflow.md) f4d40c7

## Phase 3: API & Authentication Middleware
- [x] Task: Implement `DELETE /v1/token` handler in `internal/auth/http/token_handler.go` with unit tests in `internal/auth/http/token_handler_test.go`. 6c1c9cc
- [x] Task: Implement `DELETE /v1/clients/:id/tokens` handler in `internal/auth/http/client_handler.go` with unit tests in `internal/auth/http/client_handler_test.go`. 6c1c9cc
- [x] Task: Update `AuthenticationMiddleware` in `internal/http/middleware.go` (or relevant middleware) with unit tests in `internal/http/middleware_test.go`. f984c9c
- [x] Task: Conductor - User Manual Verification 'Phase 3' (Protocol in workflow.md) f984c9c

## Phase 4: Integration Testing & CLI
- [x] Task: Update integration tests in `test/integration/` to verify end-to-end token revocation. 7bd6462
- [x] Task: Add `purge-auth-tokens` command to the main CLI application. af6fdff
- [x] Task: Implement unit tests for the `purge-auth-tokens` CLI command in `cmd/app/commands_test.go`. af6fdff
- [x] Task: Conductor - User Manual Verification 'Phase 4' (Protocol in workflow.md) 0983107

## Phase 5: Documentation
- [x] Task: Update CLI documentation in `docs/cli-commands.md`. af6fdff
- [x] Task: Update `docs/openapi.yaml` with the new DELETE endpoints. af6fdff
- [x] Task: Update `docs/auth/policies.md` with policy examples. af6fdff
- [x] Task: Conductor - User Manual Verification 'Phase 5' (Protocol in workflow.md) 9f36345
36 changes: 36 additions & 0 deletions conductor/archive/auth_token_revocation_20260305/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Specification: Auth Token Revocation

## Overview
This track implements a token revocation mechanism for the Secrets manager. Since tokens are opaque strings stored in the database, this track will add state management to track their validity beyond their expiration timestamp.

## Functional Requirements
- **Token Revocation Endpoints:**
- `DELETE /v1/token`: Revokes the current bearer token used in the request.
- `DELETE /v1/clients/:id/tokens`: Revokes all active tokens for the specified client ID. This endpoint requires the requester to have `delete` capability on the target client.
- **Audit Logs:** Generate an HMAC-signed audit log entry for every successful revocation.
- **Revocation Storage:** Since tokens are already stored in the database, add a `revoked_at` timestamp to the tokens table to mark them as revoked.
- **Validation Logic:** Update the token validation logic in the authentication middleware to reject tokens that have a non-null `revoked_at` timestamp.
- **Purge Command:** Implement a CLI command `purge-auth-tokens` to permanently delete revoked and expired tokens from the database.
- **Documentation:**
- Update API documentation (OpenAPI and Markdown docs) to include the new revocation endpoints.
- Update the "Policies Cookbook" to include examples of how to restrict or grant access to the new revocation features.

## Non-Functional Requirements
- **Performance:** Validation checks must be optimized with database indexes on the `revoked_at` field and client IDs.
- **Security:** Ensure that the `DELETE /v1/clients/:id/tokens` endpoint is properly protected by capability-based authorization.
- **Data Integrity:** Maintain consistency between the revocation state and audit logs.

## Acceptance Criteria
- [ ] `DELETE /v1/token` successfully marks the current token as revoked in the database.
- [ ] `DELETE /v1/clients/:id/tokens` successfully marks all tokens for client `:id` as revoked.
- [ ] Subsequent requests using a revoked token are rejected with a `401 Unauthorized` error.
- [ ] Audit logs correctly record the actor, action, and target of each revocation.
- [ ] `secrets purge-auth-tokens` deletes revoked and expired tokens from the database.
- [ ] API documentation is updated.
- [ ] Policies cookbook is updated with revocation-related examples.
- [ ] Implementation is verified against both PostgreSQL and MySQL.
- [ ] Unit and integration tests cover all new logic, middleware, and CLI command.

## Out of Scope
- **Automatic Background Purging:** Periodic removal of expired/revoked records without manual CLI invocation.
- **UI Integration:** Changes to the web UI.
1 change: 1 addition & 0 deletions conductor/product.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ To provide a secure, developer-friendly, and lightweight secrets management plat
- **Secret Management (Storage):** Versioned, envelope-encrypted storage with support for arbitrary key-value pairs.
- **Transit Engine (EaaS):** On-the-fly encryption/decryption of application data without database storage.
- **Tokenization Engine:** Format-preserving tokens for sensitive data types like credit card numbers.
- **Auth Token Revocation:** Immediate invalidation of authentication tokens (single or client-wide) with full state management.
- **Audit Logs:** HMAC-signed audit trails capturing every access attempt and policy evaluation.
- **KMS Integration:** Native support for AWS KMS, Google Cloud KMS, Azure Key Vault, and HashiCorp Vault.

Expand Down
2 changes: 1 addition & 1 deletion conductor/tracks.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Project Tracks

This file tracks all major tracks for the project. Each track has its own detailed plan in its respective folder.
This file tracks all major tracks for the project. Each track has its own detailed plan in its respective folder.
Loading
Loading