Skip to content

Commit bc3231c

Browse files
authored
Merge branch 'main' into dependabot/npm_and_yarn/frontend/graphql-codegen-97975e453b
2 parents 7319ae1 + 9d9ab4e commit bc3231c

File tree

24 files changed

+844
-363
lines changed

24 files changed

+844
-363
lines changed

Cargo.lock

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ version = "1.6.0"
9393

9494
# Packed bitfields
9595
[workspace.dependencies.bitflags]
96-
version = "2.8.0"
96+
version = "2.9.0"
9797

9898
# Bytes
9999
[workspace.dependencies.bytes]

crates/handlers/src/graphql/mutations/user_email.rs

Lines changed: 117 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,75 @@
66

77
use anyhow::Context as _;
88
use async_graphql::{Context, Description, Enum, ID, InputObject, Object};
9+
use mas_data_model::SiteConfig;
910
use mas_i18n::DataLocale;
1011
use mas_storage::{
11-
RepositoryAccess,
12+
BoxRepository, RepositoryAccess,
1213
queue::{ProvisionUserJob, QueueJobRepositoryExt as _, SendEmailAuthenticationCodeJob},
1314
user::{UserEmailFilter, UserEmailRepository, UserRepository},
1415
};
16+
use zeroize::Zeroizing;
1517

16-
use crate::graphql::{
17-
model::{NodeType, User, UserEmail, UserEmailAuthentication},
18-
state::ContextExt,
18+
use crate::{
19+
graphql::{
20+
Requester,
21+
model::{NodeType, User, UserEmail, UserEmailAuthentication},
22+
state::ContextExt,
23+
},
24+
passwords::PasswordManager,
1925
};
2026

27+
/// Check the password if neeed
28+
///
29+
/// Returns true if password verification is not needed, or if the password is
30+
/// correct. Returns false if the password is incorrect or missing.
31+
async fn verify_password_if_needed(
32+
requester: &Requester,
33+
config: &SiteConfig,
34+
password_manager: &PasswordManager,
35+
password: Option<String>,
36+
user: &mas_data_model::User,
37+
repo: &mut BoxRepository,
38+
) -> Result<bool, async_graphql::Error> {
39+
// If the requester is admin, they don't need to provide a password
40+
if requester.is_admin() {
41+
return Ok(true);
42+
}
43+
44+
// If password login is disabled, assume we don't want the user to reauth
45+
if !config.password_login_enabled {
46+
return Ok(true);
47+
}
48+
49+
// Else we need to check if the user has a password
50+
let Some(user_password) = repo
51+
.user_password()
52+
.active(user)
53+
.await
54+
.context("Failed to load user password")?
55+
else {
56+
// User has no password, so we don't need to verify the password
57+
return Ok(true);
58+
};
59+
60+
let Some(password) = password else {
61+
// There is a password on the user, but not provided in the input
62+
return Ok(false);
63+
};
64+
65+
let password = Zeroizing::new(password.into_bytes());
66+
67+
let res = password_manager
68+
.verify(
69+
user_password.version,
70+
password,
71+
user_password.hashed_password,
72+
)
73+
.await;
74+
75+
Ok(res.is_ok())
76+
}
77+
2178
#[derive(Default)]
2279
pub struct UserEmailMutations {
2380
_private: (),
@@ -120,6 +177,10 @@ impl AddEmailPayload {
120177
struct RemoveEmailInput {
121178
/// The ID of the email address to remove
122179
user_email_id: ID,
180+
181+
/// The user's current password. This is required if the user is not an
182+
/// admin and it has a password on its account.
183+
password: Option<String>,
123184
}
124185

125186
/// The status of the `removeEmail` mutation
@@ -130,13 +191,17 @@ enum RemoveEmailStatus {
130191

131192
/// The email address was not found
132193
NotFound,
194+
195+
/// The password provided is incorrect
196+
IncorrectPassword,
133197
}
134198

135199
/// The payload of the `removeEmail` mutation
136200
#[derive(Description)]
137201
enum RemoveEmailPayload {
138202
Removed(mas_data_model::UserEmail),
139203
NotFound,
204+
IncorrectPassword,
140205
}
141206

142207
#[Object(use_type_description)]
@@ -146,27 +211,31 @@ impl RemoveEmailPayload {
146211
match self {
147212
RemoveEmailPayload::Removed(_) => RemoveEmailStatus::Removed,
148213
RemoveEmailPayload::NotFound => RemoveEmailStatus::NotFound,
214+
RemoveEmailPayload::IncorrectPassword => RemoveEmailStatus::IncorrectPassword,
149215
}
150216
}
151217

152218
/// The email address that was removed
153219
async fn email(&self) -> Option<UserEmail> {
154220
match self {
155221
RemoveEmailPayload::Removed(email) => Some(UserEmail(email.clone())),
156-
RemoveEmailPayload::NotFound => None,
222+
RemoveEmailPayload::NotFound | RemoveEmailPayload::IncorrectPassword => None,
157223
}
158224
}
159225

160226
/// The user to whom the email address belonged
161227
async fn user(&self, ctx: &Context<'_>) -> Result<Option<User>, async_graphql::Error> {
162228
let state = ctx.state();
163-
let mut repo = state.repository().await?;
164229

165230
let user_id = match self {
166231
RemoveEmailPayload::Removed(email) => email.user_id,
167-
RemoveEmailPayload::NotFound => return Ok(None),
232+
RemoveEmailPayload::NotFound | RemoveEmailPayload::IncorrectPassword => {
233+
return Ok(None);
234+
}
168235
};
169236

237+
let mut repo = state.repository().await?;
238+
170239
let user = repo
171240
.user()
172241
.lookup(user_id)
@@ -226,6 +295,10 @@ struct StartEmailAuthenticationInput {
226295
/// The email address to add to the account
227296
email: String,
228297

298+
/// The user's current password. This is required if the user has a password
299+
/// on its account.
300+
password: Option<String>,
301+
229302
/// The language to use for the email
230303
#[graphql(default = "en")]
231304
language: String,
@@ -244,6 +317,8 @@ enum StartEmailAuthenticationStatus {
244317
Denied,
245318
/// The email address is already in use on this account
246319
InUse,
320+
/// The password provided is incorrect
321+
IncorrectPassword,
247322
}
248323

249324
/// The payload of the `startEmailAuthentication` mutation
@@ -256,6 +331,7 @@ enum StartEmailAuthenticationPayload {
256331
violations: Vec<mas_policy::Violation>,
257332
},
258333
InUse,
334+
IncorrectPassword,
259335
}
260336

261337
#[Object(use_type_description)]
@@ -268,16 +344,19 @@ impl StartEmailAuthenticationPayload {
268344
Self::RateLimited => StartEmailAuthenticationStatus::RateLimited,
269345
Self::Denied { .. } => StartEmailAuthenticationStatus::Denied,
270346
Self::InUse => StartEmailAuthenticationStatus::InUse,
347+
Self::IncorrectPassword => StartEmailAuthenticationStatus::IncorrectPassword,
271348
}
272349
}
273350

274351
/// The email authentication session that was started
275352
async fn authentication(&self) -> Option<&UserEmailAuthentication> {
276353
match self {
277354
Self::Started(authentication) => Some(authentication),
278-
Self::InvalidEmailAddress | Self::RateLimited | Self::Denied { .. } | Self::InUse => {
279-
None
280-
}
355+
Self::InvalidEmailAddress
356+
| Self::RateLimited
357+
| Self::Denied { .. }
358+
| Self::InUse
359+
| Self::IncorrectPassword => None,
281360
}
282361
}
283362

@@ -494,6 +573,20 @@ impl UserEmailMutations {
494573
.await?
495574
.context("Failed to load user")?;
496575

576+
// Validate the password input if needed
577+
if !verify_password_if_needed(
578+
requester,
579+
state.site_config(),
580+
&state.password_manager(),
581+
input.password,
582+
&user,
583+
&mut repo,
584+
)
585+
.await?
586+
{
587+
return Ok(RemoveEmailPayload::IncorrectPassword);
588+
}
589+
497590
// TODO: don't allow removing the last email address
498591

499592
repo.user_email().remove(user_email.clone()).await?;
@@ -627,6 +720,20 @@ impl UserEmailMutations {
627720
});
628721
}
629722

723+
// Validate the password input if needed
724+
if !verify_password_if_needed(
725+
requester,
726+
state.site_config(),
727+
&state.password_manager(),
728+
input.password,
729+
&browser_session.user,
730+
&mut repo,
731+
)
732+
.await?
733+
{
734+
return Ok(StartEmailAuthenticationPayload::IncorrectPassword);
735+
}
736+
630737
// Create a new authentication session
631738
let authentication = repo
632739
.user_email()

crates/iana-codegen/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ workspace = true
1515
anyhow.workspace = true
1616
async-trait.workspace = true
1717
camino.workspace = true
18-
convert_case = "0.7.1"
18+
convert_case = "0.8.0"
1919
csv = "1.3.1"
2020
reqwest.workspace = true
2121
serde.workspace = true

crates/storage-pg/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ opentelemetry-semantic-conventions.workspace = true
2626
rand.workspace = true
2727
rand_chacha.workspace = true
2828
url.workspace = true
29-
uuid = "1.14.0"
29+
uuid = "1.15.1"
3030
ulid = { workspace = true, features = ["uuid"] }
3131

3232
oauth2-types.workspace = true

crates/syn2mas/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ tracing.workspace = true
2525
futures-util = "0.3.31"
2626

2727
rand.workspace = true
28-
uuid = "1.14.0"
28+
uuid = "1.15.1"
2929
ulid = { workspace = true, features = ["uuid"] }
3030

3131
mas-config.workspace = true

crates/tasks/src/new_queue.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,14 +172,15 @@ const MAX_CONCURRENT_JOBS: usize = 10;
172172
const MAX_JOBS_TO_FETCH: usize = 5;
173173

174174
// How many attempts a job should be retried
175-
const MAX_ATTEMPTS: usize = 5;
175+
const MAX_ATTEMPTS: usize = 10;
176176

177177
/// Returns the delay to wait before retrying a job
178178
///
179-
/// Uses an exponential backoff: 1s, 2s, 4s, 8s, 16s
179+
/// Uses an exponential backoff: 5s, 10s, 20s, 40s, 1m20s, 2m40s, 5m20s, 10m50s,
180+
/// 21m40s, 43m20s
180181
fn retry_delay(attempt: usize) -> Duration {
181182
let attempt = u32::try_from(attempt).unwrap_or(u32::MAX);
182-
Duration::milliseconds(2_i64.saturating_pow(attempt) * 1000)
183+
Duration::milliseconds(2_i64.saturating_pow(attempt) * 5_000)
183184
}
184185

185186
type JobResult = Result<(), JobError>;

deny.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ ignore = [
1414
# RSA key extraction "Marvin Attack". This is only relevant when using
1515
# PKCS#1 v1.5 encryption, which we don't
1616
"RUSTSEC-2023-0071",
17+
18+
# `paste`, as used by `aws-lc-rs` is unmaintained, but we're not concerned
19+
# about it having a security vulnerability
20+
"RUSTSEC-2024-0436",
21+
22+
# rust-protobuf has an infinite recursion issue when parsing inputs. We only
23+
# use protobuf for opentelemetry output, so we are not affected
24+
"RUSTSEC-2024-0437",
1725
]
1826

1927
[licenses]

frontend/locales/en.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"clear": "Clear",
66
"close": "Close",
77
"collapse": "Collapse",
8+
"confirm": "Confirm",
89
"continue": "Continue",
910
"edit": "Edit",
1011
"expand": "Expand",
@@ -27,6 +28,7 @@
2728
"e2ee": "End-to-end encryption",
2829
"loading": "Loading…",
2930
"next": "Next",
31+
"password": "Password",
3032
"previous": "Previous",
3133
"saved": "Saved",
3234
"saving": "Saving…"
@@ -57,7 +59,9 @@
5759
"email_field_help": "Add an alternative email you can use to access this account.",
5860
"email_field_label": "Add email",
5961
"email_in_use_error": "The entered email is already in use",
60-
"email_invalid_error": "The entered email is invalid"
62+
"email_invalid_error": "The entered email is invalid",
63+
"incorrect_password_error": "Incorrect password, please try again",
64+
"password_confirmation": "Confirm your account password to add this email address"
6165
},
6266
"browser_session_details": {
6367
"current_badge": "Current"
@@ -258,7 +262,9 @@
258262
"user_email": {
259263
"delete_button_confirmation_modal": {
260264
"action": "Delete email",
261-
"body": "Delete this email?"
265+
"body": "Delete this email?",
266+
"incorrect_password": "Incorrect password, please try again",
267+
"password_confirmation": "Confirm your account password to delete this email address"
262268
},
263269
"delete_button_title": "Remove email address",
264270
"email": "Email"

0 commit comments

Comments
 (0)