Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions migrations/070_enrollment_token_superseded.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TABLE team_enrollment_tokens ADD COLUMN IF NOT EXISTS superseded_at TIMESTAMPTZ NULL;

UPDATE team_enrollment_tokens o SET superseded_at = o.revoked_at
WHERE o.superseded_at IS NULL AND o.revoked_at IS NOT NULL
AND EXISTS (SELECT 1 FROM team_enrollment_tokens n WHERE n.team_id = o.team_id AND n.id <> o.id AND n.created_at = o.revoked_at);
8 changes: 7 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ use axum::{
};
use dashmap::{DashMap, DashSet};
use rate_limit::{
EnrollmentRedeemRateLimiter, EnrollmentRegisterLimiter, EnrollmentResolveLimiter, GrantMintRateLimiter, GrantRedeemRateLimiter, InviteRateLimiter, KnockRateLimiter, RateLimiter,
EnrollmentPolicyLimiter, EnrollmentRedeemRateLimiter, EnrollmentRegisterLimiter, EnrollmentResolveLimiter, GrantMintRateLimiter, GrantRedeemRateLimiter, InviteRateLimiter, KnockRateLimiter, RateLimiter,
RedeemRateLimiter, RegisterRateLimiter, ResendVerificationRateLimiter, SearchRateLimiter, SessionCodeRateLimiter,
SyncRateLimiter, WaitlistRateLimiter,
};
Expand Down Expand Up @@ -65,18 +65,21 @@ fn signup_routes(
register_limiter: RegisterRateLimiter,
resolve_limiter: EnrollmentResolveLimiter,
token_limiter: EnrollmentRegisterLimiter,
policy_limiter: EnrollmentPolicyLimiter,
) -> Router<sqlx::PgPool> {
Router::new()
.route("/v1/auth/register", post(routes::auth::register))
.route(
"/v1/enrollment/resolve",
post(routes::team_enrollment::resolve).layer(middleware::from_fn(features::require_team_invites)),
)
.route("/v1/enrollment/policy", post(routes::team_enrollment::policy))
.layer(Extension(notifier))
.layer(Extension(auth_limiter))
.layer(Extension(register_limiter))
.layer(Extension(resolve_limiter))
.layer(Extension(token_limiter))
.layer(Extension(policy_limiter))
.layer(middleware::from_fn(rate_limit::attach_client_ip))
}

Expand Down Expand Up @@ -275,6 +278,8 @@ async fn main() {
EnrollmentResolveLimiter(RateLimiter::<std::net::IpAddr>::new(60, Duration::from_secs(60)));
let enrollment_register_limiter =
EnrollmentRegisterLimiter(RateLimiter::<uuid::Uuid>::new(500, Duration::from_secs(86400)));
let enrollment_policy_limiter =
EnrollmentPolicyLimiter(RateLimiter::<std::net::IpAddr>::new(300, Duration::from_secs(60)));
let invite_limiter = InviteRateLimiter(RateLimiter::<uuid::Uuid>::new(
invite_per_hour,
Duration::from_secs(3600),
Expand Down Expand Up @@ -341,6 +346,7 @@ async fn main() {
register_limiter,
enrollment_resolve_limiter,
enrollment_register_limiter,
enrollment_policy_limiter,
);

// Public auth + public invitation lookup — rate limited at 10/min per IP
Expand Down
3 changes: 3 additions & 0 deletions src/rate_limit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,9 @@ pub async fn attach_client_ip(mut req: Request, next: Next) -> Response {
#[derive(Clone)]
pub struct EnrollmentResolveLimiter(pub RateLimiter<IpAddr>);

#[derive(Clone)]
pub struct EnrollmentPolicyLimiter(pub RateLimiter<IpAddr>);

/// Sign-ups through one enrolment token, so a fleet rollout behind one NAT is not capped per IP.
#[derive(Clone)]
pub struct EnrollmentRegisterLimiter(pub RateLimiter<Uuid>);
Expand Down
10 changes: 2 additions & 8 deletions src/routes/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use crate::rate_limit::{
check_budget, ClientIp, EnrollmentRegisterLimiter, EnrollmentResolveLimiter, RateLimiter, RegisterRateLimiter,
ResendVerificationRateLimiter,
};
use crate::routes::team_enrollment::load_live_token;
use crate::routes::team_enrollment::{load_live_token, EnrollmentCredential};
use crate::routes::ApiError;
use crate::email::{send_password_changed_email, send_verification_email};
use crate::routes::email_undeliverable_response;
Expand Down Expand Up @@ -266,12 +266,6 @@ pub struct RegisterRequest {
pub enrollment: Option<EnrollmentCredential>,
}

#[derive(Deserialize)]
pub struct EnrollmentCredential {
pub id: Uuid,
pub secret: String,
}

#[derive(Serialize)]
pub struct RegisterResponse {
#[serde(flatten)]
Expand Down Expand Up @@ -384,7 +378,7 @@ pub async fn register(
};
let credential = body.enrollment.as_ref().filter(|_| features.team_invites);
let token = match credential {
Some(credential) => match load_live_token(&pool, credential.id, &credential.secret).await {
Some(credential) => match load_live_token(&pool, credential).await {
Ok(token) => {
check_budget(&token_limiter, token.id, "enrollment_register").await?;
Some(token)
Expand Down
Loading
Loading