package rpc import ( "context" "fmt" log "github.com/sirupsen/logrus" "net/http" "time" "git.itzana.me/StrafesNET/dev-service/pkg/cache" "git.itzana.me/StrafesNET/dev-service/pkg/datastore" "git.itzana.me/StrafesNET/dev-service/pkg/model" "git.itzana.me/StrafesNET/dev-service/pkg/ratelimit" "git.itzana.me/strafesnet/go-grpc/dev" ) // Error messages const ( ErrEmptyAPIKey = "empty API key" ErrEmptyService = "empty service name" ErrEmptyPermission = "empty permission name" ErrAppInactive = "application is disabled" ErrUserInactive = "user is disabled" ErrRateLimitExceeded = "rate limit exceeded" ErrUnauthorized = "unauthorized request" ) // Cache settings const ( ApplicationCacheDuration = 30 * time.Second ) // Dev implements the DevServiceServer interface type Dev struct { *dev.UnimplementedDevServiceServer Store datastore.Datastore Limiter *ratelimit.RateLimit Cache *cache.Cache } // Validate checks if an API key is valid and has the required permissions func (d Dev) Validate(ctx context.Context, request *dev.APIValidationRequest) (*dev.APIValidationResponse, error) { // Check for context cancellation if ctx.Err() != nil { return errorResponse(fmt.Errorf("context error: %w", ctx.Err()), http.StatusInternalServerError) } // Validate request parameters if err := d.validateRequestParams(request); err != nil { return errorResponse(err, http.StatusBadRequest) } // Get application data appCache, err := d.getApplicationData(ctx, request.Key) if err != nil { return errorResponse(err, http.StatusInternalServerError) } // Check if user is disabled if !appCache.UserActive { return buildResponse(appCache, &ratelimit.RateLimitStatus{}, false, ErrUserInactive, http.StatusForbidden), nil } // Check if application is active if !appCache.Active { return buildResponse(appCache, &ratelimit.RateLimitStatus{}, false, ErrAppInactive, http.StatusForbidden), nil } // Check rate limits limit, err := d.checkRateLimits(ctx, appCache) if err != nil { return buildResponse(appCache, createDefaultRateLimitStatus(), false, fmt.Sprintf("failed to check rate limits: %v", err), http.StatusInternalServerError), nil } // Rate limit exceeded if !limit.Allowed { return buildResponse(appCache, limit, false, ErrRateLimitExceeded, http.StatusTooManyRequests), nil } // Check permission if hasRequiredPermission(appCache.Permissions, request.Service, request.Permission) { log.WithFields(log.Fields{ "service": request.Service, "permission": request.Permission, "resource": request.Resource, "ip": request.IP, "user": appCache.UserID, "application": appCache.Name, }).Info( "request accepted", ) return buildResponse(appCache, limit, true, "", 0), nil } return buildResponse(appCache, limit, false, ErrUnauthorized, http.StatusUnauthorized), nil } // validateRequestParams validates the basic parameters of the request func (d Dev) validateRequestParams(request *dev.APIValidationRequest) error { if request.Key == "" { return fmt.Errorf(ErrEmptyAPIKey) } if request.Service == "" { return fmt.Errorf(ErrEmptyService) } if request.Permission == "" { return fmt.Errorf(ErrEmptyPermission) } return nil } // checkRateLimits checks if the application has exceeded its rate limits func (d Dev) checkRateLimits(ctx context.Context, appCache *model.ApplicationCache) (*ratelimit.RateLimitStatus, error) { rateLimitConfig := ratelimit.RateLimitConfig{ BurstLimit: appCache.RateLimit.BurstLimit, BurstDurationSeconds: appCache.RateLimit.BurstDuration, DailyLimit: appCache.RateLimit.DailyLimit, MonthlyLimit: appCache.RateLimit.MonthlyLimit, } return d.Limiter.CheckRateLimits(ctx, appCache.UserID, rateLimitConfig) } // createDefaultRateLimitStatus creates a default rate limit status with all limits at zero func createDefaultRateLimitStatus() *ratelimit.RateLimitStatus { return &ratelimit.RateLimitStatus{ Allowed: false, RemainingBurst: 0, RemainingDaily: 0, RemainingMonthly: 0, } } // getApplicationData retrieves application data from cache or datastore func (d Dev) getApplicationData(ctx context.Context, apiKey string) (*model.ApplicationCache, error) { // Check for context cancellation if ctx.Err() != nil { return nil, fmt.Errorf("context error: %w", ctx.Err()) } // Try to get from cache first appCache, err := d.Cache.GetApplicationByAPIKey(ctx, apiKey) if err == nil { return appCache, nil } // Not in cache, retrieve from store app, err := d.Store.GetApplicationByAPIKey(ctx, apiKey) if err != nil { return nil, fmt.Errorf("failed to retrieve application: %w", err) } user, err := d.Store.GetUser(ctx, app.UserID) if err != nil { return nil, fmt.Errorf("failed to retrieve user: %w", err) } appCache = &model.ApplicationCache{ Name: app.Name, UserID: app.UserID, APIKey: app.APIKey, Permissions: app.Permissions, Active: app.Active, UserActive: user.Active, RateLimit: user.RateLimit, } // Cache the application data - log the error but don't fail the operation if err := d.Cache.SetApplicationByAPIKey(ctx, appCache, ApplicationCacheDuration); err != nil { // Log the error but continue since we have the data fmt.Printf("warning: failed to cache application: %v\n", err) } return appCache, nil } // errorResponse creates a standardized error response func errorResponse(err error, statusCode int32) (*dev.APIValidationResponse, error) { return &dev.APIValidationResponse{ Valid: false, ErrorMessage: err.Error(), StatusCode: statusCode, }, nil } // hasRequiredPermission checks if the permissions list contains the required service and permission func hasRequiredPermission(permissions []model.Permission, service, permissionName string) bool { for _, p := range permissions { if p.PermissionName == permissionName && p.Service == service { return true } } return false } // buildResponse constructs a validation response with common fields func buildResponse(app *model.ApplicationCache, limit *ratelimit.RateLimitStatus, valid bool, errorMsg string, statusCode int32) *dev.APIValidationResponse { return &dev.APIValidationResponse{ Valid: valid, ErrorMessage: errorMsg, StatusCode: statusCode, RemainingBurst: limit.RemainingBurst, RemainingDaily: limit.RemainingDaily, RemainingMonthly: limit.RemainingMonthly, BurstLimit: app.RateLimit.BurstLimit, BurstDurationSeconds: app.RateLimit.BurstDuration, DailyLimit: app.RateLimit.DailyLimit, MonthlyLimit: app.RateLimit.MonthlyLimit, UserID: app.UserID, Application: app.Name, } }