344 lines
11 KiB
Go
344 lines
11 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/kms/api-key-service/internal/crypto"
|
|
"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
|
|
tokenGen *crypto.TokenGenerator
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewTokenService creates a new token service
|
|
func NewTokenService(
|
|
tokenRepo repository.StaticTokenRepository,
|
|
appRepo repository.ApplicationRepository,
|
|
permRepo repository.PermissionRepository,
|
|
grantRepo repository.GrantedPermissionRepository,
|
|
hmacKey string,
|
|
logger *zap.Logger,
|
|
) TokenService {
|
|
return &tokenService{
|
|
tokenRepo: tokenRepo,
|
|
appRepo: appRepo,
|
|
permRepo: permRepo,
|
|
grantRepo: grantRepo,
|
|
tokenGen: crypto.NewTokenGenerator(hmacKey),
|
|
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))
|
|
|
|
// Validate application exists
|
|
app, err := s.appRepo.GetByID(ctx, req.AppID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get application", zap.Error(err), zap.String("app_id", req.AppID))
|
|
return nil, fmt.Errorf("application not found: %w", err)
|
|
}
|
|
|
|
// Validate permissions exist
|
|
validPermissions, err := s.permRepo.ValidatePermissionScopes(ctx, req.Permissions)
|
|
if err != nil {
|
|
s.logger.Error("Failed to validate permissions", zap.Error(err))
|
|
return nil, fmt.Errorf("failed to validate permissions: %w", err)
|
|
}
|
|
|
|
if len(validPermissions) != len(req.Permissions) {
|
|
s.logger.Warn("Some permissions are invalid",
|
|
zap.Strings("requested", req.Permissions),
|
|
zap.Strings("valid", validPermissions))
|
|
return nil, fmt.Errorf("some requested permissions are invalid")
|
|
}
|
|
|
|
// Generate secure token
|
|
tokenInfo, err := s.tokenGen.GenerateTokenWithInfo()
|
|
if err != nil {
|
|
s.logger.Error("Failed to generate secure token", zap.Error(err))
|
|
return nil, fmt.Errorf("failed to generate token: %w", err)
|
|
}
|
|
|
|
tokenID := uuid.New()
|
|
now := time.Now()
|
|
|
|
// Create the token entity
|
|
token := &domain.StaticToken{
|
|
ID: tokenID,
|
|
AppID: req.AppID,
|
|
Owner: req.Owner,
|
|
KeyHash: tokenInfo.Hash,
|
|
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)
|
|
}
|
|
|
|
// Grant permissions to the token
|
|
var grants []*domain.GrantedPermission
|
|
for _, permScope := range validPermissions {
|
|
// Get permission by scope to get the ID
|
|
perm, err := s.permRepo.GetAvailablePermissionByScope(ctx, permScope)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get permission by scope", zap.Error(err), zap.String("scope", permScope))
|
|
continue
|
|
}
|
|
|
|
grant := &domain.GrantedPermission{
|
|
ID: uuid.New(),
|
|
TokenType: domain.TokenTypeStatic,
|
|
TokenID: tokenID,
|
|
PermissionID: perm.ID,
|
|
Scope: permScope,
|
|
CreatedBy: userID,
|
|
}
|
|
grants = append(grants, grant)
|
|
}
|
|
|
|
if len(grants) > 0 {
|
|
err = s.grantRepo.GrantPermissions(ctx, grants)
|
|
if err != nil {
|
|
s.logger.Error("Failed to grant permissions", zap.Error(err))
|
|
// Clean up the token if permission granting fails
|
|
s.tokenRepo.Delete(ctx, tokenID)
|
|
return nil, fmt.Errorf("failed to grant permissions: %w", err)
|
|
}
|
|
}
|
|
|
|
response := &domain.CreateStaticTokenResponse{
|
|
ID: tokenID,
|
|
Token: tokenInfo.Token, // Return the actual token only once
|
|
Permissions: validPermissions,
|
|
CreatedAt: now,
|
|
}
|
|
|
|
s.logger.Info("Static token created successfully",
|
|
zap.String("token_id", tokenID.String()),
|
|
zap.String("app_id", app.AppID),
|
|
zap.Strings("permissions", validPermissions))
|
|
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))
|
|
|
|
tokens, err := s.tokenRepo.GetByAppID(ctx, appID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to list tokens from repository", zap.Error(err), zap.String("app_id", appID))
|
|
return nil, fmt.Errorf("failed to list tokens: %w", err)
|
|
}
|
|
|
|
// Apply pagination manually since GetByAppID doesn't support it
|
|
start := offset
|
|
end := offset + limit
|
|
if start > len(tokens) {
|
|
tokens = []*domain.StaticToken{}
|
|
} else if end > len(tokens) {
|
|
tokens = tokens[start:]
|
|
} else {
|
|
tokens = tokens[start:end]
|
|
}
|
|
|
|
s.logger.Debug("Listed tokens successfully", zap.String("app_id", appID), zap.Int("count", len(tokens)))
|
|
return tokens, 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)))
|
|
|
|
// Validate request
|
|
if req.Token == "" {
|
|
return &domain.VerifyResponse{
|
|
Valid: false,
|
|
Error: "Token is required",
|
|
}, nil
|
|
}
|
|
|
|
// Validate application exists
|
|
app, err := s.appRepo.GetByID(ctx, req.AppID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get application", zap.Error(err), zap.String("app_id", req.AppID))
|
|
return &domain.VerifyResponse{
|
|
Valid: false,
|
|
Error: "Invalid application",
|
|
}, nil
|
|
}
|
|
|
|
switch req.Type {
|
|
case domain.TokenTypeStatic:
|
|
return s.verifyStaticToken(ctx, req, app)
|
|
case domain.TokenTypeUser:
|
|
return s.verifyUserToken(ctx, req, app)
|
|
default:
|
|
return &domain.VerifyResponse{
|
|
Valid: false,
|
|
Error: "Invalid token type",
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// verifyStaticToken verifies a static token
|
|
func (s *tokenService) verifyStaticToken(ctx context.Context, req *domain.VerifyRequest, app *domain.Application) (*domain.VerifyResponse, error) {
|
|
s.logger.Debug("Verifying static token", zap.String("app_id", req.AppID))
|
|
|
|
// Check token format
|
|
if !crypto.IsValidTokenFormat(req.Token) {
|
|
s.logger.Warn("Invalid token format", zap.String("app_id", req.AppID))
|
|
return &domain.VerifyResponse{
|
|
Valid: false,
|
|
Error: "Invalid token format",
|
|
}, nil
|
|
}
|
|
|
|
// Try to find token by testing against all stored hashes for this app
|
|
tokens, err := s.tokenRepo.GetByAppID(ctx, req.AppID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get tokens for app", zap.Error(err), zap.String("app_id", req.AppID))
|
|
return &domain.VerifyResponse{
|
|
Valid: false,
|
|
Error: "Token verification failed",
|
|
}, nil
|
|
}
|
|
|
|
var matchedToken *domain.StaticToken
|
|
for _, token := range tokens {
|
|
if s.tokenGen.VerifyToken(req.Token, token.KeyHash) {
|
|
matchedToken = token
|
|
break
|
|
}
|
|
}
|
|
|
|
if matchedToken == nil {
|
|
s.logger.Warn("Token not found or invalid", zap.String("app_id", req.AppID))
|
|
return &domain.VerifyResponse{
|
|
Valid: false,
|
|
Error: "Invalid token",
|
|
}, nil
|
|
}
|
|
|
|
// Get granted permissions for this token
|
|
permissions, err := s.grantRepo.GetGrantedPermissionScopes(ctx, domain.TokenTypeStatic, matchedToken.ID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get token permissions", zap.Error(err), zap.String("token_id", matchedToken.ID.String()))
|
|
return &domain.VerifyResponse{
|
|
Valid: false,
|
|
Error: "Failed to retrieve permissions",
|
|
}, nil
|
|
}
|
|
|
|
// Check specific permissions if requested
|
|
var permissionResults map[string]bool
|
|
if len(req.Permissions) > 0 {
|
|
permissionResults, err = s.grantRepo.HasAnyPermission(ctx, domain.TokenTypeStatic, matchedToken.ID, req.Permissions)
|
|
if err != nil {
|
|
s.logger.Error("Failed to check specific permissions", zap.Error(err))
|
|
return &domain.VerifyResponse{
|
|
Valid: false,
|
|
Error: "Failed to check permissions",
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
s.logger.Info("Static token verified successfully",
|
|
zap.String("token_id", matchedToken.ID.String()),
|
|
zap.String("app_id", req.AppID),
|
|
zap.Strings("permissions", permissions))
|
|
|
|
return &domain.VerifyResponse{
|
|
Valid: true,
|
|
Permissions: permissions,
|
|
PermissionResults: permissionResults,
|
|
TokenType: domain.TokenTypeStatic,
|
|
}, nil
|
|
}
|
|
|
|
// verifyUserToken verifies a user token (JWT-based)
|
|
func (s *tokenService) verifyUserToken(ctx context.Context, req *domain.VerifyRequest, app *domain.Application) (*domain.VerifyResponse, error) {
|
|
s.logger.Debug("Verifying user token", zap.String("app_id", req.AppID))
|
|
|
|
// TODO: Implement JWT token verification
|
|
// For now, return an error since user tokens are not fully implemented
|
|
return &domain.VerifyResponse{
|
|
Valid: false,
|
|
Error: "User token verification not yet implemented",
|
|
}, 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
|
|
}
|