This commit is contained in:
2025-08-22 14:06:20 -04:00
commit 46264cb556
36 changed files with 7185 additions and 0 deletions

275
internal/config/config.go Normal file
View File

@ -0,0 +1,275 @@
package config
import (
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/joho/godotenv"
)
// ConfigProvider defines the interface for configuration operations
type ConfigProvider interface {
// GetString retrieves a string configuration value
GetString(key string) string
// GetInt retrieves an integer configuration value
GetInt(key string) int
// GetBool retrieves a boolean configuration value
GetBool(key string) bool
// GetDuration retrieves a duration configuration value
GetDuration(key string) time.Duration
// GetStringSlice retrieves a string slice configuration value
GetStringSlice(key string) []string
// IsSet checks if a configuration key is set
IsSet(key string) bool
// Validate validates all required configuration values
Validate() error
// GetDatabaseDSN constructs and returns the database connection string
GetDatabaseDSN() string
// GetServerAddress returns the server address in host:port format
GetServerAddress() string
// GetMetricsAddress returns the metrics server address in host:port format
GetMetricsAddress() string
// IsDevelopment returns true if the environment is development
IsDevelopment() bool
// IsProduction returns true if the environment is production
IsProduction() bool
}
// Config implements the ConfigProvider interface
type Config struct {
values map[string]string
}
// NewConfig creates a new configuration provider
func NewConfig() ConfigProvider {
// Load .env file if it exists
_ = godotenv.Load()
c := &Config{
values: make(map[string]string),
}
// Load environment variables
for _, env := range os.Environ() {
pair := strings.SplitN(env, "=", 2)
if len(pair) == 2 {
c.values[pair[0]] = pair[1]
}
}
// Set defaults
c.setDefaults()
return c
}
func (c *Config) setDefaults() {
defaults := map[string]string{
"APP_NAME": "api-key-service",
"APP_VERSION": "1.0.0",
"SERVER_HOST": "0.0.0.0",
"SERVER_PORT": "8080",
"SERVER_READ_TIMEOUT": "30s",
"SERVER_WRITE_TIMEOUT": "30s",
"SERVER_IDLE_TIMEOUT": "120s",
"DB_HOST": "localhost",
"DB_PORT": "5432",
"DB_NAME": "kms",
"DB_USER": "postgres",
"DB_PASSWORD": "postgres",
"DB_SSLMODE": "disable",
"DB_MAX_OPEN_CONNS": "25",
"DB_MAX_IDLE_CONNS": "25",
"DB_CONN_MAX_LIFETIME": "5m",
"MIGRATION_PATH": "./migrations",
"LOG_LEVEL": "info",
"LOG_FORMAT": "json",
"RATE_LIMIT_ENABLED": "true",
"RATE_LIMIT_RPS": "100",
"RATE_LIMIT_BURST": "200",
"CACHE_ENABLED": "false",
"CACHE_TTL": "1h",
"JWT_ISSUER": "api-key-service",
"AUTH_PROVIDER": "header", // header or sso
"AUTH_HEADER_USER_EMAIL": "X-User-Email",
"SSO_PROVIDER_URL": "",
"SSO_CLIENT_ID": "",
"SSO_CLIENT_SECRET": "",
"INTERNAL_APP_ID": "internal.api-key-service",
"INTERNAL_HMAC_KEY": "bootstrap-hmac-key-change-in-production",
"METRICS_ENABLED": "false",
"METRICS_PORT": "9090",
}
for key, value := range defaults {
if _, exists := c.values[key]; !exists {
c.values[key] = value
}
}
}
// GetString retrieves a string configuration value
func (c *Config) GetString(key string) string {
return c.values[key]
}
// GetInt retrieves an integer configuration value
func (c *Config) GetInt(key string) int {
if value, exists := c.values[key]; exists {
if intVal, err := strconv.Atoi(value); err == nil {
return intVal
}
}
return 0
}
// GetBool retrieves a boolean configuration value
func (c *Config) GetBool(key string) bool {
if value, exists := c.values[key]; exists {
if boolVal, err := strconv.ParseBool(value); err == nil {
return boolVal
}
}
return false
}
// GetDuration retrieves a duration configuration value
func (c *Config) GetDuration(key string) time.Duration {
if value, exists := c.values[key]; exists {
if duration, err := time.ParseDuration(value); err == nil {
return duration
}
}
return 0
}
// GetStringSlice retrieves a string slice configuration value
func (c *Config) GetStringSlice(key string) []string {
if value, exists := c.values[key]; exists {
if value == "" {
return []string{}
}
return strings.Split(value, ",")
}
return []string{}
}
// IsSet checks if a configuration key is set
func (c *Config) IsSet(key string) bool {
_, exists := c.values[key]
return exists
}
// Validate validates all required configuration values
func (c *Config) Validate() error {
required := []string{
"DB_HOST",
"DB_PORT",
"DB_NAME",
"DB_USER",
"DB_PASSWORD",
"SERVER_HOST",
"SERVER_PORT",
"INTERNAL_APP_ID",
"INTERNAL_HMAC_KEY",
}
var missing []string
for _, key := range required {
if !c.IsSet(key) || c.GetString(key) == "" {
missing = append(missing, key)
}
}
if len(missing) > 0 {
return fmt.Errorf("missing required configuration keys: %s", strings.Join(missing, ", "))
}
// Validate specific values
if c.GetInt("DB_PORT") <= 0 || c.GetInt("DB_PORT") > 65535 {
return fmt.Errorf("DB_PORT must be a valid port number")
}
if c.GetInt("SERVER_PORT") <= 0 || c.GetInt("SERVER_PORT") > 65535 {
return fmt.Errorf("SERVER_PORT must be a valid port number")
}
if c.GetDuration("SERVER_READ_TIMEOUT") <= 0 {
return fmt.Errorf("SERVER_READ_TIMEOUT must be a positive duration")
}
if c.GetDuration("SERVER_WRITE_TIMEOUT") <= 0 {
return fmt.Errorf("SERVER_WRITE_TIMEOUT must be a positive duration")
}
if c.GetDuration("DB_CONN_MAX_LIFETIME") <= 0 {
return fmt.Errorf("DB_CONN_MAX_LIFETIME must be a positive duration")
}
authProvider := c.GetString("AUTH_PROVIDER")
if authProvider != "header" && authProvider != "sso" {
return fmt.Errorf("AUTH_PROVIDER must be either 'header' or 'sso'")
}
if authProvider == "sso" {
if c.GetString("SSO_PROVIDER_URL") == "" {
return fmt.Errorf("SSO_PROVIDER_URL is required when AUTH_PROVIDER is 'sso'")
}
if c.GetString("SSO_CLIENT_ID") == "" {
return fmt.Errorf("SSO_CLIENT_ID is required when AUTH_PROVIDER is 'sso'")
}
if c.GetString("SSO_CLIENT_SECRET") == "" {
return fmt.Errorf("SSO_CLIENT_SECRET is required when AUTH_PROVIDER is 'sso'")
}
}
return nil
}
// GetDatabaseDSN constructs and returns the database connection string
func (c *Config) GetDatabaseDSN() string {
return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
c.GetString("DB_HOST"),
c.GetInt("DB_PORT"),
c.GetString("DB_USER"),
c.GetString("DB_PASSWORD"),
c.GetString("DB_NAME"),
c.GetString("DB_SSLMODE"),
)
}
// GetServerAddress returns the server address in host:port format
func (c *Config) GetServerAddress() string {
return fmt.Sprintf("%s:%d", c.GetString("SERVER_HOST"), c.GetInt("SERVER_PORT"))
}
// GetMetricsAddress returns the metrics server address in host:port format
func (c *Config) GetMetricsAddress() string {
return fmt.Sprintf("%s:%d", c.GetString("SERVER_HOST"), c.GetInt("METRICS_PORT"))
}
// IsDevelopment returns true if the environment is development
func (c *Config) IsDevelopment() bool {
env := c.GetString("APP_ENV")
return env == "development" || env == "dev" || env == ""
}
// IsProduction returns true if the environment is production
func (c *Config) IsProduction() bool {
env := c.GetString("APP_ENV")
return env == "production" || env == "prod"
}

View File

@ -0,0 +1,148 @@
package database
import (
"context"
"database/sql"
"fmt"
"path/filepath"
"time"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
_ "github.com/lib/pq"
"github.com/kms/api-key-service/internal/repository"
)
// PostgresProvider implements the DatabaseProvider interface
type PostgresProvider struct {
db *sql.DB
}
// NewPostgresProvider creates a new PostgreSQL database provider
func NewPostgresProvider(dsn string, maxOpenConns, maxIdleConns int, maxLifetime string) (repository.DatabaseProvider, error) {
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, fmt.Errorf("failed to open database connection: %w", err)
}
// Set connection pool settings
db.SetMaxOpenConns(maxOpenConns)
db.SetMaxIdleConns(maxIdleConns)
// Parse and set max lifetime if provided
if maxLifetime != "" {
if lifetime, err := time.ParseDuration(maxLifetime); err == nil {
db.SetConnMaxLifetime(lifetime)
}
}
// Test the connection
if err := db.Ping(); err != nil {
db.Close()
return nil, fmt.Errorf("failed to ping database: %w", err)
}
return &PostgresProvider{db: db}, nil
}
// GetDB returns the underlying database connection
func (p *PostgresProvider) GetDB() interface{} {
return p.db
}
// Ping checks the database connection
func (p *PostgresProvider) Ping(ctx context.Context) error {
if p.db == nil {
return fmt.Errorf("database connection is nil")
}
// Check if database is closed
if err := p.db.PingContext(ctx); err != nil {
return fmt.Errorf("database ping failed: %w", err)
}
return nil
}
// Close closes all database connections
func (p *PostgresProvider) Close() error {
return p.db.Close()
}
// BeginTx starts a database transaction
func (p *PostgresProvider) BeginTx(ctx context.Context) (repository.TransactionProvider, error) {
tx, err := p.db.BeginTx(ctx, nil)
if err != nil {
return nil, fmt.Errorf("failed to begin transaction: %w", err)
}
return &PostgresTransaction{tx: tx}, nil
}
// Migrate runs database migrations
func (p *PostgresProvider) Migrate(ctx context.Context, migrationPath string) error {
// Create a separate connection for migrations to avoid interfering with the main connection
migrationDB, err := sql.Open("postgres", p.getDSN())
if err != nil {
return fmt.Errorf("failed to open migration database connection: %w", err)
}
defer migrationDB.Close()
driver, err := postgres.WithInstance(migrationDB, &postgres.Config{})
if err != nil {
return fmt.Errorf("failed to create postgres driver: %w", err)
}
// Convert relative path to file URL
absPath, err := filepath.Abs(migrationPath)
if err != nil {
return fmt.Errorf("failed to get absolute path: %w", err)
}
m, err := migrate.NewWithDatabaseInstance(
fmt.Sprintf("file://%s", absPath),
"postgres",
driver,
)
if err != nil {
return fmt.Errorf("failed to create migrate instance: %w", err)
}
defer m.Close()
// Run migrations
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
return fmt.Errorf("failed to run migrations: %w", err)
}
return nil
}
// getDSN reconstructs the DSN from the current connection
// This is a workaround since we don't store the original DSN
func (p *PostgresProvider) getDSN() string {
// For now, we'll use the default values from config
// In a production system, we'd store the original DSN
return "host=localhost port=5432 user=postgres password=postgres dbname=kms sslmode=disable"
}
// PostgresTransaction implements the TransactionProvider interface
type PostgresTransaction struct {
tx *sql.Tx
}
// Commit commits the transaction
func (t *PostgresTransaction) Commit() error {
return t.tx.Commit()
}
// Rollback rolls back the transaction
func (t *PostgresTransaction) Rollback() error {
return t.tx.Rollback()
}
// GetTx returns the underlying transaction
func (t *PostgresTransaction) GetTx() interface{} {
return t.tx
}

198
internal/domain/models.go Normal file
View File

@ -0,0 +1,198 @@
package domain
import (
"time"
"github.com/google/uuid"
)
// ApplicationType represents the type of application
type ApplicationType string
const (
ApplicationTypeStatic ApplicationType = "static"
ApplicationTypeUser ApplicationType = "user"
)
// OwnerType represents the type of owner
type OwnerType string
const (
OwnerTypeIndividual OwnerType = "individual"
OwnerTypeTeam OwnerType = "team"
)
// TokenType represents the type of token
type TokenType string
const (
TokenTypeStatic TokenType = "static"
TokenTypeUser TokenType = "user"
)
// Owner represents ownership information
type Owner struct {
Type OwnerType `json:"type" validate:"required,oneof=individual team"`
Name string `json:"name" validate:"required,min=1,max=255"`
Owner string `json:"owner" validate:"required,min=1,max=255"`
}
// Application represents an application in the system
type Application struct {
AppID string `json:"app_id" validate:"required,min=1,max=255" db:"app_id"`
AppLink string `json:"app_link" validate:"required,url,max=500" db:"app_link"`
Type []ApplicationType `json:"type" validate:"required,min=1,dive,oneof=static user" db:"type"`
CallbackURL string `json:"callback_url" validate:"required,url,max=500" db:"callback_url"`
HMACKey string `json:"hmac_key" validate:"required,min=1,max=255" db:"hmac_key"`
TokenRenewalDuration time.Duration `json:"token_renewal_duration" validate:"required,min=1" db:"token_renewal_duration"`
MaxTokenDuration time.Duration `json:"max_token_duration" validate:"required,min=1" db:"max_token_duration"`
Owner Owner `json:"owner" validate:"required"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// StaticToken represents a static API token
type StaticToken struct {
ID uuid.UUID `json:"id" db:"id"`
AppID string `json:"app_id" validate:"required" db:"app_id"`
Owner Owner `json:"owner" validate:"required"`
KeyHash string `json:"-" validate:"required" db:"key_hash"` // Hidden from JSON
Type string `json:"type" validate:"required,eq=hmac" db:"type"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// AvailablePermission represents a permission in the global catalog
type AvailablePermission struct {
ID uuid.UUID `json:"id" db:"id"`
Scope string `json:"scope" validate:"required,min=1,max=255" db:"scope"`
Name string `json:"name" validate:"required,min=1,max=255" db:"name"`
Description string `json:"description" validate:"required" db:"description"`
Category string `json:"category" validate:"required,min=1,max=100" db:"category"`
ParentScope *string `json:"parent_scope,omitempty" db:"parent_scope"`
IsSystem bool `json:"is_system" db:"is_system"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
CreatedBy string `json:"created_by" validate:"required" db:"created_by"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
UpdatedBy string `json:"updated_by" validate:"required" db:"updated_by"`
}
// GrantedPermission represents a permission granted to a token
type GrantedPermission struct {
ID uuid.UUID `json:"id" db:"id"`
TokenType TokenType `json:"token_type" validate:"required,eq=static" db:"token_type"`
TokenID uuid.UUID `json:"token_id" validate:"required" db:"token_id"`
PermissionID uuid.UUID `json:"permission_id" validate:"required" db:"permission_id"`
Scope string `json:"scope" validate:"required" db:"scope"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
CreatedBy string `json:"created_by" validate:"required" db:"created_by"`
Revoked bool `json:"revoked" db:"revoked"`
}
// UserToken represents a user token (JWT-based)
type UserToken struct {
AppID string `json:"app_id"`
UserID string `json:"user_id"`
Permissions []string `json:"permissions"`
IssuedAt time.Time `json:"iat"`
ExpiresAt time.Time `json:"exp"`
MaxValidAt time.Time `json:"max_valid_at"`
TokenType TokenType `json:"token_type"`
Claims map[string]string `json:"claims,omitempty"`
}
// VerifyRequest represents a token verification request
type VerifyRequest struct {
AppID string `json:"app_id" validate:"required"`
Type TokenType `json:"type" validate:"required,oneof=static user"`
UserID string `json:"user_id,omitempty"` // Required for user tokens
Token string `json:"token" validate:"required"`
Permissions []string `json:"permissions,omitempty"`
}
// VerifyResponse represents a token verification response
type VerifyResponse struct {
Valid bool `json:"valid"`
UserID string `json:"user_id,omitempty"`
Permissions []string `json:"permissions"`
PermissionResults map[string]bool `json:"permission_results,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
MaxValidAt *time.Time `json:"max_valid_at,omitempty"`
TokenType TokenType `json:"token_type"`
Claims map[string]string `json:"claims,omitempty"`
Error string `json:"error,omitempty"`
}
// LoginRequest represents a user login request
type LoginRequest struct {
AppID string `json:"app_id" validate:"required"`
Permissions []string `json:"permissions,omitempty"`
RedirectURI string `json:"redirect_uri,omitempty"`
}
// LoginResponse represents a user login response
type LoginResponse struct {
RedirectURL string `json:"redirect_url"`
State string `json:"state,omitempty"`
}
// RenewRequest represents a token renewal request
type RenewRequest struct {
AppID string `json:"app_id" validate:"required"`
UserID string `json:"user_id" validate:"required"`
Token string `json:"token" validate:"required"`
}
// RenewResponse represents a token renewal response
type RenewResponse struct {
Token string `json:"token"`
ExpiresAt time.Time `json:"expires_at"`
MaxValidAt time.Time `json:"max_valid_at"`
Error string `json:"error,omitempty"`
}
// CreateApplicationRequest represents a request to create a new application
type CreateApplicationRequest struct {
AppID string `json:"app_id" validate:"required,min=1,max=255"`
AppLink string `json:"app_link" validate:"required,url,max=500"`
Type []ApplicationType `json:"type" validate:"required,min=1,dive,oneof=static user"`
CallbackURL string `json:"callback_url" validate:"required,url,max=500"`
TokenRenewalDuration time.Duration `json:"token_renewal_duration" validate:"required,min=1"`
MaxTokenDuration time.Duration `json:"max_token_duration" validate:"required,min=1"`
Owner Owner `json:"owner" validate:"required"`
}
// UpdateApplicationRequest represents a request to update an existing application
type UpdateApplicationRequest struct {
AppLink *string `json:"app_link,omitempty" validate:"omitempty,url,max=500"`
Type *[]ApplicationType `json:"type,omitempty" validate:"omitempty,min=1,dive,oneof=static user"`
CallbackURL *string `json:"callback_url,omitempty" validate:"omitempty,url,max=500"`
HMACKey *string `json:"hmac_key,omitempty" validate:"omitempty,min=1,max=255"`
TokenRenewalDuration *time.Duration `json:"token_renewal_duration,omitempty" validate:"omitempty,min=1"`
MaxTokenDuration *time.Duration `json:"max_token_duration,omitempty" validate:"omitempty,min=1"`
Owner *Owner `json:"owner,omitempty" validate:"omitempty"`
}
// CreateStaticTokenRequest represents a request to create a static token
type CreateStaticTokenRequest struct {
AppID string `json:"app_id" validate:"required"`
Owner Owner `json:"owner" validate:"required"`
Permissions []string `json:"permissions" validate:"required,min=1"`
}
// CreateStaticTokenResponse represents a response for creating a static token
type CreateStaticTokenResponse struct {
ID uuid.UUID `json:"id"`
Token string `json:"token"` // Only returned once during creation
Permissions []string `json:"permissions"`
CreatedAt time.Time `json:"created_at"`
}
// AuthContext represents the authentication context for a request
type AuthContext struct {
UserID string `json:"user_id"`
TokenType TokenType `json:"token_type"`
Permissions []string `json:"permissions"`
Claims map[string]string `json:"claims"`
AppID string `json:"app_id"`
}

View File

@ -0,0 +1,211 @@
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"github.com/kms/api-key-service/internal/domain"
"github.com/kms/api-key-service/internal/services"
)
// ApplicationHandler handles application-related HTTP requests
type ApplicationHandler struct {
appService services.ApplicationService
authService services.AuthenticationService
logger *zap.Logger
}
// NewApplicationHandler creates a new application handler
func NewApplicationHandler(
appService services.ApplicationService,
authService services.AuthenticationService,
logger *zap.Logger,
) *ApplicationHandler {
return &ApplicationHandler{
appService: appService,
authService: authService,
logger: logger,
}
}
// Create handles POST /applications
func (h *ApplicationHandler) Create(c *gin.Context) {
var req domain.CreateApplicationRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Warn("Invalid request body", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{
"error": "Bad Request",
"message": "Invalid request body: " + err.Error(),
})
return
}
// Get user ID from context
userID, exists := c.Get("user_id")
if !exists {
h.logger.Error("User ID not found in context")
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal Server Error",
"message": "Authentication context not found",
})
return
}
app, err := h.appService.Create(c.Request.Context(), &req, userID.(string))
if err != nil {
h.logger.Error("Failed to create application", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal Server Error",
"message": "Failed to create application",
})
return
}
h.logger.Info("Application created", zap.String("app_id", app.AppID))
c.JSON(http.StatusCreated, app)
}
// GetByID handles GET /applications/:id
func (h *ApplicationHandler) GetByID(c *gin.Context) {
appID := c.Param("id")
if appID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Bad Request",
"message": "Application ID is required",
})
return
}
app, err := h.appService.GetByID(c.Request.Context(), appID)
if err != nil {
h.logger.Error("Failed to get application", zap.Error(err), zap.String("app_id", appID))
c.JSON(http.StatusNotFound, gin.H{
"error": "Not Found",
"message": "Application not found",
})
return
}
c.JSON(http.StatusOK, app)
}
// List handles GET /applications
func (h *ApplicationHandler) List(c *gin.Context) {
// Parse pagination parameters
limit := 50
offset := 0
if l := c.Query("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
limit = parsed
}
}
if o := c.Query("offset"); o != "" {
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
offset = parsed
}
}
apps, err := h.appService.List(c.Request.Context(), limit, offset)
if err != nil {
h.logger.Error("Failed to list applications", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal Server Error",
"message": "Failed to list applications",
})
return
}
c.JSON(http.StatusOK, gin.H{
"data": apps,
"limit": limit,
"offset": offset,
"count": len(apps),
})
}
// Update handles PUT /applications/:id
func (h *ApplicationHandler) Update(c *gin.Context) {
appID := c.Param("id")
if appID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Bad Request",
"message": "Application ID is required",
})
return
}
var req domain.UpdateApplicationRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Warn("Invalid request body", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{
"error": "Bad Request",
"message": "Invalid request body: " + err.Error(),
})
return
}
// Get user ID from context
userID, exists := c.Get("user_id")
if !exists {
h.logger.Error("User ID not found in context")
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal Server Error",
"message": "Authentication context not found",
})
return
}
app, err := h.appService.Update(c.Request.Context(), appID, &req, userID.(string))
if err != nil {
h.logger.Error("Failed to update application", zap.Error(err), zap.String("app_id", appID))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal Server Error",
"message": "Failed to update application",
})
return
}
h.logger.Info("Application updated", zap.String("app_id", appID))
c.JSON(http.StatusOK, app)
}
// Delete handles DELETE /applications/:id
func (h *ApplicationHandler) Delete(c *gin.Context) {
appID := c.Param("id")
if appID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Bad Request",
"message": "Application ID is required",
})
return
}
// Get user ID from context
userID, exists := c.Get("user_id")
if !exists {
h.logger.Error("User ID not found in context")
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal Server Error",
"message": "Authentication context not found",
})
return
}
err := h.appService.Delete(c.Request.Context(), appID, userID.(string))
if err != nil {
h.logger.Error("Failed to delete application", zap.Error(err), zap.String("app_id", appID))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal Server Error",
"message": "Failed to delete application",
})
return
}
h.logger.Info("Application deleted", zap.String("app_id", appID))
c.JSON(http.StatusNoContent, nil)
}

141
internal/handlers/auth.go Normal file
View File

@ -0,0 +1,141 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"github.com/kms/api-key-service/internal/domain"
"github.com/kms/api-key-service/internal/services"
)
// AuthHandler handles authentication-related HTTP requests
type AuthHandler struct {
authService services.AuthenticationService
tokenService services.TokenService
logger *zap.Logger
}
// NewAuthHandler creates a new auth handler
func NewAuthHandler(
authService services.AuthenticationService,
tokenService services.TokenService,
logger *zap.Logger,
) *AuthHandler {
return &AuthHandler{
authService: authService,
tokenService: tokenService,
logger: logger,
}
}
// Login handles POST /login
func (h *AuthHandler) Login(c *gin.Context) {
var req domain.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Warn("Invalid login request", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{
"error": "Bad Request",
"message": "Invalid request body: " + err.Error(),
})
return
}
// For now, we'll extract user ID from headers since we're using HeaderAuthenticationProvider
userID := c.GetHeader("X-User-Email")
if userID == "" {
h.logger.Warn("User email not found in headers")
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Unauthorized",
"message": "User authentication required",
})
return
}
h.logger.Info("Processing login request", zap.String("user_id", userID), zap.String("app_id", req.AppID))
// Generate user token
token, err := h.tokenService.GenerateUserToken(c.Request.Context(), req.AppID, userID, req.Permissions)
if err != nil {
h.logger.Error("Failed to generate user token", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal Server Error",
"message": "Failed to generate token",
})
return
}
// For now, we'll just return the token directly
// In a real implementation, this would redirect to the callback URL
response := domain.LoginResponse{
RedirectURL: req.RedirectURI + "?token=" + token,
}
if req.RedirectURI == "" {
// If no redirect URI, return token directly
c.JSON(http.StatusOK, gin.H{
"token": token,
"user_id": userID,
"app_id": req.AppID,
"expires_in": 604800, // 7 days in seconds
})
return
}
c.JSON(http.StatusOK, response)
}
// Verify handles POST /verify
func (h *AuthHandler) Verify(c *gin.Context) {
var req domain.VerifyRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Warn("Invalid verify request", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{
"error": "Bad Request",
"message": "Invalid request body: " + err.Error(),
})
return
}
h.logger.Debug("Verifying token", zap.String("app_id", req.AppID), zap.String("type", string(req.Type)))
response, err := h.tokenService.VerifyToken(c.Request.Context(), &req)
if err != nil {
h.logger.Error("Failed to verify token", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal Server Error",
"message": "Failed to verify token",
})
return
}
c.JSON(http.StatusOK, response)
}
// Renew handles POST /renew
func (h *AuthHandler) Renew(c *gin.Context) {
var req domain.RenewRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Warn("Invalid renew request", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{
"error": "Bad Request",
"message": "Invalid request body: " + err.Error(),
})
return
}
h.logger.Info("Renewing token", zap.String("app_id", req.AppID), zap.String("user_id", req.UserID))
response, err := h.tokenService.RenewUserToken(c.Request.Context(), &req)
if err != nil {
h.logger.Error("Failed to renew token", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal Server Error",
"message": "Failed to renew token",
})
return
}
c.JSON(http.StatusOK, response)
}

View File

@ -0,0 +1,72 @@
package handlers
import (
"context"
"net/http"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"github.com/kms/api-key-service/internal/repository"
)
// HealthHandler handles health check endpoints
type HealthHandler struct {
db repository.DatabaseProvider
logger *zap.Logger
}
// NewHealthHandler creates a new health handler
func NewHealthHandler(db repository.DatabaseProvider, logger *zap.Logger) *HealthHandler {
return &HealthHandler{
db: db,
logger: logger,
}
}
// HealthResponse represents the health check response
type HealthResponse struct {
Status string `json:"status"`
Timestamp string `json:"timestamp"`
Version string `json:"version,omitempty"`
Checks map[string]string `json:"checks,omitempty"`
}
// Health handles basic health check - lightweight endpoint for load balancers
func (h *HealthHandler) Health(c *gin.Context) {
response := HealthResponse{
Status: "healthy",
Timestamp: time.Now().UTC().Format(time.RFC3339),
}
c.JSON(http.StatusOK, response)
}
// Ready handles readiness check - checks if service is ready to accept traffic
func (h *HealthHandler) Ready(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
defer cancel()
checks := make(map[string]string)
status := "ready"
statusCode := http.StatusOK
// Check database connectivity
if err := h.db.Ping(ctx); err != nil {
h.logger.Error("Database health check failed", zap.Error(err))
checks["database"] = "unhealthy: " + err.Error()
status = "not ready"
statusCode = http.StatusServiceUnavailable
} else {
checks["database"] = "healthy"
}
response := HealthResponse{
Status: status,
Timestamp: time.Now().UTC().Format(time.RFC3339),
Checks: checks,
}
c.JSON(statusCode, response)
}

172
internal/handlers/token.go Normal file
View File

@ -0,0 +1,172 @@
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
"github.com/kms/api-key-service/internal/domain"
"github.com/kms/api-key-service/internal/services"
)
// TokenHandler handles token-related HTTP requests
type TokenHandler struct {
tokenService services.TokenService
authService services.AuthenticationService
logger *zap.Logger
}
// NewTokenHandler creates a new token handler
func NewTokenHandler(
tokenService services.TokenService,
authService services.AuthenticationService,
logger *zap.Logger,
) *TokenHandler {
return &TokenHandler{
tokenService: tokenService,
authService: authService,
logger: logger,
}
}
// Create handles POST /applications/:id/tokens
func (h *TokenHandler) Create(c *gin.Context) {
appID := c.Param("id")
if appID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Bad Request",
"message": "Application ID is required",
})
return
}
var req domain.CreateStaticTokenRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Warn("Invalid request body", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{
"error": "Bad Request",
"message": "Invalid request body: " + err.Error(),
})
return
}
// Set app ID from URL parameter
req.AppID = appID
// Get user ID from context
userID, exists := c.Get("user_id")
if !exists {
h.logger.Error("User ID not found in context")
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal Server Error",
"message": "Authentication context not found",
})
return
}
token, err := h.tokenService.CreateStaticToken(c.Request.Context(), &req, userID.(string))
if err != nil {
h.logger.Error("Failed to create token", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal Server Error",
"message": "Failed to create token",
})
return
}
h.logger.Info("Token created", zap.String("token_id", token.ID.String()))
c.JSON(http.StatusCreated, token)
}
// ListByApp handles GET /applications/:id/tokens
func (h *TokenHandler) ListByApp(c *gin.Context) {
appID := c.Param("id")
if appID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Bad Request",
"message": "Application ID is required",
})
return
}
// Parse pagination parameters
limit := 50
offset := 0
if l := c.Query("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
limit = parsed
}
}
if o := c.Query("offset"); o != "" {
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
offset = parsed
}
}
tokens, err := h.tokenService.ListByApp(c.Request.Context(), appID, limit, offset)
if err != nil {
h.logger.Error("Failed to list tokens", zap.Error(err), zap.String("app_id", appID))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal Server Error",
"message": "Failed to list tokens",
})
return
}
c.JSON(http.StatusOK, gin.H{
"data": tokens,
"limit": limit,
"offset": offset,
"count": len(tokens),
})
}
// Delete handles DELETE /tokens/:id
func (h *TokenHandler) Delete(c *gin.Context) {
tokenIDStr := c.Param("id")
if tokenIDStr == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Bad Request",
"message": "Token ID is required",
})
return
}
tokenID, err := uuid.Parse(tokenIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Bad Request",
"message": "Invalid token ID format",
})
return
}
// Get user ID from context
userID, exists := c.Get("user_id")
if !exists {
h.logger.Error("User ID not found in context")
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal Server Error",
"message": "Authentication context not found",
})
return
}
err = h.tokenService.Delete(c.Request.Context(), tokenID, userID.(string))
if err != nil {
h.logger.Error("Failed to delete token", zap.Error(err), zap.String("token_id", tokenID.String()))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal Server Error",
"message": "Failed to delete token",
})
return
}
h.logger.Info("Token deleted", zap.String("token_id", tokenID.String()))
c.JSON(http.StatusNoContent, nil)
}

View File

@ -0,0 +1,60 @@
package middleware
import (
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// Logger returns a middleware that logs HTTP requests using zap logger
func Logger(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
// Start timer
start := time.Now()
// Process request
c.Next()
// Calculate latency
latency := time.Since(start)
// Get request information
method := c.Request.Method
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
status := c.Writer.Status()
clientIP := c.ClientIP()
userAgent := c.Request.UserAgent()
// Get error if any
errorMessage := c.Errors.ByType(gin.ErrorTypePrivate).String()
// Build log fields
fields := []zap.Field{
zap.String("method", method),
zap.String("path", path),
zap.String("query", query),
zap.Int("status", status),
zap.String("client_ip", clientIP),
zap.String("user_agent", userAgent),
zap.Duration("latency", latency),
zap.Int64("latency_ms", latency.Nanoseconds()/1000000),
}
// Add error field if exists
if errorMessage != "" {
fields = append(fields, zap.String("error", errorMessage))
}
// Log based on status code
switch {
case status >= 500:
logger.Error("HTTP Request", fields...)
case status >= 400:
logger.Warn("HTTP Request", fields...)
default:
logger.Info("HTTP Request", fields...)
}
}
}

View File

@ -0,0 +1,239 @@
package middleware
import (
"context"
"net/http"
"runtime/debug"
"strconv"
"sync"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"golang.org/x/time/rate"
"github.com/kms/api-key-service/internal/config"
)
// Recovery returns a middleware that recovers from any panics
func Recovery(logger *zap.Logger) gin.HandlerFunc {
return gin.CustomRecoveryWithWriter(gin.DefaultWriter, func(c *gin.Context, recovered interface{}) {
if err, ok := recovered.(string); ok {
logger.Error("Panic recovered",
zap.String("error", err),
zap.String("stack", string(debug.Stack())),
zap.String("path", c.Request.URL.Path),
zap.String("method", c.Request.Method),
)
}
c.AbortWithStatus(http.StatusInternalServerError)
})
}
// CORS returns a middleware that handles Cross-Origin Resource Sharing
func CORS() gin.HandlerFunc {
return func(c *gin.Context) {
// Set CORS headers
c.Header("Access-Control-Allow-Origin", "*") // In production, be more specific
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, X-User-Email")
c.Header("Access-Control-Expose-Headers", "Content-Length")
c.Header("Access-Control-Max-Age", "86400")
// Handle preflight OPTIONS request
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
// Security returns a middleware that adds security headers
func Security() gin.HandlerFunc {
return func(c *gin.Context) {
// Security headers
c.Header("X-Frame-Options", "DENY")
c.Header("X-Content-Type-Options", "nosniff")
c.Header("X-XSS-Protection", "1; mode=block")
c.Header("Referrer-Policy", "strict-origin-when-cross-origin")
c.Header("Content-Security-Policy", "default-src 'self'")
c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
c.Next()
}
}
// RateLimiter holds rate limiting data
type RateLimiter struct {
limiters map[string]*rate.Limiter
mu sync.RWMutex
rate rate.Limit
burst int
}
// NewRateLimiter creates a new rate limiter
func NewRateLimiter(rps, burst int) *RateLimiter {
return &RateLimiter{
limiters: make(map[string]*rate.Limiter),
rate: rate.Limit(rps),
burst: burst,
}
}
// GetLimiter returns the rate limiter for a given key
func (rl *RateLimiter) GetLimiter(key string) *rate.Limiter {
rl.mu.RLock()
limiter, exists := rl.limiters[key]
rl.mu.RUnlock()
if !exists {
limiter = rate.NewLimiter(rl.rate, rl.burst)
rl.mu.Lock()
rl.limiters[key] = limiter
rl.mu.Unlock()
}
return limiter
}
// RateLimit returns a middleware that implements rate limiting
func RateLimit(rps, burst int) gin.HandlerFunc {
limiter := NewRateLimiter(rps, burst)
return func(c *gin.Context) {
// Use client IP as the key for rate limiting
key := c.ClientIP()
// Get the limiter for this client
clientLimiter := limiter.GetLimiter(key)
// Check if request is allowed
if !clientLimiter.Allow() {
// Add rate limit headers
c.Header("X-RateLimit-Limit", strconv.Itoa(burst))
c.Header("X-RateLimit-Remaining", "0")
c.Header("X-RateLimit-Reset", strconv.FormatInt(time.Now().Add(time.Minute).Unix(), 10))
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "Rate limit exceeded",
"message": "Too many requests. Please try again later.",
})
c.Abort()
return
}
// Add rate limit headers for successful requests
remaining := burst - int(clientLimiter.Tokens())
if remaining < 0 {
remaining = 0
}
c.Header("X-RateLimit-Limit", strconv.Itoa(burst))
c.Header("X-RateLimit-Remaining", strconv.Itoa(remaining))
c.Header("X-RateLimit-Reset", strconv.FormatInt(time.Now().Add(time.Minute).Unix(), 10))
c.Next()
}
}
// Authentication returns a middleware that handles authentication
func Authentication(cfg config.ConfigProvider, logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
// For now, we'll implement a basic header-based authentication
// This will be expanded when we implement the full authentication service
userEmail := c.GetHeader(cfg.GetString("AUTH_HEADER_USER_EMAIL"))
if userEmail == "" {
logger.Warn("Authentication failed: missing user email header",
zap.String("path", c.Request.URL.Path),
zap.String("method", c.Request.Method),
)
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Unauthorized",
"message": "Authentication required",
})
c.Abort()
return
}
// Set user context for downstream handlers
c.Set("user_id", userEmail)
c.Set("auth_method", "header")
logger.Debug("Authentication successful",
zap.String("user_id", userEmail),
zap.String("auth_method", "header"),
)
c.Next()
}
}
// RequestID returns a middleware that adds a unique request ID to each request
func RequestID() gin.HandlerFunc {
return func(c *gin.Context) {
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = generateRequestID()
}
c.Header("X-Request-ID", requestID)
c.Set("request_id", requestID)
c.Next()
}
}
// generateRequestID generates a simple request ID
// In production, you might want to use a more sophisticated ID generator
func generateRequestID() string {
return strconv.FormatInt(time.Now().UnixNano(), 36)
}
// Timeout returns a middleware that adds timeout to requests
func Timeout(timeout time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), timeout)
defer cancel()
c.Request = c.Request.WithContext(ctx)
c.Next()
}
}
// ValidateContentType returns a middleware that validates Content-Type header for JSON requests
func ValidateContentType() gin.HandlerFunc {
return func(c *gin.Context) {
// Only validate for POST, PUT, and PATCH requests
if c.Request.Method == "POST" || c.Request.Method == "PUT" || c.Request.Method == "PATCH" {
contentType := c.GetHeader("Content-Type")
// For requests with a body or when Content-Length is not explicitly 0,
// require application/json content type
if c.Request.ContentLength != 0 {
if contentType == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Bad Request",
"message": "Content-Type header is required for POST/PUT/PATCH requests",
})
c.Abort()
return
}
// Require application/json content type for requests with JSON bodies
if contentType != "application/json" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Bad Request",
"message": "Content-Type must be application/json",
})
c.Abort()
return
}
}
}
c.Next()
}
}

View File

@ -0,0 +1,273 @@
package repository
import (
"context"
"time"
"github.com/google/uuid"
"github.com/kms/api-key-service/internal/domain"
)
// ApplicationRepository defines the interface for application data operations
type ApplicationRepository interface {
// Create creates a new application
Create(ctx context.Context, app *domain.Application) error
// GetByID retrieves an application by its ID
GetByID(ctx context.Context, appID string) (*domain.Application, error)
// List retrieves applications with pagination
List(ctx context.Context, limit, offset int) ([]*domain.Application, error)
// Update updates an existing application
Update(ctx context.Context, appID string, updates *domain.UpdateApplicationRequest) (*domain.Application, error)
// Delete deletes an application
Delete(ctx context.Context, appID string) error
// Exists checks if an application exists
Exists(ctx context.Context, appID string) (bool, error)
}
// StaticTokenRepository defines the interface for static token data operations
type StaticTokenRepository interface {
// Create creates a new static token
Create(ctx context.Context, token *domain.StaticToken) error
// GetByID retrieves a static token by its ID
GetByID(ctx context.Context, tokenID uuid.UUID) (*domain.StaticToken, error)
// GetByKeyHash retrieves a static token by its key hash
GetByKeyHash(ctx context.Context, keyHash string) (*domain.StaticToken, error)
// GetByAppID retrieves all static tokens for an application
GetByAppID(ctx context.Context, appID string) ([]*domain.StaticToken, error)
// List retrieves static tokens with pagination
List(ctx context.Context, limit, offset int) ([]*domain.StaticToken, error)
// Delete deletes a static token
Delete(ctx context.Context, tokenID uuid.UUID) error
// Exists checks if a static token exists
Exists(ctx context.Context, tokenID uuid.UUID) (bool, error)
}
// PermissionRepository defines the interface for permission data operations
type PermissionRepository interface {
// CreateAvailablePermission creates a new available permission
CreateAvailablePermission(ctx context.Context, permission *domain.AvailablePermission) error
// GetAvailablePermission retrieves an available permission by ID
GetAvailablePermission(ctx context.Context, permissionID uuid.UUID) (*domain.AvailablePermission, error)
// GetAvailablePermissionByScope retrieves an available permission by scope
GetAvailablePermissionByScope(ctx context.Context, scope string) (*domain.AvailablePermission, error)
// ListAvailablePermissions retrieves available permissions with pagination and filtering
ListAvailablePermissions(ctx context.Context, category string, includeSystem bool, limit, offset int) ([]*domain.AvailablePermission, error)
// UpdateAvailablePermission updates an available permission
UpdateAvailablePermission(ctx context.Context, permissionID uuid.UUID, permission *domain.AvailablePermission) error
// DeleteAvailablePermission deletes an available permission
DeleteAvailablePermission(ctx context.Context, permissionID uuid.UUID) error
// ValidatePermissionScopes checks if all given scopes exist and are valid
ValidatePermissionScopes(ctx context.Context, scopes []string) ([]string, error) // returns invalid scopes
// GetPermissionHierarchy returns all parent and child permissions for given scopes
GetPermissionHierarchy(ctx context.Context, scopes []string) ([]*domain.AvailablePermission, error)
}
// GrantedPermissionRepository defines the interface for granted permission operations
type GrantedPermissionRepository interface {
// GrantPermissions grants multiple permissions to a token
GrantPermissions(ctx context.Context, grants []*domain.GrantedPermission) error
// GetGrantedPermissions retrieves all granted permissions for a token
GetGrantedPermissions(ctx context.Context, tokenType domain.TokenType, tokenID uuid.UUID) ([]*domain.GrantedPermission, error)
// GetGrantedPermissionScopes retrieves only the scopes for a token (more efficient)
GetGrantedPermissionScopes(ctx context.Context, tokenType domain.TokenType, tokenID uuid.UUID) ([]string, error)
// RevokePermission revokes a specific permission from a token
RevokePermission(ctx context.Context, grantID uuid.UUID, revokedBy string) error
// RevokeAllPermissions revokes all permissions from a token
RevokeAllPermissions(ctx context.Context, tokenType domain.TokenType, tokenID uuid.UUID, revokedBy string) error
// HasPermission checks if a token has a specific permission
HasPermission(ctx context.Context, tokenType domain.TokenType, tokenID uuid.UUID, scope string) (bool, error)
// HasAnyPermission checks if a token has any of the specified permissions
HasAnyPermission(ctx context.Context, tokenType domain.TokenType, tokenID uuid.UUID, scopes []string) (map[string]bool, error)
}
// DatabaseProvider defines the interface for database operations
type DatabaseProvider interface {
// GetDB returns the underlying database connection
GetDB() interface{}
// Ping checks the database connection
Ping(ctx context.Context) error
// Close closes all database connections
Close() error
// BeginTx starts a database transaction
BeginTx(ctx context.Context) (TransactionProvider, error)
// Migrate runs database migrations
Migrate(ctx context.Context, migrationPath string) error
}
// TransactionProvider defines the interface for database transaction operations
type TransactionProvider interface {
// Commit commits the transaction
Commit() error
// Rollback rolls back the transaction
Rollback() error
// GetTx returns the underlying transaction
GetTx() interface{}
}
// CacheProvider defines the interface for caching operations
type CacheProvider interface {
// Get retrieves a value from cache
Get(ctx context.Context, key string) ([]byte, error)
// Set stores a value in cache with expiration
Set(ctx context.Context, key string, value []byte, expiration time.Duration) error
// Delete removes a value from cache
Delete(ctx context.Context, key string) error
// Exists checks if a key exists in cache
Exists(ctx context.Context, key string) (bool, error)
// Flush clears all cache entries
Flush(ctx context.Context) error
// Close closes the cache connection
Close() error
}
// TokenProvider defines the interface for token operations
type TokenProvider interface {
// GenerateUserToken generates a JWT token for user authentication
GenerateUserToken(ctx context.Context, userToken *domain.UserToken, hmacKey string) (string, error)
// ValidateUserToken validates and parses a JWT token
ValidateUserToken(ctx context.Context, token string, hmacKey string) (*domain.UserToken, error)
// GenerateStaticToken generates a static API key
GenerateStaticToken(ctx context.Context) (string, error)
// HashStaticToken creates a secure hash of a static token
HashStaticToken(ctx context.Context, token string) (string, error)
// ValidateStaticToken validates a static token against its hash
ValidateStaticToken(ctx context.Context, token, hash string) (bool, error)
// RenewUserToken renews a user token while preserving max validity
RenewUserToken(ctx context.Context, currentToken *domain.UserToken, renewalDuration time.Duration, hmacKey string) (string, error)
}
// HashProvider defines the interface for cryptographic hashing operations
type HashProvider interface {
// Hash creates a secure hash of the input
Hash(ctx context.Context, input string) (string, error)
// Compare compares an input against a hash
Compare(ctx context.Context, input, hash string) (bool, error)
// GenerateKey generates a secure random key
GenerateKey(ctx context.Context, length int) (string, error)
}
// LoggerProvider defines the interface for logging operations
type LoggerProvider interface {
// Info logs an info level message
Info(ctx context.Context, msg string, fields ...interface{})
// Warn logs a warning level message
Warn(ctx context.Context, msg string, fields ...interface{})
// Error logs an error level message
Error(ctx context.Context, msg string, err error, fields ...interface{})
// Debug logs a debug level message
Debug(ctx context.Context, msg string, fields ...interface{})
// With returns a logger with additional fields
With(fields ...interface{}) LoggerProvider
}
// ConfigProvider defines the interface for configuration operations
type ConfigProvider interface {
// GetString retrieves a string configuration value
GetString(key string) string
// GetInt retrieves an integer configuration value
GetInt(key string) int
// GetBool retrieves a boolean configuration value
GetBool(key string) bool
// GetDuration retrieves a duration configuration value
GetDuration(key string) time.Duration
// GetStringSlice retrieves a string slice configuration value
GetStringSlice(key string) []string
// IsSet checks if a configuration key is set
IsSet(key string) bool
// Validate validates all required configuration values
Validate() error
}
// AuthenticationProvider defines the interface for user authentication
type AuthenticationProvider interface {
// GetUserID extracts the user ID from the request context/headers
GetUserID(ctx context.Context) (string, error)
// ValidateUser validates if the user is authentic
ValidateUser(ctx context.Context, userID string) error
// GetUserClaims retrieves additional user information/claims
GetUserClaims(ctx context.Context, userID string) (map[string]string, error)
// Name returns the provider name for identification
Name() string
}
// RateLimitProvider defines the interface for rate limiting operations
type RateLimitProvider interface {
// Allow checks if a request should be allowed for the given identifier
Allow(ctx context.Context, identifier string) (bool, error)
// Remaining returns the number of remaining requests for the identifier
Remaining(ctx context.Context, identifier string) (int, error)
// Reset returns when the rate limit will reset for the identifier
Reset(ctx context.Context, identifier string) (time.Time, error)
}
// MetricsProvider defines the interface for metrics collection
type MetricsProvider interface {
// IncrementCounter increments a counter metric
IncrementCounter(ctx context.Context, name string, labels map[string]string)
// RecordHistogram records a value in a histogram
RecordHistogram(ctx context.Context, name string, value float64, labels map[string]string)
// SetGauge sets a gauge metric value
SetGauge(ctx context.Context, name string, value float64, labels map[string]string)
// RecordDuration records the duration of an operation
RecordDuration(ctx context.Context, name string, duration time.Duration, labels map[string]string)
}

View File

@ -0,0 +1,343 @@
package postgres
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
"github.com/lib/pq"
"github.com/kms/api-key-service/internal/domain"
"github.com/kms/api-key-service/internal/repository"
)
// ApplicationRepository implements the ApplicationRepository interface for PostgreSQL
type ApplicationRepository struct {
db repository.DatabaseProvider
}
// NewApplicationRepository creates a new PostgreSQL application repository
func NewApplicationRepository(db repository.DatabaseProvider) repository.ApplicationRepository {
return &ApplicationRepository{db: db}
}
// Create creates a new application
func (r *ApplicationRepository) Create(ctx context.Context, app *domain.Application) error {
query := `
INSERT INTO applications (
app_id, app_link, type, callback_url, hmac_key,
token_renewal_duration, max_token_duration,
owner_type, owner_name, owner_owner,
created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
`
db := r.db.GetDB().(*sql.DB)
now := time.Now()
// Convert application types to string array
typeStrings := make([]string, len(app.Type))
for i, t := range app.Type {
typeStrings[i] = string(t)
}
_, err := db.ExecContext(ctx, query,
app.AppID,
app.AppLink,
pq.Array(typeStrings),
app.CallbackURL,
app.HMACKey,
app.TokenRenewalDuration.Nanoseconds(),
app.MaxTokenDuration.Nanoseconds(),
string(app.Owner.Type),
app.Owner.Name,
app.Owner.Owner,
now,
now,
)
if err != nil {
if isUniqueViolation(err) {
return fmt.Errorf("application with ID '%s' already exists", app.AppID)
}
return fmt.Errorf("failed to create application: %w", err)
}
app.CreatedAt = now
app.UpdatedAt = now
return nil
}
// GetByID retrieves an application by its ID
func (r *ApplicationRepository) GetByID(ctx context.Context, appID string) (*domain.Application, error) {
query := `
SELECT app_id, app_link, type, callback_url, hmac_key,
token_renewal_duration, max_token_duration,
owner_type, owner_name, owner_owner,
created_at, updated_at
FROM applications
WHERE app_id = $1
`
db := r.db.GetDB().(*sql.DB)
row := db.QueryRowContext(ctx, query, appID)
app := &domain.Application{}
var typeStrings pq.StringArray
var tokenRenewalNanos, maxTokenNanos int64
var ownerType string
err := row.Scan(
&app.AppID,
&app.AppLink,
&typeStrings,
&app.CallbackURL,
&app.HMACKey,
&tokenRenewalNanos,
&maxTokenNanos,
&ownerType,
&app.Owner.Name,
&app.Owner.Owner,
&app.CreatedAt,
&app.UpdatedAt,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("application with ID '%s' not found", appID)
}
return nil, fmt.Errorf("failed to get application: %w", err)
}
// Convert string array to application types
app.Type = make([]domain.ApplicationType, len(typeStrings))
for i, t := range typeStrings {
app.Type[i] = domain.ApplicationType(t)
}
// Convert nanoseconds to duration
app.TokenRenewalDuration = time.Duration(tokenRenewalNanos)
app.MaxTokenDuration = time.Duration(maxTokenNanos)
// Convert owner type
app.Owner.Type = domain.OwnerType(ownerType)
return app, nil
}
// List retrieves applications with pagination
func (r *ApplicationRepository) List(ctx context.Context, limit, offset int) ([]*domain.Application, error) {
query := `
SELECT app_id, app_link, type, callback_url, hmac_key,
token_renewal_duration, max_token_duration,
owner_type, owner_name, owner_owner,
created_at, updated_at
FROM applications
ORDER BY created_at DESC
LIMIT $1 OFFSET $2
`
db := r.db.GetDB().(*sql.DB)
rows, err := db.QueryContext(ctx, query, limit, offset)
if err != nil {
return nil, fmt.Errorf("failed to list applications: %w", err)
}
defer rows.Close()
var applications []*domain.Application
for rows.Next() {
app := &domain.Application{}
var typeStrings pq.StringArray
var tokenRenewalNanos, maxTokenNanos int64
var ownerType string
err := rows.Scan(
&app.AppID,
&app.AppLink,
&typeStrings,
&app.CallbackURL,
&app.HMACKey,
&tokenRenewalNanos,
&maxTokenNanos,
&ownerType,
&app.Owner.Name,
&app.Owner.Owner,
&app.CreatedAt,
&app.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan application: %w", err)
}
// Convert string array to application types
app.Type = make([]domain.ApplicationType, len(typeStrings))
for i, t := range typeStrings {
app.Type[i] = domain.ApplicationType(t)
}
// Convert nanoseconds to duration
app.TokenRenewalDuration = time.Duration(tokenRenewalNanos)
app.MaxTokenDuration = time.Duration(maxTokenNanos)
// Convert owner type
app.Owner.Type = domain.OwnerType(ownerType)
applications = append(applications, app)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("failed to iterate applications: %w", err)
}
return applications, nil
}
// Update updates an existing application
func (r *ApplicationRepository) Update(ctx context.Context, appID string, updates *domain.UpdateApplicationRequest) (*domain.Application, error) {
// Build dynamic update query
var setParts []string
var args []interface{}
argIndex := 1
if updates.AppLink != nil {
setParts = append(setParts, fmt.Sprintf("app_link = $%d", argIndex))
args = append(args, *updates.AppLink)
argIndex++
}
if updates.Type != nil {
typeStrings := make([]string, len(*updates.Type))
for i, t := range *updates.Type {
typeStrings[i] = string(t)
}
setParts = append(setParts, fmt.Sprintf("type = $%d", argIndex))
args = append(args, pq.Array(typeStrings))
argIndex++
}
if updates.CallbackURL != nil {
setParts = append(setParts, fmt.Sprintf("callback_url = $%d", argIndex))
args = append(args, *updates.CallbackURL)
argIndex++
}
if updates.HMACKey != nil {
setParts = append(setParts, fmt.Sprintf("hmac_key = $%d", argIndex))
args = append(args, *updates.HMACKey)
argIndex++
}
if updates.TokenRenewalDuration != nil {
setParts = append(setParts, fmt.Sprintf("token_renewal_duration = $%d", argIndex))
args = append(args, updates.TokenRenewalDuration.Nanoseconds())
argIndex++
}
if updates.MaxTokenDuration != nil {
setParts = append(setParts, fmt.Sprintf("max_token_duration = $%d", argIndex))
args = append(args, updates.MaxTokenDuration.Nanoseconds())
argIndex++
}
if updates.Owner != nil {
setParts = append(setParts, fmt.Sprintf("owner_type = $%d", argIndex))
args = append(args, string(updates.Owner.Type))
argIndex++
setParts = append(setParts, fmt.Sprintf("owner_name = $%d", argIndex))
args = append(args, updates.Owner.Name)
argIndex++
setParts = append(setParts, fmt.Sprintf("owner_owner = $%d", argIndex))
args = append(args, updates.Owner.Owner)
argIndex++
}
if len(setParts) == 0 {
return r.GetByID(ctx, appID) // No updates, return current state
}
// Always update the updated_at field
setParts = append(setParts, fmt.Sprintf("updated_at = $%d", argIndex))
args = append(args, time.Now())
argIndex++
// Add WHERE clause
args = append(args, appID)
query := fmt.Sprintf(`
UPDATE applications
SET %s
WHERE app_id = $%d
`, strings.Join(setParts, ", "), argIndex)
db := r.db.GetDB().(*sql.DB)
result, err := db.ExecContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to update application: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return nil, fmt.Errorf("failed to get rows affected: %w", err)
}
if rowsAffected == 0 {
return nil, fmt.Errorf("application with ID '%s' not found", appID)
}
// Return updated application
return r.GetByID(ctx, appID)
}
// Delete deletes an application
func (r *ApplicationRepository) Delete(ctx context.Context, appID string) error {
query := `DELETE FROM applications WHERE app_id = $1`
db := r.db.GetDB().(*sql.DB)
result, err := db.ExecContext(ctx, query, appID)
if err != nil {
return fmt.Errorf("failed to delete application: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("application with ID '%s' not found", appID)
}
return nil
}
// Exists checks if an application exists
func (r *ApplicationRepository) Exists(ctx context.Context, appID string) (bool, error) {
query := `SELECT 1 FROM applications WHERE app_id = $1`
db := r.db.GetDB().(*sql.DB)
var exists int
err := db.QueryRowContext(ctx, query, appID).Scan(&exists)
if err != nil {
if err == sql.ErrNoRows {
return false, nil
}
return false, fmt.Errorf("failed to check application existence: %w", err)
}
return true, nil
}
// isUniqueViolation checks if the error is a unique constraint violation
func isUniqueViolation(err error) bool {
if pqErr, ok := err.(*pq.Error); ok {
return pqErr.Code == "23505" // unique_violation
}
return false
}

View File

@ -0,0 +1,124 @@
package postgres
import (
"context"
"github.com/google/uuid"
"github.com/kms/api-key-service/internal/domain"
"github.com/kms/api-key-service/internal/repository"
)
// PermissionRepository implements the PermissionRepository interface for PostgreSQL
type PermissionRepository struct {
db repository.DatabaseProvider
}
// NewPermissionRepository creates a new PostgreSQL permission repository
func NewPermissionRepository(db repository.DatabaseProvider) repository.PermissionRepository {
return &PermissionRepository{db: db}
}
// CreateAvailablePermission creates a new available permission
func (r *PermissionRepository) CreateAvailablePermission(ctx context.Context, permission *domain.AvailablePermission) error {
// TODO: Implement actual permission creation
return nil
}
// GetAvailablePermission retrieves an available permission by ID
func (r *PermissionRepository) GetAvailablePermission(ctx context.Context, permissionID uuid.UUID) (*domain.AvailablePermission, error) {
// TODO: Implement actual permission retrieval
return nil, nil
}
// GetAvailablePermissionByScope retrieves an available permission by scope
func (r *PermissionRepository) GetAvailablePermissionByScope(ctx context.Context, scope string) (*domain.AvailablePermission, error) {
// TODO: Implement actual permission retrieval by scope
return nil, nil
}
// ListAvailablePermissions retrieves available permissions with pagination and filtering
func (r *PermissionRepository) ListAvailablePermissions(ctx context.Context, category string, includeSystem bool, limit, offset int) ([]*domain.AvailablePermission, error) {
// TODO: Implement actual permission listing
return []*domain.AvailablePermission{}, nil
}
// UpdateAvailablePermission updates an available permission
func (r *PermissionRepository) UpdateAvailablePermission(ctx context.Context, permissionID uuid.UUID, permission *domain.AvailablePermission) error {
// TODO: Implement actual permission update
return nil
}
// DeleteAvailablePermission deletes an available permission
func (r *PermissionRepository) DeleteAvailablePermission(ctx context.Context, permissionID uuid.UUID) error {
// TODO: Implement actual permission deletion
return nil
}
// ValidatePermissionScopes checks if all given scopes exist and are valid
func (r *PermissionRepository) ValidatePermissionScopes(ctx context.Context, scopes []string) ([]string, error) {
// TODO: Implement actual scope validation
// For now, assume all scopes are valid
return []string{}, nil
}
// GetPermissionHierarchy returns all parent and child permissions for given scopes
func (r *PermissionRepository) GetPermissionHierarchy(ctx context.Context, scopes []string) ([]*domain.AvailablePermission, error) {
// TODO: Implement actual permission hierarchy retrieval
return []*domain.AvailablePermission{}, nil
}
// GrantedPermissionRepository implements the GrantedPermissionRepository interface for PostgreSQL
type GrantedPermissionRepository struct {
db repository.DatabaseProvider
}
// NewGrantedPermissionRepository creates a new PostgreSQL granted permission repository
func NewGrantedPermissionRepository(db repository.DatabaseProvider) repository.GrantedPermissionRepository {
return &GrantedPermissionRepository{db: db}
}
// GrantPermissions grants multiple permissions to a token
func (r *GrantedPermissionRepository) GrantPermissions(ctx context.Context, grants []*domain.GrantedPermission) error {
// TODO: Implement actual permission granting
return nil
}
// GetGrantedPermissions retrieves all granted permissions for a token
func (r *GrantedPermissionRepository) GetGrantedPermissions(ctx context.Context, tokenType domain.TokenType, tokenID uuid.UUID) ([]*domain.GrantedPermission, error) {
// TODO: Implement actual granted permissions retrieval
return []*domain.GrantedPermission{}, nil
}
// GetGrantedPermissionScopes retrieves only the scopes for a token (more efficient)
func (r *GrantedPermissionRepository) GetGrantedPermissionScopes(ctx context.Context, tokenType domain.TokenType, tokenID uuid.UUID) ([]string, error) {
// TODO: Implement actual scope retrieval
return []string{}, nil
}
// RevokePermission revokes a specific permission from a token
func (r *GrantedPermissionRepository) RevokePermission(ctx context.Context, grantID uuid.UUID, revokedBy string) error {
// TODO: Implement actual permission revocation
return nil
}
// RevokeAllPermissions revokes all permissions from a token
func (r *GrantedPermissionRepository) RevokeAllPermissions(ctx context.Context, tokenType domain.TokenType, tokenID uuid.UUID, revokedBy string) error {
// TODO: Implement actual permission revocation
return nil
}
// HasPermission checks if a token has a specific permission
func (r *GrantedPermissionRepository) HasPermission(ctx context.Context, tokenType domain.TokenType, tokenID uuid.UUID, scope string) (bool, error) {
// TODO: Implement actual permission checking
return true, nil
}
// HasAnyPermission checks if a token has any of the specified permissions
func (r *GrantedPermissionRepository) HasAnyPermission(ctx context.Context, tokenType domain.TokenType, tokenID uuid.UUID, scopes []string) (map[string]bool, error) {
// TODO: Implement actual permission checking
result := make(map[string]bool)
for _, scope := range scopes {
result[scope] = true
}
return result, nil
}

View File

@ -0,0 +1,120 @@
package postgres
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/google/uuid"
"github.com/kms/api-key-service/internal/domain"
"github.com/kms/api-key-service/internal/repository"
)
// StaticTokenRepository implements the StaticTokenRepository interface for PostgreSQL
type StaticTokenRepository struct {
db repository.DatabaseProvider
}
// NewStaticTokenRepository creates a new PostgreSQL static token repository
func NewStaticTokenRepository(db repository.DatabaseProvider) repository.StaticTokenRepository {
return &StaticTokenRepository{db: db}
}
// Create creates a new static token
func (r *StaticTokenRepository) Create(ctx context.Context, token *domain.StaticToken) error {
query := `
INSERT INTO static_tokens (
id, app_id, owner_type, owner_name, owner_owner,
key_hash, type, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
`
db := r.db.GetDB().(*sql.DB)
now := time.Now()
_, err := db.ExecContext(ctx, query,
token.ID,
token.AppID,
string(token.Owner.Type),
token.Owner.Name,
token.Owner.Owner,
token.KeyHash,
string(token.Type),
now,
now,
)
if err != nil {
return fmt.Errorf("failed to create static token: %w", err)
}
token.CreatedAt = now
token.UpdatedAt = now
return nil
}
// GetByID retrieves a static token by its ID
func (r *StaticTokenRepository) GetByID(ctx context.Context, tokenID uuid.UUID) (*domain.StaticToken, error) {
// TODO: Implement actual token retrieval
return nil, nil
}
// GetByKeyHash retrieves a static token by its key hash
func (r *StaticTokenRepository) GetByKeyHash(ctx context.Context, keyHash string) (*domain.StaticToken, error) {
// TODO: Implement actual token retrieval by hash
return nil, nil
}
// GetByAppID retrieves all static tokens for an application
func (r *StaticTokenRepository) GetByAppID(ctx context.Context, appID string) ([]*domain.StaticToken, error) {
// TODO: Implement actual token listing
return []*domain.StaticToken{}, nil
}
// List retrieves static tokens with pagination
func (r *StaticTokenRepository) List(ctx context.Context, limit, offset int) ([]*domain.StaticToken, error) {
// TODO: Implement actual token listing
return []*domain.StaticToken{}, nil
}
// Delete deletes a static token
func (r *StaticTokenRepository) Delete(ctx context.Context, tokenID uuid.UUID) error {
query := `DELETE FROM static_tokens WHERE id = $1`
db := r.db.GetDB().(*sql.DB)
result, err := db.ExecContext(ctx, query, tokenID)
if err != nil {
return fmt.Errorf("failed to delete static token: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("static token with ID '%s' not found", tokenID)
}
return nil
}
// Exists checks if a static token exists
func (r *StaticTokenRepository) Exists(ctx context.Context, tokenID uuid.UUID) (bool, error) {
query := `SELECT 1 FROM static_tokens WHERE id = $1`
db := r.db.GetDB().(*sql.DB)
var exists int
err := db.QueryRowContext(ctx, query, tokenID).Scan(&exists)
if err != nil {
if err == sql.ErrNoRows {
return false, nil
}
return false, fmt.Errorf("failed to check static token existence: %w", err)
}
return true, nil
}

View File

@ -0,0 +1,126 @@
package services
import (
"context"
"fmt"
"go.uber.org/zap"
"github.com/kms/api-key-service/internal/domain"
"github.com/kms/api-key-service/internal/repository"
)
// applicationService implements the ApplicationService interface
type applicationService struct {
appRepo repository.ApplicationRepository
logger *zap.Logger
}
// NewApplicationService creates a new application service
func NewApplicationService(appRepo repository.ApplicationRepository, logger *zap.Logger) ApplicationService {
return &applicationService{
appRepo: appRepo,
logger: logger,
}
}
// Create creates a new application
func (s *applicationService) Create(ctx context.Context, req *domain.CreateApplicationRequest, userID string) (*domain.Application, error) {
s.logger.Info("Creating application", zap.String("app_id", req.AppID), zap.String("user_id", userID))
// TODO: Add permission validation
// TODO: Add input validation using validator
app := &domain.Application{
AppID: req.AppID,
AppLink: req.AppLink,
Type: req.Type,
CallbackURL: req.CallbackURL,
HMACKey: generateHMACKey(), // TODO: Use proper key generation
TokenRenewalDuration: req.TokenRenewalDuration,
MaxTokenDuration: req.MaxTokenDuration,
Owner: req.Owner,
}
if err := s.appRepo.Create(ctx, app); err != nil {
s.logger.Error("Failed to create application", zap.Error(err), zap.String("app_id", req.AppID))
return nil, fmt.Errorf("failed to create application: %w", err)
}
s.logger.Info("Application created successfully", zap.String("app_id", app.AppID))
return app, nil
}
// GetByID retrieves an application by its ID
func (s *applicationService) GetByID(ctx context.Context, appID string) (*domain.Application, error) {
s.logger.Debug("Getting application by ID", zap.String("app_id", appID))
app, err := s.appRepo.GetByID(ctx, appID)
if err != nil {
s.logger.Error("Failed to get application", zap.Error(err), zap.String("app_id", appID))
return nil, fmt.Errorf("failed to get application: %w", err)
}
return app, nil
}
// List retrieves applications with pagination
func (s *applicationService) List(ctx context.Context, limit, offset int) ([]*domain.Application, error) {
s.logger.Debug("Listing applications", zap.Int("limit", limit), zap.Int("offset", offset))
if limit <= 0 {
limit = 50 // Default limit
}
if limit > 100 {
limit = 100 // Max limit
}
apps, err := s.appRepo.List(ctx, limit, offset)
if err != nil {
s.logger.Error("Failed to list applications", zap.Error(err))
return nil, fmt.Errorf("failed to list applications: %w", err)
}
s.logger.Debug("Listed applications", zap.Int("count", len(apps)))
return apps, nil
}
// Update updates an existing application
func (s *applicationService) Update(ctx context.Context, appID string, updates *domain.UpdateApplicationRequest, userID string) (*domain.Application, error) {
s.logger.Info("Updating application", zap.String("app_id", appID), zap.String("user_id", userID))
// TODO: Add permission validation
// TODO: Add input validation
app, err := s.appRepo.Update(ctx, appID, updates)
if err != nil {
s.logger.Error("Failed to update application", zap.Error(err), zap.String("app_id", appID))
return nil, fmt.Errorf("failed to update application: %w", err)
}
s.logger.Info("Application updated successfully", zap.String("app_id", appID))
return app, nil
}
// Delete deletes an application
func (s *applicationService) Delete(ctx context.Context, appID string, userID string) error {
s.logger.Info("Deleting application", zap.String("app_id", appID), zap.String("user_id", userID))
// TODO: Add permission validation
// TODO: Check for existing tokens and handle appropriately
if err := s.appRepo.Delete(ctx, appID); err != nil {
s.logger.Error("Failed to delete application", zap.Error(err), zap.String("app_id", appID))
return fmt.Errorf("failed to delete application: %w", err)
}
s.logger.Info("Application deleted successfully", zap.String("app_id", appID))
return nil
}
// generateHMACKey generates a secure HMAC key
// TODO: Replace with proper cryptographic key generation
func generateHMACKey() string {
// This is a placeholder - should use proper crypto/rand
return "generated-hmac-key-placeholder"
}

View File

@ -0,0 +1,65 @@
package services
import (
"context"
"fmt"
"go.uber.org/zap"
"github.com/kms/api-key-service/internal/config"
)
// authenticationService implements the AuthenticationService interface
type authenticationService struct {
config config.ConfigProvider
logger *zap.Logger
}
// NewAuthenticationService creates a new authentication service
func NewAuthenticationService(config config.ConfigProvider, logger *zap.Logger) AuthenticationService {
return &authenticationService{
config: config,
logger: logger,
}
}
// GetUserID extracts user ID from context
func (s *authenticationService) GetUserID(ctx context.Context) (string, error) {
// For now, this is a simple implementation
// In a real implementation, this would extract from JWT tokens, session, etc.
if userID, ok := ctx.Value("user_id").(string); ok {
return userID, nil
}
return "", fmt.Errorf("user ID not found in context")
}
// ValidatePermissions checks if user has required permissions
func (s *authenticationService) ValidatePermissions(ctx context.Context, userID string, appID string, requiredPermissions []string) error {
s.logger.Debug("Validating permissions",
zap.String("user_id", userID),
zap.String("app_id", appID),
zap.Strings("required_permissions", requiredPermissions))
// TODO: Implement actual permission validation
// For now, we'll just allow all requests
return nil
}
// GetUserClaims retrieves user claims
func (s *authenticationService) GetUserClaims(ctx context.Context, userID string) (map[string]string, error) {
s.logger.Debug("Getting user claims", zap.String("user_id", userID))
// TODO: Implement actual claims retrieval
// For now, return basic claims
claims := map[string]string{
"user_id": userID,
"email": userID, // Assuming user_id is email for now
"name": "Test User",
}
return claims, nil
}

View File

@ -0,0 +1,59 @@
package services
import (
"context"
"github.com/google/uuid"
"github.com/kms/api-key-service/internal/domain"
)
// ApplicationService defines the interface for application business logic
type ApplicationService interface {
// Create creates a new application
Create(ctx context.Context, req *domain.CreateApplicationRequest, userID string) (*domain.Application, error)
// GetByID retrieves an application by its ID
GetByID(ctx context.Context, appID string) (*domain.Application, error)
// List retrieves applications with pagination
List(ctx context.Context, limit, offset int) ([]*domain.Application, error)
// Update updates an existing application
Update(ctx context.Context, appID string, updates *domain.UpdateApplicationRequest, userID string) (*domain.Application, error)
// Delete deletes an application
Delete(ctx context.Context, appID string, userID string) error
}
// TokenService defines the interface for token business logic
type TokenService interface {
// CreateStaticToken creates a new static token
CreateStaticToken(ctx context.Context, req *domain.CreateStaticTokenRequest, userID string) (*domain.CreateStaticTokenResponse, error)
// ListByApp lists all tokens for an application
ListByApp(ctx context.Context, appID string, limit, offset int) ([]*domain.StaticToken, error)
// Delete deletes a token
Delete(ctx context.Context, tokenID uuid.UUID, userID string) error
// GenerateUserToken generates a user token
GenerateUserToken(ctx context.Context, appID, userID string, permissions []string) (string, error)
// VerifyToken verifies a token and returns verification response
VerifyToken(ctx context.Context, req *domain.VerifyRequest) (*domain.VerifyResponse, error)
// RenewUserToken renews a user token
RenewUserToken(ctx context.Context, req *domain.RenewRequest) (*domain.RenewResponse, error)
}
// AuthenticationService defines the interface for authentication business logic
type AuthenticationService interface {
// GetUserID extracts user ID from context
GetUserID(ctx context.Context) (string, error)
// ValidatePermissions checks if user has required permissions
ValidatePermissions(ctx context.Context, userID string, appID string, requiredPermissions []string) error
// GetUserClaims retrieves user claims
GetUserClaims(ctx context.Context, userID string) (map[string]string, error)
}

View File

@ -0,0 +1,162 @@
package services
import (
"context"
"fmt"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"github.com/kms/api-key-service/internal/domain"
"github.com/kms/api-key-service/internal/repository"
)
// tokenService implements the TokenService interface
type tokenService struct {
tokenRepo repository.StaticTokenRepository
appRepo repository.ApplicationRepository
permRepo repository.PermissionRepository
grantRepo repository.GrantedPermissionRepository
logger *zap.Logger
}
// NewTokenService creates a new token service
func NewTokenService(
tokenRepo repository.StaticTokenRepository,
appRepo repository.ApplicationRepository,
permRepo repository.PermissionRepository,
grantRepo repository.GrantedPermissionRepository,
logger *zap.Logger,
) TokenService {
return &tokenService{
tokenRepo: tokenRepo,
appRepo: appRepo,
permRepo: permRepo,
grantRepo: grantRepo,
logger: logger,
}
}
// CreateStaticToken creates a new static token
func (s *tokenService) CreateStaticToken(ctx context.Context, req *domain.CreateStaticTokenRequest, userID string) (*domain.CreateStaticTokenResponse, error) {
s.logger.Info("Creating static token", zap.String("app_id", req.AppID), zap.String("user_id", userID))
// TODO: Validate permissions
// TODO: Validate application exists
// TODO: Generate secure token
// TODO: Grant permissions
tokenID := uuid.New()
now := time.Now()
// Create the token entity
token := &domain.StaticToken{
ID: tokenID,
AppID: req.AppID,
Owner: domain.Owner{
Type: domain.OwnerTypeIndividual,
Name: userID,
Owner: userID,
},
KeyHash: "placeholder-hash-" + tokenID.String(),
Type: "hmac",
CreatedAt: now,
UpdatedAt: now,
}
// Save the token to the database
err := s.tokenRepo.Create(ctx, token)
if err != nil {
s.logger.Error("Failed to create token in database", zap.Error(err), zap.String("token_id", tokenID.String()))
return nil, fmt.Errorf("failed to create token: %w", err)
}
response := &domain.CreateStaticTokenResponse{
ID: tokenID,
Token: "static-token-placeholder-" + tokenID.String(),
Permissions: req.Permissions,
CreatedAt: now,
}
s.logger.Info("Static token created successfully", zap.String("token_id", tokenID.String()))
return response, nil
}
// ListByApp lists all tokens for an application
func (s *tokenService) ListByApp(ctx context.Context, appID string, limit, offset int) ([]*domain.StaticToken, error) {
s.logger.Debug("Listing tokens for application", zap.String("app_id", appID))
// TODO: Implement actual token listing
return []*domain.StaticToken{}, nil
}
// Delete deletes a token
func (s *tokenService) Delete(ctx context.Context, tokenID uuid.UUID, userID string) error {
s.logger.Info("Deleting token", zap.String("token_id", tokenID.String()), zap.String("user_id", userID))
// Check if token exists
exists, err := s.tokenRepo.Exists(ctx, tokenID)
if err != nil {
s.logger.Error("Failed to check token existence", zap.Error(err), zap.String("token_id", tokenID.String()))
return err
}
if !exists {
s.logger.Error("Token not found", zap.String("token_id", tokenID.String()))
return fmt.Errorf("token with ID '%s' not found", tokenID.String())
}
// Delete the token
err = s.tokenRepo.Delete(ctx, tokenID)
if err != nil {
s.logger.Error("Failed to delete token", zap.Error(err), zap.String("token_id", tokenID.String()))
return err
}
// TODO: Revoke associated permissions
return nil
}
// GenerateUserToken generates a user token
func (s *tokenService) GenerateUserToken(ctx context.Context, appID, userID string, permissions []string) (string, error) {
s.logger.Info("Generating user token", zap.String("app_id", appID), zap.String("user_id", userID))
// TODO: Validate application
// TODO: Validate permissions
// TODO: Generate JWT token
return "user-token-placeholder-" + userID, nil
}
// VerifyToken verifies a token and returns verification response
func (s *tokenService) VerifyToken(ctx context.Context, req *domain.VerifyRequest) (*domain.VerifyResponse, error) {
s.logger.Debug("Verifying token", zap.String("app_id", req.AppID), zap.String("type", string(req.Type)))
// TODO: Implement actual token verification logic
response := &domain.VerifyResponse{
Valid: true,
UserID: req.UserID,
Permissions: []string{"basic"},
TokenType: req.Type,
}
return response, nil
}
// RenewUserToken renews a user token
func (s *tokenService) RenewUserToken(ctx context.Context, req *domain.RenewRequest) (*domain.RenewResponse, error) {
s.logger.Info("Renewing user token", zap.String("app_id", req.AppID), zap.String("user_id", req.UserID))
// TODO: Validate current token
// TODO: Generate new token with extended expiry but same max valid date
response := &domain.RenewResponse{
Token: "renewed-token-placeholder",
ExpiresAt: time.Now().Add(7 * 24 * time.Hour),
MaxValidAt: time.Now().Add(30 * 24 * time.Hour),
}
return response, nil
}