215 lines
6.7 KiB
Go
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,
|
|
}
|
|
}
|