From 985d7cbc563afd0bb34ef0fc5538663bdec691d4 Mon Sep 17 00:00:00 2001 From: Harry Bairstow Date: Tue, 8 Aug 2023 19:38:00 +0100 Subject: [PATCH 1/3] feat: initial suspension --- migrations/1691518766_add-suspension.sql | 5 ++++ migrations/new.sh | 1 - src/error.rs | 18 +++++++++++++ src/handlers/push_message.rs | 27 ++++++++++++++++--- src/providers/fcm.rs | 33 +++++++++++++++++++----- src/stores/tenant.rs | 21 +++++++++++++++ 6 files changed, 94 insertions(+), 11 deletions(-) create mode 100644 migrations/1691518766_add-suspension.sql diff --git a/migrations/1691518766_add-suspension.sql b/migrations/1691518766_add-suspension.sql new file mode 100644 index 00000000..21b4e44d --- /dev/null +++ b/migrations/1691518766_add-suspension.sql @@ -0,0 +1,5 @@ +alter table public.tenants + add suspended bool not null default false; + +alter table public.tenants + add suspended_reason text; \ No newline at end of file diff --git a/migrations/new.sh b/migrations/new.sh index e4d5d2ee..cf447139 100755 --- a/migrations/new.sh +++ b/migrations/new.sh @@ -1,3 +1,2 @@ -#!/bin/bash DESCRIPTION=$1 touch "./$(date +%s)_$DESCRIPTION.sql" \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index 175c65e2..09d5b7ea 100644 --- a/src/error.rs +++ b/src/error.rs @@ -171,6 +171,12 @@ pub enum Error { #[error("invalid apns creds")] BadApnsCredentials, + + #[error("invalid device token")] + ClientDeleted, + + #[error("invalid tenant configuration")] + TenantSuspended, } impl IntoResponse for Error { @@ -525,6 +531,18 @@ impl IntoResponse for Error { location: ErrorLocation::Path, } ]), + Error::ClientDeleted => crate::handlers::Response::new_failure(StatusCode::ACCEPTED, vec![ + ResponseError { + name: "client_deleted".to_string(), + message: "Request Accepted, client deleted due to invalid token".to_string(), + }, + ], vec![]), + Error::TenantSuspended => crate::handlers::Response::new_failure(StatusCode::ACCEPTED, vec![ + ResponseError { + name: "tenant_suspended".to_string(), + message: "Request Accepted, tenant suspended due to invalid configuration".to_string(), + }, + ], vec![]), e => { warn!("Error does not have response clause, {:?}", e); diff --git a/src/handlers/push_message.rs b/src/handlers/push_message.rs index 3da32659..ad79c3a0 100644 --- a/src/handlers/push_message.rs +++ b/src/handlers/push_message.rs @@ -358,10 +358,29 @@ pub async fn handler_internal( "fetched provider" ); - provider - .send_notification(client.token, body.payload) - .await - .map_err(|e| (e, analytics.clone()))?; + match provider.send_notification(client.token, body.payload).await { + Ok(_) => Ok(()), + Err(error) => match error { + Error::BadDeviceToken => { + state.client_store.delete_client(&tenant_id, &id); + Err(Error::ClientDeleted) + } + Error::BadApnsCredentials => { + state + .tenant_store + .suspend_tenant(&tenant_id, "Invalid APNS Credentials"); + Err(Error::TenantSuspended) + } + Error::BadFcmApiKey => { + state + .tenant_store + .suspend_tenant(&tenant_id, "Invalid FCM Credentials"); + Err(Error::TenantSuspended) + } + e => Err(e), + }, + } + .map_err(|e| (e, analytics.clone()))?; info!( %request_id, diff --git a/src/providers/fcm.rs b/src/providers/fcm.rs index 54a92bb4..156361ef 100644 --- a/src/providers/fcm.rs +++ b/src/providers/fcm.rs @@ -1,11 +1,12 @@ use { crate::{ blob::DecryptedPayloadBlob, + error::Error, handlers::push_message::MessagePayload, providers::PushProvider, }, async_trait::async_trait, - fcm::{MessageBuilder, NotificationBuilder}, + fcm::{ErrorReason, FcmError, FcmResponse, MessageBuilder, NotificationBuilder}, std::fmt::{Debug, Formatter}, tracing::span, }; @@ -36,12 +37,12 @@ impl PushProvider for FcmProvider { let mut message_builder = MessageBuilder::new(self.api_key.as_str(), token.as_str()); - if payload.is_encrypted() { + let result = if payload.is_encrypted() { message_builder.data(&payload)?; let fcm_message = message_builder.finalize(); - let _ = self.client.send(fcm_message).await?; + self.client.send(fcm_message).await } else { let blob = DecryptedPayloadBlob::from_base64_encoded(payload.clone().blob)?; @@ -55,10 +56,30 @@ impl PushProvider for FcmProvider { let fcm_message = message_builder.finalize(); - let _ = self.client.send(fcm_message).await?; + self.client.send(fcm_message).await + }; + + match result { + Ok(val) => match val { + FcmResponse { error, .. } => { + if let Some(error) = error { + match error { + ErrorReason::MissingRegistration + | ErrorReason::InvalidRegistration + | ErrorReason::NotRegistered => Err(Error::BadDeviceToken), + ErrorReason::InvalidApnsCredential => Err(Error::BadApnsCredentials), + _ => Ok(()), + } + } else { + Ok(()) + } + } + }, + Err(e) => match e { + FcmError::Unauthorized => Err(Error::BadFcmApiKey), + _ => Ok(()), + }, } - - Ok(()) } } diff --git a/src/stores/tenant.rs b/src/stores/tenant.rs index ba879c9a..6a613a88 100644 --- a/src/stores/tenant.rs +++ b/src/stores/tenant.rs @@ -95,6 +95,10 @@ pub struct Tenant { pub apns_key_id: Option, pub apns_team_id: Option, + // Suspension + pub suspended: bool, + pub suspended_reason: Option, + pub created_at: DateTime, pub updated_at: DateTime, } @@ -261,6 +265,7 @@ pub trait TenantStore { id: &str, params: TenantApnsUpdateAuth, ) -> Result; + async fn suspend_tenant(&self, id: &str, reason: &str) -> Result<()>; } #[async_trait] @@ -373,6 +378,18 @@ impl TenantStore for PgPool { Ok(res) } + + async fn suspend_tenant(&self, id: &str, reason: &str) -> Result<()> { + sqlx::query_as::( + "UPDATE public.tenants SET suspended = true, suspended_reason = $2::text WHERE id = \ + $1 RETURNING *;", + ) + .bind(id) + .bind(reason) + .await?; + + Ok(()) + } } #[cfg(not(feature = "multitenant"))] @@ -435,4 +452,8 @@ impl TenantStore for DefaultTenantStore { ) -> Result { panic!("Shouldn't have run in single tenant mode") } + + async fn suspend_tenant(&self, id: &str, reason: &str) -> Result<()> { + panic!("Shouldn't have run in single tenant mode") + } } From df8d370e586ac4aac1376ca0d7ed1509b81acd6d Mon Sep 17 00:00:00 2001 From: Harry Bairstow Date: Tue, 8 Aug 2023 19:40:34 +0100 Subject: [PATCH 2/3] feat: return data & restrict requests --- src/handlers/get_tenant.rs | 4 ++++ src/handlers/push_message.rs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/handlers/get_tenant.rs b/src/handlers/get_tenant.rs index 9cf58fde..959ac5fc 100644 --- a/src/handlers/get_tenant.rs +++ b/src/handlers/get_tenant.rs @@ -23,6 +23,8 @@ pub struct GetTenantResponse { enabled_providers: Vec, apns_topic: Option, apns_type: Option, + suspended: bool, + suspended_reason: Option, } pub async fn handler( @@ -64,6 +66,8 @@ pub async fn handler( enabled_providers: tenant.providers().iter().map(Into::into).collect(), apns_topic: None, apns_type: None, + suspended: tenant.suspended, + suspended_reason: tenant.suspended_reason, }; if providers.contains(&ProviderKind::Apns) { diff --git a/src/handlers/push_message.rs b/src/handlers/push_message.rs index ad79c3a0..51ce924e 100644 --- a/src/handlers/push_message.rs +++ b/src/handlers/push_message.rs @@ -346,6 +346,10 @@ pub async fn handler_internal( "fetched tenant" ); + if tenant.suspended { + return Err((Error::TenantSuspended, analytics.clone())); + } + let mut provider = tenant .provider(&client.push_type) .map_err(|e| (e, analytics.clone()))?; From b5d17b60836fccd2ff3c7daf6ad3c7adc49afad5 Mon Sep 17 00:00:00 2001 From: Harry Bairstow Date: Tue, 8 Aug 2023 19:46:21 +0100 Subject: [PATCH 3/3] feat: backup domain --- terraform/ecs/main.tf | 13 ++++++++++ terraform/ecs/variables.tf | 12 +++++++++ terraform/main.tf | 51 +++++++++++++++++++++++--------------- 3 files changed, 56 insertions(+), 20 deletions(-) diff --git a/terraform/ecs/main.tf b/terraform/ecs/main.tf index 8c8d7c64..9e184da2 100644 --- a/terraform/ecs/main.tf +++ b/terraform/ecs/main.tf @@ -240,6 +240,19 @@ resource "aws_route53_record" "dns_load_balancer" { } } + +resource "aws_route53_record" "backup_dns_load_balancer" { + zone_id = var.backup_route53_zone_id + name = var.backup_fqdn + type = "A" + + alias { + name = aws_lb.application_load_balancer.dns_name + zone_id = aws_lb.application_load_balancer.zone_id + evaluate_target_health = true + } +} + # Security Groups resource "aws_security_group" "app_ingress" { name = "${var.app_name}-ingress-to-app" diff --git a/terraform/ecs/variables.tf b/terraform/ecs/variables.tf index 89d2bc0a..f4f66e52 100644 --- a/terraform/ecs/variables.tf +++ b/terraform/ecs/variables.tf @@ -52,6 +52,18 @@ variable "acm_certificate_arn" { type = string } +variable "backup_acm_certificate_arn" { + type = string +} + +variable "backup_fqdn" { + type = string +} + +variable "backup_route53_zone_id" { + type = string +} + variable "public_subnets" { type = set(string) } diff --git a/terraform/main.tf b/terraform/main.tf index 352fb9a6..c05acb5f 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -2,7 +2,8 @@ locals { app_name = "push" environment = terraform.workspace - fqdn = local.environment == "prod" ? var.public_url : "${local.environment}.${var.public_url}" + fqdn = local.environment == "prod" ? var.public_url : "${local.environment}.${var.public_url}" + backup_fqdn = replace(local.fqdn, ".com", ".org") latest_release_name = data.github_release.latest_release.name version = coalesce(var.image_version, substr(local.latest_release_name, 1, length(local.latest_release_name))) @@ -66,6 +67,13 @@ module "dns" { fqdn = local.fqdn } +module "backup_dns" { + source = "github.com/WalletConnect/terraform-modules.git?ref=52a74ee5bcaf5cacb5664c6f88d9dbce28500581//modules/dns" + + hosted_zone_name = replace(var.public_url, ".com", ".org") + fqdn = local.backup_fqdn +} + module "database_cluster" { source = "terraform-aws-modules/rds-aurora/aws" version = "7.7.0" @@ -143,25 +151,28 @@ module "analytics" { module "ecs" { source = "./ecs" - app_name = "${local.environment}-${local.app_name}" - environment = local.environment - prometheus_endpoint = aws_prometheus_workspace.prometheus.prometheus_endpoint - database_url = local.database_url - tenant_database_url = local.tenant_database_url - image = "${data.aws_ecr_repository.repository.repository_url}:${local.version}" - image_version = local.version - acm_certificate_arn = module.dns.certificate_arn - cpu = 512 - fqdn = local.fqdn - memory = 1024 - private_subnets = module.vpc.private_subnets - public_subnets = module.vpc.public_subnets - region = var.region - route53_zone_id = module.dns.zone_id - vpc_cidr = module.vpc.vpc_cidr_block - vpc_id = module.vpc.vpc_id - telemetry_sample_ratio = local.environment == "prod" ? 0.25 : 1.0 - allowed_origins = local.environment == "prod" ? "https://cloud.walletconnect.com" : "*" + app_name = "${local.environment}-${local.app_name}" + environment = local.environment + prometheus_endpoint = aws_prometheus_workspace.prometheus.prometheus_endpoint + database_url = local.database_url + tenant_database_url = local.tenant_database_url + image = "${data.aws_ecr_repository.repository.repository_url}:${local.version}" + image_version = local.version + acm_certificate_arn = module.dns.certificate_arn + cpu = 512 + fqdn = local.fqdn + memory = 1024 + private_subnets = module.vpc.private_subnets + public_subnets = module.vpc.public_subnets + region = var.region + route53_zone_id = module.dns.zone_id + backup_acm_certificate_arn = module.backup_dns.certificate_arn + backup_fqdn = local.backup_fqdn + backup_route53_zone_id = module.backup_dns.zone_id + vpc_cidr = module.vpc.vpc_cidr_block + vpc_id = module.vpc.vpc_id + telemetry_sample_ratio = local.environment == "prod" ? 0.25 : 1.0 + allowed_origins = local.environment == "prod" ? "https://cloud.walletconnect.com" : "*" aws_otel_collector_ecr_repository_url = data.aws_ecr_repository.aws_otel_collector.repository_url