Skip to content

Commit fdae24e

Browse files
committed
rate_limit
1 parent b549759 commit fdae24e

File tree

9 files changed

+118
-70
lines changed

9 files changed

+118
-70
lines changed

lib/bencher_config/src/config_tx.rs

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use bencher_json::{
1313
use bencher_rbac::init_rbac;
1414
use bencher_schema::context::{ApiContext, Database, DbConnection};
1515
#[cfg(feature = "plus")]
16-
use bencher_schema::model::server::QueryServer;
16+
use bencher_schema::{context::RateLimit, model::server::QueryServer};
1717
use bencher_token::TokenKey;
1818
#[cfg(feature = "plus")]
1919
use diesel::connection::SimpleConnection;
@@ -52,6 +52,8 @@ pub enum ConfigTxError {
5252
DatabaseConnection(String, diesel::ConnectionError),
5353
#[error("Failed to parse data store: {0}")]
5454
DataStore(bencher_schema::context::DataStoreError),
55+
#[error("Failed to configure rate limits: {0}")]
56+
RateLimit(Box<bencher_schema::context::RateLimitError>),
5557
#[error("Failed to register endpoint: {0}")]
5658
Register(dropshot::ApiDescriptionRegisterError),
5759
#[error("Failed to create server: {0}")]
@@ -104,7 +106,8 @@ impl ConfigTx {
104106
restart_tx,
105107
#[cfg(feature = "plus")]
106108
plus,
107-
)?;
109+
)
110+
.await?;
108111
debug!(log, "Configuring TLS");
109112
let tls = server.tls.take().map(|json_tls| match json_tls {
110113
JsonTls::AsFile {
@@ -158,7 +161,7 @@ impl ConfigTx {
158161
}
159162
}
160163

161-
fn into_context(
164+
async fn into_context(
162165
log: &Logger,
163166
console: JsonConsole,
164167
security: JsonSecurity,
@@ -190,17 +193,20 @@ fn into_context(
190193
None
191194
};
192195

196+
let database = Database {
197+
path: json_database.file,
198+
connection: Arc::new(tokio::sync::Mutex::new(database_connection)),
199+
data_store,
200+
};
201+
193202
info!(&log, "Loading secret key");
194203
let token_key = TokenKey::new(
195204
security.issuer.unwrap_or_else(|| console_url.to_string()),
196205
&security.secret_key,
197206
);
198207

199208
#[cfg(feature = "plus")]
200-
let rate_limit = plus
201-
.as_ref()
202-
.and_then(|plus| plus.rate_limit.map(Into::into))
203-
.unwrap_or_default();
209+
let rate_limit = plus.as_ref().and_then(|plus| plus.rate_limit);
204210

205211
info!(&log, "Configuring Bencher Plus");
206212
#[cfg(feature = "plus")]
@@ -215,17 +221,25 @@ fn into_context(
215221
#[cfg(feature = "plus")]
216222
let is_bencher_cloud = bencher_json::is_bencher_cloud(&console_url) && biller.is_some();
217223

224+
#[cfg(feature = "plus")]
225+
let rate_limit = RateLimit::new(
226+
log,
227+
&database.connection,
228+
&licensor,
229+
is_bencher_cloud,
230+
rate_limit,
231+
)
232+
.await
233+
.map_err(Box::new)
234+
.map_err(ConfigTxError::RateLimit)?;
235+
218236
debug!(&log, "Creating API context");
219237
Ok(ApiContext {
220238
console_url,
221239
token_key,
222240
rbac: init_rbac().map_err(ConfigTxError::Polar)?.into(),
223241
messenger: smtp.into(),
224-
database: Database {
225-
path: json_database.file,
226-
connection: Arc::new(tokio::sync::Mutex::new(database_connection)),
227-
data_store,
228-
},
242+
database,
229243
restart_tx,
230244
#[cfg(feature = "plus")]
231245
rate_limit,

lib/bencher_schema/src/context/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ pub use indexer::{IndexError, Indexer};
2727
pub use messenger::ServerStatsBody;
2828
pub use messenger::{Body, ButtonBody, Email, Message, Messenger, NewUserBody};
2929
#[cfg(feature = "plus")]
30-
pub use rate_limit::RateLimit;
30+
pub use rate_limit::{RateLimit, RateLimitError};
3131
pub use rbac::{Rbac, RbacError};
3232
#[cfg(feature = "plus")]
3333
pub use stats::StatsSettings;

lib/bencher_schema/src/context/rate_limit.rs

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
use std::time::Duration;
22

3-
use bencher_json::{system::config::JsonRateLimit, DateTime};
3+
use bencher_json::{system::config::JsonRateLimit, DateTime, PlanLevel};
4+
use bencher_license::Licensor;
5+
use slog::Logger;
6+
7+
use crate::{
8+
error::BencherResource,
9+
model::{
10+
organization::{plan::LicenseUsage, QueryOrganization},
11+
project::{branch::QueryBranch, threshold::QueryThreshold, QueryProject},
12+
user::QueryUser,
13+
},
14+
};
15+
16+
use super::DbConnection;
417

518
const DAY: Duration = Duration::from_secs(24 * 60 * 60);
619
const UNCLAIMED_RATE_LIMIT: u32 = u8::MAX as u32;
@@ -12,6 +25,46 @@ pub struct RateLimit {
1225
pub claimed: u32,
1326
}
1427

28+
#[derive(Debug, thiserror::Error)]
29+
pub enum RateLimitError {
30+
#[error("User ({uuid}) has exceeded the daily rate limit ({rate_limit}) for {resource} creation. Please, reduce your daily usage.", uuid = user.uuid)]
31+
User {
32+
user: QueryUser,
33+
resource: BencherResource,
34+
rate_limit: u32,
35+
},
36+
#[error("Organization ({uuid}) has exceeded the daily rate limit ({rate_limit}) for {resource} creation. Please, reduce your daily usage.", uuid = organization.uuid)]
37+
Organization {
38+
organization: QueryOrganization,
39+
resource: BencherResource,
40+
rate_limit: u32,
41+
},
42+
#[error("Unclaimed project ({uuid}) has exceeded the daily rate limit ({rate_limit}) for {resource} creation. Please, reduce your daily usage or claim the project: https://bencher.dev/auth/signup?claim={uuid}", uuid = project.uuid)]
43+
UnclaimedProject {
44+
project: QueryProject,
45+
resource: BencherResource,
46+
rate_limit: u32,
47+
},
48+
#[error("Claimed project ({uuid}) has exceeded the daily rate limit ({rate_limit}) for {resource} creation. Please, reduce your daily usage.", uuid = project.uuid)]
49+
ClaimedProject {
50+
project: QueryProject,
51+
resource: BencherResource,
52+
rate_limit: u32,
53+
},
54+
#[error("Branch ({uuid}) has exceeded the daily rate limit ({rate_limit}) for {resource} creation. Please, reduce your daily usage.", uuid = branch.uuid)]
55+
Branch {
56+
branch: QueryBranch,
57+
resource: BencherResource,
58+
rate_limit: u32,
59+
},
60+
#[error("Threshold ({uuid}) has exceeded the daily rate limit ({rate_limit}) for {resource} creation. Please, reduce your daily usage.", uuid = threshold.uuid)]
61+
Threshold {
62+
threshold: QueryThreshold,
63+
resource: BencherResource,
64+
rate_limit: u32,
65+
},
66+
}
67+
1568
impl From<JsonRateLimit> for RateLimit {
1669
fn from(json: JsonRateLimit) -> Self {
1770
let JsonRateLimit {
@@ -38,6 +91,36 @@ impl Default for RateLimit {
3891
}
3992

4093
impl RateLimit {
94+
pub async fn new(
95+
log: &Logger,
96+
conn: &tokio::sync::Mutex<DbConnection>,
97+
licensor: &Licensor,
98+
is_bencher_cloud: bool,
99+
rate_limit: Option<JsonRateLimit>,
100+
) -> Result<Self, RateLimitError> {
101+
let Some(rate_limit) = rate_limit else {
102+
return Ok(Self::default());
103+
};
104+
105+
if !is_bencher_cloud {
106+
match LicenseUsage::get_for_server(conn, licensor, Some(PlanLevel::Team)).await {
107+
Ok(license_usages) if license_usages.is_empty() => {
108+
slog::warn!(log, "Custom rate limits are set, but there is no valid Bencher Plus license key! This is a violation of the Bencher License: https://bencher.dev/legal/license");
109+
slog::warn!(
110+
log,
111+
"Please purchase a license key: https://bencher.dev/pricing"
112+
);
113+
},
114+
Ok(_) => {},
115+
Err(e) => {
116+
slog::error!(log, "Failed to check license for custom rate limits: {e}");
117+
},
118+
}
119+
}
120+
121+
Ok(rate_limit.into())
122+
}
123+
41124
pub fn window(&self) -> (DateTime, DateTime) {
42125
let end_time = chrono::Utc::now();
43126
let start_time = end_time - self.window;

lib/bencher_schema/src/macros/rate_limit.rs

Lines changed: 2 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,3 @@
1-
use crate::{
2-
error::BencherResource,
3-
model::{
4-
organization::QueryOrganization,
5-
project::{branch::QueryBranch, threshold::QueryThreshold, QueryProject},
6-
user::QueryUser,
7-
},
8-
};
9-
10-
#[derive(Debug, thiserror::Error)]
11-
pub enum RateLimitError {
12-
#[error("User ({uuid}) has exceeded the daily rate limit ({rate_limit}) for {resource} creation. Please, reduce your daily usage.", uuid = user.uuid)]
13-
User {
14-
user: QueryUser,
15-
resource: BencherResource,
16-
rate_limit: u32,
17-
},
18-
#[error("Organization ({uuid}) has exceeded the daily rate limit ({rate_limit}) for {resource} creation. Please, reduce your daily usage.", uuid = organization.uuid)]
19-
Organization {
20-
organization: QueryOrganization,
21-
resource: BencherResource,
22-
rate_limit: u32,
23-
},
24-
#[error("Unclaimed project ({uuid}) has exceeded the daily rate limit ({rate_limit}) for {resource} creation. Please, reduce your daily usage or claim the project: https://bencher.dev/auth/signup?claim={uuid}", uuid = project.uuid)]
25-
UnclaimedProject {
26-
project: QueryProject,
27-
resource: BencherResource,
28-
rate_limit: u32,
29-
},
30-
#[error("Claimed project ({uuid}) has exceeded the daily rate limit ({rate_limit}) for {resource} creation. Please, reduce your daily usage.", uuid = project.uuid)]
31-
ClaimedProject {
32-
project: QueryProject,
33-
resource: BencherResource,
34-
rate_limit: u32,
35-
},
36-
#[error("Branch ({uuid}) has exceeded the daily rate limit ({rate_limit}) for {resource} creation. Please, reduce your daily usage.", uuid = branch.uuid)]
37-
Branch {
38-
branch: QueryBranch,
39-
resource: BencherResource,
40-
rate_limit: u32,
41-
},
42-
#[error("Threshold ({uuid}) has exceeded the daily rate limit ({rate_limit}) for {resource} creation. Please, reduce your daily usage.", uuid = threshold.uuid)]
43-
Threshold {
44-
threshold: QueryThreshold,
45-
resource: BencherResource,
46-
rate_limit: u32,
47-
},
48-
}
49-
501
#[macro_export]
512
macro_rules! fn_rate_limit {
523
($table:ident, $resource:ident) => {
@@ -80,7 +31,7 @@ macro_rules! fn_rate_limit {
8031
if creation_count >= context.rate_limit.unclaimed =>
8132
{
8233
Err($crate::error::too_many_requests(
83-
$crate::macros::rate_limit::RateLimitError::UnclaimedProject {
34+
$crate::context::RateLimitError::UnclaimedProject {
8435
project: query_project,
8536
resource: $crate::error::BencherResource::$resource,
8637
rate_limit: context.rate_limit.unclaimed,
@@ -91,7 +42,7 @@ macro_rules! fn_rate_limit {
9142
if creation_count >= context.rate_limit.claimed =>
9243
{
9344
Err($crate::error::too_many_requests(
94-
$crate::macros::rate_limit::RateLimitError::ClaimedProject {
45+
$crate::context::RateLimitError::ClaimedProject {
9546
project: query_project,
9647
resource: $crate::error::BencherResource::$resource,
9748
rate_limit: context.rate_limit.claimed,

lib/bencher_schema/src/model/organization/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ pub struct InsertOrganization {
306306
impl InsertOrganization {
307307
#[cfg(feature = "plus")]
308308
pub async fn rate_limit(context: &ApiContext, query_user: &QueryUser) -> Result<(), HttpError> {
309-
use crate::macros::rate_limit::RateLimitError;
309+
use crate::context::RateLimitError;
310310

311311
let resource = BencherResource::Organization;
312312
let (start_time, end_time) = context.rate_limit.window();

lib/bencher_schema/src/model/project/branch/head.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ impl InsertHead {
206206
context: &ApiContext,
207207
query_branch: &QueryBranch,
208208
) -> Result<(), HttpError> {
209-
use crate::{error::BencherResource, macros::rate_limit::RateLimitError};
209+
use crate::{context::RateLimitError, error::BencherResource};
210210

211211
let resource = BencherResource::Head;
212212
let (start_time, end_time) = context.rate_limit.window();

lib/bencher_schema/src/model/project/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ impl InsertProject {
503503
context: &ApiContext,
504504
query_organization: &QueryOrganization,
505505
) -> Result<(), HttpError> {
506-
use crate::macros::rate_limit::RateLimitError;
506+
use crate::context::RateLimitError;
507507

508508
let resource = BencherResource::Project;
509509
let (start_time, end_time) = context.rate_limit.window();

lib/bencher_schema/src/model/project/threshold/model.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ impl InsertModel {
128128
context: &crate::ApiContext,
129129
query_threshold: &QueryThreshold,
130130
) -> Result<(), HttpError> {
131-
use crate::{conn_lock, error::issue_error, macros::rate_limit::RateLimitError};
131+
use crate::{conn_lock, context::RateLimitError, error::issue_error};
132132

133133
let resource = BencherResource::Model;
134134
let (start_time, end_time) = context.rate_limit.window();

lib/bencher_schema/src/model/user/token.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ impl InsertToken {
9898
context: &crate::ApiContext,
9999
query_user: &QueryUser,
100100
) -> Result<(), HttpError> {
101-
use crate::{conn_lock, macros::rate_limit::RateLimitError};
101+
use crate::{conn_lock, context::RateLimitError};
102102

103103
let resource = BencherResource::Token;
104104
let (start_time, end_time) = context.rate_limit.window();

0 commit comments

Comments
 (0)