Files
dev-service/pkg/rpc/dev.go
itzaname d832734f0d
All checks were successful
continuous-integration/drone/push Build is passing
Add user active check
2025-06-27 23:57:28 -04:00

215 lines
6.7 KiB
Go

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,
}
}