github.com/cozy/cozy-stack@v0.0.0-20240327093429-939e4a21320e/pkg/limits/rate_limiting.go (about)

     1  package limits
     2  
     3  import (
     4  	"errors"
     5  	"time"
     6  
     7  	"github.com/cozy/cozy-stack/pkg/prefixer"
     8  	"github.com/redis/go-redis/v9"
     9  )
    10  
    11  // CounterType os an enum for the type of counters used by rate-limiting.
    12  type CounterType int
    13  
    14  // ErrRateLimitReached is the error returned when we were under the limit
    15  // before the check, and reach the limit.
    16  var ErrRateLimitReached = errors.New("Rate limit reached")
    17  
    18  // ErrRateLimitExceeded is the error returned when the limit was already
    19  // reached before the check.
    20  var ErrRateLimitExceeded = errors.New("Rate limit exceeded")
    21  
    22  const (
    23  	// AuthType is used for counting the number of login attempts.
    24  	AuthType CounterType = iota
    25  	// TwoFactorGenerationType is used for counting the number of times a 2FA
    26  	// is generated.
    27  	TwoFactorGenerationType
    28  	// TwoFactorType is used for counting the number of 2FA attempts.
    29  	TwoFactorType
    30  	// OAuthClientType is used for counting the number of OAuth clients.
    31  	// creations/updates.
    32  	OAuthClientType
    33  	// SharingInviteType is used for counting the number of sharing invitations
    34  	// sent to a given instance.
    35  	SharingInviteType
    36  	// SharingPublicLinkType is used for counting the number of public sharing
    37  	// link consultations
    38  	SharingPublicLinkType
    39  	// JobThumbnailType is used for counting the number of thumbnail jobs
    40  	// executed by an instance
    41  	JobThumbnailType
    42  	// JobShareTrackType is used for counting the number of updates of the
    43  	// io.cozy.shared database
    44  	JobShareTrackType
    45  	// JobShareReplicateType is used for counting the number of replications
    46  	JobShareReplicateType
    47  	// JobShareUploadType is used for counting the file uploads
    48  	JobShareUploadType
    49  	// JobKonnectorType is used for counting the number of konnector executions
    50  	JobKonnectorType
    51  	// JobZipType is used for cozies exports
    52  	JobZipType
    53  	// JobSendMailType is used for mail sending
    54  	JobSendMailType
    55  	// JobServiceType is used for generic services
    56  	// Ex: categorization or matching for banking
    57  	JobServiceType
    58  	// JobNotificationType is used for mobile notifications pushing
    59  	JobNotificationType
    60  	// SendHintByMail is used for sending the password hint by email
    61  	SendHintByMail
    62  	// JobNotesPersistType is used for saving notes to the VFS
    63  	JobNotesPersistType
    64  	// JobClientType is used for the jobs associated to a @client trigger
    65  	JobClientType
    66  	// ExportType is used for creating an export of the data
    67  	ExportType
    68  	// WebhookTriggerType is used for calling a webhook trigger
    69  	WebhookTriggerType
    70  	// JobCleanClientType is used for cleaning unused OAuth clients
    71  	JobCleanClientType
    72  	// ConfirmFlagshipType is used when the user is asked to manually certify
    73  	// that an OAuth client is the flagship app.
    74  	ConfirmFlagshipType
    75  	// MagicLinkType is used when sending emails with a magic link that can
    76  	// authenticate the user into a Cozy
    77  	MagicLinkType
    78  	// ResendOnboardingMailType is used for resending the onboarding link by email
    79  	ResendOnboardingMailType
    80  )
    81  
    82  type counterConfig struct {
    83  	Prefix string
    84  	Limit  int64
    85  	Period time.Duration
    86  }
    87  
    88  var configs = []counterConfig{
    89  	// AuthType
    90  	{
    91  		Prefix: "auth",
    92  		Limit:  1000,
    93  		Period: 1 * time.Hour,
    94  	},
    95  	// TwoFactorGenerationType
    96  	{
    97  		Prefix: "two-factor-generation",
    98  		Limit:  20,
    99  		Period: 1 * time.Hour,
   100  	},
   101  	// TwoFactorType
   102  	{
   103  		Prefix: "two-factor",
   104  		Limit:  10,
   105  		Period: 5 * time.Minute,
   106  	},
   107  	// OAuthClientType
   108  	{
   109  		Prefix: "oauth-client",
   110  		Limit:  50,
   111  		Period: 1 * time.Hour,
   112  	},
   113  	// SharingInviteType
   114  	{
   115  		Prefix: "sharing-invite",
   116  		Limit:  20,
   117  		Period: 1 * time.Hour,
   118  	},
   119  	// SharingPublicLink
   120  	{
   121  		Prefix: "sharing-public-link",
   122  		Limit:  2000,
   123  		Period: 1 * time.Hour,
   124  	},
   125  	// JobThumbnail
   126  	{
   127  		Prefix: "job-thumbnail",
   128  		Limit:  20000,
   129  		Period: 1 * time.Hour,
   130  	},
   131  	// JobShareTrack
   132  	{
   133  		Prefix: "job-share-track",
   134  		Limit:  20000,
   135  		Period: 1 * time.Hour,
   136  	},
   137  	// JobShareReplicate
   138  	{
   139  		Prefix: "job-share-replicate",
   140  		Limit:  2000,
   141  		Period: 1 * time.Hour,
   142  	},
   143  	// JobShareUpload
   144  	{
   145  		Prefix: "job-share-upload",
   146  		Limit:  1000,
   147  		Period: 1 * time.Hour,
   148  	},
   149  	// JobKonnector
   150  	{
   151  		Prefix: "job-konnector",
   152  		Limit:  100,
   153  		Period: 1 * time.Hour,
   154  	},
   155  	// JobZip
   156  	{
   157  		Prefix: "job-zip",
   158  		Limit:  100,
   159  		Period: 1 * time.Hour,
   160  	},
   161  	// JobSendMail
   162  	{
   163  		Prefix: "job-sendmail",
   164  		Limit:  200,
   165  		Period: 1 * time.Hour,
   166  	},
   167  	// JobService
   168  	{
   169  		Prefix: "job-service",
   170  		Limit:  200,
   171  		Period: 1 * time.Hour,
   172  	},
   173  	// JobNotification
   174  	{
   175  		Prefix: "job-push",
   176  		Limit:  30,
   177  		Period: 1 * time.Hour,
   178  	},
   179  	// SendHintByMail
   180  	{
   181  		Prefix: "send-hint",
   182  		Limit:  2,
   183  		Period: 1 * time.Hour,
   184  	},
   185  	// JobNotesPersistType
   186  	{
   187  		Prefix: "job-notes-persist",
   188  		Limit:  100,
   189  		Period: 1 * time.Hour,
   190  	},
   191  	// JobClientType
   192  	{
   193  		Prefix: "job-client",
   194  		Limit:  100,
   195  		Period: 1 * time.Hour,
   196  	},
   197  	// ExportType
   198  	{
   199  		Prefix: "export",
   200  		Limit:  5,
   201  		Period: 24 * time.Hour,
   202  	},
   203  	// WebhookTriggerType
   204  	{
   205  		Prefix: "webhook-trigger",
   206  		Limit:  30,
   207  		Period: 1 * time.Hour,
   208  	},
   209  	// JobCleanClientType
   210  	{
   211  		Prefix: "job-clean-clients",
   212  		Limit:  100,
   213  		Period: 1 * time.Hour,
   214  	},
   215  	// ConfirmFlagshipType
   216  	{
   217  		Prefix: "confirm-flagship",
   218  		Limit:  30,
   219  		Period: 1 * time.Hour,
   220  	},
   221  	// MagicLinkType
   222  	{
   223  		Prefix: "magic-link",
   224  		Limit:  30,
   225  		Period: 1 * time.Hour,
   226  	},
   227  	// ResendOnboardingMailType
   228  	{
   229  		Prefix: "resend-onboarding-mail",
   230  		Limit:  2,
   231  		Period: 1 * time.Hour,
   232  	},
   233  }
   234  
   235  // Counter is an interface for counting number of attempts that can be used to
   236  // rate limit the number of logins and 2FA tries, and thus block bruteforce
   237  // attacks.
   238  type Counter interface {
   239  	Increment(key string, timeLimit time.Duration) (int64, error)
   240  	Reset(key string) error
   241  }
   242  
   243  // RateLimiter allow to rate limite the access to some resource.
   244  type RateLimiter struct {
   245  	counter Counter
   246  }
   247  
   248  // NewRateLimiter instantiate a new [RateLimiter].
   249  //
   250  // The backend selection is done based on the `client` argument. If a client is
   251  // given, the redis backend is chosen, if nil is provided the inmemory backend would
   252  // be chosen.
   253  func NewRateLimiter(client redis.UniversalClient) *RateLimiter {
   254  	if client == nil {
   255  		return &RateLimiter{NewInMemory()}
   256  	}
   257  
   258  	return &RateLimiter{NewRedis(client)}
   259  }
   260  
   261  // CheckRateLimit returns an error if the counter for the given type and
   262  // instance has reached the limit.
   263  func (r *RateLimiter) CheckRateLimit(p prefixer.Prefixer, ct CounterType) error {
   264  	return r.CheckRateLimitKey(p.DomainName(), ct)
   265  }
   266  
   267  // CheckRateLimitKey allows to check the rate-limit for a key
   268  func (r *RateLimiter) CheckRateLimitKey(customKey string, ct CounterType) error {
   269  	cfg := configs[ct]
   270  	key := cfg.Prefix + ":" + customKey
   271  
   272  	val, err := r.counter.Increment(key, cfg.Period)
   273  	if err != nil {
   274  		return err
   275  	}
   276  
   277  	// The first time we reach the limit, we provide a specific error message.
   278  	// This allows to log a warning only once if needed.
   279  	if val == cfg.Limit+1 {
   280  		return ErrRateLimitReached
   281  	}
   282  
   283  	if val > cfg.Limit {
   284  		return ErrRateLimitExceeded
   285  	}
   286  
   287  	return nil
   288  }
   289  
   290  // ResetCounter sets again to zero the counter for the given type and instance.
   291  func (r *RateLimiter) ResetCounter(p prefixer.Prefixer, ct CounterType) {
   292  	cfg := configs[ct]
   293  	key := cfg.Prefix + ":" + p.DomainName()
   294  
   295  	_ = r.counter.Reset(key)
   296  }
   297  
   298  // IsLimitReachedOrExceeded return true if the limit has been reached or
   299  // exceeded, false otherwise.
   300  func IsLimitReachedOrExceeded(err error) bool {
   301  	return errors.Is(err, ErrRateLimitReached) || errors.Is(err, ErrRateLimitExceeded)
   302  }
   303  
   304  // GetMaximumLimit returns the limit of a CounterType
   305  func GetMaximumLimit(ct CounterType) int64 {
   306  	return configs[ct].Limit
   307  }
   308  
   309  // SetMaximumLimit sets a new limit for a CounterType
   310  func SetMaximumLimit(ct CounterType, newLimit int64) {
   311  	configs[ct].Limit = newLimit
   312  }