Skip to content
Open
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
1 change: 1 addition & 0 deletions changelog.d/5-internal/WPB-22965
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
brig: migrate blacklist (BlockListStore) from Cassandra to PostgreSQL
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ data:
migrateConversationCodes: {{ .migrateConversationCodes }}
migrateTeamFeatures: {{ .migrateTeamFeatures }}
migrateDomainRegistration: {{ .migrateDomainRegistration }}
migrateBlockList: {{ .migrateBlockList }}
migrationOptions:
{{ toYaml .migrationOptions | indent 6 }}

Expand Down
5 changes: 5 additions & 0 deletions charts/wire-server/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ galley:
teamFeatures: cassandra
domainRegistration: cassandra
user: cassandra
blockList: cassandra
settings:
httpPoolSize: 128
maxTeamSize: 10000
Expand Down Expand Up @@ -1017,6 +1018,10 @@ background-worker:
# It's important to set `settings.postgresMigration.domainRegistration` to `migration-to-postgresql`
# before starting the migration.
migrateDomainRegistration: false
# This will start the migration of blacklist data.
# It's important to set `settings.postgresMigration.blockList` to `migration-to-postgresql`
# before starting the migration.
migrateBlockList: false

backendNotificationPusher:
pushBackoffMinWait: 10000 # in microseconds, so 10ms
Expand Down
10 changes: 10 additions & 0 deletions docs/src/developer/reference/config-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -2106,12 +2106,14 @@ galley:
teamFeatures: postgresql
domainRegistration: postgresql
user: postgresql
blockList: postgresql
background-worker:
config:
migrateConversations: false
migrateConversationCodes: false
migrateTeamFeatures: false
migrateDomainRegistration: false
migrateBlockList: false
```

#### Migration for existing installations
Expand Down Expand Up @@ -2142,6 +2144,7 @@ The current settings and their background-worker flags are:
- `conversationCodes` -> `migrateConversationCodes`
- `teamFeatures` -> `migrateTeamFeatures`
- `domainRegistration` -> `migrateDomainRegistration`
- `blockList` -> `migrateBlockList`

**Migration pattern per migration setting**

Expand All @@ -2161,12 +2164,14 @@ The current settings and their background-worker flags are:
conversationCodes: migration-to-postgresql
teamFeatures: migration-to-postgresql
domainRegistration: cassandra
blockList: cassandra
background-worker:
config:
migrateConversations: false
migrateConversationCodes: false
migrateTeamFeatures: false
migrateDomainRegistration: false
migrateBlockList: false
```

This change should restart the affected pods, and new writes will follow the
Expand All @@ -2181,6 +2186,7 @@ The current settings and their background-worker flags are:
migrateConversationCodes: true
migrateTeamFeatures: true
migrateDomainRegistration: true
migrateBlockList: true
```

During migration, Cassandra rows are not deleted. Writes and migration share
Expand All @@ -2196,6 +2202,7 @@ The current settings and their background-worker flags are:
- `conversationCodes`: `wire_conv_codes_migration_finished`
- `teamFeatures`: `wire_team_features_migration_finished`
- `domainRegistration`: `wire_domain_registration_migration_finished`
- `blockList`: `wire_block_list_migration_finished`

3. Cut over reads and writes to PostgreSQL for the selected migration
setting(s). This configuration must be used from now on for every new
Expand All @@ -2209,12 +2216,14 @@ The current settings and their background-worker flags are:
conversationCodes: postgresql
teamFeatures: postgresql
domainRegistration: cassandra
blockList: cassandra
background-worker:
config:
migrateConversations: false
migrateConversationCodes: false
migrateTeamFeatures: false
migrateDomainRegistration: false
migrateBlockList: false
```

**How to run migrations independently or in batches**
Expand Down Expand Up @@ -2303,6 +2312,7 @@ migrateConversations: false
migrateConversationCodes: false
migrateTeamFeatures: false
migrateDomainRegistration: false
migrateBlockList: false

# migration settings
migrationOptions:
Expand Down
1 change: 1 addition & 0 deletions hack/helm_vars/common.yaml.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ conversationCodesStore: {{ $preferredStore }}
teamFeaturesStore: {{ $preferredStore }}
domainRegistration: {{ $preferredStore }}
userStore: {{ $preferredStore }}
blockListStore: {{ $preferredStore }}

{{- if (eq (env "UPLOAD_XML_S3_BASE_URL") "") }}
uploadXml: {}
Expand Down
1 change: 1 addition & 0 deletions hack/helm_vars/wire-server/values.yaml.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ galley:
teamFeatures: {{ .Values.teamFeaturesStore }}
domainRegistration: {{ .Values.domainRegistration }}
user: {{ .Values.userStore }}
blockList: {{ .Values.blockListStore }}
settings:
maxConvAndTeamSize: 16
maxTeamSize: 32
Expand Down
1 change: 1 addition & 0 deletions integration/integration.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ library
Test.Login
Test.Meetings
Test.MessageTimer
Test.Migration.BlockList
Test.Migration.Conversation
Test.Migration.ConversationCodes
Test.Migration.DomainRegistration
Expand Down
15 changes: 15 additions & 0 deletions integration/test/API/BrigInternal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,21 @@ getPasswordResetCode domain email = do
req <- baseRequest domain Brig Unversioned "i/users/password-reset-code"
submit "GET" $ req & addQueryParams [("email", email)]

addBlacklist :: (HasCallStack, MakesValue domain) => domain -> String -> App Response
addBlacklist domain email = do
req <- baseRequest domain Brig Unversioned "i/users/blacklist"
submit "POST" $ req & addQueryParams [("email", email)]

deleteBlacklist :: (HasCallStack, MakesValue domain) => domain -> String -> App Response
deleteBlacklist domain email = do
req <- baseRequest domain Brig Unversioned "i/users/blacklist"
submit "DELETE" $ req & addQueryParams [("email", email)]

checkBlacklist :: (HasCallStack, MakesValue domain) => domain -> String -> App Response
checkBlacklist domain email = do
req <- baseRequest domain Brig Unversioned "i/users/blacklist"
submit "GET" $ req & addQueryParams [("email", email)]

data PutSSOId = PutSSOId
{ scimExternalId :: Maybe String,
subject :: Maybe String,
Expand Down
68 changes: 68 additions & 0 deletions integration/test/Test/Migration/BlockList.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2026 Wire Swiss GmbH <opensource@wire.com>
--
-- This program is free software: you can redistribute it and/or modify it under
-- the terms of the GNU Affero General Public License as published by the Free
-- Software Foundation, either version 3 of the License, or (at your option) any
-- later version.
--
-- This program is distributed in the hope that it will be useful, but WITHOUT
-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-- details.
--
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see <https://www.gnu.org/licenses/>.

module Test.Migration.BlockList (testBlockListMigration) where

import qualified API.BrigInternal as BrigInternal
import API.Common
import Control.Monad.Codensity
import Control.Monad.Reader
import Test.Migration.Util (waitForMigration)
import Testlib.Prelude
import Testlib.ResourcePool

-- | Migrate the 'blacklist' store (brig) from Cassandra to PostgreSQL.
--
-- The blacklist holds email keys with no read-back payload, so the migration is
-- a straight copy: a key blacklisted in Cassandra must survive the cutover and
-- remain deletable once PostgreSQL is the sole source of truth.
testBlockListMigration :: (HasCallStack) => App ()
testBlockListMigration = do
resourcePool <- asks (.resourcePool)
email <- randomEmail
runCodensity (acquireResources 1 resourcePool) $ \[backend] -> do
let domain = backend.berDomain

-- Cassandra: blacklist an email key and confirm it is reported as such.
runCodensity (startDynamicBackend backend (conf "cassandra" False)) $ \_ -> do
assertSuccess =<< BrigInternal.addBlacklist domain email
assertStatus 200 =<< BrigInternal.checkBlacklist domain email

-- migration-to-postgresql with the worker running: backfill the existing key
-- and confirm it is still blacklisted once the migration is finished.
runCodensity (startDynamicBackend backend (conf "migration-to-postgresql" True)) $ \_ -> do
waitForMigration domain counterName
assertStatus 200 =<< BrigInternal.checkBlacklist domain email

-- PostgreSQL only: the migrated key must persist, and deleting it must
-- remove it.
runCodensity (startDynamicBackend backend (conf "postgresql" False)) $ \_ -> do
assertStatus 200 =<< BrigInternal.checkBlacklist domain email
assertSuccess =<< BrigInternal.deleteBlacklist domain email
assertStatus 404 =<< BrigInternal.checkBlacklist domain email
where
conf :: String -> Bool -> ServiceOverrides
conf db runMigration =
def
{ brigCfg = setField "postgresMigration.blockList" db,
backgroundWorkerCfg =
setField "postgresMigration.blockList" db
>=> setField "migrateBlockList" runMigration
}

counterName :: String
counterName = "^wire_block_list_migration_finished"
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CREATE TABLE IF NOT EXISTS blacklist (
key text PRIMARY KEY
);
4 changes: 4 additions & 0 deletions libs/wire-subsystems/src/Wire/BlockListStore/Cassandra.hs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

module Wire.BlockListStore.Cassandra
( interpretBlockListStoreToCassandra,
selectAllBlacklist,
)
where

Expand Down Expand Up @@ -60,3 +61,6 @@ keySelect = "SELECT key FROM blacklist WHERE key = ?"

keyDelete :: PrepQuery W (Identity Text) ()
keyDelete = "DELETE FROM blacklist WHERE key = ?"

selectAllBlacklist :: PrepQuery R () (Identity Text)
selectAllBlacklist = "SELECT key FROM blacklist"
45 changes: 45 additions & 0 deletions libs/wire-subsystems/src/Wire/BlockListStore/DualWrite.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2026 Wire Swiss GmbH <opensource@wire.com>
--
-- This program is free software: you can redistribute it and/or modify it under
-- the terms of the GNU Affero General Public License as published by the Free
-- Software Foundation, either version 3 of the License, or (at your option) any
-- later version.
--
-- This program is distributed in the hope that it will be useful, but WITHOUT
-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-- details.
--
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see <https://www.gnu.org/licenses/>.

module Wire.BlockListStore.DualWrite
( interpretBlockListStoreToCassandraAndPostgres,
)
where

import Cassandra (ClientState)
import Imports
import Polysemy
import Wire.BlockListStore
import Wire.BlockListStore qualified as BlockListStore
import Wire.BlockListStore.Cassandra qualified as Cassandra
import Wire.BlockListStore.Postgres qualified as Postgres
import Wire.Postgres (PGConstraints)

-- | Cassandra is the source of truth during migration; writes are mirrored to Postgres.
interpretBlockListStoreToCassandraAndPostgres ::
(PGConstraints r) =>
ClientState ->
InterpreterFor BlockListStore r
interpretBlockListStoreToCassandraAndPostgres cs = interpret $ \case
Insert key -> do
Cassandra.interpretBlockListStoreToCassandra cs $ BlockListStore.insert key
Postgres.interpretBlockListStoreToPostgres $ BlockListStore.insert key
Exists key ->
Cassandra.interpretBlockListStoreToCassandra cs $ BlockListStore.exists key
Delete key -> do
Cassandra.interpretBlockListStoreToCassandra cs $ BlockListStore.delete key
Postgres.interpretBlockListStoreToPostgres $ BlockListStore.delete key
Loading
Loading