add logging

This commit is contained in:
jay brown
2025-03-14 14:15:13 -07:00
parent 67fe6a6055
commit eb5129da97
2 changed files with 84 additions and 6 deletions
+29
View File
@@ -0,0 +1,29 @@
You are an expert AI programming assistant specializing in building APIs with Go, using the standard library's net/http package and the new ServeMux introduced in Go 1.22.
Always use the latest stable version of Go (1.22 or newer) and be familiar with RESTful API design principles, best practices, and Go idioms.
Follow the user's requirements carefully & to the letter.
First think step-by-step - describe your plan for the API structure, endpoints, and data flow in pseudocode, written out in great detail.
Confirm the plan, then write code!
Write correct, up-to-date, bug-free, fully functional, secure, and efficient Go code for APIs.
Use echo package for rest API development:
Implement proper error handling, including custom error types when beneficial.
Use appropriate status codes and format JSON responses correctly.
Implement input validation for API endpoints.
Utilize Go's built-in concurrency features when beneficial for API performance.
Follow RESTful API design principles and best practices.
Include necessary imports, package declarations, and any required setup code.
Implement proper logging using the custom logger `func (b *LogConfig) GetLogger() *slog.Logger` in the internal/logger package.
Use middleware for cross-cutting concerns (e.g., logging, authentication).
Implement rate limiting and authentication/authorization when appropriate, using standard library features or simple custom implementations.
Leave NO todos, placeholders, or missing pieces in the API implementation.
Be concise in explanations, but provide brief comments for complex logic or Go-specific idioms.
If unsure about a best practice or implementation detail, say so instead of guessing.
Offer suggestions for testing the API endpoints using Go's testing package.
Always prioritize security, scalability, and maintainability in your API designs and implementations.
Leverage the power and simplicity of Go's standard library to create efficient and idiomatic APIs.
+55 -6
View File
@@ -2,6 +2,8 @@ package rbac
import (
"errors"
"fmt"
"log/slog"
"net/http"
"strings"
@@ -17,6 +19,7 @@ type JWTConfig struct {
RequiredGroups []string // Groups that are required for access (any one of these)
ContextKey string // Key to store the claims in the context
ErrorHandler func(echo.Context, error) error
Logger *slog.Logger // Logger instance for middleware
}
// DefaultJWTConfig is the default JWT auth middleware config
@@ -45,36 +48,56 @@ func extractToken(config JWTConfig) func(echo.Context) (string, error) {
}
// validateGroups checks if the user belongs to any of the required groups
func validateGroups(claims jwt.MapClaims, requiredGroups []string) bool {
func validateGroups(claims jwt.MapClaims, requiredGroups []string, logger *slog.Logger) bool {
if len(requiredGroups) == 0 {
logger.Debug("no required groups specified, access granted")
return true
}
groups, ok := claims["cognito:groups"].([]interface{})
if !ok {
logger.Warn("no groups found in claims", "user", claims["sub"])
return false
}
userGroups := make([]string, 0)
for _, group := range groups {
if groupStr, ok := group.(string); ok {
userGroups = append(userGroups, groupStr)
}
}
for _, reqGroup := range requiredGroups {
for _, group := range groups {
if groupStr, ok := group.(string); ok && strings.EqualFold(groupStr, reqGroup) {
logger.Debug("group validation successful",
"user", claims["sub"],
"required_group", reqGroup,
"user_groups", userGroups)
return true
}
}
}
logger.Warn("group validation failed",
"user", claims["sub"],
"required_groups", requiredGroups,
"user_groups", userGroups)
return false
}
// parseJWT parses and validates the JWT token
func parseJWT(tokenString string, keyProvider KeyProvider) (*jwt.Token, jwt.MapClaims, error) {
func parseJWT(tokenString string, keyProvider KeyProvider, logger *slog.Logger) (*jwt.Token, jwt.MapClaims, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
logger.Error("unexpected signing method", "method", token.Method.Alg())
return nil, errors.New("unexpected signing method")
}
// Get key ID from token header
kid, ok := token.Header["kid"].(string)
if !ok {
logger.Error("no key ID in token header")
return nil, errors.New("no key ID (kid) in token header")
}
@@ -83,14 +106,19 @@ func parseJWT(tokenString string, keyProvider KeyProvider) (*jwt.Token, jwt.MapC
})
if err != nil {
logger.Error("failed to parse JWT", "error", err)
return nil, nil, err
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok || !token.Valid {
logger.Error("invalid token claims")
return nil, nil, errors.New("invalid token")
}
logger.Debug("JWT parsed successfully",
"user", claims["sub"],
"email", claims["email"])
return token, claims, nil
}
@@ -112,6 +140,9 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
if config.KeyProvider == nil {
panic("JWT middleware requires a KeyProvider")
}
if config.Logger == nil {
config.Logger = slog.Default()
}
extractor := extractToken(config)
@@ -119,21 +150,39 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
return func(c echo.Context) error {
tokenString, err := extractor(c)
if err != nil {
config.Logger.Error("failed to extract token",
"error", err,
"path", c.Request().URL.Path,
"method", c.Request().Method)
return config.ErrorHandler(c, err)
}
_, claims, err := parseJWT(tokenString, config.KeyProvider)
_, claims, err := parseJWT(tokenString, config.KeyProvider, config.Logger)
if err != nil {
config.Logger.Error("failed to validate JWT",
"error", err,
"path", c.Request().URL.Path,
"method", c.Request().Method)
return config.ErrorHandler(c, err)
}
// Check required groups if specified
if !validateGroups(claims, config.RequiredGroups) {
if !validateGroups(claims, config.RequiredGroups, config.Logger) {
config.Logger.Warn("insufficient permissions",
"user", claims["sub"],
"path", c.Request().URL.Path,
"method", c.Request().Method)
return echo.NewHTTPError(http.StatusForbidden, "insufficient permissions")
}
// Store user information in context
c.Set(config.ContextKey, claims)
config.Logger.Info("authenticated request",
"user", claims["sub"],
"path", c.Request().URL.Path,
"method", c.Request().Method)
return next(c)
}
}
@@ -151,14 +200,14 @@ func tokenFromHeader(header string, authScheme string) func(echo.Context) (strin
return func(c echo.Context) (string, error) {
auth := c.Request().Header.Get(header)
if auth == "" {
return "", errors.New("missing auth header")
return "", fmt.Errorf("missing auth header: %s", header)
}
l := len(authScheme)
if len(auth) > l+1 && auth[:l] == authScheme {
return auth[l+1:], nil
}
return "", errors.New("invalid auth header")
return "", fmt.Errorf("invalid auth header format for scheme: %s", authScheme)
}
}