pkg/limits/rate_limiting.go
Rama master · réplica de lectura del repositorio original.
package limits
import (
"errors"
"time"
"github.com/cozy/cozy-stack/pkg/prefixer"
"github.com/redis/go-redis/v9"
)
// CounterType os an enum for the type of counters used by rate-limiting.
type CounterType int
// ErrRateLimitReached is the error returned when we were under the limit
// before the check, and reach the limit.
var ErrRateLimitReached = errors.New("Rate limit reached")
// ErrRateLimitExceeded is the error returned when the limit was already
// reached before the check.
var ErrRateLimitExceeded = errors.New("Rate limit exceeded")
const (
// AuthType is used for counting the number of login attempts.
AuthType CounterType = iota
// TwoFactorGenerationType is used for counting the number of times a 2FA
// is generated.
TwoFactorGenerationType
// TwoFactorType is used for counting the number of 2FA attempts.
TwoFactorType
// OAuthClientType is used for counting the number of OAuth clients.
// creations/updates.
OAuthClientType
// SharingInviteType is used for counting the number of sharing invitations
// sent to a given instance.
SharingInviteType
// SharingPublicLinkType is used for counting the number of public sharing
// link consultations
SharingPublicLinkType
// JobThumbnailType is used for counting the number of thumbnail jobs
// executed by an instance
JobThumbnailType
// JobShareTrackType is used for counting the number of updates of the
// io.cozy.shared database
JobShareTrackType
// JobShareReplicateType is used for counting the number of replications
JobShareReplicateType
// JobShareUploadType is used for counting the file uploads
JobShareUploadType
// JobKonnectorType is used for counting the number of konnector executions
JobKonnectorType
// JobZipType is used for cozies exports
JobZipType
// JobSendMailType is used for mail sending
JobSendMailType
// JobServiceType is used for generic services
// Ex: categorization or matching for banking
JobServiceType
// JobNotificationType is used for mobile notifications pushing
JobNotificationType
// SendHintByMail is used for sending the password hint by email
SendHintByMail
// JobNotesPersistType is used for saving notes to the VFS
JobNotesPersistType
// JobClientType is used for the jobs associated to a @client trigger
JobClientType
// ExportType is used for creating an export of the data
ExportType
// WebhookTriggerType is used for calling a webhook trigger
WebhookTriggerType
// JobCleanClientType is used for cleaning unused OAuth clients
JobCleanClientType
// ConfirmFlagshipType is used when the user is asked to manually certify
// that an OAuth client is the flagship app.
ConfirmFlagshipType
// MagicLinkType is used when sending emails with a magic link that can
// authenticate the user into a Cozy
MagicLinkType
// ResendOnboardingMailType is used for resending the onboarding link by email
ResendOnboardingMailType
)
type counterConfig struct {
Prefix string
Limit int64
Period time.Duration
}
var configs = []counterConfig{
// AuthType
{
Prefix: "auth",
Limit: 1000,
Period: 1 * time.Hour,
},
// TwoFactorGenerationType
{
Prefix: "two-factor-generation",
Limit: 20,
Period: 1 * time.Hour,
},
// TwoFactorType
{
Prefix: "two-factor",
Limit: 10,
Period: 5 * time.Minute,
},
// OAuthClientType
{
Prefix: "oauth-client",
Limit: 50,
Period: 1 * time.Hour,
},
// SharingInviteType
{
Prefix: "sharing-invite",
Limit: 20,
Period: 1 * time.Hour,
},
// SharingPublicLink
{
Prefix: "sharing-public-link",
Limit: 2000,
Period: 1 * time.Hour,
},
// JobThumbnail
{
Prefix: "job-thumbnail",
Limit: 20000,
Period: 1 * time.Hour,
},
// JobShareTrack
{
Prefix: "job-share-track",
Limit: 20000,
Period: 1 * time.Hour,
},
// JobShareReplicate
{
Prefix: "job-share-replicate",
Limit: 2000,
Period: 1 * time.Hour,
},
// JobShareUpload
{
Prefix: "job-share-upload",
Limit: 1000,
Period: 1 * time.Hour,
},
// JobKonnector
{
Prefix: "job-konnector",
Limit: 100,
Period: 1 * time.Hour,
},
// JobZip
{
Prefix: "job-zip",
Limit: 100,
Period: 1 * time.Hour,
},
// JobSendMail
{
Prefix: "job-sendmail",
Limit: 200,
Period: 1 * time.Hour,
},
// JobService
{
Prefix: "job-service",
Limit: 200,
Period: 1 * time.Hour,
},
// JobNotification
{
Prefix: "job-push",
Limit: 30,
Period: 1 * time.Hour,
},
// SendHintByMail
{
Prefix: "send-hint",
Limit: 2,
Period: 1 * time.Hour,
},
// JobNotesPersistType
{
Prefix: "job-notes-persist",
Limit: 100,
Period: 1 * time.Hour,
},
// JobClientType
{
Prefix: "job-client",
Limit: 100,
Period: 1 * time.Hour,
},
// ExportType
{
Prefix: "export",
Limit: 5,
Period: 24 * time.Hour,
},
// WebhookTriggerType
{
Prefix: "webhook-trigger",
Limit: 30,
Period: 1 * time.Hour,
},
// JobCleanClientType
{
Prefix: "job-clean-clients",
Limit: 100,
Period: 1 * time.Hour,
},
// ConfirmFlagshipType
{
Prefix: "confirm-flagship",
Limit: 30,
Period: 1 * time.Hour,
},
// MagicLinkType
{
Prefix: "magic-link",
Limit: 30,
Period: 1 * time.Hour,
},
// ResendOnboardingMailType
{
Prefix: "resend-onboarding-mail",
Limit: 2,
Period: 1 * time.Hour,
},
}
// Counter is an interface for counting number of attempts that can be used to
// rate limit the number of logins and 2FA tries, and thus block bruteforce
// attacks.
type Counter interface {
Increment(key string, timeLimit time.Duration) (int64, error)
Reset(key string) error
}
// RateLimiter allow to rate limite the access to some resource.
type RateLimiter struct {
counter Counter
}
// NewRateLimiter instantiate a new [RateLimiter].
//
// The backend selection is done based on the `client` argument. If a client is
// given, the redis backend is chosen, if nil is provided the inmemory backend would
// be chosen.
func NewRateLimiter(client redis.UniversalClient) *RateLimiter {
if client == nil {
return &RateLimiter{NewInMemory()}
}
return &RateLimiter{NewRedis(client)}
}
// CheckRateLimit returns an error if the counter for the given type and
// instance has reached the limit.
func (r *RateLimiter) CheckRateLimit(p prefixer.Prefixer, ct CounterType) error {
return r.CheckRateLimitKey(p.DomainName(), ct)
}
// CheckRateLimitKey allows to check the rate-limit for a key
func (r *RateLimiter) CheckRateLimitKey(customKey string, ct CounterType) error {
cfg := configs[ct]
key := cfg.Prefix + ":" + customKey
val, err := r.counter.Increment(key, cfg.Period)
if err != nil {
return err
}
// The first time we reach the limit, we provide a specific error message.
// This allows to log a warning only once if needed.
if val == cfg.Limit+1 {
return ErrRateLimitReached
}
if val > cfg.Limit {
return ErrRateLimitExceeded
}
return nil
}
// ResetCounter sets again to zero the counter for the given type and instance.
func (r *RateLimiter) ResetCounter(p prefixer.Prefixer, ct CounterType) {
cfg := configs[ct]
key := cfg.Prefix + ":" + p.DomainName()
_ = r.counter.Reset(key)
}
// IsLimitReachedOrExceeded return true if the limit has been reached or
// exceeded, false otherwise.
func IsLimitReachedOrExceeded(err error) bool {
return errors.Is(err, ErrRateLimitReached) || errors.Is(err, ErrRateLimitExceeded)
}
// GetMaximumLimit returns the limit of a CounterType
func GetMaximumLimit(ct CounterType) int64 {
return configs[ct].Limit
}
// SetMaximumLimit sets a new limit for a CounterType
func SetMaximumLimit(ct CounterType, newLimit int64) {
configs[ct].Limit = newLimit
}