Files
kalzu-value-store/auth/auth.go
ryyst e6d87d025f fix: secure admin endpoints with authentication middleware (issue #4)
- Add config parameter to AuthService constructor
- Implement proper config-based auth checks in middleware
- Wrap all admin endpoints (users, groups, tokens) with authentication
- Apply granular scopes: admin:users:*, admin:groups:*, admin:tokens:*
- Maintain backward compatibility when config is nil

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-21 12:15:38 +03:00

230 lines
5.9 KiB
Go

package auth
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
badger "github.com/dgraph-io/badger/v4"
"github.com/sirupsen/logrus"
"kvs/types"
"kvs/utils"
)
// AuthContext holds authentication information for a request
type AuthContext struct {
UserUUID string `json:"user_uuid"`
Scopes []string `json:"scopes"`
Groups []string `json:"groups"`
}
// AuthService handles authentication operations
type AuthService struct {
db *badger.DB
logger *logrus.Logger
config *types.Config
}
// NewAuthService creates a new authentication service
func NewAuthService(db *badger.DB, logger *logrus.Logger, config *types.Config) *AuthService {
return &AuthService{
db: db,
logger: logger,
config: config,
}
}
// StoreAPIToken stores an API token in BadgerDB with TTL
func (s *AuthService) StoreAPIToken(tokenString string, userUUID string, scopes []string, expiresAt int64) error {
tokenHash := utils.HashToken(tokenString)
apiToken := types.APIToken{
TokenHash: tokenHash,
UserUUID: userUUID,
Scopes: scopes,
IssuedAt: time.Now().Unix(),
ExpiresAt: expiresAt,
}
tokenData, err := json.Marshal(apiToken)
if err != nil {
return err
}
return s.db.Update(func(txn *badger.Txn) error {
entry := badger.NewEntry([]byte(TokenStorageKey(tokenHash)), tokenData)
// Set TTL to the token expiration time
ttl := time.Until(time.Unix(expiresAt, 0))
if ttl > 0 {
entry = entry.WithTTL(ttl)
}
return txn.SetEntry(entry)
})
}
// GetAPIToken retrieves an API token from BadgerDB by hash
func (s *AuthService) GetAPIToken(tokenHash string) (*types.APIToken, error) {
var apiToken types.APIToken
err := s.db.View(func(txn *badger.Txn) error {
item, err := txn.Get([]byte(TokenStorageKey(tokenHash)))
if err != nil {
return err
}
return item.Value(func(val []byte) error {
return json.Unmarshal(val, &apiToken)
})
})
if err != nil {
return nil, err
}
return &apiToken, nil
}
// ExtractTokenFromHeader extracts the Bearer token from the Authorization header
func ExtractTokenFromHeader(r *http.Request) (string, error) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
return "", fmt.Errorf("missing authorization header")
}
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
return "", fmt.Errorf("invalid authorization header format")
}
return parts[1], nil
}
// GetUserGroups retrieves all groups that a user belongs to
func (s *AuthService) GetUserGroups(userUUID string) ([]string, error) {
var user types.User
err := s.db.View(func(txn *badger.Txn) error {
item, err := txn.Get([]byte(UserStorageKey(userUUID)))
if err != nil {
return err
}
return item.Value(func(val []byte) error {
return json.Unmarshal(val, &user)
})
})
if err != nil {
return nil, err
}
return user.Groups, nil
}
// AuthenticateRequest validates the JWT token and returns authentication context
func (s *AuthService) AuthenticateRequest(r *http.Request) (*AuthContext, error) {
// Extract token from header
tokenString, err := ExtractTokenFromHeader(r)
if err != nil {
return nil, err
}
// Validate JWT token
claims, err := ValidateJWT(tokenString)
if err != nil {
return nil, fmt.Errorf("invalid token: %v", err)
}
// Verify token exists in our database (not revoked)
tokenHash := utils.HashToken(tokenString)
_, err = s.GetAPIToken(tokenHash)
if err == badger.ErrKeyNotFound {
return nil, fmt.Errorf("token not found or revoked")
}
if err != nil {
return nil, fmt.Errorf("failed to verify token: %v", err)
}
// Get user's groups
groups, err := s.GetUserGroups(claims.UserUUID)
if err != nil {
s.logger.WithError(err).WithField("user_uuid", claims.UserUUID).Warn("Failed to get user groups")
groups = []string{} // Continue with empty groups on error
}
return &AuthContext{
UserUUID: claims.UserUUID,
Scopes: claims.Scopes,
Groups: groups,
}, nil
}
// CheckResourcePermission checks if a user has permission to perform an operation on a resource
func (s *AuthService) CheckResourcePermission(authCtx *AuthContext, resourceKey string, operation string) bool {
// Get resource metadata
var metadata types.ResourceMetadata
err := s.db.View(func(txn *badger.Txn) error {
item, err := txn.Get([]byte(ResourceMetadataKey(resourceKey)))
if err != nil {
return err
}
return item.Value(func(val []byte) error {
return json.Unmarshal(val, &metadata)
})
})
// If no metadata exists, use default permissions
if err == badger.ErrKeyNotFound {
metadata = types.ResourceMetadata{
OwnerUUID: authCtx.UserUUID, // Treat requester as owner for new resources
GroupUUID: "",
Permissions: types.DefaultPermissions,
}
} else if err != nil {
s.logger.WithError(err).WithField("resource_key", resourceKey).Warn("Failed to get resource metadata")
return false
}
// Check user relationship to resource
isOwner, isGroupMember := CheckUserResourceRelationship(authCtx.UserUUID, &metadata, authCtx.Groups)
// Check permission
return CheckPermission(metadata.Permissions, operation, isOwner, isGroupMember)
}
// GetAuthContext retrieves auth context from request context
func GetAuthContext(ctx context.Context) *AuthContext {
if authCtx, ok := ctx.Value("auth").(*AuthContext); ok {
return authCtx
}
return nil
}
// HasUsers checks if any users exist in the database
func (s *AuthService) HasUsers() (bool, error) {
var hasUsers bool
err := s.db.View(func(txn *badger.Txn) error {
opts := badger.DefaultIteratorOptions
opts.PrefetchValues = false // We only need to check if keys exist
iterator := txn.NewIterator(opts)
defer iterator.Close()
// Look for any key starting with "user:"
prefix := []byte("user:")
for iterator.Seek(prefix); iterator.ValidForPrefix(prefix); iterator.Next() {
hasUsers = true
return nil // Found at least one user, can exit early
}
return nil
})
return hasUsers, err
}