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 }