add logging
This commit is contained in:
@@ -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.
|
||||||
@@ -2,6 +2,8 @@ package rbac
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -17,6 +19,7 @@ type JWTConfig struct {
|
|||||||
RequiredGroups []string // Groups that are required for access (any one of these)
|
RequiredGroups []string // Groups that are required for access (any one of these)
|
||||||
ContextKey string // Key to store the claims in the context
|
ContextKey string // Key to store the claims in the context
|
||||||
ErrorHandler func(echo.Context, error) error
|
ErrorHandler func(echo.Context, error) error
|
||||||
|
Logger *slog.Logger // Logger instance for middleware
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultJWTConfig is the default JWT auth middleware config
|
// 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
|
// 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 {
|
if len(requiredGroups) == 0 {
|
||||||
|
logger.Debug("no required groups specified, access granted")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
groups, ok := claims["cognito:groups"].([]interface{})
|
groups, ok := claims["cognito:groups"].([]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
|
logger.Warn("no groups found in claims", "user", claims["sub"])
|
||||||
return false
|
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 _, reqGroup := range requiredGroups {
|
||||||
for _, group := range groups {
|
for _, group := range groups {
|
||||||
if groupStr, ok := group.(string); ok && strings.EqualFold(groupStr, reqGroup) {
|
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
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.Warn("group validation failed",
|
||||||
|
"user", claims["sub"],
|
||||||
|
"required_groups", requiredGroups,
|
||||||
|
"user_groups", userGroups)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseJWT parses and validates the JWT token
|
// 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) {
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||||
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
|
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
|
||||||
|
logger.Error("unexpected signing method", "method", token.Method.Alg())
|
||||||
return nil, errors.New("unexpected signing method")
|
return nil, errors.New("unexpected signing method")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get key ID from token header
|
// Get key ID from token header
|
||||||
kid, ok := token.Header["kid"].(string)
|
kid, ok := token.Header["kid"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
logger.Error("no key ID in token header")
|
||||||
return nil, errors.New("no key ID (kid) 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 {
|
if err != nil {
|
||||||
|
logger.Error("failed to parse JWT", "error", err)
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
claims, ok := token.Claims.(jwt.MapClaims)
|
claims, ok := token.Claims.(jwt.MapClaims)
|
||||||
if !ok || !token.Valid {
|
if !ok || !token.Valid {
|
||||||
|
logger.Error("invalid token claims")
|
||||||
return nil, nil, errors.New("invalid token")
|
return nil, nil, errors.New("invalid token")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.Debug("JWT parsed successfully",
|
||||||
|
"user", claims["sub"],
|
||||||
|
"email", claims["email"])
|
||||||
return token, claims, nil
|
return token, claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +140,9 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
|
|||||||
if config.KeyProvider == nil {
|
if config.KeyProvider == nil {
|
||||||
panic("JWT middleware requires a KeyProvider")
|
panic("JWT middleware requires a KeyProvider")
|
||||||
}
|
}
|
||||||
|
if config.Logger == nil {
|
||||||
|
config.Logger = slog.Default()
|
||||||
|
}
|
||||||
|
|
||||||
extractor := extractToken(config)
|
extractor := extractToken(config)
|
||||||
|
|
||||||
@@ -119,21 +150,39 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
|
|||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
tokenString, err := extractor(c)
|
tokenString, err := extractor(c)
|
||||||
if err != nil {
|
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)
|
return config.ErrorHandler(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, claims, err := parseJWT(tokenString, config.KeyProvider)
|
_, claims, err := parseJWT(tokenString, config.KeyProvider, config.Logger)
|
||||||
if err != nil {
|
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)
|
return config.ErrorHandler(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check required groups if specified
|
// 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")
|
return echo.NewHTTPError(http.StatusForbidden, "insufficient permissions")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store user information in context
|
// Store user information in context
|
||||||
c.Set(config.ContextKey, claims)
|
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)
|
return next(c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,14 +200,14 @@ func tokenFromHeader(header string, authScheme string) func(echo.Context) (strin
|
|||||||
return func(c echo.Context) (string, error) {
|
return func(c echo.Context) (string, error) {
|
||||||
auth := c.Request().Header.Get(header)
|
auth := c.Request().Header.Get(header)
|
||||||
if auth == "" {
|
if auth == "" {
|
||||||
return "", errors.New("missing auth header")
|
return "", fmt.Errorf("missing auth header: %s", header)
|
||||||
}
|
}
|
||||||
|
|
||||||
l := len(authScheme)
|
l := len(authScheme)
|
||||||
if len(auth) > l+1 && auth[:l] == authScheme {
|
if len(auth) > l+1 && auth[:l] == authScheme {
|
||||||
return auth[l+1:], nil
|
return auth[l+1:], nil
|
||||||
}
|
}
|
||||||
return "", errors.New("invalid auth header")
|
return "", fmt.Errorf("invalid auth header format for scheme: %s", authScheme)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user