heddle_api/v2/
notifications.rs1use crate::heddle::api::common::{CallFailureCode, ErrorReason};
8use crate::heddle::api::v1alpha2::{
9 NotificationPreferences, NotificationRule, SetNotificationPreferencesRequest,
10 UnsubscribeNotificationsRequest,
11 notification_rule::{Channel, Delivery},
12};
13
14pub const MAX_EFFECTIVE_DELIVERIES: usize = 4096;
15pub const MAX_NOTIFICATION_PREFERENCES_BYTES: usize = 1024 * 1024;
16pub const LOCKED_EMAIL_KINDS: &[&str] = &["account_security", "security_surface"];
17
18#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
19pub enum NotificationValidationError {
20 #[error("unknown channel or unspecified/unknown delivery")]
21 InvalidRule,
22 #[error("DIGEST requires the email channel")]
23 DigestRequiresEmail,
24 #[error("digest interval must be zero, one hour, one day or one week with zero nanos")]
25 InvalidDigestInterval,
26 #[error("security/recovery email must remain IMMEDIATE")]
27 LockedEmail,
28 #[error("unsubscribe requires DISABLED delivery")]
29 UnsubscribeDelivery,
30 #[error("notification preferences exceed the read projection bound")]
31 ProjectionTooLarge,
32}
33
34impl NotificationValidationError {
35 pub const fn code(self) -> CallFailureCode {
36 match self {
37 Self::LockedEmail => CallFailureCode::FailedPrecondition,
38 Self::ProjectionTooLarge => CallFailureCode::ResourceExhausted,
39 _ => CallFailureCode::InvalidArgument,
40 }
41 }
42
43 pub const fn reason(self) -> ErrorReason {
44 match self {
45 Self::LockedEmail => ErrorReason::PolicyDenied,
46 Self::ProjectionTooLarge => ErrorReason::QuotaExceeded,
47 _ => ErrorReason::FieldInvalid,
48 }
49 }
50}
51
52pub fn validate_notification_rule(
56 rule: &NotificationRule,
57) -> Result<(), NotificationValidationError> {
58 let channel =
59 Channel::try_from(rule.channel).map_err(|_| NotificationValidationError::InvalidRule)?;
60 let delivery =
61 Delivery::try_from(rule.delivery).map_err(|_| NotificationValidationError::InvalidRule)?;
62 if delivery == Delivery::Unspecified {
63 return Err(NotificationValidationError::InvalidRule);
64 }
65 let matches_locked = rule.kind.is_empty()
66 || rule.kind == "*"
67 || LOCKED_EMAIL_KINDS.contains(&rule.kind.as_str());
68 if matches_locked
69 && matches!(channel, Channel::Email | Channel::Unspecified)
70 && delivery != Delivery::Immediate
71 {
72 return Err(NotificationValidationError::LockedEmail);
73 }
74 if delivery == Delivery::Digest && channel != Channel::Email {
75 return Err(NotificationValidationError::DigestRequiresEmail);
76 }
77 Ok(())
78}
79
80pub fn validate_notification_preferences_write(
81 request: &SetNotificationPreferencesRequest,
82) -> Result<(), NotificationValidationError> {
83 let preferences = request
84 .preferences
85 .as_ref()
86 .ok_or(NotificationValidationError::InvalidRule)?;
87 if let Some(interval) = &preferences.digest_interval {
88 validate_digest_interval(interval)?;
89 }
90 for digest_override in &preferences.digest_overrides {
91 let interval = digest_override
92 .digest_interval
93 .as_ref()
94 .ok_or(NotificationValidationError::InvalidDigestInterval)?;
95 validate_digest_interval(interval)?;
96 }
97 for rule in &preferences.rules {
98 validate_notification_rule(rule)?;
99 }
100 Ok(())
101}
102
103fn validate_digest_interval(
104 interval: &prost_types::Duration,
105) -> Result<(), NotificationValidationError> {
106 if interval.nanos != 0 || !matches!(interval.seconds, 0 | 3600 | 86400 | 604800) {
107 return Err(NotificationValidationError::InvalidDigestInterval);
108 }
109 Ok(())
110}
111
112pub fn validate_unsubscribe_notifications(
113 request: &UnsubscribeNotificationsRequest,
114) -> Result<(), NotificationValidationError> {
115 for rule in &request.rules {
116 validate_notification_rule(rule)?;
117 if rule.delivery != Delivery::Disabled as i32 {
118 return Err(NotificationValidationError::UnsubscribeDelivery);
119 }
120 }
121 Ok(())
122}
123
124pub fn validate_notification_preferences_bound(
127 preferences: &NotificationPreferences,
128) -> Result<(), NotificationValidationError> {
129 use prost::Message;
130 if preferences.effective_delivery.len() > MAX_EFFECTIVE_DELIVERIES
131 || preferences.encoded_len() > MAX_NOTIFICATION_PREFERENCES_BYTES
132 {
133 return Err(NotificationValidationError::ProjectionTooLarge);
134 }
135 Ok(())
136}