From 3e90c02a2916555c6d27c1c7f9214d2936963e92 Mon Sep 17 00:00:00 2001 From: jschaul Date: Tue, 2 Mar 2021 22:57:49 +0100 Subject: [PATCH 01/18] describe how to look at swagger locally (#1388) --- docs/developer/how-to.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 docs/developer/how-to.md diff --git a/docs/developer/how-to.md b/docs/developer/how-to.md new file mode 100644 index 00000000000..bd0d0a1ec7e --- /dev/null +++ b/docs/developer/how-to.md @@ -0,0 +1,19 @@ +# Developer how-to's + +The following assume you have a working developer environment with all the dependencies listed in [./dependencies.md](./dependencies.md) available to you. + +## How to look at the swagger docs / UI locally + +Terminal 1: +* Set up backing services: `./deploy/dockerephemeral/run.sh` + +Terminal 2: +* Compile all services: `make services` +* Run services including nginz: `export INTEGRATION_USE_NGINZ=1; ./services/start-services-only.sh` + +Open your browser at: + +- http://localhost:8080/api/swagger-ui for the swagger 2.0 endpoints (in development as of Feb 2021 - more endpoints will be added here as time goes on) +- http://localhost:8080/swagger-ui/ for the old swagger 1.2 API (old swagger, endpoints will disappear from here (and become available in the previous link) as time progresses) + +Swagger json (for swagger 2.0 endpoints) is available under http://localhost:8080/api/swagger.json From 13a8053a9cd3ab7f181cf26bd3620ab6a003d7de Mon Sep 17 00:00:00 2001 From: fisx Date: Wed, 3 Mar 2021 14:57:42 +0100 Subject: [PATCH 02/18] Add-license. (#1394) --- libs/polysemy-wire-zoo/src/Polysemy/TinyLog.hs | 1 + .../types-common/src/Data/CommaSeparatedList.hs | 17 +++++++++++++++++ libs/wire-api/src/Wire/API/UserMap.hs | 17 +++++++++++++++++ .../brig/test/integration/API/TeamUserSearch.hs | 17 +++++++++++++++++ tools/db/repair-handles/repair-handles/Main.hs | 17 +++++++++++++++++ tools/db/repair-handles/src/Work.hs | 17 +++++++++++++++++ 6 files changed, 86 insertions(+) diff --git a/libs/polysemy-wire-zoo/src/Polysemy/TinyLog.hs b/libs/polysemy-wire-zoo/src/Polysemy/TinyLog.hs index 3cb3e0ead11..3fb97a482b9 100644 --- a/libs/polysemy-wire-zoo/src/Polysemy/TinyLog.hs +++ b/libs/polysemy-wire-zoo/src/Polysemy/TinyLog.hs @@ -14,6 +14,7 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . + module Polysemy.TinyLog where import Imports diff --git a/libs/types-common/src/Data/CommaSeparatedList.hs b/libs/types-common/src/Data/CommaSeparatedList.hs index bd936c76090..8f944bde33e 100644 --- a/libs/types-common/src/Data/CommaSeparatedList.hs +++ b/libs/types-common/src/Data/CommaSeparatedList.hs @@ -1,5 +1,22 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2020 Wire Swiss GmbH +-- +-- 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 . + module Data.CommaSeparatedList where import Control.Lens ((?~)) diff --git a/libs/wire-api/src/Wire/API/UserMap.hs b/libs/wire-api/src/Wire/API/UserMap.hs index 91ede022277..a3d7ae9b014 100644 --- a/libs/wire-api/src/Wire/API/UserMap.hs +++ b/libs/wire-api/src/Wire/API/UserMap.hs @@ -1,5 +1,22 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2020 Wire Swiss GmbH +-- +-- 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 . + module Wire.API.UserMap where import Control.Lens ((?~), (^.)) diff --git a/services/brig/test/integration/API/TeamUserSearch.hs b/services/brig/test/integration/API/TeamUserSearch.hs index 5e12ba0c076..bd3ddbb9912 100644 --- a/services/brig/test/integration/API/TeamUserSearch.hs +++ b/services/brig/test/integration/API/TeamUserSearch.hs @@ -1,5 +1,22 @@ {-# OPTIONS_GHC -Wno-unused-imports #-} +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2020 Wire Swiss GmbH +-- +-- 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 . + module API.TeamUserSearch (tests) where import API.Search.Util (executeTeamUserSearch, refreshIndex) diff --git a/tools/db/repair-handles/repair-handles/Main.hs b/tools/db/repair-handles/repair-handles/Main.hs index d94393f0709..bcb4a8bd82d 100644 --- a/tools/db/repair-handles/repair-handles/Main.hs +++ b/tools/db/repair-handles/repair-handles/Main.hs @@ -1,3 +1,20 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2020 Wire Swiss GmbH +-- +-- 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 . + import qualified Work import Prelude diff --git a/tools/db/repair-handles/src/Work.hs b/tools/db/repair-handles/src/Work.hs index 869e257841b..d8ab725020b 100644 --- a/tools/db/repair-handles/src/Work.hs +++ b/tools/db/repair-handles/src/Work.hs @@ -16,6 +16,23 @@ -- with this program. If not, see . {-# LANGUAGE RecordWildCards #-} +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2020 Wire Swiss GmbH +-- +-- 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 . + module Work where import Brig.Data.Instances () From d63c6fb4852a0574c4dd92e838446debc7cb8cf3 Mon Sep 17 00:00:00 2001 From: fisx Date: Thu, 4 Mar 2021 09:34:52 +0100 Subject: [PATCH 03/18] Move docs around. (#1399) --- docs/reference/user/registration.md | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/docs/reference/user/registration.md b/docs/reference/user/registration.md index 331932e774e..59c464c05fb 100644 --- a/docs/reference/user/registration.md +++ b/docs/reference/user/registration.md @@ -141,18 +141,7 @@ Set-Cookie: zuid=... ## Blocking creation of personal users, new teams {#RefRestrictRegistration} -There are some unauthenticated end-points that allow arbitrary users on the open internet to do things like create a new team. This is desired in the cloud, and not an issue on many on-prem solutions (eg. all of those that are not exposed to the global IP address space). However, if you run an on-prem setup that is open to the world, you likely want to block this. - -Brig has a server option for this: - -```yaml -optSettings: - setRestrictUserCreation: true -``` - -If `setRestrictUserCreation` is `true`, requests to `/register` that create a new personal account or a new team are answered with `403 forbidden`. - -If you operate an instance with restricted user creation, you can still create new teams (and, if you really want to, personal users): see [`/deploy/services-demo/create_test_team_admins.sh`](https://github.com/wireapp/wire-server/blob/b9a84f9b654a69c9a296761b36c042dc993236d3/deploy/services-demo/create_test_team_admins.sh) to see how. +[moved here](https://docs.wire.com/how-to/install/configuration-options.html#blocking-creation-of-personal-users-new-teams) ### Details From 79b30eef8c66ed9239d55aff1a9bd424b71c70b9 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Thu, 4 Mar 2021 16:41:13 +0100 Subject: [PATCH 04/18] Optimize /users/list-clients to only fetch required things from DB (#1398) --- services/brig/src/Brig/API/Client.hs | 8 ++++---- services/brig/src/Brig/API/Public.hs | 3 +-- services/brig/src/Brig/Data/Client.hs | 18 ++++++++++++++---- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 153d50d9af0..e37ee9b77f3 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -26,7 +26,7 @@ module Brig.API.Client removeLegalHoldClient, lookupClient, lookupClients, - lookupClientsBulk, + lookupPubClientsBulk, Data.lookupPrekeyIds, Data.lookupUsersClientIds, @@ -96,12 +96,12 @@ lookupClients = \case lookupLocalClients :: UserId -> AppIO [Client] lookupLocalClients = Data.lookupClients -lookupClientsBulk :: [Qualified UserId] -> ExceptT ClientError AppIO (QualifiedUserMap (Set Client)) -lookupClientsBulk qualifiedUids = do +lookupPubClientsBulk :: [Qualified UserId] -> ExceptT ClientError AppIO (QualifiedUserMap (Set PubClient)) +lookupPubClientsBulk qualifiedUids = do domain <- viewFederationDomain let (_remoteUsers, localUsers) = partitionRemoteOrLocalIds domain qualifiedUids -- FUTUREWORK: Implement federation - QualifiedUserMap . Map.singleton domain <$> Data.lookupClientsBulk localUsers + QualifiedUserMap . Map.singleton domain <$> Data.lookupPubClientsBulk localUsers -- nb. We must ensure that the set of clients known to brig is always -- a superset of the clients known to galley. diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 4bc85bd7fb2..bfa26092ace 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -71,7 +71,6 @@ import qualified Data.Map.Strict as Map import Data.Misc (IpAddr (..)) import Data.Qualified (Qualified (..), partitionRemoteOrLocalIds) import Data.Range -import qualified Data.Set as Set import Data.Swagger ( ApiKeyLocation (..), ApiKeyParams (..), @@ -1151,7 +1150,7 @@ getClientH (zusr ::: clt ::: _) = listClientsBulk :: UserId -> Range 1 MaxUsersForListClientsBulk [Qualified UserId] -> Handler (Public.QualifiedUserMap (Set Public.PubClient)) listClientsBulk _zusr limitedUids = do - Set.map API.pubClient <$$> API.lookupClientsBulk (fromRange limitedUids) !>> clientError + API.lookupPubClientsBulk (fromRange limitedUids) !>> clientError getClient :: UserId -> ClientId -> Handler (Maybe Public.Client) getClient zusr clientId = do diff --git a/services/brig/src/Brig/Data/Client.hs b/services/brig/src/Brig/Data/Client.hs index f0ab686aea5..404ecaa5054 100644 --- a/services/brig/src/Brig/Data/Client.hs +++ b/services/brig/src/Brig/Data/Client.hs @@ -25,7 +25,7 @@ module Brig.Data.Client hasClient, lookupClient, lookupClients, - lookupClientsBulk, + lookupPubClientsBulk, lookupClientIds, lookupUsersClientIds, Brig.Data.Client.updateClientLabel, @@ -126,12 +126,16 @@ lookupClient u c = fmap toClient <$> retry x1 (query1 selectClient (params Quorum (u, c))) -lookupClientsBulk :: (MonadClient m) => [UserId] -> m (UserMap (Imports.Set Client)) -lookupClientsBulk uids = liftClient $ do +lookupPubClientsBulk :: (MonadClient m) => [UserId] -> m (UserMap (Imports.Set PubClient)) +lookupPubClientsBulk uids = liftClient $ do userClientTuples <- pooledMapConcurrentlyN 50 getClientSetWithUser uids pure $ UserMap $ Map.fromList userClientTuples where - getClientSetWithUser u = (u,) . Set.fromList <$> lookupClients u + getClientSetWithUser :: MonadClient m => UserId -> m (UserId, Imports.Set PubClient) + getClientSetWithUser u = (u,) . Set.fromList . map toPubClient <$> executeQuery u + + executeQuery :: MonadClient m => UserId -> m [(ClientId, Maybe ClientClass)] + executeQuery u = retry x1 (query selectPubClients (params Quorum (Identity u))) lookupClients :: MonadClient m => UserId -> m [Client] lookupClients u = @@ -212,6 +216,9 @@ selectClientIds = "SELECT client from clients where user = ?" selectClients :: PrepQuery R (Identity UserId) (ClientId, ClientType, UTCTimeMillis, Maybe Text, Maybe ClientClass, Maybe CookieLabel, Maybe Latitude, Maybe Longitude, Maybe Text) selectClients = "SELECT client, type, tstamp, label, class, cookie, lat, lon, model from clients where user = ?" +selectPubClients :: PrepQuery R (Identity UserId) (ClientId, Maybe ClientClass) +selectPubClients = "SELECT client, class from clients where user = ?" + selectClient :: PrepQuery R (UserId, ClientId) (ClientId, ClientType, UTCTimeMillis, Maybe Text, Maybe ClientClass, Maybe CookieLabel, Maybe Latitude, Maybe Longitude, Maybe Text) selectClient = "SELECT client, type, tstamp, label, class, cookie, lat, lon, model from clients where user = ? and client = ?" @@ -252,6 +259,9 @@ toClient (cid, cty, tme, lbl, cls, cok, lat, lon, mdl) = clientModel = mdl } +toPubClient :: (ClientId, Maybe ClientClass) -> PubClient +toPubClient = uncurry PubClient + ------------------------------------------------------------------------------- -- Best-effort optimistic locking for prekeys via DynamoDB From ed707bea4357f6b2eb7832b5296c086b2148c541 Mon Sep 17 00:00:00 2001 From: fisx Date: Tue, 9 Mar 2021 09:28:16 +0100 Subject: [PATCH 05/18] Fix: permissions for idp crud operations. (#1405) (before, team ownership was required in some cases, but that was a bug.) --- libs/galley-types/src/Galley/Types/Teams.hs | 2 ++ services/spar/src/Spar/API.hs | 6 +++--- services/spar/src/Spar/Error.hs | 2 -- services/spar/src/Spar/Intra/Brig.hs | 21 +++++++++++---------- services/spar/src/Spar/Intra/Galley.hs | 11 ----------- 5 files changed, 16 insertions(+), 26 deletions(-) diff --git a/libs/galley-types/src/Galley/Types/Teams.hs b/libs/galley-types/src/Galley/Types/Teams.hs index 991f6f5e403..bdf1b8f42b1 100644 --- a/libs/galley-types/src/Galley/Types/Teams.hs +++ b/libs/galley-types/src/Galley/Types/Teams.hs @@ -321,6 +321,7 @@ data HiddenPerm | ChangeTeamSearchVisibility | ViewTeamSearchVisibility | ViewSameTeamEmails + | ReadIdp | CreateUpdateDeleteIdp | CreateReadDeleteScimToken | -- | this has its own permission because we're not sure how @@ -351,6 +352,7 @@ roleHiddenPermissions role = HiddenPermissions p p ChangeLegalHoldUserSettings, ChangeTeamSearchVisibility, ChangeTeamFeature TeamFeatureAppLock {- the other features can only be changed in stern -}, + ReadIdp, CreateUpdateDeleteIdp, CreateReadDeleteScimToken, DownloadTeamMembersCsv diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index 8a32d3ccc17..dd5fc00d427 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -51,7 +51,7 @@ import Data.Id import Data.Proxy import Data.String.Conversions import Data.Time -import Galley.Types.Teams (HiddenPerm (CreateUpdateDeleteIdp)) +import Galley.Types.Teams (HiddenPerm (CreateUpdateDeleteIdp, ReadIdp)) import Imports import OpenSSL.Random (randBytes) import qualified SAML2.WebSSO as SAML @@ -213,7 +213,7 @@ idpGetRaw zusr idpid = do idpGetAll :: Maybe UserId -> Spar IdPList idpGetAll zusr = withDebugLog "idpGetAll" (const Nothing) $ do - teamid <- Brig.getZUsrOwnedTeam zusr + teamid <- Brig.getZUsrCheckPerm zusr ReadIdp _idplProviders <- wrapMonadClientWithEnv $ Data.getIdPConfigsByTeam teamid pure IdPList {..} @@ -281,7 +281,7 @@ idpCreate zusr (IdPMetadataValue raw xml) midpid = idpCreateXML zusr raw xml mid -- | We generate a new UUID for each IdP used as IdPConfig's path, thereby ensuring uniqueness. idpCreateXML :: Maybe UserId -> Text -> SAML.IdPMetadata -> Maybe SAML.IdPId -> Spar IdP idpCreateXML zusr raw idpmeta mReplaces = withDebugLog "idpCreate" (Just . show . (^. SAML.idpId)) $ do - teamid <- Brig.getZUsrOwnedTeam zusr + teamid <- Brig.getZUsrCheckPerm zusr CreateUpdateDeleteIdp Galley.assertSSOEnabled teamid assertNoScimOrNoIdP teamid idp <- validateNewIdP idpmeta teamid mReplaces diff --git a/services/spar/src/Spar/Error.hs b/services/spar/src/Spar/Error.hs index 67f58872e70..eca6424fc53 100644 --- a/services/spar/src/Spar/Error.hs +++ b/services/spar/src/Spar/Error.hs @@ -71,7 +71,6 @@ data SparCustomError = SparIdPNotFound | SparMissingZUsr | SparNotInTeam - | SparNotTeamOwner | SparNoPermission LT | SparSSODisabled | SparInitLoginWithAuth @@ -161,7 +160,6 @@ renderSparError (SAML.BadSamlResponseInvalidSignature msg) = Right $ Wai.Error s renderSparError (SAML.CustomError SparIdPNotFound) = Right $ Wai.Error status404 "not-found" "Could not find IdP." renderSparError (SAML.CustomError SparMissingZUsr) = Right $ Wai.Error status400 "client-error" "[header] 'Z-User' required" renderSparError (SAML.CustomError SparNotInTeam) = Right $ Wai.Error status403 "no-team-member" "Requesting user is not a team member or not a member of this team." -renderSparError (SAML.CustomError SparNotTeamOwner) = Right $ Wai.Error status403 "insufficient-permissions" "You need to be a team owner." renderSparError (SAML.CustomError (SparNoPermission perm)) = Right $ Wai.Error status403 "insufficient-permissions" ("You need permission " <> cs perm <> ".") renderSparError (SAML.CustomError SparSSODisabled) = Right $ Wai.Error status403 "sso-disabled" "Please ask customer support to enable this feature for your team." renderSparError (SAML.CustomError SparInitLoginWithAuth) = Right $ Wai.Error status403 "login-with-auth" "This end-point is only for login, not binding." diff --git a/services/spar/src/Spar/Intra/Brig.hs b/services/spar/src/Spar/Intra/Brig.hs index bfa165310bf..abdd9667895 100644 --- a/services/spar/src/Spar/Intra/Brig.hs +++ b/services/spar/src/Spar/Intra/Brig.hs @@ -47,7 +47,7 @@ module Spar.Intra.Brig createBrigUserSAML, createBrigUserNoSAML, updateEmail, - getZUsrOwnedTeam, + getZUsrCheckPerm, authorizeScimTokenManagement, ensureReAuthorised, ssoLogin, @@ -71,13 +71,13 @@ import Data.Handle (Handle (Handle, fromHandle)) import Data.Id (Id (Id), TeamId, UserId) import Data.Misc (PlainTextPassword) import Data.String.Conversions -import Galley.Types.Teams (HiddenPerm (CreateReadDeleteScimToken)) +import Galley.Types.Teams (HiddenPerm (CreateReadDeleteScimToken), IsPerm) import Imports import Network.HTTP.Types.Method import qualified Network.Wai.Utilities.Error as Wai import qualified SAML2.WebSSO as SAML import Spar.Error -import Spar.Intra.Galley as Galley (MonadSparToGalley, assertHasPermission, assertIsTeamOwner) +import Spar.Intra.Galley as Galley (MonadSparToGalley, assertHasPermission) import Spar.Scim.Types (ValidExternalId (..), runValidExternalId) import qualified System.Logger.Class as Log import qualified Text.Email.Parser @@ -423,18 +423,19 @@ deleteBrigUser buid = do getBrigUserTeam :: (HasCallStack, MonadSparToBrig m) => HavePendingInvitations -> UserId -> m (Maybe TeamId) getBrigUserTeam ifpend = fmap (userTeam =<<) . getBrigUser ifpend --- | Get the team that the user is an owner of. This is used for authorization. It will fail --- if the user is not in status 'Active'. -getZUsrOwnedTeam :: - (HasCallStack, SAML.SP m, MonadSparToBrig m, MonadSparToGalley m) => +-- | Pull team id for z-user from brig. Check permission in galley. Return team id. Fail if +-- permission check fails or the user is not in status 'Active'. +getZUsrCheckPerm :: + (HasCallStack, SAML.SP m, MonadSparToBrig m, MonadSparToGalley m, IsPerm perm, Show perm) => Maybe UserId -> + perm -> m TeamId -getZUsrOwnedTeam Nothing = throwSpar SparMissingZUsr -getZUsrOwnedTeam (Just uid) = do +getZUsrCheckPerm Nothing _ = throwSpar SparMissingZUsr +getZUsrCheckPerm (Just uid) perm = do getBrigUserTeam NoPendingInvitations uid >>= maybe (throwSpar SparNotInTeam) - (\teamid -> teamid <$ Galley.assertIsTeamOwner teamid uid) + (\teamid -> teamid <$ Galley.assertHasPermission teamid perm uid) authorizeScimTokenManagement :: (HasCallStack, SAML.SP m, MonadSparToBrig m, MonadSparToGalley m) => Maybe UserId -> m TeamId authorizeScimTokenManagement Nothing = throwSpar SparMissingZUsr diff --git a/services/spar/src/Spar/Intra/Galley.hs b/services/spar/src/Spar/Intra/Galley.hs index 86a7c1d4f0e..d8a0bc6291c 100644 --- a/services/spar/src/Spar/Intra/Galley.hs +++ b/services/spar/src/Spar/Intra/Galley.hs @@ -28,7 +28,6 @@ import Data.Id (TeamId, UserId) import Data.String.Conversions (cs) import Galley.Types.Teams import Imports -import Network.HTTP.Types (status403) import Network.HTTP.Types.Method import Spar.Error import qualified System.Logger.Class as Log @@ -53,16 +52,6 @@ getTeamMembers tid = do then (^. teamMembers) <$> parseResponse @TeamMemberList "galley" resp else rethrow "galley" resp --- | If user is not owner, throw 'SparNotTeamOwner'. -assertIsTeamOwner :: (HasCallStack, MonadError SparError m, MonadSparToGalley m) => TeamId -> UserId -> m () -assertIsTeamOwner tid uid = do - r <- - call $ - method GET - . (paths ["i", "teams", toByteString' tid, "is-team-owner", toByteString' uid]) - when (responseStatus r == status403) $ do - throwSpar SparNotTeamOwner - -- | user is member of a given team and has a given permission there. assertHasPermission :: (HasCallStack, MonadSparToGalley m, MonadError SparError m, IsPerm perm, Show perm) => From 151afec7b1f5a7630a094cf000875fbf9035866d Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 9 Mar 2021 15:18:43 +0100 Subject: [PATCH 06/18] stack-deps.nix: Use nixpkgs from niv (#1406) --- stack-deps.nix | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/stack-deps.nix b/stack-deps.nix index df02e439944..721a20da338 100644 --- a/stack-deps.nix +++ b/stack-deps.nix @@ -1,7 +1,6 @@ -with (import {}); let pkgs = import ./nix; - native_libs = lib.optionals stdenv.isDarwin (with darwin.apple_sdk.frameworks; [ + native_libs = pkgs.lib.optionals pkgs.stdenv.isDarwin (with pkgs.darwin.apple_sdk.frameworks; [ Cocoa CoreServices ]); From f6c6bd1e31efc98fda715f21de7654aa2abda063 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Wed, 10 Mar 2021 10:32:17 +0100 Subject: [PATCH 07/18] Return UserProfile when getting user by qualified handle (#1397) * Rename /users/handles// to /users/by-handle// and return `UserProfile` instead of `UserHandleInfo`. This makes searching for a user by exact handle more efficient for clients and reduces chatter between federated backends. Renaming the API path makes it more consistent with what it is doing and also removes any confusion between /users/handles/ and /users/handles//. --- .../src/Wire/API/Federation/API/Brig.hs | 7 +-- services/brig/src/Brig/API/Federation.hs | 13 ++--- services/brig/src/Brig/API/Public.hs | 22 ++++----- services/brig/src/Brig/API/User.hs | 30 +++++++----- services/brig/src/Brig/Federation/Client.hs | 2 +- .../brig/test/integration/API/Federation.hs | 15 ++++-- .../brig/test/integration/API/User/Handles.hs | 49 ++++++++++++++++++- .../brig/test/integration/Federation/User.hs | 13 +++-- services/brig/test/integration/Util.hs | 4 +- 9 files changed, 111 insertions(+), 44 deletions(-) diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Brig.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Brig.hs index c81e8389750..82cbdcfc598 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API/Brig.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Brig.hs @@ -24,7 +24,7 @@ import qualified Network.HTTP.Types as HTTP import Servant.API import Servant.API.Generic import qualified Wire.API.Federation.GRPC.Types as Proto -import Wire.API.User.Handle (UserHandleInfo) +import Wire.API.User (UserProfile) -- Maybe this module should be called Brig newtype Api routes = Api @@ -34,10 +34,7 @@ newtype Api routes = Api :> "users" :> "by-handle" :> QueryParam' '[Required, Strict] "handle" Handle - -- FUTUREWORK(federation): Make this return UserProfile, at that point there would - -- be interesting questions like whether to expose email or not and how - -- we code that part. I want to avoid solving this until federator works - :> Get '[JSON] UserHandleInfo + :> Get '[JSON] UserProfile } deriving (Generic) diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index 58a535266b7..aad3354937b 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -19,23 +19,24 @@ module Brig.API.Federation where import Brig.API.Error (handleNotFound, throwStd) import Brig.API.Handler (Handler) -import Brig.App (viewFederationDomain) -import Brig.Types (UserHandleInfo (UserHandleInfo)) -import qualified Brig.User.Handle as API +import qualified Brig.API.User as API import Data.Handle (Handle) -import Data.Qualified (Qualified (Qualified)) import Imports import Servant (ServerT) import Servant.API.Generic (ToServantApi) import Servant.Server.Generic (genericServerT) import qualified Wire.API.Federation.API.Brig as FederationAPIBrig +import Wire.API.User (UserProfile) federationSitemap :: ServerT (ToServantApi FederationAPIBrig.Api) Handler federationSitemap = genericServerT (FederationAPIBrig.Api getUserByHandle) -getUserByHandle :: Handle -> Handler UserHandleInfo +getUserByHandle :: Handle -> Handler UserProfile getUserByHandle handle = do maybeOwnerId <- lift $ API.lookupHandle handle case maybeOwnerId of Nothing -> throwStd handleNotFound - Just ownerId -> UserHandleInfo . Qualified ownerId <$> viewFederationDomain + Just ownerId -> do + lift (API.lookupProfilesOfLocalUsers Nothing [ownerId]) >>= \case + [] -> throwStd handleNotFound + user : _ -> pure user diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index bfa26092ace..72bd3955a98 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -270,14 +270,14 @@ type GetHandleInfoUnqualified = -- Doc.returns (Doc.ref Public.modelUserHandleInfo) -- Doc.response 200 "Handle info" Doc.end -- Doc.errorResponse handleNotFound -type GetHandleInfoQualified = +type GetUserByHandleQualfied = Summary "Get information on a user handle" :> ZAuthServant :> "users" - :> "handles" + :> "by-handle" :> Capture "domain" Domain :> Capture' '[Description "The user handle"] "handle" Handle - :> Get '[Servant.JSON] Public.UserHandleInfo + :> Get '[Servant.JSON] Public.UserProfile -- See Note [ephemeral user sideeffect] type ListUsersByUnqualifiedIdsOrHandles = @@ -367,7 +367,7 @@ type OutsideWorldAPI = :<|> GetUserQualified :<|> GetSelf :<|> GetHandleInfoUnqualified - :<|> GetHandleInfoQualified + :<|> GetUserByHandleQualfied :<|> ListUsersByUnqualifiedIdsOrHandles :<|> ListUsersByIdsOrHandles :<|> ListClientsBulk @@ -401,7 +401,7 @@ servantSitemap = :<|> getUserH :<|> getSelf :<|> getHandleInfoUnqualifiedH - :<|> getHandleInfoH + :<|> getUserByHandleH :<|> listUsersByUnqualifiedIdsOrHandles :<|> listUsersByIdsOrHandles :<|> listClientsBulk @@ -1420,15 +1420,15 @@ checkHandlesH (_ ::: _ ::: req) = do getHandleInfoUnqualifiedH :: UserId -> Handle -> Handler Public.UserHandleInfo getHandleInfoUnqualifiedH self handle = do domain <- viewFederationDomain - getHandleInfoH self domain handle + Public.UserHandleInfo . Public.profileQualifiedId <$> getUserByHandleH self domain handle -getHandleInfoH :: UserId -> Domain -> Handle -> Handler Public.UserHandleInfo -getHandleInfoH self domain handle = +getUserByHandleH :: UserId -> Domain -> Handle -> Handler Public.UserProfile +getUserByHandleH self domain handle = ifNothing (notFound "handle not found") =<< getHandleInfo self (Qualified handle domain) -- FUTUREWORK: use 'runMaybeT' to simplify this. -getHandleInfo :: UserId -> Qualified Handle -> Handler (Maybe Public.UserHandleInfo) +getHandleInfo :: UserId -> Qualified Handle -> Handler (Maybe Public.UserProfile) getHandleInfo self handle = do domain <- viewFederationDomain if qDomain handle == domain @@ -1443,9 +1443,9 @@ getHandleInfo self handle = do Just ownerId -> do ownerProfile <- lift $ API.lookupProfile self (Qualified ownerId domain) owner <- filterHandleResults self (maybeToList ownerProfile) - return $ Public.UserHandleInfo . Public.profileQualifiedId <$> listToMaybe owner + return $ listToMaybe owner getRemoteHandleInfo = do - Log.info $ (Log.msg $ Log.val "getHandleInfo - remote lookup") Log.~~ Log.field "domain" (show (qDomain handle)) + Log.info $ Log.msg (Log.val "getHandleInfo - remote lookup") Log.~~ Log.field "domain" (show (qDomain handle)) Federation.getUserHandleInfo handle changeHandleH :: UserId ::: ConnId ::: JsonRequest Public.HandleUpdate -> Handler Response diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 41c4fb6060d..e8d0b457516 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -41,6 +41,7 @@ module Brig.API.User lookupAccountsByIdentity, lookupProfile, lookupProfiles, + lookupProfilesOfLocalUsers, Data.lookupName, Data.lookupLocale, Data.lookupUser, @@ -1082,46 +1083,53 @@ lookupProfiles :: lookupProfiles self others = do domain <- viewFederationDomain let (_remoteUsers, localUsers) = partitionRemoteOrLocalIds domain others - localProfiles <- lookupProfilesOfLocalUsers self localUsers + localProfiles <- lookupProfilesOfLocalUsers (Just self) localUsers -- FUTUREWORK(federation, #1267): fetch remote profiles remoteProfiles <- pure [] pure (localProfiles <> remoteProfiles) +-- FUTUREWORK: This function encodes a few business rules about exposing email +-- ids, but it is also very complex. Maybe this can be made easy by extracting a +-- pure function and writing tests for that. lookupProfilesOfLocalUsers :: - -- | User 'self' on whose behalf the profiles are requested. - UserId -> + -- | This is present only when an authenticated user is requesting access. + Maybe UserId -> -- | The users ('others') for which to obtain the profiles. [UserId] -> AppIO [UserProfile] -lookupProfilesOfLocalUsers self others = do +lookupProfilesOfLocalUsers requestingUser others = do users <- Data.lookupUsers NoPendingInvitations others >>= mapM userGC - css <- toMap <$> Data.lookupConnectionStatus (map userId users) [self] + css <- case requestingUser of + Just localReqUser -> toMap <$> Data.lookupConnectionStatus (map userId users) [localReqUser] + Nothing -> mempty emailVisibility' <- view (settings . emailVisibility) emailVisibility'' <- case emailVisibility' of EmailVisibleIfOnTeam -> pure EmailVisibleIfOnTeam' - EmailVisibleIfOnSameTeam -> EmailVisibleIfOnSameTeam' <$> getSelfInfo + EmailVisibleIfOnSameTeam -> case requestingUser of + Just localReqUser -> EmailVisibleIfOnSameTeam' <$> getSelfInfo localReqUser + Nothing -> pure EmailVisibleToSelf' EmailVisibleToSelf -> pure EmailVisibleToSelf' return $ map (toProfile emailVisibility'' css) users where toMap :: [ConnectionStatus] -> Map UserId Relation toMap = Map.fromList . map (csFrom &&& csStatus) - getSelfInfo :: AppIO (Maybe (TeamId, Team.TeamMember)) - getSelfInfo = do + getSelfInfo :: UserId -> AppIO (Maybe (TeamId, Team.TeamMember)) + getSelfInfo selfId = do -- FUTUREWORK: it is an internal error for the two lookups (for 'User' and 'TeamMember') -- to return 'Nothing'. we could throw errors here if that happens, rather than just -- returning an empty profile list from 'lookupProfiles'. - mUser <- Data.lookupUser NoPendingInvitations self + mUser <- Data.lookupUser NoPendingInvitations selfId case userTeam =<< mUser of Nothing -> pure Nothing - Just tid -> (tid,) <$$> Intra.getTeamMember self tid + Just tid -> (tid,) <$$> Intra.getTeamMember selfId tid toProfile :: EmailVisibility' -> Map UserId Relation -> User -> UserProfile toProfile emailVisibility'' css u = let cs = Map.lookup (userId u) css profileEmail' = getEmailForProfile u emailVisibility'' baseProfile = - if userId u == self || cs == Just Accepted || cs == Just Sent + if Just (userId u) == requestingUser || cs == Just Accepted || cs == Just Sent then connectedProfile u else publicProfile u in baseProfile {profileEmail = profileEmail'} diff --git a/services/brig/src/Brig/Federation/Client.hs b/services/brig/src/Brig/Federation/Client.hs index f7a8745867f..1a89bdfa0bc 100644 --- a/services/brig/src/Brig/Federation/Client.hs +++ b/services/brig/src/Brig/Federation/Client.hs @@ -42,7 +42,7 @@ import qualified Wire.API.Federation.GRPC.Types as Proto -- This is not correct, we should figure out how we communicate failure -- scenarios to the clients. -- See https://wearezeta.atlassian.net/browse/SQCORE-491 for the issue on error handling improvements. -getUserHandleInfo :: Qualified Handle -> Handler (Maybe UserHandleInfo) +getUserHandleInfo :: Qualified Handle -> Handler (Maybe UserProfile) getUserHandleInfo (Qualified handle domain) = do Log.info $ Log.msg $ T.pack "Brig-federation: handle lookup call on remote backend" federatorClient <- mkFederatorClient diff --git a/services/brig/test/integration/API/Federation.hs b/services/brig/test/integration/API/Federation.hs index dddb768d716..e512da454b9 100644 --- a/services/brig/test/integration/API/Federation.hs +++ b/services/brig/test/integration/API/Federation.hs @@ -23,6 +23,7 @@ import Brig.Types import Data.ByteString.Conversion (toByteString') import Imports import Test.Tasty +import Test.Tasty.HUnit (assertEqual) import Util tests :: Manager -> Brig -> IO TestTree @@ -41,9 +42,17 @@ testGetUserByHandleSuccess brig = do hdl <- randomHandle putHandle brig uid hdl !!! const 200 === statusCode - get (brig . paths ["federation", "users", "by-handle"] . queryItem "handle" (toByteString' hdl)) !!! do - const 200 === statusCode - const (Just (UserHandleInfo quid)) === (responseJsonMaybe) + profile <- + responseJsonError + =<< get + ( brig + . paths ["federation", "users", "by-handle"] + . queryItem "handle" (toByteString' hdl) + . expect2xx + ) + liftIO $ do + assertEqual "should return correct user Id" quid (profileQualifiedId profile) + assertEqual "should not have email address" Nothing (profileEmail profile) testGetUserByHandleNotFound :: Brig -> Http () testGetUserByHandleNotFound brig = do diff --git a/services/brig/test/integration/API/User/Handles.hs b/services/brig/test/integration/API/User/Handles.hs index 1db3aadd785..83fa4b5fa90 100644 --- a/services/brig/test/integration/API/User/Handles.hs +++ b/services/brig/test/integration/API/User/Handles.hs @@ -35,6 +35,7 @@ import Data.ByteString.Conversion import Data.Handle (Handle (Handle)) import Data.Id hiding (client) import qualified Data.List1 as List1 +import Data.Qualified (Qualified (qDomain)) import qualified Data.UUID as UUID import qualified Galley.Types.Teams.SearchVisibility as Team import Gundeck.Types.Notification hiding (target) @@ -56,7 +57,9 @@ tests _cl _at conf p b c g = test p "handles/race" $ testHandleRace b, test p "handles/query" $ testHandleQuery conf b, test p "handles/query - team-search-visibility SearchVisibilityStandard" $ testHandleQuerySearchVisibilityStandard conf b, - test p "handles/query - team-search-visibility SearchVisibilityNoNameOutsideTeam" $ testHandleQuerySearchVisibilityNoNameOutsideTeam conf b g + test p "handles/query - team-search-visibility SearchVisibilityNoNameOutsideTeam" $ testHandleQuerySearchVisibilityNoNameOutsideTeam conf b g, + test p "GET /users/handles/" $ testGetUserByUnqualifiedHandle b, + test p "GET /users/by-handle//" $ testGetUserByQualifiedHandle b ] testHandleUpdate :: Brig -> Cannon -> Http () @@ -212,6 +215,50 @@ testHandleQuerySearchVisibilityNoNameOutsideTeam _opts brig galley = do assertCanFind brig member2 owner1 assertCanFind brig extern owner1 +testGetUserByUnqualifiedHandle :: Brig -> Http () +testGetUserByUnqualifiedHandle brig = do + user <- randomUser brig + handle <- randomHandle + _ <- putHandle brig (userId user) handle + requestingUser <- randomId + get + ( brig + . paths ["users", "handles", toByteString' handle] + . zUser requestingUser + ) + !!! do + const 200 === statusCode + const (Right (UserHandleInfo (userQualifiedId user))) === responseJsonEither + +testGetUserByQualifiedHandle :: Brig -> Http () +testGetUserByQualifiedHandle brig = do + user <- randomUser brig + handle <- randomHandle + let domain = qDomain (userQualifiedId user) + _ <- putHandle brig (userId user) handle + unconnectedUser <- randomUser brig + profileForUnconnectedUser <- + responseJsonError + =<< get + ( brig + . paths ["users", "by-handle", toByteString' domain, toByteString' handle] + . zUser (userId unconnectedUser) + . expect2xx + ) + liftIO $ + assertEqual + "Id should match" + (userQualifiedId user) + (profileQualifiedId profileForUnconnectedUser) + + -- N.B. Internally this endpoint uses same implementation as getting a user + -- profile by id. So, it is not necessary to test rest of the cases. + liftIO $ + assertEqual + "Email shouldn't be shown to unconnected user" + Nothing + (profileEmail profileForUnconnectedUser) + assertCanFind :: (Monad m, MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> User -> User -> m () assertCanFind brig from target = do liftIO $ assertBool "assertCanFind: Target must have a handle set" (isJust $ userHandle target) diff --git a/services/brig/test/integration/Federation/User.hs b/services/brig/test/integration/Federation/User.hs index 6ade297af75..3330276c87d 100644 --- a/services/brig/test/integration/Federation/User.hs +++ b/services/brig/test/integration/Federation/User.hs @@ -61,16 +61,21 @@ testHandleLookup brig brigTwo = do u <- randomUser brigTwo h <- randomHandle void $ putHandle brigTwo (userId u) h + + -- Verify if creating user and setting handle succeeded self <- selfUser <$> getSelfProfile brigTwo (userId u) let handle = fromJust (userHandle self) liftIO $ assertEqual "creating user with handle should return handle" h (fromHandle handle) + + -- Get result from brig two for comparison let domain = qDomain $ userQualifiedId self - resultTwo <- userHandleId <$> getUserInfoFromHandle brigTwo domain handle + resultViaBrigTwo <- getUserInfoFromHandle brigTwo domain handle + -- query the local-namespace brig for a user sitting on the other backend -- which should involve the following network traffic: -- -- brig-integration -> brig -> federator -> fed2-federator -> fed2-brig -- (and back) - result <- userHandleId <$> getUserInfoFromHandle brig domain handle - liftIO $ assertEqual "remote handle lookup via federator should work in the happy case" result (Qualified (userId u) domain) - liftIO $ assertEqual "querying brig1 or brig2 about remote user should give same result" resultTwo result + resultViaBrigOne <- getUserInfoFromHandle brig domain handle + liftIO $ assertEqual "remote handle lookup via federator should work in the happy case" (profileQualifiedId resultViaBrigOne) (userQualifiedId u) + liftIO $ assertEqual "querying brig1 or brig2 about the same user should give same result" resultViaBrigTwo resultViaBrigOne diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs index d1d8cbdafc0..32bfad08fbf 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -439,13 +439,13 @@ getUserInfoFromHandle :: Brig -> Domain -> Handle -> - m UserHandleInfo + m UserProfile getUserInfoFromHandle brig domain handle = do u <- randomId responseJsonError =<< get ( brig - . paths ["users", "handles", toByteString' (domainText domain), toByteString' handle] + . paths ["users", "by-handle", toByteString' (domainText domain), toByteString' handle] . zUser u . expect2xx ) From 6b5ab02b301dba5a9575e4ef16e5b905f504537c Mon Sep 17 00:00:00 2001 From: fisx Date: Thu, 11 Mar 2021 12:08:19 +0100 Subject: [PATCH 08/18] Tweak docs about team search visibility configuration. (#1407) * rm trailing whitespace. * Fix broken link. * Explain backoffice/stern UI widget. --- docs/reference/config-options.md | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/docs/reference/config-options.md b/docs/reference/config-options.md index be7153baba0..6dcf4555367 100644 --- a/docs/reference/config-options.md +++ b/docs/reference/config-options.md @@ -23,7 +23,7 @@ table must be backfilled. Even when the flag is `disabled`, galley will keep writing to the `biling_team_member` table, this flag only affects the reads and has -been added in order to deploy new code and backfill data in +been added in order to deploy new code and backfill data in production. ## Feature flags @@ -74,13 +74,29 @@ future. ### Team Search Visibility Is a team allowed to change its team search visibility settings? If enabled -for the team, it can be configured so that non team users do not show up in search. +for the team, it can be configured so that non-team users do not show up in search. This sets the default setting for all teams, and can be overridden for individual teams by customer support / backoffice. [Allowed -values](https://github.com/wireapp/wire-server/blob/custom-search-visibility-limit-name-search/libs/galley-types/src/Galley/Types/Teams.hs#L382-L385): +values](https://github.com/wireapp/wire-server/blob/151afec7b1f5a7630a094cf000875fbf9035866d/libs/galley-types/src/Galley/Types/Teams.hs#L229-L235): `disabled-by-default`, `enabled-by-default`. +Disabled by default in the wire cloud. + +[Backoffice hook](https://github.com/wireapp/wire-server/blob/151afec7b1f5a7630a094cf000875fbf9035866d/tools/stern/src/Stern/API.hs#L615-L618) looks like this: + +``` +GET /teams/{tid}/search-visibility + -- Shows the current TeamSearchVisibility value for the given team + +PUT /teams/{tid}/search-visibility + -- Set specific search visibility for the team + +pull-down-menu "body": + "standard" + "no-name-outside-team" +``` + ### Email Visibility [Allowd values](https://github.com/wireapp/wire-server/blob/0126651a25aabc0c5589edc2b1988bb06550a03a/services/brig/src/Brig/Options.hs#L304-L306) and their [description](https://github.com/wireapp/wire-server/blob/0126651a25aabc0c5589edc2b1988bb06550a03a/services/brig/src/Brig/Options.hs#L290-L299). From 9f14855bb2c4d4ccd0c30732cfb7fa13cfec8d3d Mon Sep 17 00:00:00 2001 From: fisx Date: Thu, 11 Mar 2021 14:15:37 +0100 Subject: [PATCH 09/18] Spar errors (#1392) * Cleanup code layout. * Keep error response status intact during scim error handling. --- services/spar/src/Spar/Scim.hs | 36 +++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/services/spar/src/Spar/Scim.hs b/services/spar/src/Spar/Scim.hs index 2feb83e0e70..f536c6ae93a 100644 --- a/services/spar/src/Spar/Scim.hs +++ b/services/spar/src/Spar/Scim.hs @@ -87,6 +87,7 @@ import qualified Web.Scim.Class.Auth as Scim.Auth import qualified Web.Scim.Class.User as Scim.User import qualified Web.Scim.Handler as Scim import qualified Web.Scim.Schema.Error as Scim +import qualified Web.Scim.Schema.Schema as Scim.Schema import qualified Web.Scim.Server as Scim -- | SCIM config for our server. @@ -116,25 +117,28 @@ apiScim = wrapScimErrors = over _Spar $ \act -> \env -> do result :: Either SomeException (Either SparError a) <- try (act env) case result of - -- We caught an exception that's not a Spar exception at all. It is wrapped into - -- Scim.serverError. - Left someException -> - pure $ - Left . SAML.CustomError . SparScimError $ - Scim.serverError (cs (displayException someException)) - -- We caught a 'SparScimError' exception. It is left as-is. + Left someException -> do + -- We caught an exception that's not a Spar exception at all. It is wrapped into + -- Scim.serverError. + pure . Left . SAML.CustomError . SparScimError $ + Scim.serverError (cs (displayException someException)) Right err@(Left (SAML.CustomError (SparScimError _))) -> + -- We caught a 'SparScimError' exception. It is left as-is. pure err - -- We caught some other Spar exception. It is wrapped into Scim.serverError. - -- - -- TODO: does it have to be logged? Right (Left sparError) -> do - err <- sparToServerErrorWithLogging (sparCtxLogger env) sparError - pure $ - Left . SAML.CustomError . SparScimError $ - Scim.serverError (cs (errBody err)) - -- No exceptions! Good. - Right (Right x) -> pure $ Right x + -- We caught some other Spar exception. It is rendered and wrapped into a scim error + -- with the same status and message, and no scim error type. + err :: ServerError <- sparToServerErrorWithLogging (sparCtxLogger env) sparError + pure . Left . SAML.CustomError . SparScimError $ + Scim.ScimError + { schemas = [Scim.Schema.Error20], + status = Scim.Status $ errHTTPCode err, + scimType = Nothing, + detail = Just . cs $ errBody err + } + Right (Right x) -> do + -- No exceptions! Good. + pure $ Right x -- | This is similar to 'Scim.siteServer, but does not include the 'Scim.groupServer', -- as we don't support it (we don't implement 'Web.Scim.Class.Group.GroupDB'). From 03797e92daaff4938e39282fbbd9641a54c011aa Mon Sep 17 00:00:00 2001 From: jschaul Date: Thu, 11 Mar 2021 18:29:05 +0100 Subject: [PATCH 10/18] Forward grpc traffic to federator via ingress (or nginz for local integration tests) (#1386) * integration tests now also deploy an ingress with a self-signed TLS certificate * changes to allow federation traffic to reach the federator: * locally: nginz and its config to forward grpc traffic to federator (generated certificates locally, open an extra port on nginz, and use nginz as the "ingress" for local tests) * on real environments: nginx-ingress-services changes to terminate TLS & forward grpc traffic from `federator.` to federator * for integration tests: there is a `federation-test-helper` service which adds an SRV dns record in kubernetes for integration test discovery which redirects traffic to the ingress. * drive-by fixes to nginz config * example grpcurl usage bash script under ./hack/federation * example grpc python client under ./hack/federation * side-effect: you can no longer disable TLS at ingress level. --- Makefile | 6 +- charts/federator/templates/service.yaml | 4 +- .../templates/federation-test-helper.yaml | 19 + .../templates/ingress.yaml | 4 +- .../templates/ingress_federator.yaml | 27 ++ .../templates/secret.yaml | 6 +- charts/nginx-ingress-services/values.yaml | 6 +- .../conf/nginz/integration-ca-key.pem | 27 ++ .../conf/nginz/integration-ca.pem | 19 + .../conf/nginz/integration-leaf-key.pem | 27 ++ .../conf/nginz/integration-leaf.pem | 20 + deploy/services-demo/conf/nginz/nginx.conf | 36 ++ deploy/services-demo/conf/nginz/upstreams | 3 + hack/bin/find-latest-docker-tag.sh | 13 +- hack/bin/integration-setup.sh | 21 +- hack/bin/selfsigned.sh | 41 ++ hack/federation/grpcurlhandle.sh | 37 ++ hack/federation/python-client/.gitignore | 3 + hack/federation/python-client/README.md | 34 ++ hack/federation/python-client/client.py | 26 ++ hack/federation/python-client/poetry.lock | 403 ++++++++++++++++++ hack/federation/python-client/pyproject.toml | 16 + .../nginx-ingress-controller/values.yaml | 10 + .../nginx-ingress-services/values.yaml | 75 ++++ services/federator/src/Federator/Remote.hs | 15 +- services/nginz/Dockerfile | 1 + services/nginz/Makefile | 1 + shell.nix | 3 + 28 files changed, 879 insertions(+), 24 deletions(-) create mode 100644 charts/nginx-ingress-services/templates/federation-test-helper.yaml create mode 100644 charts/nginx-ingress-services/templates/ingress_federator.yaml create mode 100644 deploy/services-demo/conf/nginz/integration-ca-key.pem create mode 100644 deploy/services-demo/conf/nginz/integration-ca.pem create mode 100644 deploy/services-demo/conf/nginz/integration-leaf-key.pem create mode 100644 deploy/services-demo/conf/nginz/integration-leaf.pem create mode 100755 hack/bin/selfsigned.sh create mode 100755 hack/federation/grpcurlhandle.sh create mode 100644 hack/federation/python-client/.gitignore create mode 100644 hack/federation/python-client/README.md create mode 100644 hack/federation/python-client/client.py create mode 100644 hack/federation/python-client/poetry.lock create mode 100644 hack/federation/python-client/pyproject.toml create mode 100644 hack/helm_vars/nginx-ingress-controller/values.yaml create mode 100644 hack/helm_vars/nginx-ingress-services/values.yaml diff --git a/Makefile b/Makefile index 1e540b93c70..f78a00257b3 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ DOCKER_TAG ?= $(USER) # default helm chart version must be 0.0.42 for local development (because 42 is the answer to the universe and everything) HELM_SEMVER ?= 0.0.42 # The list of helm charts needed for integration tests on kubernetes -CHARTS_INTEGRATION := wire-server databases-ephemeral fake-aws +CHARTS_INTEGRATION := wire-server databases-ephemeral fake-aws nginx-ingress-controller nginx-ingress-services # The list of helm charts to publish on S3 # FUTUREWORK: after we "inline local subcharts", # (e.g. move charts/brig to charts/wire-server/brig) @@ -273,8 +273,8 @@ kube-restart-%: kubectl delete pod -n $(NAMESPACE) -l wireService=$(*) kubectl delete pod -n $(NAMESPACE)-fed2 -l wireService=$(*) -.PHONY: latest-brig-tag -latest-brig-tag: +.PHONY: latest-tag +latest-tag: ./hack/bin/find-latest-docker-tag.sh .PHONY: release-chart-% diff --git a/charts/federator/templates/service.yaml b/charts/federator/templates/service.yaml index 749c6ee67da..65da250677e 100644 --- a/charts/federator/templates/service.yaml +++ b/charts/federator/templates/service.yaml @@ -10,11 +10,11 @@ metadata: spec: type: ClusterIP ports: - - name: http + - name: internal port: {{ .Values.service.internalFederatorPort }} targetPort: {{ .Values.service.internalFederatorPort }} - - name: wire-server-federator + - name: federator-ext port: {{ .Values.service.externalFederatorPort }} targetPort: {{ .Values.service.externalFederatorPort }} selector: diff --git a/charts/nginx-ingress-services/templates/federation-test-helper.yaml b/charts/nginx-ingress-services/templates/federation-test-helper.yaml new file mode 100644 index 00000000000..4c065770523 --- /dev/null +++ b/charts/nginx-ingress-services/templates/federation-test-helper.yaml @@ -0,0 +1,19 @@ +# Assumes that the controller is deployed in the same namespace. Only used for +# enabling discovery by creating SRV records while running integration tests. +{{- if (and .Values.federator.enabled .Values.federator.integrationTestHelper) }} +apiVersion: v1 +kind: Service +metadata: + name: federation-test-helper + namespace: {{ .Release.namespace }} +spec: + ports: + - name: wire-server-federator + port: 443 + protocol: TCP + targetPort: https + selector: + app: nginx-ingress + component: controller + type: ClusterIP +{{- end }} diff --git a/charts/nginx-ingress-services/templates/ingress.yaml b/charts/nginx-ingress-services/templates/ingress.yaml index 8a07536c4be..34dbcdb35c9 100644 --- a/charts/nginx-ingress-services/templates/ingress.yaml +++ b/charts/nginx-ingress-services/templates/ingress.yaml @@ -5,9 +5,8 @@ metadata: annotations: kubernetes.io/ingress.class: "nginx" spec: - # This assumes you have created the given cert + # This assumes you have created the given cert (see secret.yaml) # https://github.com/kubernetes/ingress-nginx/blob/master/docs/examples/PREREQUISITES.md#tls-certificates -{{- if .Values.tls.enabled }} tls: - hosts: - {{ .Values.config.dns.https }} @@ -21,7 +20,6 @@ spec: - {{ .Values.config.dns.accountPages }} {{- end }} secretName: {{ include "nginx-ingress-services.getCertificateSecretName" . | quote }} -{{- end }} rules: - host: {{ .Values.config.dns.https }} http: diff --git a/charts/nginx-ingress-services/templates/ingress_federator.yaml b/charts/nginx-ingress-services/templates/ingress_federator.yaml new file mode 100644 index 00000000000..ea375b0ec44 --- /dev/null +++ b/charts/nginx-ingress-services/templates/ingress_federator.yaml @@ -0,0 +1,27 @@ +{{- if .Values.federator.enabled }} +# We use a separate ingress for federator/grpc since we can't forward +# both normal http1 traffic and grpc traffic in the same kubernetes ingress it appears. +# Setting backend-protocol annotation to "GRPC" for everything is likely incorrect. +# see also example https://github.com/kubernetes/ingress-nginx/blob/master/docs/examples/grpc/ingress.yaml +# and docs https://kubernetes.github.io/ingress-nginx/examples/grpc/ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: federator-ingress + annotations: + kubernetes.io/ingress.class: "nginx" + nginx.ingress.kubernetes.io/ssl-redirect: "true" + nginx.ingress.kubernetes.io/backend-protocol: "GRPC" +spec: + tls: + - hosts: + - {{ .Values.config.dns.federator }} + secretName: {{ include "nginx-ingress-services.getCertificateSecretName" . | quote }} + rules: + - host: {{ .Values.config.dns.federator }} + http: + paths: + - backend: + serviceName: federator + servicePort: federator-ext # name must be below 15 chars +{{- end }} diff --git a/charts/nginx-ingress-services/templates/secret.yaml b/charts/nginx-ingress-services/templates/secret.yaml index d360b78c3b4..e0472b0fb4e 100644 --- a/charts/nginx-ingress-services/templates/secret.yaml +++ b/charts/nginx-ingress-services/templates/secret.yaml @@ -7,13 +7,13 @@ metadata: release: "{{ .Release.Name }}" heritage: "{{ .Release.Service }}" type: kubernetes.io/tls -{{ if and .Values.tls.enabled .Values.tls.useCertManager -}} -{{- /* NOTE: providing `data` (and empty strings) allows to manage this secret resource with Helm if cert-manager is user */ -}} +{{ if .Values.tls.useCertManager -}} +{{- /* NOTE: providing `data` (and empty strings) allows to manage this secret resource with Helm if cert-manager is used */ -}} data: tls.crt: "" tls.key: "" {{- end -}} -{{- if and .Values.tls.enabled (not .Values.tls.useCertManager) -}} +{{- if (not .Values.tls.useCertManager) -}} data: {{- /* for_helm_linting is necessary only since the 'with' block below does not throw an error upon an empty .Values.secrets */}} for_helm_linting: {{ required "No .secrets found in configuration. Did you forget to helm -f path/to/secrets.yaml ?" .Values.secrets | quote | b64enc | quote }} diff --git a/charts/nginx-ingress-services/values.yaml b/charts/nginx-ingress-services/values.yaml index 72db6ea37c9..fc87dbebccc 100644 --- a/charts/nginx-ingress-services/values.yaml +++ b/charts/nginx-ingress-services/values.yaml @@ -6,7 +6,9 @@ teamSettings: # Account pages may be useful to enable password reset or email validation done after the initial registration accountPages: enabled: false - +federator: + enabled: false + integrationTestHelper: false # If you want to use TLS termination on the ingress, # then set this variable to true and ensure that there # is a valid wildcard TLS certificate @@ -59,6 +61,8 @@ service: # ssl: nginz-ssl. # webapp: webapp. # fakeS3: assets. +# federator: federator. +# ^ federator is ignored unless federator.enabled == true # teamSettings: teams. # ^ teamSettings is ignored unless teamSettings.enabled == true # accountPages: account. diff --git a/deploy/services-demo/conf/nginz/integration-ca-key.pem b/deploy/services-demo/conf/nginz/integration-ca-key.pem new file mode 100644 index 00000000000..961e87aa67d --- /dev/null +++ b/deploy/services-demo/conf/nginz/integration-ca-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEApwf/2d2YraQDpCipPVtYR+7BNu47AgkD7kFvGhoxJhDP7CsU +VdpqU5gsVVo8kvhkh4k1tsJyuWWeKn6piNSXxUCFIc80KkUPgsYf5v+RBXr73Fdg +ezHQNhNi0dRZCh+YG/hN7pOX46+B0PyKwUEMTeUqizkmFU5tILPMMyDAGx1Bp2LB +oJi4u+48fzTDMaWSXnCVF04G9+A4LDzw0fPdDMgKLEiXJ8GPoPs0cNs6MJoFDgpe +gzy1mv7X7otmRVTaafZGd4TTo6lGC2VVSS5tpj4Qfz/PxyCLK7tf5033HNWEJzAw +6izRXp849VferHuYEbP+2lexNk9tl45BsFhkrwIDAQABAoIBAQCFkzYeSsJginuG ++iVttfEBhYPqo9V4qTEFhjqNS0jmwiclHMZkagkB1P4PO9yZRB9Q7H+SKiqI7STx +ot19WVYOHqzY/tUewJ/I2xyEJPkawuFLsmyr2IhD1nj+iKy0FdQU+huIoWukX6SX +Nn7YUWa/nHbLY+Z6v38x2deBQ72dcBtDcOh1vtUR3fVfsiX5uzCcfvNZAw4cCyB2 +j8ySDIiP10Ic81da3FIeCm8g2yp3DrnvTa77xsr0IfSykB3UcSrGqDwZxs9pS82Q +1fog//4xAfBYC9LEcnQrCvz2kqLSLICtjkgK+dlzgvY3rZMq9c/OY1nR7Wp2BIyp +kKB5AEnRAoGBANTM3fq4YGzUodf+Xla4MDvQFJsYjQuig/CJboQ7JSFZi2uLnSHX ++7JDiHtQd3uifYMhzSxXXKV82CK7SsJOQlIVoCZ5eTsyYGyAu1fUqfBvfHYN4Gbr +3QyZJE0Hut2rvn5DaT/dpgh7Uy9QWKhpAsmxzhKa/iADUTiNAO8pxxRFAoGBAMjw +iZV43XWLvzP90P5jANHuk9tR/B5cM9zK40aWglNsMlK9cUgW3ovohMzTFce/LQWy +zGZ1WZZcUUcR/pHot3fyjWKeJadZhSZ/7hN/0d/UDuFY5nQ8eGQoy2qrrtY+6MMU +Eiz09EFnKKA7hUoDnbhOH1hCKsfrOVse55RDkTZjAoGABrzRzm1mCCwXT7prDD3a +sRoefOajGJo1qTkAuckRnOOz6VzLRdYLzxIaUSU0E0MKzEsWru+5LDgus7LQZCSM +LwMmRfGUqA4pRWYyCE7gbo9pFmfMEhYnso1qu9Gh1gDpECBcRbxj1GLrOFVH6VUh +1Hb/ulET+LmCKdM1E110Qy0CgYEAimbDHSUGxHPg2pq0XMMsSWyegq3RjcfMIQPN +z0zTr0oSz1KUuCaoWo1pCvtJQS+4fvhMOTYS4rHreZw3T6CO3hs+rvJm1QGf6Iit +HtknYZfaN/TXprAP7Ez87xgZcJAcGmG0syp1Iqc/ID5e7D/ZXpzQkiXg+ZpXAyAi +OcjgOCkCgYEAmsCsqtPn5vgB+/vr0n28UsFS4Of9whlgEPYndNss3nAmVEohQJRg +QlBlJd2iDa7R0TrJZCuAwuqK7TxB/RoHL8UkryUt2nag39GYAyE+lfPM558/AWyt +9yyLQNfiJnqTC2Ne2j7EyicBLha4J9NoBeNE5UqLlzrH4LRJ3fRX9Ps= +-----END RSA PRIVATE KEY----- diff --git a/deploy/services-demo/conf/nginz/integration-ca.pem b/deploy/services-demo/conf/nginz/integration-ca.pem new file mode 100644 index 00000000000..1fecfda3e77 --- /dev/null +++ b/deploy/services-demo/conf/nginz/integration-ca.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDAjCCAeqgAwIBAgIUJ+1IpVKexlyGhgZaOZi2Ka01Q2QwDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjEwMzA5MjA0MDAwWhcN +MjYwMzA4MjA0MDAwWjAZMRcwFQYDVQQDEw5jYS5leGFtcGxlLmNvbTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAKcH/9ndmK2kA6QoqT1bWEfuwTbuOwIJ +A+5BbxoaMSYQz+wrFFXaalOYLFVaPJL4ZIeJNbbCcrllnip+qYjUl8VAhSHPNCpF +D4LGH+b/kQV6+9xXYHsx0DYTYtHUWQofmBv4Te6Tl+OvgdD8isFBDE3lKos5JhVO +bSCzzDMgwBsdQadiwaCYuLvuPH80wzGlkl5wlRdOBvfgOCw88NHz3QzICixIlyfB +j6D7NHDbOjCaBQ4KXoM8tZr+1+6LZkVU2mn2RneE06OpRgtlVUkubaY+EH8/z8cg +iyu7X+dN9xzVhCcwMOos0V6fOPVX3qx7mBGz/tpXsTZPbZeOQbBYZK8CAwEAAaNC +MEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFLJ +Dc8PpKsUGFi33sK+r5Xgi97XMA0GCSqGSIb3DQEBCwUAA4IBAQBtiav0b3r4k3CC +N1DRNNpcvpHR8odPJkxR2r0kCLRqwu2YDxQYxCK5wCPNcpzGDd6nyg/nLWBzl4Vd +UwBIUPSVavDAeQ8VL0YNCNhQlL4x6z5rT1aSRdp0VlRnnl4zjilWSN/IB5Y61i0Q +Ww7Sd5T9hZUCOJm5bAeLo4+cxkOwN6jdT2wmLtgFkj4CFVvS/8nL8ZwC1qvt3mLs +E8q7/KelKgddU5AET8Viim20m6ZxgGNWIX33LBiJ3Rg3QuJMnWyfBI6PCORRrdTb +0AH/F3iUKQL8Zv6twr++S7VPeDllVgEXkq457ZBi2qJPikL5cUprV/0bWQxgf4+1 +L70TaNMy +-----END CERTIFICATE----- diff --git a/deploy/services-demo/conf/nginz/integration-leaf-key.pem b/deploy/services-demo/conf/nginz/integration-leaf-key.pem new file mode 100644 index 00000000000..0444962bd82 --- /dev/null +++ b/deploy/services-demo/conf/nginz/integration-leaf-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA7AcV1TSTJRGMOM+qB2F4wWbsV+M2C/4KWt0LKC6k3EeqYo+R +n9utlcH42S3UgzjB9zVMXYItwtGSCNVbxQ4s/nCNsyZR2JB689gnR1hetnKCFJTY +2ETBsqFgoyNHU1ZkoTIq0ZQVeOEkcn2qnM981xuPvptY1ChdFtD52ybJRhAcgrHX ++P3aWns0N7Hn7EgFWinGDCZ27QXA/iiuSteVr/3Fh39RF7+YYix4Y7TRqXQqRhK/ +sp7fZQjLeXB/oECh5tEm/WEFJoX4mDNf96GAVzHynjNysCSZd44iZxbMlgNynYv0 +1Q/LLddI1KBz4MqqtBvHgh61ddtcM6vi15iK8QIDAQABAoIBAGMj9r+/fa8sV8Hl +OH4xKw/Rs6SXA7S12JOiFXWjYgxE7gTIWVrUY21sNKLE5WbZCGBWOuVsMNyMOtML +C6kR80RM8rg1eFuHFJ4oRRdqvx3Tq2AJxavR9aqaroTX5LXUrCApemLd2McVisGs +l2WWodGY+iAOEJnM+o0C6nBrMlAC1fHQGDaIeGCdrRWS1RO47z+wMxnd9n0Lmek1 +YHg+Cylg16ZbZvcl8KfJ3CzD8QGpu4hF1BMzJwzYg2qmLUPrlhB1bJomW8asbZtu +MyKsr5zl95f7lOtn3DwsxNOYHlQFqYs8u4rogOqb09yIRoYm1n4v6Z1DKdQ1cm/U +yIPcp4ECgYEA+P7ZPF412TLlHo337ryF10SvuWJJCYZLrVaeiTecgbqbbqtUcR9c +NnTzd8LTBMAtl6lcyTzrTi5+RGrGiicKunZiUnWl8g67E7EMUsM/tWrZah3yOPDa +yTNtXtpGkBqa4+sujky3WqCDicBpATeeE9xdIP3KS+OjKUR1gKs1dgUCgYEA8qrZ +LyWGeI07C0ql3I6iFlhwbSMfQ+8mx8yYj+5rCEiY3Ns/D7WbxvqXOgBVD+QK26a9 +4f20z6HTXBnPAmYoMlG/uh+MJj2VlgsgElRy4fi33ky+F67NWcniaO5gU0iO5P0m +W6yKBuEg34NAuBZ85Q/HzZoCXGijaugA0H+DyP0CgYEAtSuRWthdq/TZOxsnTCEF +7XT/uw6lf/WkOLJu1f6NtOLXV3X4Efs0eDcuM3ShX7KJOfG0HoRh3df8bcGXRkxU +BQpATilmHjLTsec/xTRltZXSLeEuLnopZv4xVTPS5eVF0BJ+JHHzc1CZhPBoNseG +EINli6y9tewi+tLiAEYStxUCgYBj46Hs/1RgETqpxjuKE4hzDEqTEi8PhsT+36A5 +NxoF2eRanUFTFR2fhY10iah6FxPFINjuysF/6owXUGRfB3Aqbm4ujkfhgd4uWjwE +b5CupfQB5LMnCoRrHmg9a0ppXee8KNx+RgK3HDqs5tlgLZmRrLJBVtD7+vKx0fhr +2uGDfQKBgQCVu/KGMBrS1M/05nrg6MS8vrDO0Wwyk3X9/mmGh651omQeW39IvFq+ +5rI0w46f6mslksFYqCJhfxXIKHN0sB3cBw/290j6FY3+TxpD2sJLIOeTllr+ivvA +4BBs0XcecrPF7Ykw+E9hOHTo+dBRoTz1yUPHJYsTN5vI8los9XIyBg== +-----END RSA PRIVATE KEY----- diff --git a/deploy/services-demo/conf/nginz/integration-leaf.pem b/deploy/services-demo/conf/nginz/integration-leaf.pem new file mode 100644 index 00000000000..b2d0f850d22 --- /dev/null +++ b/deploy/services-demo/conf/nginz/integration-leaf.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDXDCCAkSgAwIBAgIUcLcIBd4tb4neS1GuVAk3wGKOclQwDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjEwMzA5MjA0MDAwWhcN +MjIwMzA5MjA0MDAwWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +7AcV1TSTJRGMOM+qB2F4wWbsV+M2C/4KWt0LKC6k3EeqYo+Rn9utlcH42S3UgzjB +9zVMXYItwtGSCNVbxQ4s/nCNsyZR2JB689gnR1hetnKCFJTY2ETBsqFgoyNHU1Zk +oTIq0ZQVeOEkcn2qnM981xuPvptY1ChdFtD52ybJRhAcgrHX+P3aWns0N7Hn7EgF +WinGDCZ27QXA/iiuSteVr/3Fh39RF7+YYix4Y7TRqXQqRhK/sp7fZQjLeXB/oECh +5tEm/WEFJoX4mDNf96GAVzHynjNysCSZd44iZxbMlgNynYv01Q/LLddI1KBz4Mqq +tBvHgh61ddtcM6vi15iK8QIDAQABo4G0MIGxMA4GA1UdDwEB/wQEAwIFoDAdBgNV +HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4E +FgQU8Xe12Pp5uejfckcNvS4m6s/3aCgwHwYDVR0jBBgwFoAUUskNzw+kqxQYWLfe +wr6vleCL3tcwMgYDVR0RAQH/BCgwJoIZKi5pbnRlZ3JhdGlvbi5leGFtcGxlLmNv +bYIJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQCcNy2uaVgh2aSxIwkbBJdk +496vmzpdyvNZP5x3NHNVsUo3Ldb7llt72Rdwe0szGeZ0Df3s5y9F7jEmmM8efINF +jTvh6R7zWJCQor64JHgeh0qFur8DV6hv7CH3j/RA3Cu+bmwxd6DDlOwroU97NOcf +fKvRD3F5iH5wcz174NNq+E0lWX5QsscM8K4nQ/JfVbvmVmi5N4MCigTAxzsRF/Dr +ZylL5ikwP/rcUyVySbm/5JT4bLFnhhLjHORPLDwM3l/AMKEJUzv9kt/HLoZAA9Im +la+k91bt//8XMhmfP01IRkuMMfN9/nVkfhwmIk5b2MqTiWZzjDVqHulYMq4BNU54 +-----END CERTIFICATE----- diff --git a/deploy/services-demo/conf/nginz/nginx.conf b/deploy/services-demo/conf/nginz/nginx.conf index e767c24c339..f4b73e0a03d 100644 --- a/deploy/services-demo/conf/nginz/nginx.conf +++ b/deploy/services-demo/conf/nginz/nginx.conf @@ -106,23 +106,48 @@ http { # server { + # plain TCP/http listening for integration tests only. listen 8080; listen 8081; + # for nginx-without-tls, we need to use a separate port for http2 traffic, + # as nginx cannot handle unencrypted http1 and http2 trafic on the same + # port. + # This port is only used for trying out nginx grpc forwarding without TLS locally and should not + # be ported to any production nginz config. + listen 8090 http2; + + ######## TLS/SSL block start ############## + # + # Most integration tests simply use the http ports 8080 and 8081 + # But to also test tls forwarding, this port can be used. + # This applies only locally, as for kubernetes (helm chart) based deployments, + # TLS is terminated at the ingress level, not at nginz level + listen 8443 ssl http2; + + # self-signed certificates generated using wire-server/hack/bin/selfsigned.sh + ssl_certificate integration-leaf.pem; + ssl_certificate_key integration-leaf-key.pem; + + ######## TLS/SSL block end ############## + zauth_keystore resources/zauth/pubkeys.txt; zauth_acl conf/nginz/zauth_acl.txt; location /status { + set $sanitized_request $request; zauth off; return 200; } location /i/status { + set $sanitized_request $request; zauth off; return 200; } location /vts { + set $sanitized_request $request; zauth off; vhost_traffic_status_display; vhost_traffic_status_display_format html; @@ -132,6 +157,17 @@ http { # Service Routing # + # Federator endpoints: expose the federatorExternal port (Inward grpc + # service) + location /wire.federator.Inward { + set $sanitized_request $request; + zauth off; + grpc_pass grpc://federator_external; + # FUTUREWORK(federation): are any other settings + # (e.g. timeouts, body size, buffers, headers,...) + # useful/recommended/important-for-security?) + } + # Brig Endpoints # ## brig unauthenticated endpoints diff --git a/deploy/services-demo/conf/nginz/upstreams b/deploy/services-demo/conf/nginz/upstreams index aca8c4c76e8..90a417dc2c2 100644 --- a/deploy/services-demo/conf/nginz/upstreams +++ b/deploy/services-demo/conf/nginz/upstreams @@ -33,3 +33,6 @@ upstream spar { keepalive 32; server 127.0.0.1:8088 max_fails=3 weight=1; } +upstream federator_external { + server 127.0.0.1:8098 max_fails=3 weight=1; +} diff --git a/hack/bin/find-latest-docker-tag.sh b/hack/bin/find-latest-docker-tag.sh index ca1a64c2d7c..cca6ca5d642 100755 --- a/hack/bin/find-latest-docker-tag.sh +++ b/hack/bin/find-latest-docker-tag.sh @@ -5,6 +5,13 @@ # they are not uploaded simulaneously, so this script is subject to race conditions and CI failures. # Use at your own risk! -curl -sSL 'https://quay.io/api/v1/repository/wire/brig/tag/?limit=50&page=1&onlyActiveTags=true' \ - | jq -r '.tags[].name' \ - | sort | uniq | grep -v latest | grep -v 'pr\.' | tail -1 +function lookup() { + image=$1 + echo "latest tag for $image:" + curl -sSL "https://quay.io/api/v1/repository/wire/$image/tag/?limit=50&page=1&onlyActiveTags=true" \ + | jq -r '.tags[].name' \ + | sort --version-sort | uniq | grep -v latest | grep -v 'pr\.' | tail -1 +} + +lookup brig +lookup nginz diff --git a/hack/bin/integration-setup.sh b/hack/bin/integration-setup.sh index 6b6973b1848..254694f3406 100755 --- a/hack/bin/integration-setup.sh +++ b/hack/bin/integration-setup.sh @@ -14,7 +14,7 @@ kubectl create namespace "${NAMESPACE}" > /dev/null 2>&1 || true ${DIR}/integration-cleanup.sh -charts=( fake-aws databases-ephemeral wire-server ) +charts=( fake-aws databases-ephemeral wire-server nginx-ingress-controller nginx-ingress-services ) echo "updating recursive dependencies ..." for chart in "${charts[@]}"; do @@ -36,21 +36,30 @@ function printLogs() { trap printLogs ERR -FEDERATION_DOMAIN="federator.$NAMESPACE.svc.cluster.local" +FEDERATION_DOMAIN="federation-test-helper.$NAMESPACE.svc.cluster.local" for chart in "${charts[@]}"; do kubectl -n ${NAMESPACE} get pods valuesfile="${DIR}/../helm_vars/${chart}/values.yaml" + + declare -a options=() + if [ -f "$valuesfile" ]; then - option="-f $valuesfile" - else - option="" + options+=(-f "$valuesfile") fi + + if [[ "$chart" == "nginx-ingress-services" ]]; then + # Federation domain is also the SRV record created by the + # federation-test-helper service. Maybe we can find a way to make these + # differ, so we don't make any silly assumptions in the code. + options+=("--set" "config.dns.federator=$FEDERATION_DOMAIN") + fi + # default is 5m but may not be enough on a fresh install including cassandra migrations TIMEOUT=10m set -x helm upgrade --install --namespace "${NAMESPACE}" "${NAMESPACE}-${chart}" "${CHARTS_DIR}/${chart}" \ - $option \ + ${options[*]} \ --set brig.config.optSettings.setFederationDomain="$FEDERATION_DOMAIN" \ --set galley.config.settings.federationDomain="$FEDERATION_DOMAIN" \ --wait \ diff --git a/hack/bin/selfsigned.sh b/hack/bin/selfsigned.sh new file mode 100755 index 00000000000..46332c72cb1 --- /dev/null +++ b/hack/bin/selfsigned.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +# Create a self-signed x509 certificate in the current working directory. +# Requires 'cfssl' to be on your PATH (see https://github.com/cloudflare/cfssl) +# These certificates are only meant for integration tests that explicitly disable certificate checking + +set -e +TEMP=${TEMP:-/tmp} +CSR="$TEMP/csr.json" +OUTPUTNAME_CA="integration-ca" +OUTPUTNAME_LEAF_CERT="integration-leaf" + +command -v cfssl >/dev/null 2>&1 || { echo >&2 "cfssl is not installed, aborting. See https://github.com/cloudflare/cfssl"; exit 1; } +command -v cfssljson >/dev/null 2>&1 || { echo >&2 "cfssljson is not installed, aborting. See https://github.com/cloudflare/cfssl"; exit 1; } + +echo '{ + "CN": "ca.example.com", + "key": { + "algo": "rsa", + "size": 2048 + } +}' > "$CSR" + +# generate CA key and cert +cfssl gencert -initca "$CSR" | cfssljson -bare "$OUTPUTNAME_CA" + +echo '{ + "key": { + "algo": "rsa", + "size": 2048 + } +}' > "$CSR" + +# generate cert and key based on CA given comma-separated hostnames as SANs +cfssl gencert -ca "$OUTPUTNAME_CA.pem" -ca-key "$OUTPUTNAME_CA-key.pem" -hostname=*.integration.example.com,localhost "$CSR" | cfssljson -bare "$OUTPUTNAME_LEAF_CERT" + +# cleanup unneeded files +rm "$OUTPUTNAME_LEAF_CERT.csr" +rm "$OUTPUTNAME_CA.csr" + + diff --git a/hack/federation/grpcurlhandle.sh b/hack/federation/grpcurlhandle.sh new file mode 100755 index 00000000000..7cbdcc904f4 --- /dev/null +++ b/hack/federation/grpcurlhandle.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +command -v grpcurl >/dev/null 2>&1 || { echo >&2 "grpcurl is not installed, aborting. Maybe try ' nix-env -iA nixpkgs.grpcurl '?"; exit 1; } + +path=$(echo -n users/by-handle | base64) +queryK=$(echo -n handle | base64) +queryV=$(echo -n alice | base64) + +function getHandle() { + echo "" + echo "===> getHandle: $1" + if [ -z "$SERVERNAME" ]; then + AUTHORITY="" + else + AUTHORITY="-authority $SERVERNAME" + fi + set -x + grpcurl -d @ -format json $AUTHORITY $MODE -proto ../../libs/wire-api-federation/proto/router.proto "$HOST:$PORT" wire.federator.Inward/call </dev/null # stop outputting commands and don't print the set +x line + echo "===|" + echo +} + +HOST=localhost +PORT=8443 +MODE="-cacert ../../services/nginz/integration-test/conf/nginz/integration-ca.pem" +SERVERNAME="federator.integration.example.com" +getHandle "local nginz on port 8443 using self-signed cert" + +# HOST= +# PORT=443 # tls port of currently-deployed ingress in 'grpc' namespace +# MODE="-insecure" +# SERVERNAME="federator.integration.example.com" +# # making an insecure/ignore-certificates connection over TLS works: +# getHandle "description" diff --git a/hack/federation/python-client/.gitignore b/hack/federation/python-client/.gitignore new file mode 100644 index 00000000000..8006d530463 --- /dev/null +++ b/hack/federation/python-client/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +router_pb2.py +router_pb2_grpc.py diff --git a/hack/federation/python-client/README.md b/hack/federation/python-client/README.md new file mode 100644 index 00000000000..330863cc64c --- /dev/null +++ b/hack/federation/python-client/README.md @@ -0,0 +1,34 @@ +Install python dependencies and generate python files from proto: + +``` +poetry install +poetry run python -m grpc_tools.protoc -I../../../libs/wire-api-federation/proto --python_out=. --grpc_python_out=. ../../../libs/wire-api-federation/proto/router.proto +``` + +Run services locally: + +``` +../../../services/start-services-only.sh +``` + +Run example python client to make a grpc request to federator (which will redirect it to brig): + +``` +poetry run python client.py +``` + +expected output should be: + +``` +starting client... +request: path: "users/by-handle" +query { + key: "handle" + value: "alice" +} + +response: httpResponse { + responseStatus: 404 + responseBody: "Handle not found." +} +``` diff --git a/hack/federation/python-client/client.py b/hack/federation/python-client/client.py new file mode 100644 index 00000000000..d234a68e72f --- /dev/null +++ b/hack/federation/python-client/client.py @@ -0,0 +1,26 @@ +import grpc + +from router_pb2 import * +import router_pb2_grpc + +# make sure to run ./services/start-services-only.sh first to have federator and brig up! +print("starting client...") + +channel = grpc.insecure_channel('localhost:8098') # public-facing federator port +stub = router_pb2_grpc.InwardStub(channel) + +def handle_search(handle): + param = QueryParam(key="handle".encode("utf-8"), value=handle.encode("utf-8")) + return Request( + path="users/by-handle".encode("utf-8"), + query=[param], + method=GET, + component=Brig) + +req = handle_search("alice") + +print("request: ", req) + +response = stub.call(req) + +print("response: ", response) diff --git a/hack/federation/python-client/poetry.lock b/hack/federation/python-client/poetry.lock new file mode 100644 index 00000000000..d73be5fe9bf --- /dev/null +++ b/hack/federation/python-client/poetry.lock @@ -0,0 +1,403 @@ +[[package]] +category = "main" +description = "Disable App Nap on macOS >= 10.9" +marker = "sys_platform == \"darwin\"" +name = "appnope" +optional = false +python-versions = "*" +version = "0.1.2" + +[[package]] +category = "main" +description = "Specifications for callback functions passed in to an API" +name = "backcall" +optional = false +python-versions = "*" +version = "0.2.0" + +[[package]] +category = "main" +description = "Cross-platform colored terminal text." +marker = "sys_platform == \"win32\"" +name = "colorama" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.4.4" + +[[package]] +category = "main" +description = "Decorators for Humans" +name = "decorator" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" +version = "4.4.2" + +[[package]] +category = "main" +description = "HTTP/2-based RPC framework" +name = "grpcio" +optional = false +python-versions = "*" +version = "1.35.0" + +[package.dependencies] +six = ">=1.5.2" + +[package.extras] +protobuf = ["grpcio-tools (>=1.35.0)"] + +[[package]] +category = "main" +description = "Protobuf code generator for gRPC" +name = "grpcio-tools" +optional = false +python-versions = "*" +version = "1.35.0" + +[package.dependencies] +grpcio = ">=1.35.0" +protobuf = ">=3.5.0.post1,<4.0dev" +setuptools = "*" + +[[package]] +category = "main" +description = "IPython: Productive Interactive Computing" +name = "ipython" +optional = false +python-versions = ">=3.7" +version = "7.20.0" + +[package.dependencies] +appnope = "*" +backcall = "*" +colorama = "*" +decorator = "*" +jedi = ">=0.16" +pexpect = ">4.3" +pickleshare = "*" +prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" +pygments = "*" +setuptools = ">=18.5" +traitlets = ">=4.2" + +[package.extras] +all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.14)", "pygments", "qtconsole", "requests", "testpath"] +doc = ["Sphinx (>=1.3)"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["notebook", "ipywidgets"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.14)"] + +[[package]] +category = "main" +description = "Vestigial utilities from IPython" +name = "ipython-genutils" +optional = false +python-versions = "*" +version = "0.2.0" + +[[package]] +category = "main" +description = "An autocompletion tool for Python that can be used for text editors." +name = "jedi" +optional = false +python-versions = ">=3.6" +version = "0.18.0" + +[package.dependencies] +parso = ">=0.8.0,<0.9.0" + +[package.extras] +qa = ["flake8 (3.8.3)", "mypy (0.782)"] +testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"] + +[[package]] +category = "main" +description = "A Python Parser" +name = "parso" +optional = false +python-versions = ">=3.6" +version = "0.8.1" + +[package.extras] +qa = ["flake8 (3.8.3)", "mypy (0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] + +[[package]] +category = "main" +description = "Pexpect allows easy control of interactive console applications." +marker = "sys_platform != \"win32\"" +name = "pexpect" +optional = false +python-versions = "*" +version = "4.8.0" + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +category = "main" +description = "Tiny 'shelve'-like database with concurrency support" +name = "pickleshare" +optional = false +python-versions = "*" +version = "0.7.5" + +[[package]] +category = "main" +description = "Library for building powerful interactive command lines in Python" +name = "prompt-toolkit" +optional = false +python-versions = ">=3.6.1" +version = "3.0.16" + +[package.dependencies] +wcwidth = "*" + +[[package]] +category = "main" +description = "Protocol Buffers" +name = "protobuf" +optional = false +python-versions = "*" +version = "3.14.0" + +[package.dependencies] +six = ">=1.9" + +[[package]] +category = "main" +description = "Run a subprocess in a pseudo terminal" +marker = "sys_platform != \"win32\"" +name = "ptyprocess" +optional = false +python-versions = "*" +version = "0.7.0" + +[[package]] +category = "main" +description = "Pygments is a syntax highlighting package written in Python." +name = "pygments" +optional = false +python-versions = ">=3.5" +version = "2.7.4" + +[[package]] +category = "main" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.15.0" + +[[package]] +category = "main" +description = "Traitlets Python configuration system" +name = "traitlets" +optional = false +python-versions = ">=3.7" +version = "5.0.5" + +[package.dependencies] +ipython-genutils = "*" + +[package.extras] +test = ["pytest"] + +[[package]] +category = "main" +description = "Measures the displayed width of unicode strings in a terminal" +name = "wcwidth" +optional = false +python-versions = "*" +version = "0.2.5" + +[metadata] +content-hash = "d2b3bd7740916466586fd07c5fe115152e9f580b9c89ccb26f2fa38420aa163f" +lock-version = "1.0" +python-versions = "^3.8" + +[metadata.files] +appnope = [ + {file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"}, + {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"}, +] +backcall = [ + {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, + {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +decorator = [ + {file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"}, + {file = "decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"}, +] +grpcio = [ + {file = "grpcio-1.35.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:95cc4d2067deced18dc807442cf8062a93389a86abf8d40741120054389d3f29"}, + {file = "grpcio-1.35.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:d186a0ce291f4386e28a7042ec31c85250b0c2e25d2794b87fa3c15ff473c46c"}, + {file = "grpcio-1.35.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:c8d0a6a58a42275c6cb616e7cb9f9fcf5eba1e809996546e561cd818b8f7cff7"}, + {file = "grpcio-1.35.0-cp27-cp27m-win32.whl", hash = "sha256:8d08f90d72a8e8d9af087476337da76d26749617b0a092caff4e684ce267af21"}, + {file = "grpcio-1.35.0-cp27-cp27m-win_amd64.whl", hash = "sha256:0072ec4563ab4268c4c32e936955085c2d41ea175b662363496daedd2273372c"}, + {file = "grpcio-1.35.0-cp27-cp27mu-linux_armv7l.whl", hash = "sha256:aca45d2ccb693c9227fbf21144891422a42dc4b76b52af8dd1d4e43afebe321d"}, + {file = "grpcio-1.35.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:87147b1b306c88fe7dca7e3dff8aefd1e63d6aed86e224f9374ddf283f17d7f1"}, + {file = "grpcio-1.35.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:22edfc278070d54f3ab7f741904e09155a272fe934e842babbf84476868a50de"}, + {file = "grpcio-1.35.0-cp35-cp35m-linux_armv7l.whl", hash = "sha256:f3654a52f72ba28953dbe2e93208099f4903f4b3c07dc7ff4db671c92968111d"}, + {file = "grpcio-1.35.0-cp35-cp35m-macosx_10_10_intel.whl", hash = "sha256:dc2589370ef84eb1cc53530070d658a7011d2ee65f18806581809c11cd016136"}, + {file = "grpcio-1.35.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:f0c27fd16582a303e5baf6cffd9345c9ac5f855d69a51232664a0b888a77ba80"}, + {file = "grpcio-1.35.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:b2985f73611b637271b00d9c4f177e65cc3193269bc9760f16262b1a12757265"}, + {file = "grpcio-1.35.0-cp35-cp35m-manylinux2014_i686.whl", hash = "sha256:acb489b7aafdcf960f1a0000a1f22b45e5b6ccdf8dba48f97617d627f4133195"}, + {file = "grpcio-1.35.0-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:16fd33030944672e49e0530dec2c60cd4089659ccdf327e99569b3b29246a0b6"}, + {file = "grpcio-1.35.0-cp35-cp35m-win32.whl", hash = "sha256:1757e81c09132851e85495b802fe4d4fbef3547e77fa422a62fb4f7d51785be0"}, + {file = "grpcio-1.35.0-cp35-cp35m-win_amd64.whl", hash = "sha256:35b72884e09cbc46c564091f4545a39fa66d132c5676d1a6e827517fff47f2c1"}, + {file = "grpcio-1.35.0-cp36-cp36m-linux_armv7l.whl", hash = "sha256:17940a7dc461066f28816df48be44f24d3b9f150db344308ee2aeae033e1af0b"}, + {file = "grpcio-1.35.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:75ea903edc42a8c6ec61dbc5f453febd79d8bdec0e1bad6df7088c34282e8c42"}, + {file = "grpcio-1.35.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:b180a3ec4a5d6f96d3840c83e5f8ab49afac9fa942921e361b451d7a024efb00"}, + {file = "grpcio-1.35.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e163c27d2062cd3eb07057f23f8d1330925beaba16802312b51b4bad33d74098"}, + {file = "grpcio-1.35.0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:764b50ba1a15a2074cdd1a841238f2dead0a06529c495a46821fae84cb9c7342"}, + {file = "grpcio-1.35.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:088c8bea0f6b596937fefacf2c8df97712e7a3dd49496975049cc95dbf02af1a"}, + {file = "grpcio-1.35.0-cp36-cp36m-win32.whl", hash = "sha256:1aa53f82362c7f2791fe0cdd9a3b3aec325c11d8f0dfde600f91907dfaa8546b"}, + {file = "grpcio-1.35.0-cp36-cp36m-win_amd64.whl", hash = "sha256:efb3d67405eb8030db6f27920b4be023fabfb5d4e09c34deab094a7c473a5472"}, + {file = "grpcio-1.35.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:44aaa6148d18a8e836f99dadcdec17b27bc7ec0995b2cc12c94e61826040ec90"}, + {file = "grpcio-1.35.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:18ad7644e23757420ea839ac476ef861e4f4841c8566269b7c91c100ca1943b3"}, + {file = "grpcio-1.35.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:859a0ceb23d7189362cc06fe7e906e9ed5c7a8f3ac960cc04ce13fe5847d0b62"}, + {file = "grpcio-1.35.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3e7d4428ed752fdfe2dddf2a404c93d3a2f62bf4b9109c0c10a850c698948891"}, + {file = "grpcio-1.35.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:a36151c335280b09afd5123f3b25085027ae2b10682087a4342fb6f635b928fb"}, + {file = "grpcio-1.35.0-cp37-cp37m-win32.whl", hash = "sha256:dfecb2acd3acb8bb50e9aa31472c6e57171d97c1098ee67cd283a6fe7d56a926"}, + {file = "grpcio-1.35.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e87e55fba98ebd7b4c614dcef9940dc2a7e057ad8bba5f91554934d47319a35b"}, + {file = "grpcio-1.35.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:da44bf613eed5d9e8df0785463e502a416de1be6e4ac31edbe99c9111abaed5f"}, + {file = "grpcio-1.35.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:9e503eaf853199804a954dc628c5207e67d6c7848dcba42a997fbe718618a2b1"}, + {file = "grpcio-1.35.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:6ba3d7acf70acde9ce27e22921db921b84a71be578b32739536c32377b65041a"}, + {file = "grpcio-1.35.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:048c01d1eb5c2ae7cba2254b98938d2fc81f6dc10d172d9261d65266adb0fdb3"}, + {file = "grpcio-1.35.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:efd896e8ca7adb2654cf014479a5e1f74e4f776b6b2c0fbf95a6c92787a6631a"}, + {file = "grpcio-1.35.0-cp38-cp38-win32.whl", hash = "sha256:8a29a26b9f39701ce15aa1d5aa5e96e0b5f7028efe94f95341a4ed8dbe4bed78"}, + {file = "grpcio-1.35.0-cp38-cp38-win_amd64.whl", hash = "sha256:aea3d592a7ece84739b92d212cd16037c51d84a259414f64b51c14e946611f3d"}, + {file = "grpcio-1.35.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2f8e8d35d4799aa1627a212dbe8546594abf4064056415c31bd1b3b8f2a62027"}, + {file = "grpcio-1.35.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:9f0da13b215068e7434b161a35d0b4e92140ffcfa33ddda9c458199ea1d7ce45"}, + {file = "grpcio-1.35.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:7ae408780b79c9b9b91a2592abd1d7abecd05675d988ea75038580f420966b59"}, + {file = "grpcio-1.35.0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:0f714e261e1d63615476cda4ee808a79cca62f8f09e2943c136c2f87ec5347b1"}, + {file = "grpcio-1.35.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:7ee7d54da9d176d3c9a0f47c04d7ff6fdc6ee1c17643caff8c33d6c8a70678a4"}, + {file = "grpcio-1.35.0-cp39-cp39-win32.whl", hash = "sha256:94c3b81089a86d3c5877d22b07ebc66b5ed1d84771e24b001844e29a5b6178dd"}, + {file = "grpcio-1.35.0-cp39-cp39-win_amd64.whl", hash = "sha256:399ee377b312ac652b07ef4365bbbba009da361fa7708c4d3d4ce383a1534ea7"}, + {file = "grpcio-1.35.0.tar.gz", hash = "sha256:7bd0ebbb14dde78bf66a1162efd29d3393e4e943952e2f339757aa48a184645c"}, +] +grpcio-tools = [ + {file = "grpcio-tools-1.35.0.tar.gz", hash = "sha256:9e2a41cba9c5a20ae299d0fdd377fe231434fa04cbfbfb3807293c6ec10b03cf"}, + {file = "grpcio_tools-1.35.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:cfa49e6d62b313862a6007ae02016bd89a2fa184b0aab0d0e524cb24ecc2fdb4"}, + {file = "grpcio_tools-1.35.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:f66cd905ffcbe2294c9dee6d0de8064c3a49861a9b1770c18cb8a15be3bc0da5"}, + {file = "grpcio_tools-1.35.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:56663ba4a49ca585e4333dfebc5ed7e91ad3d75b838aced4f922fb4e365376cc"}, + {file = "grpcio_tools-1.35.0-cp27-cp27m-win32.whl", hash = "sha256:252bfaa0004d80d927a77998c8b3a81fb47620e41af1664bdba8837d722c4ead"}, + {file = "grpcio_tools-1.35.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ffa66fc4e80aff4f68599e786aa3295f4a0d6761ed63d75c32261f5de77aa0fd"}, + {file = "grpcio_tools-1.35.0-cp27-cp27mu-linux_armv7l.whl", hash = "sha256:3cea2d07343801cb2a0d2f71fe7d6d7ffa6fe8fc0e1f6243c6867d0bb04557a1"}, + {file = "grpcio_tools-1.35.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:15c37528936774d8f734d75540848134fb5710ca27801ce4ac73c8a6cca0494e"}, + {file = "grpcio_tools-1.35.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:f3861211a450a312b7645d4eaf16c78f1d9e896e58a8c3be871f5881362d3fee"}, + {file = "grpcio_tools-1.35.0-cp35-cp35m-linux_armv7l.whl", hash = "sha256:5fdb6a65f66ee6cdc49455ea03ca435ae86ef1869dc929a8652cc19b5f950d22"}, + {file = "grpcio_tools-1.35.0-cp35-cp35m-macosx_10_10_intel.whl", hash = "sha256:8bfd05f26af9ea069f2f3c48740a315470fc4a434189544fea3b3508b71be9a0"}, + {file = "grpcio_tools-1.35.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:f7074cfd79989424e4bd903ff5618c1420a7c81ad97836256f3927447b74c027"}, + {file = "grpcio_tools-1.35.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:adea0bd93978284f1590a3880d79621881f7029b2fac330f64f491af2b554707"}, + {file = "grpcio_tools-1.35.0-cp35-cp35m-manylinux2014_i686.whl", hash = "sha256:b23e0a64cdbf4c3bcdf8e6ad0cdd8b8a582a4c50d5ed4eddc4c81dc8d5ba0c60"}, + {file = "grpcio_tools-1.35.0-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:7cc68c42bcbebd76731686f22870930f110309e1e69244df428f8fb161b7645b"}, + {file = "grpcio_tools-1.35.0-cp35-cp35m-win32.whl", hash = "sha256:aa9cb65231a7efd77e83e149b1905335eda1bbadd301dd1bffcbfea69fd5bd56"}, + {file = "grpcio_tools-1.35.0-cp35-cp35m-win_amd64.whl", hash = "sha256:11e6dffd2e58737ade63a00a51da83b474b5740665914103f003049acff5be8e"}, + {file = "grpcio_tools-1.35.0-cp36-cp36m-linux_armv7l.whl", hash = "sha256:59d80997e780dc52911e263e30ca2334e7b3bd12c10dc81625dcc34273fa744b"}, + {file = "grpcio_tools-1.35.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:179b2eb274d8c29e1e18c21fb69c5101e3196617c7abb193a80e194ea9b274be"}, + {file = "grpcio_tools-1.35.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:1687b0033beff82ac35f14fbbd5e7eb0cab39e60f8be0a25a7f4ba92d66578c8"}, + {file = "grpcio_tools-1.35.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:9e956751b1b96ce343088550d155827f8312d85f09067f6ede0a4778273b787b"}, + {file = "grpcio_tools-1.35.0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:4ca85f9deee58473c017ee62aaa8c12dfda223eeabed5dd013c009af275bc4f2"}, + {file = "grpcio_tools-1.35.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:853d030ff74ce90244bb77c5a8d5c2b2d84b24df477fc422d44fa81d512124d6"}, + {file = "grpcio_tools-1.35.0-cp36-cp36m-win32.whl", hash = "sha256:add160d4697a5366ee1420b59621bde69a3eaaba35170e60bd376f0ea6e24fe5"}, + {file = "grpcio_tools-1.35.0-cp36-cp36m-win_amd64.whl", hash = "sha256:dbaaad0132a9e70439e93d26611443ee3aaaa62547b7d18655ac754b4984ea25"}, + {file = "grpcio_tools-1.35.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:bbc6986e29ab3bb39db9a0e31cdbb0ced80cead2ef0453c40dfdfacbab505950"}, + {file = "grpcio_tools-1.35.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:8631df0e357b28da4ef617306a08f70c21cf85c049849f4a556b95069c146d61"}, + {file = "grpcio_tools-1.35.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6e2b07dbe25c6022eeae972b4eee2058836dea345a3253082524240a00daa9f"}, + {file = "grpcio_tools-1.35.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:30f83ccc6d09be07d7f15d05f29acd5017140f330ba3a218ae7b7e19db02bda6"}, + {file = "grpcio_tools-1.35.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:88184383f24af8f8cbbb4020846af53634d8632b486479a3b98ea29c1470372e"}, + {file = "grpcio_tools-1.35.0-cp37-cp37m-win32.whl", hash = "sha256:579cf4538d8ec25314c45ef84bb140fad8888446ed7a69913965fd7d9bc188d5"}, + {file = "grpcio_tools-1.35.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9203e0105db476131f32ff3c3213b5aa6b77b25553ffe0d09d973913b2320856"}, + {file = "grpcio_tools-1.35.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:c910dec8903fb9d16fd1b111de57401a46e4d5f74c6d009a12a945d696603eb0"}, + {file = "grpcio_tools-1.35.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:cc9bcd34a653c2353dd43fc395ceb560271551f2fae30bcafede2e4ad0c101c4"}, + {file = "grpcio_tools-1.35.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4241301b8e594c5c265f06c600b492372e867a4bb80dc205b545088c39e010d0"}, + {file = "grpcio_tools-1.35.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:8bae2611a8e09617922ff4cb11de6fd5f59b91c75a14a318c7d378f427584be1"}, + {file = "grpcio_tools-1.35.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:0d5028f548fa2b99494baf992dd0e23676361b1a217322be44f6c13b5133f6b3"}, + {file = "grpcio_tools-1.35.0-cp38-cp38-win32.whl", hash = "sha256:8d2c507c093a0ae3df62201ef92ceabcc34ac3f7e53026f12357f8c3641e809a"}, + {file = "grpcio_tools-1.35.0-cp38-cp38-win_amd64.whl", hash = "sha256:994adfe39a1755424e3c33c434786a9fa65090a50515303dfa8125cbec4a5940"}, + {file = "grpcio_tools-1.35.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:51bf36ae34f70a8d6ccee5d9d2e52a9e65251670b405f91b7b547a73788f90fb"}, + {file = "grpcio_tools-1.35.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:e00dc8d641001409963f78b0b8bf83834eb87c0090357ebc862f874dd0e6dbb5"}, + {file = "grpcio_tools-1.35.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:5f279dee8b77bf93996592ada3bf56ad44fa9b0e780099172f1a7093a506eb67"}, + {file = "grpcio_tools-1.35.0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:713b496dd02fc868da0d59cc09536c62452d52035d0b694204d5054e75fe4929"}, + {file = "grpcio_tools-1.35.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:15fa3c66e6b0ba2e434eccf8cdbce68e4e37b5fe440dbeffb9efd599aa23910f"}, + {file = "grpcio_tools-1.35.0-cp39-cp39-win32.whl", hash = "sha256:ee0f750b5d8d628349e903438bb506196c4c5cee0007e81800d95cd0a2b23e6f"}, + {file = "grpcio_tools-1.35.0-cp39-cp39-win_amd64.whl", hash = "sha256:c8451c60e106310436c123f3243c115db21ccb957402edbe73b1bb68276e4aa4"}, +] +ipython = [ + {file = "ipython-7.20.0-py3-none-any.whl", hash = "sha256:1918dea4bfdc5d1a830fcfce9a710d1d809cbed123e85eab0539259cb0f56640"}, + {file = "ipython-7.20.0.tar.gz", hash = "sha256:1923af00820a8cf58e91d56b89efc59780a6e81363b94464a0f17c039dffff9e"}, +] +ipython-genutils = [ + {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, + {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"}, +] +jedi = [ + {file = "jedi-0.18.0-py2.py3-none-any.whl", hash = "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93"}, + {file = "jedi-0.18.0.tar.gz", hash = "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"}, +] +parso = [ + {file = "parso-0.8.1-py2.py3-none-any.whl", hash = "sha256:15b00182f472319383252c18d5913b69269590616c947747bc50bf4ac768f410"}, + {file = "parso-0.8.1.tar.gz", hash = "sha256:8519430ad07087d4c997fda3a7918f7cfa27cb58972a8c89c2a0295a1c940e9e"}, +] +pexpect = [ + {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, + {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, +] +pickleshare = [ + {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, + {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, +] +prompt-toolkit = [ + {file = "prompt_toolkit-3.0.16-py3-none-any.whl", hash = "sha256:62c811e46bd09130fb11ab759012a4ae385ce4fb2073442d1898867a824183bd"}, + {file = "prompt_toolkit-3.0.16.tar.gz", hash = "sha256:0fa02fa80363844a4ab4b8d6891f62dd0645ba672723130423ca4037b80c1974"}, +] +protobuf = [ + {file = "protobuf-3.14.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:629b03fd3caae7f815b0c66b41273f6b1900a579e2ccb41ef4493a4f5fb84f3a"}, + {file = "protobuf-3.14.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:5b7a637212cc9b2bcf85dd828b1178d19efdf74dbfe1ddf8cd1b8e01fdaaa7f5"}, + {file = "protobuf-3.14.0-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:43b554b9e73a07ba84ed6cf25db0ff88b1e06be610b37656e292e3cbb5437472"}, + {file = "protobuf-3.14.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:5e9806a43232a1fa0c9cf5da8dc06f6910d53e4390be1fa06f06454d888a9142"}, + {file = "protobuf-3.14.0-cp35-cp35m-win32.whl", hash = "sha256:1c51fda1bbc9634246e7be6016d860be01747354ed7015ebe38acf4452f470d2"}, + {file = "protobuf-3.14.0-cp35-cp35m-win_amd64.whl", hash = "sha256:4b74301b30513b1a7494d3055d95c714b560fbb630d8fb9956b6f27992c9f980"}, + {file = "protobuf-3.14.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:86a75477addde4918e9a1904e5c6af8d7b691f2a3f65587d73b16100fbe4c3b2"}, + {file = "protobuf-3.14.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ecc33531a213eee22ad60e0e2aaea6c8ba0021f0cce35dbf0ab03dee6e2a23a1"}, + {file = "protobuf-3.14.0-cp36-cp36m-win32.whl", hash = "sha256:72230ed56f026dd664c21d73c5db73ebba50d924d7ba6b7c0d81a121e390406e"}, + {file = "protobuf-3.14.0-cp36-cp36m-win_amd64.whl", hash = "sha256:0fc96785262042e4863b3f3b5c429d4636f10d90061e1840fce1baaf59b1a836"}, + {file = "protobuf-3.14.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4e75105c9dfe13719b7293f75bd53033108f4ba03d44e71db0ec2a0e8401eafd"}, + {file = "protobuf-3.14.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2a7e2fe101a7ace75e9327b9c946d247749e564a267b0515cf41dfe450b69bac"}, + {file = "protobuf-3.14.0-cp37-cp37m-win32.whl", hash = "sha256:b0d5d35faeb07e22a1ddf8dce620860c8fe145426c02d1a0ae2688c6e8ede36d"}, + {file = "protobuf-3.14.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8971c421dbd7aad930c9bd2694122f332350b6ccb5202a8b7b06f3f1a5c41ed5"}, + {file = "protobuf-3.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9616f0b65a30851e62f1713336c931fcd32c057202b7ff2cfbfca0fc7d5e3043"}, + {file = "protobuf-3.14.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:22bcd2e284b3b1d969c12e84dc9b9a71701ec82d8ce975fdda19712e1cfd4e00"}, + {file = "protobuf-3.14.0-py2.py3-none-any.whl", hash = "sha256:0e247612fadda953047f53301a7b0407cb0c3cb4ae25a6fde661597a04039b3c"}, + {file = "protobuf-3.14.0.tar.gz", hash = "sha256:1d63eb389347293d8915fb47bee0951c7b5dab522a4a60118b9a18f33e21f8ce"}, +] +ptyprocess = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] +pygments = [ + {file = "Pygments-2.7.4-py3-none-any.whl", hash = "sha256:bc9591213a8f0e0ca1a5e68a479b4887fdc3e75d0774e5c71c31920c427de435"}, + {file = "Pygments-2.7.4.tar.gz", hash = "sha256:df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337"}, +] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] +traitlets = [ + {file = "traitlets-5.0.5-py3-none-any.whl", hash = "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426"}, + {file = "traitlets-5.0.5.tar.gz", hash = "sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396"}, +] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +] diff --git a/hack/federation/python-client/pyproject.toml b/hack/federation/python-client/pyproject.toml new file mode 100644 index 00000000000..705b94f1a0e --- /dev/null +++ b/hack/federation/python-client/pyproject.toml @@ -0,0 +1,16 @@ +[tool.poetry] +name = "wire-api-federation" +version = "0.1.0" +description = "" +authors = ["Your Name "] + +[tool.poetry.dependencies] +python = "^3.8" +grpcio-tools = "^1.35.0" +ipython = "^7.20.0" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/hack/helm_vars/nginx-ingress-controller/values.yaml b/hack/helm_vars/nginx-ingress-controller/values.yaml new file mode 100644 index 00000000000..53b1c575066 --- /dev/null +++ b/hack/helm_vars/nginx-ingress-controller/values.yaml @@ -0,0 +1,10 @@ +nginx-ingress: + controller: + kind: DaemonSet + service: + type: NodePort + externalTrafficPolicy: Local + nodePorts: + # choose a random free port + https: null + http: null diff --git a/hack/helm_vars/nginx-ingress-services/values.yaml b/hack/helm_vars/nginx-ingress-services/values.yaml new file mode 100644 index 00000000000..1ca723babe2 --- /dev/null +++ b/hack/helm_vars/nginx-ingress-services/values.yaml @@ -0,0 +1,75 @@ +teamSettings: + enabled: true +accountPages: + enabled: true +federator: + enabled: true + integrationTestHelper: true +tls: + useCertManager: false + +config: + dns: + https: nginz-https.integration.example.com + ssl: nginz-ssl.integration.example.com + webapp: webapp.integration.example.com + fakeS3: assets.integration.example.com + teamSettings: teams.integration.example.com + accountPages: account.integration.example.com + # federator: dynamically set by hack/bin/integration-setup.sh + +secrets: + # self-signed certificates generated using wire-server/hack/bin/selfsigned.sh + # Note: currently these certificates are untrustable and don't match the domain queried. + # FUTUREWORK(federation): generate certificates on-the-fly valid for the respective federation domain, i.e. + # federator.$NAMESPACE.svc.cluster.local or *.$NAMESPACE.svc.cluster.local + # and find a way to add the CA cert to the local trust store when making requests. + # This can probably be built on top of the certificates generated with wire-server/hack/bin/selfsigned.sh + tlsWildcardCert: | + -----BEGIN CERTIFICATE----- + MIIDFDCCAfygAwIBAgIUaSFDTpHbxVsmWDkcLj3jqQevbBswDQYJKoZIhvcNAQEL + BQAwIjEgMB4GA1UEAxMXaW50ZWdyYXRpb24uZXhhbXBsZS5jb20wHhcNMjEwMzAz + MTUyOTAwWhcNMjYwMzAyMTUyOTAwWjAiMSAwHgYDVQQDExdpbnRlZ3JhdGlvbi5l + eGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL0MPFET + VlPRAH22BxnWfqExFxFeBsB1IMJu2B0OALhSX+hUzNyTCVEXwQcwyh9T2e86D8Q3 + hoh5V0PoCuBP36KMdq7duiJdq5nZOh1wtlB7xrEObiUAstrd+r0yhSpBHi1BMGFL + YZL4OrBiQ7JzU6haWx+7Wq1upuqYKaB6ZcceqMoUyunrtEX/a1KlzMimq8FE5zjs + XyVUPt759wJNetiEz02Jc17rOzXGRafwEzF14iAAkuJGlZ6BugDLBSULk4QScYwv + xP+RrUHPIfyDVRfIjlM+wTp7sCbIy7Gkf8qgVyQnCFl4Axcmf1N+NF1/AysVCK2T + Inq/XvqNEbIwDvsCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF + MAMBAf8wHQYDVR0OBBYEFLQmruxKfRnOVVXfqNFO9SvfrGY6MA0GCSqGSIb3DQEB + CwUAA4IBAQAaxADjN+EIlkWzA43EpTZU4OSwzmlWyMe84n5FHM+wHAqDYggfb5vP + os88BwM+0ljoz6hcADBd1JHBlF15XzkpNmdz+9q6Y8RRmuJxZKUkml+GGLvE6Lbg + RGiv/XhsMgvbs3NTyUUUuyxGazRMqlrdrKVxDMCCuIYOA9m0CooZ/S8u3E2noDzA + 36bbcfKOlLcFtrnvVGpaSzb9/Ws1Trsj37HClsx3AybySRw8qb9IvVW7SLSKl/78 + iV+xnuiLcRmeMSuzZt5XPPSg0oblKTihiOpHfu5UNinvo/fBXJ4vqvh9eENyM0Rj + dyZ/xC223/Q8bUkv23LmQ/UDR4ljcYPa + -----END CERTIFICATE----- + tlsWildcardKey: | + -----BEGIN RSA PRIVATE KEY----- + MIIEpAIBAAKCAQEAvQw8URNWU9EAfbYHGdZ+oTEXEV4GwHUgwm7YHQ4AuFJf6FTM + 3JMJURfBBzDKH1PZ7zoPxDeGiHlXQ+gK4E/foox2rt26Il2rmdk6HXC2UHvGsQ5u + JQCy2t36vTKFKkEeLUEwYUthkvg6sGJDsnNTqFpbH7tarW6m6pgpoHplxx6oyhTK + 6eu0Rf9rUqXMyKarwUTnOOxfJVQ+3vn3Ak162ITPTYlzXus7NcZFp/ATMXXiIACS + 4kaVnoG6AMsFJQuThBJxjC/E/5GtQc8h/INVF8iOUz7BOnuwJsjLsaR/yqBXJCcI + WXgDFyZ/U340XX8DKxUIrZMier9e+o0RsjAO+wIDAQABAoIBAQCqgLKV3P7rMYFj + 4ByfbRlggEnx2//y7LDTK+22pr53f7QIcxMhjWvFu8rYlWR2xMW2QYOe0QWBaQ8P + q+TDUsa8cDtKk6gg/qKaa5VCoDmOdVRKtF0a+npVdAeFRF9eMMTqw/TCi55BU6h7 + FOVBuUomePfesreh3D6nLv28QygwYSTgbYClAdqGaNo6DhoD5jQ3ELWwOIjLgW+u + GPd4k+88Te6Vj8gD3OZqQFBcFXFvLNpy5L5gloD6gJfNqmz2Qw83+TfGe4uZnufN + k5HDgDde7UImFKLl7JQ0ZQ1nrQhwAeeqJizH7278pIs1qxDpgsPJ0ud8OoVtSLEL + er/eBt9BAoGBAOsJmStv9XlKIipoO+hhOQ3d0t+51QU7B8OvAT3XFNsWeSabYy52 + YuOsXwMF3wk/d6ek/u+FEF86IMI102EBFPoinbfxRWwTfE5qege8JgRs9STanZZr + Ys6U0p10blftPS76vd8A/OpWbdHUi60vyEgUguKo+TjZEB9riYJR4heDAoGBAM3o + mHIgyIOJWpdwBEAt126+ZvkhcbXAMPTf7k3+mLjpZq9rfbcKtZU7YW2T/dHYxsQ+ + aOb9+gnmLq7di+zUdlvd14sgJEDRtIWcVC/tlLAt7swDpPT9JbEcL6eTJX1EyROh + 4B0+gssWh8E39c73tzFkiRl/DrSRfpvuPvwmz5kpAoGAZqT5dIPfk0mx5A1DZHfZ + H9opNrWEd1VRTb9G7ofYvtlwrVCdHvRquX1UvRA6WGKUUe13vIjDHqNXHRm+p5V+ + YMLvWB6RL+LOnbxYcLpVbAddg+vJeKCLNSa/WC455kJgPv0YIKTgz0JRkZqeKVM9 + x2TVyED9HjuFlAM1uWkjMRsCgYEAjPdXHpMpEzw+o/yRPGrl2TBLCPYHhflsyshf + uk+5uKY5oZDCgUS4qdD8U2uE0lxJP+LGKJXpz0sh3J9aAyo1WZFX1iyMBUBMCUjM + Lf/F0pOvr0YzcXG5kzYLvfq0KL2lt2YUK5E3M9hZ2kL4atgWN59vaOAebipJdnE1 + 96SObXkCgYAAw7VFQXsmvJaZNxbfFfVc8SPzT0JuYlfeB5XHZ0SzxD4oJfePpgYJ + ZosmZOqR2C0ZCQ290pnf4b6eW0qooNN4DhfrswUecYifxGO2JqJz3mBUD46lT2Q1 + CvYZq7JCfRRW0AaoSmK5uFr4CGg9rMNew8B2EizrWRazghu4dC80cg== + -----END RSA PRIVATE KEY----- diff --git a/services/federator/src/Federator/Remote.hs b/services/federator/src/Federator/Remote.hs index 2da6d24eb4e..9a170a59603 100644 --- a/services/federator/src/Federator/Remote.hs +++ b/services/federator/src/Federator/Remote.hs @@ -56,8 +56,6 @@ interpretRemote = interpret $ \case . Log.field "error" (show err) pure $ Left (RemoteErrorDiscoveryFailure err vDomain) Right target -> do - -- FUTUREWORK(federation): Make this use TLS, maybe make it configurable - -- FUTUREWORK(federation): Cache this client and use it for many requests eitherClient <- mkGrpcClient target case eitherClient of Right client -> @@ -68,9 +66,20 @@ callInward :: MonadIO m => GrpcClient -> Request -> m (GRpcReply Response) callInward client request = liftIO $ gRpcCall @'MsgProtoBuf @Inward @"Inward" @"call" client request +-- FUTUREWORK(federation): Make this use TLS with real certificate validation +-- FUTUREWORK(federation): Allow a configurable trust store to be used in TLS certificate validation +-- See also https://github.com/lucasdicioccio/http2-client/issues/76 +-- FUTUREWORK(federation): Cache this client and use it for many requests mkGrpcClient :: Members '[Embed IO, TinyLog] r => SrvTarget -> Sem r (Either RemoteError GrpcClient) mkGrpcClient target@(SrvTarget host port) = do - let cfg = grpcClientConfigSimple (cs host) (fromInteger $ toInteger port) False + -- FUTUREWORK(federation): grpcClientConfigSimple using TLS is INSECURE and IGNORES any certificates and there's no way + -- to change that (at least not when using the default functions from mu or http2-grpc-client) + -- See https://github.com/haskell-grpc-native/http2-grpc-haskell/issues/47 + -- While early testing, this is "convenient" but needs to be fixed! + let cfg = grpcClientConfigSimple (cs host) (fromInteger $ toInteger port) True + -- Note: setupGrpcClient' is unsafe and throws exceptions in IO, e.g. when it can't connect. Don't be fooled by the Either, + -- errors appear to never happen in the left side so this is dead code. + -- FUTUREWORK(federation): report setupGrpcClient' buggy behaviour to upstream. eitherClient <- setupGrpcClient' cfg case eitherClient of Left err -> do diff --git a/services/nginz/Dockerfile b/services/nginz/Dockerfile index fe83c8e6473..0b378bf41bf 100644 --- a/services/nginz/Dockerfile +++ b/services/nginz/Dockerfile @@ -33,6 +33,7 @@ ENV CONFIG --prefix=/etc/nginx \ --user=nginx \ --group=nginx \ --with-http_ssl_module \ + --with-http_v2_module \ --with-http_stub_status_module \ --with-http_realip_module \ --with-http_gunzip_module \ diff --git a/services/nginz/Makefile b/services/nginz/Makefile index ca1c53abb8e..b2cf4569c76 100644 --- a/services/nginz/Makefile +++ b/services/nginz/Makefile @@ -50,6 +50,7 @@ CONFIG_OPTIONS = \ ADDITIONAL_MODULES = \ --with-http_ssl_module \ + --with-http_v2_module \ --with-http_stub_status_module \ --with-http_realip_module \ --with-http_gunzip_module \ diff --git a/shell.nix b/shell.nix index e29276bc227..c0acac564ee 100644 --- a/shell.nix +++ b/shell.nix @@ -63,6 +63,9 @@ in pkgs.mkShell { pkgs.gnumake pkgs.haskell-language-server pkgs.telepresence + pkgs.jq + pkgs.grpcurl + pkgs.cfssl pinned.stack pinned.helm From 80b34f1f71f76ba371bab94e2689da60fcf1d910 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Wed, 17 Mar 2021 19:46:56 +0000 Subject: [PATCH 11/18] Add spar-migrate-data executable (#1400) --- .../spar/migrate-data}/src/Main.hs | 11 +- .../src/Spar/DataMigration}/Options.hs | 34 +-- .../src/Spar/DataMigration/RIO.hs | 38 ++++ .../src/Spar/DataMigration/Run.hs | 113 ++++++++++ .../src/Spar/DataMigration/Types.hs | 70 +++++++ .../src/Spar/DataMigration/V1_ExternalIds.hs | 196 ++++++++++++++++++ services/spar/package.yaml | 9 + services/spar/schema/src/Main.hs | 4 +- services/spar/schema/src/V13.hs | 38 ++++ services/spar/spar.cabal | 91 +++++++- services/spar/src/Spar/Data.hs | 2 +- stack.yaml | 1 - tools/db/migrate-external-ids/Makefile | 27 --- tools/db/migrate-external-ids/README.md | 3 - .../migrate-external-ids.cabal | 84 -------- tools/db/migrate-external-ids/package.yaml | 51 ----- tools/db/migrate-external-ids/src/Work.hs | 159 -------------- 17 files changed, 563 insertions(+), 368 deletions(-) rename {tools/db/migrate-external-ids => services/spar/migrate-data}/src/Main.hs (88%) rename {tools/db/migrate-external-ids/src => services/spar/migrate-data/src/Spar/DataMigration}/Options.hs (77%) create mode 100644 services/spar/migrate-data/src/Spar/DataMigration/RIO.hs create mode 100644 services/spar/migrate-data/src/Spar/DataMigration/Run.hs create mode 100644 services/spar/migrate-data/src/Spar/DataMigration/Types.hs create mode 100644 services/spar/migrate-data/src/Spar/DataMigration/V1_ExternalIds.hs create mode 100644 services/spar/schema/src/V13.hs delete mode 100644 tools/db/migrate-external-ids/Makefile delete mode 100644 tools/db/migrate-external-ids/README.md delete mode 100644 tools/db/migrate-external-ids/migrate-external-ids.cabal delete mode 100644 tools/db/migrate-external-ids/package.yaml delete mode 100644 tools/db/migrate-external-ids/src/Work.hs diff --git a/tools/db/migrate-external-ids/src/Main.hs b/services/spar/migrate-data/src/Main.hs similarity index 88% rename from tools/db/migrate-external-ids/src/Main.hs rename to services/spar/migrate-data/src/Main.hs index b3e10848400..9d31fcfb8c1 100644 --- a/tools/db/migrate-external-ids/src/Main.hs +++ b/services/spar/migrate-data/src/Main.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE OverloadedStrings #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2020 Wire Swiss GmbH @@ -17,13 +15,10 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Main - ( main, - ) -where +module Main where import Imports -import qualified Work +import qualified Spar.DataMigration.Run as Run main :: IO () -main = Work.main +main = Run.main diff --git a/tools/db/migrate-external-ids/src/Options.hs b/services/spar/migrate-data/src/Spar/DataMigration/Options.hs similarity index 77% rename from tools/db/migrate-external-ids/src/Options.hs rename to services/spar/migrate-data/src/Spar/DataMigration/Options.hs index aa3c0850ff1..6d8b596f8bc 100644 --- a/tools/db/migrate-external-ids/src/Options.hs +++ b/services/spar/migrate-data/src/Spar/DataMigration/Options.hs @@ -1,6 +1,3 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TemplateHaskell #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2020 Wire Swiss GmbH @@ -18,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Options +module Spar.DataMigration.Options ( setCasSpar, setCasBrig, setDebug, @@ -28,8 +25,6 @@ module Options cPort, cKeyspace, settingsParser, - Debug (..), - DryRun (..), ) where @@ -38,32 +33,7 @@ import Control.Lens import Data.Text.Strict.Lens import Imports import Options.Applicative - -data MigratorSettings = MigratorSettings - { _setCasSpar :: !CassandraSettings, - _setCasBrig :: !CassandraSettings, - _setDebug :: Debug, - _setDryRun :: DryRun, - _setPageSize :: Int32 - } - deriving (Show) - -data CassandraSettings = CassandraSettings - { _cHosts :: !String, - _cPort :: !Word16, - _cKeyspace :: !C.Keyspace - } - deriving (Show) - -data Debug = Debug | NoDebug - deriving (Show) - -data DryRun = DryRun | NoDryRun - deriving (Show) - -makeLenses ''MigratorSettings - -makeLenses ''CassandraSettings +import Spar.DataMigration.Types settingsParser :: Parser MigratorSettings settingsParser = diff --git a/services/spar/migrate-data/src/Spar/DataMigration/RIO.hs b/services/spar/migrate-data/src/Spar/DataMigration/RIO.hs new file mode 100644 index 00000000000..5e57556c78a --- /dev/null +++ b/services/spar/migrate-data/src/Spar/DataMigration/RIO.hs @@ -0,0 +1,38 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2020 Wire Swiss GmbH +-- +-- 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 . + +module Spar.DataMigration.RIO where + +import Imports + +newtype RIO env a = RIO {unRIO :: ReaderT env IO a} + deriving newtype (Functor, Applicative, Monad, MonadIO, MonadReader env) + +runRIO :: env -> RIO env a -> IO a +runRIO e f = runReaderT (unRIO f) e + +modifyRef :: (env -> IORef a) -> (a -> a) -> RIO env () +modifyRef get_ mod' = do + ref <- asks get_ + liftIO (modifyIORef ref mod') + +readRef :: (env -> IORef b) -> RIO env b +readRef g = do + ref <- asks g + liftIO $ readIORef ref diff --git a/services/spar/migrate-data/src/Spar/DataMigration/Run.hs b/services/spar/migrate-data/src/Spar/DataMigration/Run.hs new file mode 100644 index 00000000000..00e7be6f81b --- /dev/null +++ b/services/spar/migrate-data/src/Spar/DataMigration/Run.hs @@ -0,0 +1,113 @@ +{-# LANGUAGE RecordWildCards #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2020 Wire Swiss GmbH +-- +-- 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 . + +module Spar.DataMigration.Run where + +import qualified Cassandra as C +import qualified Cassandra.Settings as C +import Control.Lens +import Control.Monad.Catch (finally) +import qualified Data.Text as Text +import Data.Time (UTCTime, getCurrentTime) +import Imports +import qualified Options.Applicative as Opts +import Spar.DataMigration.Options (settingsParser) +import Spar.DataMigration.Types +import qualified System.Logger as Log + +main :: IO () +main = do + settings <- Opts.execParser (Opts.info (Opts.helper <*> settingsParser) desc) + migrate + settings + [] + where + desc = Opts.header "Spar Cassandra Data Migrations" <> Opts.fullDesc + +migrate :: MigratorSettings -> [Migration] -> IO () +migrate settings ms = do + env <- mkEnv settings + runMigrations env ms `finally` cleanup env + +mkEnv :: MigratorSettings -> IO Env +mkEnv settings = do + lgr <- initLogger settings + spar <- initCassandra (settings ^. setCasSpar) lgr + brig <- initCassandra (settings ^. setCasBrig) lgr + pure $ Env spar brig lgr (settings ^. setPageSize) (settings ^. setDebug) (settings ^. setDryRun) + where + initLogger s = + Log.new + . Log.setOutput Log.StdOut + . Log.setFormat Nothing + . Log.setBufSize 0 + . Log.setLogLevel + (if s ^. setDebug == Debug then Log.Debug else Log.Info) + $ Log.defSettings + initCassandra cas l = + C.init + . C.setLogger (C.mkLogger l) + . C.setContacts (cas ^. cHosts) [] + . C.setPortNumber (fromIntegral $ cas ^. cPort) + . C.setKeyspace (cas ^. cKeyspace) + . C.setProtocolVersion C.V4 + $ C.defSettings + +cleanup :: (MonadIO m) => Env -> m () +cleanup env = do + C.shutdown (sparCassandra env) + C.shutdown (brigCassandra env) + Log.close (logger env) + +runMigrations :: Env -> [Migration] -> IO () +runMigrations env migrations = do + vmax <- latestMigrationVersion env + let pendingMigrations = filter (\m -> version m > vmax) migrations + if null pendingMigrations + then info env "No new migrations." + else info env "New migrations found." + mapM_ (runMigration env) pendingMigrations + +runMigration :: Env -> Migration -> IO () +runMigration env@Env {..} (Migration ver txt mig) = do + info env $ "Running: [" <> show (migrationVersion ver) <> "] " <> Text.unpack txt + mig env + unless (dryRun == DryRun) $ + persistVersion env ver txt =<< liftIO getCurrentTime + +latestMigrationVersion :: Env -> IO MigrationVersion +latestMigrationVersion Env {..} = + MigrationVersion . maybe 0 fromIntegral + <$> C.runClient + sparCassandra + (C.query1 cql (C.params C.Quorum ())) + where + cql :: C.QueryString C.R () (Identity Int32) + cql = "select version from data_migration where id=1 order by version desc limit 1" + +persistVersion :: Env -> MigrationVersion -> Text -> UTCTime -> IO () +persistVersion Env {..} (MigrationVersion v) desc time = + C.runClient sparCassandra $ + C.write cql (C.params C.Quorum (fromIntegral v, desc, time)) + where + cql :: C.QueryString C.W (Int32, Text, UTCTime) () + cql = "insert into data_migration (id, version, descr, date) values (1,?,?,?)" + +info :: Env -> String -> IO () +info Env {..} msg = Log.info logger $ Log.msg $ msg diff --git a/services/spar/migrate-data/src/Spar/DataMigration/Types.hs b/services/spar/migrate-data/src/Spar/DataMigration/Types.hs new file mode 100644 index 00000000000..1e2f15de20c --- /dev/null +++ b/services/spar/migrate-data/src/Spar/DataMigration/Types.hs @@ -0,0 +1,70 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2020 Wire Swiss GmbH +-- +-- 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 . + +module Spar.DataMigration.Types where + +import qualified Cassandra as C +import Control.Lens +import Imports +import Numeric.Natural (Natural) +import qualified System.Logger as Logger + +data Migration = Migration + { version :: MigrationVersion, + text :: Text, + action :: Env -> IO () + } + +newtype MigrationVersion = MigrationVersion {migrationVersion :: Natural} + deriving (Show, Eq, Ord) + +data Env = Env + { sparCassandra :: C.ClientState, + brigCassandra :: C.ClientState, + logger :: Logger.Logger, + pageSize :: Int32, + debug :: Debug, + dryRun :: DryRun + } + +data Debug = Debug | NoDebug + deriving (Show, Eq) + +data DryRun = DryRun | NoDryRun + deriving (Show, Eq) + +data MigratorSettings = MigratorSettings + { _setCasSpar :: !CassandraSettings, + _setCasBrig :: !CassandraSettings, + _setDebug :: Debug, + _setDryRun :: DryRun, + _setPageSize :: Int32 + } + deriving (Show) + +data CassandraSettings = CassandraSettings + { _cHosts :: !String, + _cPort :: !Word16, + _cKeyspace :: !C.Keyspace + } + deriving (Show) + +makeLenses ''MigratorSettings + +makeLenses ''CassandraSettings diff --git a/services/spar/migrate-data/src/Spar/DataMigration/V1_ExternalIds.hs b/services/spar/migrate-data/src/Spar/DataMigration/V1_ExternalIds.hs new file mode 100644 index 00000000000..70d5898018c --- /dev/null +++ b/services/spar/migrate-data/src/Spar/DataMigration/V1_ExternalIds.hs @@ -0,0 +1,196 @@ +{-# LANGUAGE RecordWildCards #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2020 Wire Swiss GmbH +-- +-- 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 . + +module Spar.DataMigration.V1_ExternalIds where + +import Cassandra +import qualified Cassandra as C +import Control.Lens +import Data.Conduit +import qualified Data.Conduit.Combinators as CC +import Data.Conduit.Internal (zipSources) +import qualified Data.Conduit.List as CL +import Data.Id +import qualified Data.Map.Strict as Map +import Imports +import Spar.DataMigration.RIO (RIO (..), modifyRef, readRef, runRIO) +import Spar.DataMigration.Types hiding (logger) +import qualified Spar.DataMigration.Types as Types +import System.Logger (Logger) +import qualified System.Logger as Log + +migration :: Migration +migration = + Migration + { version = MigrationVersion 1, + text = "Backfill spar.scim_external", + action = migrationAction + } + +type LegacyExternalId = (Text, UserId) + +type UserTeam = (UserId, Maybe TeamId) + +type NewExternalId = (TeamId, Text, UserId) + +data ResolveTeamResult + = UserHasNoTeam UserId Text + | NewExternalId NewExternalId + +--------------------------------------------------------------------------------- + +class HasMigEnv env where + migEnv :: env -> Env + +askMigEnv :: HasMigEnv env => RIO env Env +askMigEnv = asks migEnv + +class HasFailCount env where + failCount :: env -> IORef Int32 + +class HasLogger env where + logger :: env -> Logger + +logDebug :: HasLogger env => String -> RIO env () +logDebug msg = do + log_ :: Logger <- asks logger + Log.debug log_ (Log.msg msg) + +logWarn :: HasLogger env => String -> RIO env () +logWarn msg = do + log_ :: Logger <- asks logger + Log.warn log_ (Log.msg msg) + +class HasSpar env where + sparClientState :: env -> C.ClientState + +runSpar :: HasSpar env => Client a -> RIO env a +runSpar cl = do + cs <- asks sparClientState + runClient cs cl + +class HasBrig env where + brigClientState :: env -> C.ClientState + +runBrig :: HasBrig env => Client a -> RIO env a +runBrig cl = do + cs <- asks brigClientState + runClient cs cl + +--------------------------------------------------------------------------------- + +data V1Env = V1Env {v1FailCount :: IORef Int32, migrationEnv :: Env} + +instance HasSpar V1Env where + sparClientState = sparCassandra . migrationEnv + +instance HasBrig V1Env where + brigClientState = brigCassandra . migrationEnv + +instance HasLogger V1Env where + logger = Types.logger . migrationEnv + +instance HasMigEnv V1Env where + migEnv = migrationEnv + +instance HasFailCount V1Env where + failCount = v1FailCount + +migrationAction :: Env -> IO () +migrationAction migrationEnv = do + v1FailCount <- newIORef 0 + let v1env = V1Env {..} + runRIO v1env migrationMain + +migrationMain :: + ( HasSpar env, + HasBrig env, + HasLogger env, + HasMigEnv env, + HasFailCount env + ) => + RIO env () +migrationMain = do + runConduit $ + zipSources + (CL.sourceList [(1 :: Int32) ..]) + readLegacyExternalIds + .| CC.mapM resolveTeam + .| sink + + count <- readRef failCount + when (count > 0) $ + logWarn (show count <> " external ids have *NOT* been migrated.\nAn external id fails to be migrated if the mapped user doesn't exist or doesn't have a team.") + +readLegacyExternalIds :: (HasSpar env, HasMigEnv env) => ConduitM () [LegacyExternalId] (RIO env) () +readLegacyExternalIds = do + pSize <- lift $ pageSize <$> askMigEnv + transPipe runSpar $ + paginateC select (paramsP Quorum () pSize) x5 + where + select :: PrepQuery R () LegacyExternalId + select = "SELECT external, user FROM scim_external_ids" + +resolveTeam :: (HasLogger env, HasBrig env) => (Int32, [LegacyExternalId]) -> RIO env [ResolveTeamResult] +resolveTeam (page, exts) = do + userToTeam <- Map.fromList <$> readUserTeam (fmap snd exts) + logDebug $ "Page " <> show page + pure $ + exts <&> \(extid, uid) -> + case uid `Map.lookup` userToTeam of + Just (Just tid) -> NewExternalId (tid, extid, uid) + _ -> UserHasNoTeam uid extid + where + readUserTeam :: HasBrig env => [UserId] -> RIO env [UserTeam] + readUserTeam uids = + runBrig $ do + query select (params Quorum (Identity uids)) + where + select :: PrepQuery R (Identity [UserId]) UserTeam + select = "SELECT id, team FROM user where id in ?" + +sink :: + ( HasSpar env, + HasLogger env, + HasMigEnv env, + HasFailCount env + ) => + ConduitM [ResolveTeamResult] Void (RIO env) () +sink = go + where + go = do + mbResult <- await + for_ mbResult $ \results -> do + for_ results $ \case + UserHasNoTeam uid extid -> do + lift $ do + modifyRef failCount (+ 1) + dbg <- debug <$> askMigEnv + when (dbg == Debug) $ + logDebug ("No team for user " <> show uid <> " from extid " <> show extid) + NewExternalId (tid, extid, uid) -> + lift $ + dryRun <$> askMigEnv >>= \case + DryRun -> pure () + NoDryRun -> + runSpar $ + write insert (params Quorum (tid, extid, uid)) + go + insert :: PrepQuery W (TeamId, Text, UserId) () + insert = "INSERT INTO scim_external (team, external_id, user) VALUES (?, ?, ?)" diff --git a/services/spar/package.yaml b/services/spar/package.yaml index 2f6ab38c02c..7c022c8cc78 100644 --- a/services/spar/package.yaml +++ b/services/spar/package.yaml @@ -125,6 +125,15 @@ executables: dependencies: - spar + spar-migrate-data: + main: Main.hs + source-dirs: migrate-data/src + ghc-options: -threaded -rtsopts -with-rtsopts=-N + dependencies: + - spar + - conduit + - cql + spar-integration: main: Spec.hs source-dirs: diff --git a/services/spar/schema/src/Main.hs b/services/spar/schema/src/Main.hs index 02eea23c361..a62657f3365 100644 --- a/services/spar/schema/src/Main.hs +++ b/services/spar/schema/src/Main.hs @@ -27,6 +27,7 @@ import qualified V1 import qualified V10 import qualified V11 import qualified V12 +import qualified V13 import qualified V2 import qualified V3 import qualified V4 @@ -57,7 +58,8 @@ main = do V9.migration, V10.migration, V11.migration, - V12.migration + V12.migration, + V13.migration -- When adding migrations here, don't forget to update -- 'schemaVersion' in Spar.Data diff --git a/services/spar/schema/src/V13.hs b/services/spar/schema/src/V13.hs new file mode 100644 index 00000000000..89bf0e90aba --- /dev/null +++ b/services/spar/schema/src/V13.hs @@ -0,0 +1,38 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2020 Wire Swiss GmbH +-- +-- 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 . + +module V13 + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = Migration 13 "Create table `data_migration`" $ do + schema' + [r| + CREATE TABLE data_migration ( + id int, + version int, + descr text, + date timestamp, + PRIMARY KEY (id, version) + ); + |] diff --git a/services/spar/spar.cabal b/services/spar/spar.cabal index 175d8ae933d..47afdf5f0a4 100644 --- a/services/spar/spar.cabal +++ b/services/spar/spar.cabal @@ -4,7 +4,7 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: d5476ea0ce8d09ebda5cea51897773fb0372e5cae747e17a5dc89f7460c75604 +-- hash: fbd5d150ee2941c2fd21350f666dd90c23a8caca64a688b5d5dd100f31e5ce7a name: spar version: 0.1 @@ -310,6 +310,94 @@ executable spar-integration , zauth default-language: Haskell2010 +executable spar-migrate-data + main-is: Main.hs + other-modules: + Spar.DataMigration.Options + Spar.DataMigration.RIO + Spar.DataMigration.Run + Spar.DataMigration.Types + Spar.DataMigration.V1_ExternalIds + Paths_spar + hs-source-dirs: + migrate-data/src + default-extensions: AllowAmbiguousTypes BangPatterns ConstraintKinds DataKinds DefaultSignatures DerivingStrategies DeriveFunctor DeriveGeneric DeriveLift DeriveTraversable EmptyCase FlexibleContexts FlexibleInstances FunctionalDependencies GADTs InstanceSigs KindSignatures LambdaCase MultiParamTypeClasses MultiWayIf NamedFieldPuns NoImplicitPrelude OverloadedStrings PackageImports PatternSynonyms PolyKinds QuasiQuotes RankNTypes ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeFamilyDependencies TypeOperators UndecidableInstances ViewPatterns + ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -j -Wno-redundant-constraints -Werror -threaded -rtsopts -with-rtsopts=-N + build-depends: + HsOpenSSL + , aeson + , aeson-pretty + , aeson-qq + , attoparsec + , base + , base64-bytestring + , bilge + , binary + , brig-types + , bytestring + , bytestring-conversion + , case-insensitive + , cassandra-util + , conduit + , containers + , cookie + , cql + , cryptonite + , data-default + , email-validate + , errors + , exceptions + , extended + , extra + , galley-types + , ghc-prim + , hscim + , http-api-data + , http-client + , http-client-tls + , http-media + , http-types + , imports + , insert-ordered-containers + , interpolate + , lens + , memory + , metrics-core + , metrics-wai + , mtl + , network-uri + , optparse-applicative + , prometheus-client + , raw-strings-qq + , retry + , saml2-web-sso >=0.18 + , scientific + , servant + , servant-multipart + , servant-server + , servant-swagger + , spar + , string-conversions + , swagger2 + , text + , text-latin1 + , time + , tinylog + , transformers + , types-common + , unordered-containers + , uri-bytestring + , uuid + , wai + , wai-middleware-prometheus + , wai-utilities + , warp + , wire-api + , x509 + , xml-conduit + , yaml + default-language: Haskell2010 + executable spar-schema main-is: Main.hs other-modules: @@ -318,6 +406,7 @@ executable spar-schema V10 V11 V12 + V13 V2 V3 V4 diff --git a/services/spar/src/Spar/Data.hs b/services/spar/src/Spar/Data.hs index 37ec5a000dd..a46e8c675d6 100644 --- a/services/spar/src/Spar/Data.hs +++ b/services/spar/src/Spar/Data.hs @@ -110,7 +110,7 @@ import qualified Prelude -- | A lower bound: @schemaVersion <= whatWeFoundOnCassandra@, not @==@. schemaVersion :: Int32 -schemaVersion = 12 +schemaVersion = 13 ---------------------------------------------------------------------- -- helpers diff --git a/stack.yaml b/stack.yaml index 93ff143c09d..636e0f67ec1 100644 --- a/stack.yaml +++ b/stack.yaml @@ -45,7 +45,6 @@ packages: - tools/db/find-undead - tools/db/move-team - tools/db/repair-handles -- tools/db/migrate-external-ids - tools/makedeb - tools/rex - tools/stern diff --git a/tools/db/migrate-external-ids/Makefile b/tools/db/migrate-external-ids/Makefile deleted file mode 100644 index 250b26ea1df..00000000000 --- a/tools/db/migrate-external-ids/Makefile +++ /dev/null @@ -1,27 +0,0 @@ -LANG := en_US.UTF-8 - -SHELL := /usr/bin/env bash - -default: all - -all: install - -.PHONY: init -init: - mkdir -p dist - -.PHONY: clean -clean: - stack clean - -.PHONY: compile -compile: - stack build - -.PHONY: install -install: init - stack install --pedantic --local-bin-path=dist - -.PHONY: fast -fast: init - stack install --fast --local-bin-path=dist diff --git a/tools/db/migrate-external-ids/README.md b/tools/db/migrate-external-ids/README.md deleted file mode 100644 index ced87615a13..00000000000 --- a/tools/db/migrate-external-ids/README.md +++ /dev/null @@ -1,3 +0,0 @@ -This tool fills entries in the table `spar.scim_external` (introduced in release [2021-02-16](https://github.com/wireapp/wire-server/releases/tag/v2021-02-16)) from table `spar.scim_external_ids` and `brig.user`. It is meant to be used in the migration from `spar.scim_external_ids` to `spar.scim_external`. - -Next steps after this migration: spar will stop using `spar.scim_external_ids` once `spar.scim_external` is fully populated. After that, `spar.scim_external_ids` can be removed. diff --git a/tools/db/migrate-external-ids/migrate-external-ids.cabal b/tools/db/migrate-external-ids/migrate-external-ids.cabal deleted file mode 100644 index 09c5e4b65b4..00000000000 --- a/tools/db/migrate-external-ids/migrate-external-ids.cabal +++ /dev/null @@ -1,84 +0,0 @@ -cabal-version: 1.12 - --- This file has been generated from package.yaml by hpack version 0.33.0. --- --- see: https://github.com/sol/hpack --- --- hash: a2ee5cfae0c9a0b1391da88f47fff1d23787bb3d3aa6b74ed3debed373032c2f - -name: migrate-external-ids -version: 1.0.0 -synopsis: Migrate from spar.scim_external_ids to spar.scim_external -category: Network -author: Wire Swiss GmbH -maintainer: Wire Swiss GmbH -copyright: (c) 2020 Wire Swiss GmbH -license: AGPL-3 -build-type: Simple - -library - exposed-modules: - Main - Options - Work - other-modules: - Paths_migrate_external_ids - hs-source-dirs: - src - default-extensions: AllowAmbiguousTypes BangPatterns ConstraintKinds DataKinds DefaultSignatures DerivingStrategies DeriveFunctor DeriveGeneric DeriveLift DeriveTraversable EmptyCase FlexibleContexts FlexibleInstances FunctionalDependencies GADTs InstanceSigs KindSignatures LambdaCase MultiParamTypeClasses MultiWayIf NamedFieldPuns NoImplicitPrelude OverloadedStrings PackageImports PatternSynonyms PolyKinds QuasiQuotes RankNTypes ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeFamilyDependencies TypeOperators UndecidableInstances ViewPatterns RecordWildCards - ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -funbox-strict-fields -threaded -with-rtsopts=-N -with-rtsopts=-T -rtsopts - build-depends: - attoparsec - , base - , bytestring - , bytestring-conversion - , cassandra-util - , conduit - , containers - , cql - , extended - , imports - , lens - , mtl - , optparse-applicative - , text - , time - , tinylog - , types-common - , unliftio - , uuid - , wire-api - default-language: Haskell2010 - -executable migrate-external-ids - main-is: Main.hs - other-modules: - Options - Work - Paths_migrate_external_ids - hs-source-dirs: - src - default-extensions: AllowAmbiguousTypes BangPatterns ConstraintKinds DataKinds DefaultSignatures DerivingStrategies DeriveFunctor DeriveGeneric DeriveLift DeriveTraversable EmptyCase FlexibleContexts FlexibleInstances FunctionalDependencies GADTs InstanceSigs KindSignatures LambdaCase MultiParamTypeClasses MultiWayIf NamedFieldPuns NoImplicitPrelude OverloadedStrings PackageImports PatternSynonyms PolyKinds QuasiQuotes RankNTypes ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeFamilyDependencies TypeOperators UndecidableInstances ViewPatterns RecordWildCards - ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -funbox-strict-fields -threaded -with-rtsopts=-N -with-rtsopts=-T -rtsopts - build-depends: - attoparsec - , base - , bytestring - , bytestring-conversion - , cassandra-util - , conduit - , containers - , cql - , extended - , imports - , lens - , mtl - , optparse-applicative - , text - , time - , tinylog - , types-common - , unliftio - , uuid - , wire-api - default-language: Haskell2010 diff --git a/tools/db/migrate-external-ids/package.yaml b/tools/db/migrate-external-ids/package.yaml deleted file mode 100644 index d27b10142f5..00000000000 --- a/tools/db/migrate-external-ids/package.yaml +++ /dev/null @@ -1,51 +0,0 @@ -defaults: - local: ../../../package-defaults.yaml -name: migrate-external-ids -version: '1.0.0' -synopsis: Migrate from spar.scim_external_ids to spar.scim_external -category: Network -author: Wire Swiss GmbH -maintainer: Wire Swiss GmbH -copyright: (c) 2020 Wire Swiss GmbH -license: AGPL-3 -ghc-options: -- -funbox-strict-fields -- -threaded -- -with-rtsopts=-N -- -with-rtsopts=-T -- -rtsopts - -dependencies: -- attoparsec -- base -- bytestring -- bytestring-conversion -- cassandra-util -- conduit -- containers -- cql -- extended -- imports -- lens -- mtl -- optparse-applicative -- text -- time -- tinylog -- types-common -- unliftio -- uuid -- wire-api - -library: - source-dirs: src - default-extensions: - - RecordWildCards - -executables: - migrate-external-ids: - main: Main.hs - source-dirs: src - - default-extensions: - - RecordWildCards diff --git a/tools/db/migrate-external-ids/src/Work.hs b/tools/db/migrate-external-ids/src/Work.hs deleted file mode 100644 index 5ac3d1e7f70..00000000000 --- a/tools/db/migrate-external-ids/src/Work.hs +++ /dev/null @@ -1,159 +0,0 @@ -{-# OPTIONS_GHC -Wno-orphans -Wno-unused-imports -Wno-name-shadowing #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2020 Wire Swiss GmbH --- --- 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 . - -module Work where - -import Cassandra -import qualified Cassandra as C -import qualified Cassandra.Settings as C -import Control.Lens -import Data.Conduit -import qualified Data.Conduit.Combinators as CC -import Data.Conduit.Internal (zipSources) -import qualified Data.Conduit.List as CL -import Data.Functor.Identity -import Data.Id -import qualified Data.Map.Strict as Map -import Data.Misc -import Database.CQL.Protocol (Tuple) -import Imports -import Options -import Options.Applicative -import System.Logger (Logger) -import qualified System.Logger as Log -import UnliftIO.Async (pooledMapConcurrentlyN) -import Wire.API.Team.Feature - -data Env = Env - { envBrig :: ClientState, - envSpar :: ClientState, - envLogger :: Logger, - envPageSize :: Int32, - envDebug :: Debug, - envDryDrun :: DryRun, - envFailCount :: IORef Int - } - -main :: IO () -main = do - s <- execParser (info (helper <*> settingsParser) desc) - lgr <- initLogger s - brig <- initCas (s ^. setCasBrig) lgr - spar <- initCas (s ^. setCasSpar) lgr - failCount <- newIORef 0 - let env = Env brig spar lgr (s ^. setPageSize) (s ^. setDebug) (s ^. setDryRun) failCount - runCommand env - where - desc = - header "migrate-external-ids" - <> progDesc "Migrate from spar." - <> fullDesc - initLogger s = - Log.new - . Log.setOutput Log.StdOut - . Log.setFormat Nothing - . Log.setBufSize 0 - . Log.setLogLevel - ( case s ^. setDebug of - Debug -> Log.Debug - _ -> Log.Warn - ) - $ Log.defSettings - initCas cas l = - C.init - . C.setLogger (C.mkLogger l) - . C.setContacts (cas ^. cHosts) [] - . C.setPortNumber (fromIntegral $ cas ^. cPort) - . C.setKeyspace (cas ^. cKeyspace) - . C.setProtocolVersion C.V4 - $ C.defSettings - -runCommand :: Env -> IO () -runCommand env@Env {..} = do - runConduit $ - zipSources - (CL.sourceList [(1 :: Int32) ..]) - (readLegacyExternalIds env) - .| CC.mapM (resolveTeam env) - .| sinkMain env - - failCount <- readIORef envFailCount - when (failCount > 0) $ - Log.warn envLogger (Log.msg @String (show failCount <> " external ids have *NOT* been migrated.\nAn external id fails to be migrated if the mapped user doesn't exist or doesn't have a team.")) - -type LegacyExternalId = (Text, UserId) - -type UserTeam = (UserId, Maybe TeamId) - -type NewExternalId = (TeamId, Text, UserId) - -data ResolveTeamResult - = UserHasNoTeam UserId Text - | NewExternalId NewExternalId - -readLegacyExternalIds :: Env -> ConduitM () [LegacyExternalId] IO () -readLegacyExternalIds Env {..} = - transPipe (runClient envSpar) $ - paginateC select (paramsP Quorum () envPageSize) x5 - where - select :: PrepQuery R () LegacyExternalId - select = "SELECT external, user FROM scim_external_ids" - -resolveTeam :: Env -> (Int32, [LegacyExternalId]) -> IO [ResolveTeamResult] -resolveTeam env@Env {..} (page, exts) = do - Log.info envLogger (Log.msg @String ("Processing page " <> show page)) - userToTeam <- Map.fromList <$> readUserTeam env (fmap snd exts) - pure $ - exts <&> \(extid, uid) -> - case uid `Map.lookup` userToTeam of - Just (Just tid) -> NewExternalId (tid, extid, uid) - _ -> UserHasNoTeam uid extid - where - readUserTeam :: Env -> [UserId] -> IO [UserTeam] - readUserTeam Env {..} uids = - runClient envBrig $ do - query select (params Quorum (Identity uids)) - where - select :: PrepQuery R (Identity [UserId]) UserTeam - select = "SELECT id, team FROM user where id in ?" - -isDebug :: Debug -> Bool -isDebug Debug = True -isDebug NoDebug = False - -sinkMain :: Env -> ConduitM [ResolveTeamResult] Void IO () -sinkMain Env {..} = go - where - go = do - mbResult <- await - for_ mbResult $ \results -> do - for_ results $ \case - UserHasNoTeam uid extid -> do - modifyIORef envFailCount (+ 1) - when (isDebug envDebug) $ - Log.debug envLogger (Log.msg @String ("No team for user " <> show uid <> " from extid " <> show extid)) - NewExternalId (tid, extid, uid) -> do - case envDryDrun of - DryRun -> pure () - NoDryRun -> - runClient envSpar $ - write insert (params Quorum (tid, extid, uid)) - go - insert :: PrepQuery W (TeamId, Text, UserId) () - insert = "INSERT INTO scim_external (team, external_id, user) VALUES (?, ?, ?)" From cd2351c7e758b627e71e170199830817b188446d Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Thu, 18 Mar 2021 12:30:05 +0000 Subject: [PATCH 12/18] Add spar-migrate-data deb and migration (#1413) --- services/spar/Makefile | 37 ++++++++++++------- .../src/Spar/DataMigration/Run.hs | 3 +- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/services/spar/Makefile b/services/spar/Makefile index 68494770c4a..f0eab9d7a66 100644 --- a/services/spar/Makefile +++ b/services/spar/Makefile @@ -1,17 +1,18 @@ LANG := en_US.UTF-8 -SHELL := /usr/bin/env bash -NAME := spar -VERSION ?= -BUILD_NUMBER ?= 0 -BUILD_LABEL ?= local -BUILD := $(BUILD_NUMBER)$(shell [ "${BUILD_LABEL}" == "" ] && echo "" || echo ".${BUILD_LABEL}") -EXE_IT := ../../dist/$(NAME)-integration -EXE_SCHEMA := ../../dist/$(NAME)-schema -DEB := dist/$(NAME)_$(VERSION)+$(BUILD)_amd64.deb -DEB_SCHEMA := dist/$(NAME)-schema_$(VERSION)+$(BUILD)_amd64.deb -EXECUTABLES := $(NAME) $(NAME)-integration $(NAME)-schema -DOCKER_USER ?= quay.io/wire -DOCKER_TAG ?= local +SHELL := /usr/bin/env bash +NAME := spar +VERSION ?= +BUILD_NUMBER ?= 0 +BUILD_LABEL ?= local +BUILD := $(BUILD_NUMBER)$(shell [ "${BUILD_LABEL}" == "" ] && echo "" || echo ".${BUILD_LABEL}") +EXE_IT := ../../dist/$(NAME)-integration +EXE_SCHEMA := ../../dist/$(NAME)-schema +DEB := dist/$(NAME)_$(VERSION)+$(BUILD)_amd64.deb +DEB_SCHEMA := dist/$(NAME)-schema_$(VERSION)+$(BUILD)_amd64.deb +DEB_MIGRATE_DATA := dist/$(NAME)-migrate-data_$(VERSION)+$(BUILD)_amd64.deb +EXECUTABLES := $(NAME) $(NAME)-integration $(NAME)-schema +DOCKER_USER ?= quay.io/wire +DOCKER_TAG ?= local guard-%: @ if [ "${${*}}" = "" ]; then \ @@ -42,7 +43,7 @@ compile: stack build --fast --test --bench --no-run-benchmarks --no-copy-bins spar .PHONY: dist -dist: guard-VERSION install $(DEB) $(DEB_SCHEMA) .metadata +dist: guard-VERSION install $(DEB) $(DEB_SCHEMA) $(DEB_MIGRATE_DATA) .metadata .metadata: echo -e "NAME=$(NAME)\nVERSION=$(VERSION)\nBUILD_NUMBER=$(BUILD)" \ @@ -64,6 +65,14 @@ $(DEB_SCHEMA): install --architecture=amd64 \ --output-dir=dist +$(DEB_MIGRATE_DATA): install + makedeb --name=$(NAME)-migrate-data \ + --version=$(VERSION) \ + --debian-dir=schema/deb \ + --build=$(BUILD) \ + --architecture=amd64 \ + --output-dir=dist + .PHONY: i i: ../integration.sh bash -c "\ diff --git a/services/spar/migrate-data/src/Spar/DataMigration/Run.hs b/services/spar/migrate-data/src/Spar/DataMigration/Run.hs index 00e7be6f81b..db968e5da92 100644 --- a/services/spar/migrate-data/src/Spar/DataMigration/Run.hs +++ b/services/spar/migrate-data/src/Spar/DataMigration/Run.hs @@ -29,6 +29,7 @@ import Imports import qualified Options.Applicative as Opts import Spar.DataMigration.Options (settingsParser) import Spar.DataMigration.Types +import qualified Spar.DataMigration.V1_ExternalIds as V1 import qualified System.Logger as Log main :: IO () @@ -36,7 +37,7 @@ main = do settings <- Opts.execParser (Opts.info (Opts.helper <*> settingsParser) desc) migrate settings - [] + [V1.migration] where desc = Opts.header "Spar Cassandra Data Migrations" <> Opts.fullDesc From 9955384af1933f082bae1010ec7bdbe63123ae5e Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Thu, 18 Mar 2021 15:35:29 +0000 Subject: [PATCH 13/18] Fix debian package for spar-migrate-data (#1415) --- services/spar/Makefile | 2 +- services/spar/migrate-data/deb/DEBIAN/control | 5 +++++ .../deb/opt/spar/spar-migrate-data/spar-migrate-data | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 services/spar/migrate-data/deb/DEBIAN/control create mode 120000 services/spar/migrate-data/deb/opt/spar/spar-migrate-data/spar-migrate-data diff --git a/services/spar/Makefile b/services/spar/Makefile index f0eab9d7a66..b3846b0a816 100644 --- a/services/spar/Makefile +++ b/services/spar/Makefile @@ -68,7 +68,7 @@ $(DEB_SCHEMA): install $(DEB_MIGRATE_DATA): install makedeb --name=$(NAME)-migrate-data \ --version=$(VERSION) \ - --debian-dir=schema/deb \ + --debian-dir=migrate-data/deb \ --build=$(BUILD) \ --architecture=amd64 \ --output-dir=dist diff --git a/services/spar/migrate-data/deb/DEBIAN/control b/services/spar/migrate-data/deb/DEBIAN/control new file mode 100644 index 00000000000..37be452be5a --- /dev/null +++ b/services/spar/migrate-data/deb/DEBIAN/control @@ -0,0 +1,5 @@ +Package: spar-migrate-data +Version: <>+<> +Maintainer: Wire Swiss GmbH +Architecture: amd64 +Description: Run data migrations for Spar diff --git a/services/spar/migrate-data/deb/opt/spar/spar-migrate-data/spar-migrate-data b/services/spar/migrate-data/deb/opt/spar/spar-migrate-data/spar-migrate-data new file mode 120000 index 00000000000..5035b35c3eb --- /dev/null +++ b/services/spar/migrate-data/deb/opt/spar/spar-migrate-data/spar-migrate-data @@ -0,0 +1 @@ +../../../../../../../dist/spar-migrate-data \ No newline at end of file From cc6509e0bbb47aad961051115a1ed8ee4a32cd1b Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Mon, 22 Mar 2021 12:14:57 +0100 Subject: [PATCH 14/18] Handle errors which could happen while talking to remote federator (#1408) Co-authored-by: jschaul --- libs/wire-api-federation/package.yaml | 1 + libs/wire-api-federation/proto/router.proto | 36 ++++++++- .../src/Wire/API/Federation/GRPC/Client.hs | 41 ++++++++++ .../src/Wire/API/Federation/GRPC/Types.hs | 76 ++++++++++++++++--- .../Wire/API/Federation/GRPC/TypesSpec.hs | 3 +- .../wire-api-federation.cabal | 5 +- services/brig/brig.cabal | 3 +- services/brig/package.yaml | 57 +++++++------- services/brig/src/Brig/API/Error.hs | 16 +++- services/brig/src/Brig/API/Public.hs | 47 +++++++++++- services/brig/src/Brig/Federation/Client.hs | 56 ++++++++++---- services/federator/federator.cabal | 6 +- services/federator/package.yaml | 1 + .../federator/src/Federator/ExternalServer.hs | 4 +- .../federator/src/Federator/InternalServer.hs | 43 ++++++++--- services/federator/src/Federator/Remote.hs | 16 ++-- .../integration/Test/Federator/InwardSpec.hs | 12 ++- .../unit/Test/Federator/ExternalServer.hs | 6 +- .../unit/Test/Federator/InternalServer.hs | 75 +++++++++++------- stack.yaml | 10 ++- stack.yaml.lock | 40 ++++++---- 21 files changed, 416 insertions(+), 138 deletions(-) create mode 100644 libs/wire-api-federation/src/Wire/API/Federation/GRPC/Client.hs diff --git a/libs/wire-api-federation/package.yaml b/libs/wire-api-federation/package.yaml index d4d442d5320..1e9cac9e418 100644 --- a/libs/wire-api-federation/package.yaml +++ b/libs/wire-api-federation/package.yaml @@ -23,6 +23,7 @@ dependencies: - exceptions >=0.6 - http-types - http2-client +- http2-client-grpc - imports - mtl - mu-grpc-client diff --git a/libs/wire-api-federation/proto/router.proto b/libs/wire-api-federation/proto/router.proto index 6498db40abd..9d1e7c6a449 100644 --- a/libs/wire-api-federation/proto/router.proto +++ b/libs/wire-api-federation/proto/router.proto @@ -20,13 +20,43 @@ message HTTPResponse { bytes responseBody = 2; } -message Response { +message InwardResponse { oneof response { HTTPResponse httpResponse = 1; string err = 2; } } +message OutwardResponse { + oneof response { + HTTPResponse httpResponse = 1; + OutwardError err = 2; + } +} + +message OutwardError { + enum ErrorType { + RemoteNotFound = 0; + DiscoveryFailed = 1; + ConnectionRefused = 2; + TLSFailure = 3; + InvalidCertificate = 4; + VersionMismatch = 5; + FederationDeniedByRemote = 6; + FederationDeniedLocally = 7; + RemoteFederatorError = 8; + InvalidRequest = 9; + } + + ErrorType type = 1; + ErrorPayload payload = 2; +} + +message ErrorPayload { + string label = 1; + string msg = 2; +} + // The envelope message which is sent from brig to the Outward service of a local federator message FederatedRequest { string domain = 1; @@ -72,7 +102,7 @@ enum Method { // Example: 'brig' will use Outward.call on an internal server on // 127.0.0.1:8097 to (indirectly) talk to the federated world. service Outward { - rpc call (FederatedRequest) returns (Response); + rpc call (FederatedRequest) returns (OutwardResponse); } // 'Inward' is exposed to the public internet (via nginx) and serves as a @@ -82,5 +112,5 @@ service Outward { // talk to a component inside the group of servers of another-domain // which is not directly accessible. service Inward { - rpc call (Request) returns (Response); + rpc call (Request) returns (InwardResponse); } diff --git a/libs/wire-api-federation/src/Wire/API/Federation/GRPC/Client.hs b/libs/wire-api-federation/src/Wire/API/Federation/GRPC/Client.hs new file mode 100644 index 00000000000..43d76a0d3fa --- /dev/null +++ b/libs/wire-api-federation/src/Wire/API/Federation/GRPC/Client.hs @@ -0,0 +1,41 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2020 Wire Swiss GmbH +-- +-- 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 . + +module Wire.API.Federation.GRPC.Client where + +import Control.Exception +import qualified Data.Text as T +import Imports +import Mu.GRpc.Client.Record (setupGrpcClient') +import Network.GRPC.Client.Helpers + +newtype GrpcClientErr = GrpcClientErr {reason :: Text} + deriving (Show, Eq) + +-- | Note: setupGrpcClient' is unsafe and throws exceptions in IO, e.g. when it can't connect. +-- FUTUREWORK(federation): report setupGrpcClient' buggy behaviour to upstream. +createGrpcClient :: MonadIO m => GrpcClientConfig -> m (Either GrpcClientErr GrpcClient) +createGrpcClient cfg = do + res <- liftIO $ try @IOException $ setupGrpcClient' cfg + pure $ case res of + Left err -> Left (GrpcClientErr (T.pack (show err <> errorInfo))) + Right (Left err) -> Left (GrpcClientErr (T.pack (show err <> errorInfo))) + Right (Right client) -> Right client + where + errorInfo = + "Host: " <> show (_grpcClientConfigHost cfg) + <> (" Port: " <> show (_grpcClientConfigPort cfg)) diff --git a/libs/wire-api-federation/src/Wire/API/Federation/GRPC/Types.hs b/libs/wire-api-federation/src/Wire/API/Federation/GRPC/Types.hs index 1e30ee121cc..d31637a7d9a 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/GRPC/Types.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/GRPC/Types.hs @@ -59,30 +59,86 @@ data HTTPResponse = HTTPResponse deriving (Arbitrary) via (GenericUniform HTTPResponse) -- | FUTUREWORK(federation): Make this a better ADT for the errors -data Response - = ResponseHTTPResponse HTTPResponse - | ResponseErr Text +data InwardResponse + = InwardResponseHTTPResponse HTTPResponse + | InwardResponseErr Text deriving (Typeable, Show, Eq, Generic) - deriving (Arbitrary) via (GenericUniform Response) + deriving (Arbitrary) via (GenericUniform InwardResponse) -instance ToSchema Router "Response" Response where +instance ToSchema Router "InwardResponse" InwardResponse where toSchema r = let protoChoice = case r of - (ResponseHTTPResponse res) -> Z (FSchematic (toSchema res)) - (ResponseErr e) -> S (Z (FPrimitive e)) + (InwardResponseHTTPResponse res) -> Z (FSchematic (toSchema res)) + (InwardResponseErr e) -> S (Z (FPrimitive e)) in TRecord (Field (FUnion protoChoice) :* Nil) -instance FromSchema Router "Response" Response where +instance FromSchema Router "InwardResponse" InwardResponse where fromSchema (TRecord (Field (FUnion protoChoice) :* Nil)) = case protoChoice of - Z (FSchematic res) -> ResponseHTTPResponse $ fromSchema res - S (Z (FPrimitive e)) -> ResponseErr e + Z (FSchematic res) -> InwardResponseHTTPResponse $ fromSchema res + S (Z (FPrimitive e)) -> InwardResponseErr e S (S x) -> -- I don't understand why this empty case is needed, but there is some -- explanation here: -- https://github.com/well-typed/generics-sop/issues/116 case x of +data OutwardResponse + = OutwardResponseHTTPResponse HTTPResponse + | OutwardResponseError OutwardError + deriving (Typeable, Show, Eq, Generic) + deriving (Arbitrary) via (GenericUniform OutwardResponse) + +instance ToSchema Router "OutwardResponse" OutwardResponse where + toSchema r = + let protoChoice = case r of + OutwardResponseHTTPResponse res -> Z (FSchematic (toSchema res)) + OutwardResponseError err -> S (Z (FSchematic (toSchema err))) + in TRecord (Field (FUnion protoChoice) :* Nil) + +instance FromSchema Router "OutwardResponse" OutwardResponse where + fromSchema (TRecord (Field (FUnion protoChoice) :* Nil)) = + case protoChoice of + Z (FSchematic res) -> OutwardResponseHTTPResponse $ fromSchema res + S (Z (FSchematic err)) -> OutwardResponseError $ fromSchema err + S (S x) -> case x of + +type OutwardErrorFieldMapping = + '[ "outwardErrorType" ':-> "type", + "outwardErrorPayload" ':-> "payload" + ] + +data OutwardError = OutwardError + { outwardErrorType :: OutwardErrorType, + outwardErrorPayload :: Maybe ErrorPayload + } + deriving (Typeable, Show, Eq, Generic) + deriving (Arbitrary) via (GenericUniform OutwardError) + deriving + (ToSchema Router "OutwardError", FromSchema Router "OutwardError") + via (CustomFieldMapping "OutwardError" OutwardErrorFieldMapping OutwardError) + +data OutwardErrorType + = RemoteNotFound + | DiscoveryFailed + | ConnectionRefused + | TLSFailure + | InvalidCertificate + | VersionMismatch + | FederationDeniedByRemote + | FederationDeniedLocally + | RemoteFederatorError + | InvalidRequest + deriving (Typeable, Show, Eq, Generic, ToSchema Router "OutwardError.ErrorType", FromSchema Router "OutwardError.ErrorType") + deriving (Arbitrary) via (GenericUniform OutwardErrorType) + +data ErrorPayload = ErrorPayload + { label :: Text, + msg :: Text + } + deriving (Typeable, Show, Eq, Generic, ToSchema Router "ErrorPayload", FromSchema Router "ErrorPayload") + deriving (Arbitrary) via (GenericUniform ErrorPayload) + -- | This type exists to avoid orphan instances of ToSchema and FromSchema newtype HTTPMethod = HTTPMethod {unwrapMethod :: HTTP.StdMethod} deriving (Typeable, Eq, Show, Generic) diff --git a/libs/wire-api-federation/test/Test/Wire/API/Federation/GRPC/TypesSpec.hs b/libs/wire-api-federation/test/Test/Wire/API/Federation/GRPC/TypesSpec.hs index 7843e1f300b..c729f3092c5 100644 --- a/libs/wire-api-federation/test/Test/Wire/API/Federation/GRPC/TypesSpec.hs +++ b/libs/wire-api-federation/test/Test/Wire/API/Federation/GRPC/TypesSpec.hs @@ -33,7 +33,8 @@ spec = describe "Protobuf Serialization" $ do muSchemaRoundtrip @Router @"Component" @Component muSchemaRoundtrip @Router @"HTTPResponse" @HTTPResponse - muSchemaRoundtrip @Router @"Response" @Response + muSchemaRoundtrip @Router @"OutwardResponse" @OutwardResponse + muSchemaRoundtrip @Router @"InwardResponse" @InwardResponse muSchemaRoundtrip @Router @"Method" @HTTPMethod muSchemaRoundtrip @Router @"QueryParam" @QueryParam muSchemaRoundtrip @Router @"Request" @Request diff --git a/libs/wire-api-federation/wire-api-federation.cabal b/libs/wire-api-federation/wire-api-federation.cabal index d9e5c793f12..ac26ac74429 100644 --- a/libs/wire-api-federation/wire-api-federation.cabal +++ b/libs/wire-api-federation/wire-api-federation.cabal @@ -4,7 +4,7 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: ded3eec9b64106982feca5f8e2647af3a3df94928506d00e80622ec7d7596c73 +-- hash: 01600efb0e0bbba28ada06a50504371edf98d6e5e21c60aaee925634534b7ca6 name: wire-api-federation version: 0.1.0 @@ -26,6 +26,7 @@ library Wire.API.Federation.API.Brig Wire.API.Federation.API.Conversation Wire.API.Federation.Event + Wire.API.Federation.GRPC.Client Wire.API.Federation.GRPC.Helper Wire.API.Federation.GRPC.Types Wire.API.Federation.Util.Aeson @@ -47,6 +48,7 @@ library , exceptions >=0.6 , http-types , http2-client + , http2-client-grpc , imports , mtl , mu-grpc-client @@ -88,6 +90,7 @@ test-suite spec , hspec-discover , http-types , http2-client + , http2-client-grpc , imports , metrics-wai , mtl diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 510eb396967..2e3aa618662 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -4,7 +4,7 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: ed5a26f3be12829ad529ee559312e31bc37ccd967c6af5733efe14e4916e215c +-- hash: a60631234fbd3fa2d41c55e85e9d8c49eeff1b92567df91633244ee09d225955 name: brig version: 1.35.0 @@ -209,6 +209,7 @@ library , singletons >=2.0 , smtp-mail >=0.1 , sodium-crypto-sign >=0.1 + , sop-core , split >=0.2 , ssl-util , statistics >=0.13 diff --git a/services/brig/package.yaml b/services/brig/package.yaml index 455b99e3a32..3f431ac564e 100644 --- a/services/brig/package.yaml +++ b/services/brig/package.yaml @@ -19,8 +19,8 @@ library: - amazonka-ses >=1.3.7 - amazonka-sns >=1.3.7 - amazonka-sqs >=1.3.7 - - attoparsec >=0.12 - async >=2.1 + - attoparsec >=0.12 - auto-update >=0.1 - base ==4.* - base-prelude @@ -32,11 +32,11 @@ library: - bytestring >=0.10 - bytestring-conversion >=0.2 - cassandra-util >=0.16.2 - - currency-codes >=2.0 - conduit >=1.2.8 - containers >=0.5 - cookie >=0.4 - cryptobox-haskell >=0.1.1 + - currency-codes >=2.0 - data-default >=0.5 - data-timeout >=0.3 - directory >=1.2 @@ -45,38 +45,40 @@ library: - either >=4.3 - enclosed-exceptions >=1.0 - errors >=1.4 - - extra >=1.3 - exceptions >=0.5 - extended - - geoip2 >=0.3.1.0 - - galley-types >=0.75.3 - - gundeck-types >=1.32.1 - - imports + - extra >=1.3 - filepath >=1.3 - fsnotify >=0.2 - - iso639 >=0.1 + - galley-types >=0.75.3 + - geoip2 >=0.3.1.0 + - gundeck-types >=1.32.1 - hashable >=1.2 - - html-entities >=1.1 - - http-client >=0.5 - - http-types >=0.8 - - http-client-openssl >=0.2 - HaskellNet >=0.3 - HaskellNet-SSL >=0.3 - HsOpenSSL >=0.10 - HsOpenSSL-x509-system >=0.1 + - html-entities >=1.1 + - http-client >=0.5 + - http-client-openssl >=0.2 + - http-types >=0.8 + - imports - insert-ordered-containers - iproute >=1.5 + - iso639 >=0.1 - lens >=3.8 - lens-aeson >=1.0 - lifted-base >=0.2 - - mime - - mime-mail >=0.4 - metrics-core >=0.3 - metrics-wai >=0.3 + - mime + - mime-mail >=0.4 - monad-control >=1.0 - MonadRandom >=0.5 - - multihash >=0.1.3 - mtl >=2.1 + - mu-grpc-client + - mu-rpc + - multihash >=0.1.3 - mwc-random - network >=2.4 - network-conduit-tls @@ -85,32 +87,32 @@ library: - pem >=0.2 - polysemy - polysemy-wire-zoo - - proto-lens >=0.1 - prometheus-client - - resourcet >=1.1 + - proto-lens >=0.1 + - random-shuffle >=0.0.3 - resource-pool >=0.2 + - resourcet >=1.1 + - retry >=0.7 - ropes >=0.4.20 - safe >=0.3 - - scientific >=0.3.4 - - scrypt >=0.5 - - smtp-mail >=0.1 - - mu-grpc-client - - mu-rpc - - split >=0.2 - safe-exceptions >=0.1 - saml2-web-sso + - scientific >=0.3.4 + - scrypt >=0.5 - semigroups >=0.15 - servant - servant-server - servant-swagger - servant-swagger-ui - singletons >=2.0 - - stomp-queue >=0.3 - - string-conversions - - ssl-util - - random-shuffle >=0.0.3 + - smtp-mail >=0.1 - sodium-crypto-sign >=0.1 + - sop-core + - split >=0.2 + - ssl-util - statistics >=0.13 + - stomp-queue >=0.3 + - string-conversions - swagger >=0.1 - swagger2 - tagged >=0.7 @@ -124,7 +126,6 @@ library: - transformers-base >=0.4 - types-common >=0.16 - types-common-journal >=0.1 - - retry >=0.7 - unliftio >=0.2 - unliftio-core >=0.1 - unordered-containers >=0.2 diff --git a/services/brig/src/Brig/API/Error.hs b/services/brig/src/Brig/API/Error.hs index 6fe9f9123f3..59d6274875f 100644 --- a/services/brig/src/Brig/API/Error.hs +++ b/services/brig/src/Brig/API/Error.hs @@ -504,10 +504,13 @@ customerExtensionBlockedDomain domain = Wai.Error (mkStatus 451 "Unavailable For -------------------------------------------------------------------------------- -- Federation +noFederationStatus :: Status +noFederationStatus = status403 + federationNotEnabled :: forall a. Typeable a => NonEmpty (Qualified (Id (Remote a))) -> Wai.Error federationNotEnabled qualifiedIds = Wai.Error - status403 + noFederationStatus "federation-not-enabled" ("Federation is not enabled, but remote qualified IDs (" <> idType <> ") were found: " <> rendered) where @@ -517,7 +520,7 @@ federationNotEnabled qualifiedIds = federationNotImplemented :: forall a. Typeable a => NonEmpty (IdMapping a) -> Wai.Error federationNotImplemented qualified = Wai.Error - status403 + noFederationStatus "federation-not-implemented" ("Federation is not implemented, but ID mappings (" <> idType <> ") found: " <> rendered) where @@ -529,6 +532,13 @@ federationNotImplemented qualified = federationNotImplemented' :: Wai.Error federationNotImplemented' = Wai.Error - status403 + noFederationStatus "federation-not-implemented" "Federation is not yet implemented for this endpoint" + +federationNotConfigured :: Wai.Error +federationNotConfigured = + Wai.Error + noFederationStatus + "federation-not-enabled" + "no federator configured on brig" diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 72bd3955a98..cf5dd7745f5 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -75,16 +75,22 @@ import Data.Swagger ( ApiKeyLocation (..), ApiKeyParams (..), HasInfo (info), + HasProperties (properties), + HasRequired (required), HasSchema (..), HasSecurity (security), HasSecurityDefinitions (securityDefinitions), HasTitle (title), + NamedSchema (..), SecurityRequirement (..), SecurityScheme (..), SecuritySchemeType (SecuritySchemeApiKey), Swagger, + SwaggerType (SwaggerObject), ToSchema (..), + declareSchemaRef, description, + type_, ) import qualified Data.Swagger.Build.Api as Doc import qualified Data.Text as Text @@ -92,6 +98,8 @@ import qualified Data.Text.Ascii as Ascii import Data.Text.Encoding (decodeLatin1) import Data.Text.Lazy (pack) import qualified Data.ZAuth.Token as ZAuth +import GHC.TypeLits (KnownNat, KnownSymbol, Nat, Symbol, symbolVal) +import GHC.TypeNats (natVal) import Imports hiding (head) import Network.HTTP.Types.Status import Network.Wai (Response, lazyRequestBody) @@ -186,6 +194,34 @@ instance ToSchema Empty404 where type CheckUserExistsResponse = [Empty200, Empty404] +data RestError (status :: Nat) (label :: Symbol) (message :: Symbol) = RestError + deriving (Generic) + deriving (HasStatus) via (WithStatus status (RestError status "" "")) + +instance (KnownNat status, KnownSymbol label, KnownSymbol message) => ToJSON (RestError status label message) where + toJSON _ = + object + [ "code" .= natVal (Proxy @status), + "label" .= symbolVal (Proxy @label), + "message" .= symbolVal (Proxy @message) + ] + +instance ToSchema (RestError status label message) where + declareNamedSchema _ = do + natSchema <- declareSchemaRef (Proxy @Integer) + textSchema <- declareSchemaRef (Proxy @Text) + pure $ + NamedSchema (Just "Error") $ + mempty + & type_ ?~ SwaggerObject + & properties + .~ InsOrdHashMap.fromList + [ ("code", natSchema), + ("label", textSchema), + ("message", textSchema) + ] + & required .~ ["code", "label", "message"] + -- Note [document responses] -- -- Ideally we want to document responses with UVerb and swagger, but this is @@ -1417,15 +1453,20 @@ checkHandlesH (_ ::: _ ::: req) = do free <- lift $ API.checkHandles handles (fromRange num) return $ json (free :: [Handle]) +-- | This endpoint returns UserHandleInfo instead of UserProfile for backwards compatibility. getHandleInfoUnqualifiedH :: UserId -> Handle -> Handler Public.UserHandleInfo getHandleInfoUnqualifiedH self handle = do domain <- viewFederationDomain Public.UserHandleInfo . Public.profileQualifiedId <$> getUserByHandleH self domain handle +-- | This endpoint returns UserProfile instead of UserHandleInfo to reduce +-- traffic between backends in a federated scenario. getUserByHandleH :: UserId -> Domain -> Handle -> Handler Public.UserProfile -getUserByHandleH self domain handle = - ifNothing (notFound "handle not found") - =<< getHandleInfo self (Qualified handle domain) +getUserByHandleH self domain handle = do + maybeProfile <- getHandleInfo self (Qualified handle domain) + case maybeProfile of + Nothing -> throwStd handleNotFound + Just u -> pure u -- FUTUREWORK: use 'runMaybeT' to simplify this. getHandleInfo :: UserId -> Qualified Handle -> Handler (Maybe Public.UserProfile) diff --git a/services/brig/src/Brig/Federation/Client.hs b/services/brig/src/Brig/Federation/Client.hs index 1a89bdfa0bc..648c0691bf9 100644 --- a/services/brig/src/Brig/Federation/Client.hs +++ b/services/brig/src/Brig/Federation/Client.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} @@ -20,7 +21,7 @@ module Brig.Federation.Client where -import Brig.API.Error (notFound, throwStd) +import Brig.API.Error (federationNotConfigured, notFound, throwStd) import Brig.API.Handler (Handler) import Brig.App (federator) import Brig.Types.User @@ -33,9 +34,12 @@ import qualified Data.Text as T import qualified Data.Text.Lazy as LT import Imports import Mu.GRpc.Client.TyApps +import qualified Network.HTTP.Types.Status as HTTP +import qualified Network.Wai.Utilities.Error as Wai import qualified System.Logger.Class as Log import Util.Options (epHost, epPort) import Wire.API.Federation.API.Brig +import Wire.API.Federation.GRPC.Client import qualified Wire.API.Federation.GRPC.Types as Proto -- FUTUREWORK(federation): As of now, any failure in making a remote call results in 404. @@ -65,15 +69,15 @@ mkFederatorClient :: Handler GrpcClient mkFederatorClient = do maybeFederatorEndpoint <- view federator federatorEndpoint <- case maybeFederatorEndpoint of - Nothing -> throwStd $ notFound "no federator configured" + Nothing -> throwStd $ federationNotConfigured Just ep -> pure ep - - eitherClient <- setupGrpcClient' (grpcClientConfigSimple (T.unpack (federatorEndpoint ^. epHost)) (fromIntegral (federatorEndpoint ^. epPort)) False) + let cfg = grpcClientConfigSimple (T.unpack (federatorEndpoint ^. epHost)) (fromIntegral (federatorEndpoint ^. epPort)) False + eitherClient <- createGrpcClient cfg case eitherClient of - Left _ -> throwStd $ notFound "federator unreachable" + Left err -> grpc500 $ "Local federator unreachable: " <> LT.pack (show err) Right c -> pure c -callRemote :: MonadIO m => GrpcClient -> Proto.ValidatedFederatedRequest -> m (GRpcReply Proto.Response) +callRemote :: MonadIO m => GrpcClient -> Proto.ValidatedFederatedRequest -> m (GRpcReply Proto.OutwardResponse) callRemote fedClient call = liftIO $ gRpcCall @'MsgProtoBuf @Proto.Outward @"Outward" @"call" fedClient (Proto.validatedFederatedRequestToFederatedRequest call) -- FUTUREWORK(federation) All of this code is only exercised in the test which @@ -81,11 +85,37 @@ callRemote fedClient call = liftIO $ gRpcCall @'MsgProtoBuf @Proto.Outward @"Out -- test client side of federated code without needing another backend. We could -- do this either by mocking the second backend in integration tests or making -- all of this independent of the Handler monad and write unit tests. -expectOk :: GRpcReply Proto.Response -> Handler Proto.HTTPResponse +expectOk :: GRpcReply Proto.OutwardResponse -> Handler Proto.HTTPResponse expectOk = \case - GRpcTooMuchConcurrency _tmc -> throwStd $ notFound "Too much concurrency" - GRpcErrorCode errCode -> throwStd $ notFound $ LT.pack $ "GRPCError: " <> show errCode - GRpcErrorString errStr -> throwStd $ notFound $ LT.pack $ "GRPCError: " <> show errStr - GRpcClientError clErr -> throwStd $ notFound $ LT.pack $ "GRPC ClietnError: " <> show clErr - GRpcOk (Proto.ResponseErr err) -> throwStd $ notFound $ LT.pack $ "Remote component errored: " <> show err - GRpcOk (Proto.ResponseHTTPResponse res) -> pure res + GRpcTooMuchConcurrency _tmc -> + grpc500 "too much concurrency" + GRpcErrorCode errCode -> + grpc500 $ "GRpcErrorCode=" <> LT.pack (show errCode) + GRpcErrorString errStr -> + grpc500 $ "GRpcErrorString=" <> LT.pack errStr + GRpcClientError clErr -> + grpc500 $ "GRpcClientError=" <> LT.pack (show clErr) + GRpcOk (Proto.OutwardResponseError err) -> do + let errWithStatus = errWithPayloadAndStatus (Proto.outwardErrorPayload err) + case Proto.outwardErrorType err of + Proto.RemoteNotFound -> throwStd $ errWithStatus HTTP.status422 + Proto.DiscoveryFailed -> throwStd $ errWithStatus HTTP.status500 + Proto.ConnectionRefused -> throwStd $ errWithStatus (HTTP.Status 521 "Web Server Is Down") + Proto.TLSFailure -> throwStd $ errWithStatus (HTTP.Status 525 "SSL Handshake Failure") + Proto.InvalidCertificate -> throwStd $ errWithStatus (HTTP.Status 526 "Invalid SSL Certificate") + Proto.VersionMismatch -> throwStd $ errWithStatus (HTTP.Status 531 "Version Mismatch") + Proto.FederationDeniedByRemote -> throwStd $ errWithStatus (HTTP.Status 532 "Federation Denied") + Proto.FederationDeniedLocally -> throwStd $ errWithStatus HTTP.status400 + Proto.RemoteFederatorError -> throwStd $ errWithStatus (HTTP.Status 533 "Unexpected Federation Response") + Proto.InvalidRequest -> throwStd $ errWithStatus HTTP.status500 + GRpcOk (Proto.OutwardResponseHTTPResponse res) -> pure res + +errWithPayloadAndStatus :: Maybe Proto.ErrorPayload -> HTTP.Status -> Wai.Error +errWithPayloadAndStatus maybePayload code = + case maybePayload of + Nothing -> Wai.Error code "unknown-federation-error" "no payload present" + Just Proto.ErrorPayload {..} -> Wai.Error code (LT.fromStrict label) (LT.fromStrict msg) + +grpc500 :: LT.Text -> Handler a +grpc500 msg = do + throwStd $ Wai.Error HTTP.status500 "federator-grpc-failure" msg diff --git a/services/federator/federator.cabal b/services/federator/federator.cabal index ab867c2eda4..9f1e8ee67f3 100644 --- a/services/federator/federator.cabal +++ b/services/federator/federator.cabal @@ -4,7 +4,7 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: 8bde54eff8f6513c4fc8cef3a948b4e11394409373fc64963477ca9a031d2cab +-- hash: 602624ecd1e3d98115bc7e42a62bd6e5751aa2ed7f79433832d8c802d45c0bac name: federator version: 1.0.0 @@ -57,6 +57,7 @@ library , http-client-openssl , http-types , http2-client + , http2-client-grpc , imports , lens , metrics-core @@ -121,6 +122,7 @@ executable federator , http-client-openssl , http-types , http2-client + , http2-client-grpc , imports , lens , metrics-core @@ -190,6 +192,7 @@ executable federator-integration , http-client-openssl , http-types , http2-client + , http2-client-grpc , imports , lens , metrics-core @@ -263,6 +266,7 @@ test-suite federator-tests , http-client-openssl , http-types , http2-client + , http2-client-grpc , imports , lens , metrics-core diff --git a/services/federator/package.yaml b/services/federator/package.yaml index 9d52cb20ac0..b4b7bb1dc16 100644 --- a/services/federator/package.yaml +++ b/services/federator/package.yaml @@ -27,6 +27,7 @@ dependencies: - extended - http-client - http2-client +- http2-client-grpc - imports - lens - metrics-core diff --git a/services/federator/src/Federator/ExternalServer.hs b/services/federator/src/Federator/ExternalServer.hs index d348c912960..e47175ec32d 100644 --- a/services/federator/src/Federator/ExternalServer.hs +++ b/services/federator/src/Federator/ExternalServer.hs @@ -44,14 +44,14 @@ import Wire.API.Federation.GRPC.Types -- reached, some discussion here: -- https://wearezeta.atlassian.net/wiki/spaces/CORE/pages/224166764/Limiting+access+to+federation+endpoints -- Also, see comment in 'Federator.Brig.interpretBrig' -callLocal :: (Members '[Brig, Embed IO] r) => Request -> Sem r Response +callLocal :: (Members '[Brig, Embed IO] r) => Request -> Sem r InwardResponse callLocal Request {..} = do -- FUTUREWORK(federation): before making a request, check the sender domain and only make the call if the allowlist (use Util.federateWith) allows it. (resStatus, resBody) <- brigCall (unwrapMethod method) path query body -- FUTUREWORK(federation): Decide what to do with 5xx statuses let statusW32 = fromIntegral $ HTTP.statusCode resStatus bodyBS = maybe mempty LBS.toStrict resBody - pure $ ResponseHTTPResponse $ HTTPResponse statusW32 bodyBS + pure $ InwardResponseHTTPResponse $ HTTPResponse statusW32 bodyBS routeToInternal :: (Members '[Brig, Embed IO, Polysemy.Error ServerError] r) => SingleServerT info Inward (Sem r) _ routeToInternal = singleService (Mu.method @"call" callLocal) diff --git a/services/federator/src/Federator/InternalServer.hs b/services/federator/src/Federator/InternalServer.hs index d96ede53a58..4a3d0f2eb78 100644 --- a/services/federator/src/Federator/InternalServer.hs +++ b/services/federator/src/Federator/InternalServer.hs @@ -24,11 +24,12 @@ import Control.Lens (view) import Data.Domain (domainText) import Data.Either.Validation (Validation (..)) import qualified Data.Text as Text +import qualified Data.Text.Encoding as Text import Federator.App (Federator, runAppT) -import Federator.Discovery (DiscoverFederator, runFederatorDiscovery) +import Federator.Discovery (DiscoverFederator, LookupError (LookupErrorDNSError, LookupErrorSrvNotAvailable), runFederatorDiscovery) import Federator.Env (Env, applog, dnsResolver, runSettings) import Federator.Options (RunSettings) -import Federator.Remote (Remote, RemoteError, discoverAndCall, interpretRemote) +import Federator.Remote (Remote, RemoteError (RemoteErrorClientFailure, RemoteErrorDiscoveryFailure), discoverAndCall, interpretRemote) import Federator.Util import Federator.Utils.PolysemyServerError (absorbServerError) import Imports @@ -46,26 +47,44 @@ import Wire.API.Federation.GRPC.Types import Wire.Network.DNS.Effect (DNSLookup) import qualified Wire.Network.DNS.Effect as Lookup -callOutward :: Members '[Remote, Polysemy.Reader RunSettings] r => FederatedRequest -> Sem r Response +callOutward :: Members '[Remote, Polysemy.Reader RunSettings] r => FederatedRequest -> Sem r OutwardResponse callOutward req = do case validateFederatedRequest req of Success vReq -> do allowedRemote <- federateWith (vDomain vReq) if allowedRemote then mkRemoteResponse <$> discoverAndCall vReq - else pure $ ResponseErr ("federating with domain [" <> domainText (vDomain vReq) <> "] is not allowed (see federator configuration)") - Failure errs -> pure $ ResponseErr ("component -> local federator: invalid FederatedRequest: " <> Text.pack (show errs)) + else pure $ mkOutwardErr FederationDeniedLocally "federation-not-allowed" ("federating with domain [" <> domainText (vDomain vReq) <> "] is not allowed (see federator configuration)") + Failure errs -> + pure $ mkOutwardErr InvalidRequest "invalid-request-to-federator" ("validation failed with: " <> Text.pack (show errs)) -- FUTUREWORK(federation): Make these errors less stringly typed -mkRemoteResponse :: Either RemoteError (GRpcReply Response) -> Response +mkRemoteResponse :: Either RemoteError (GRpcReply InwardResponse) -> OutwardResponse mkRemoteResponse reply = case reply of - Right (GRpcOk res) -> res - Right (GRpcTooMuchConcurrency _) -> ResponseErr "remote federator -> local federator: too much concurrency" - Right (GRpcErrorCode grpcErr) -> ResponseErr ("remote federator -> local federator: " <> Text.pack (show grpcErr)) - Right (GRpcErrorString grpcErr) -> ResponseErr ("remote federator -> local federator: error string: " <> Text.pack grpcErr) - Right (GRpcClientError clientErr) -> ResponseErr ("remote federator -> local federator: client error: " <> Text.pack (show clientErr)) - Left err -> ResponseErr ("remote federator -> local federator: " <> Text.pack (show err)) + Right (GRpcOk (InwardResponseHTTPResponse res)) -> + OutwardResponseHTTPResponse res + Right (GRpcOk (InwardResponseErr err)) -> + mkOutwardErr RemoteFederatorError "remote-federator-returned-error" err + Right (GRpcTooMuchConcurrency _) -> + mkOutwardErr RemoteFederatorError "too-much-concurrency" "Too much concurrency" + Right (GRpcErrorCode grpcErr) -> + mkOutwardErr RemoteFederatorError "grpc-error-code" ("code=" <> Text.pack (show grpcErr)) + Right (GRpcErrorString grpcErr) -> + mkOutwardErr RemoteFederatorError "grpc-error-string" ("error=" <> Text.pack grpcErr) + Right (GRpcClientError clientErr) -> + mkOutwardErr RemoteFederatorError "grpc-client-error" ("error=" <> Text.pack (show clientErr)) + Left (RemoteErrorDiscoveryFailure err domain) -> + case err of + LookupErrorSrvNotAvailable _srvDomain -> + mkOutwardErr RemoteNotFound "srv-record-not-found" ("domain=" <> domainText domain) + LookupErrorDNSError dnsErr -> + mkOutwardErr DiscoveryFailed "srv-lookup-dns-error" ("domain=" <> domainText domain <> "error=" <> Text.decodeUtf8 dnsErr) + Left (RemoteErrorClientFailure cltErr srvTarget) -> + mkOutwardErr RemoteFederatorError "cannot-connect-to-remote-federator" ("target=" <> Text.pack (show srvTarget) <> "error=" <> Text.pack (show cltErr)) + +mkOutwardErr :: OutwardErrorType -> Text -> Text -> OutwardResponse +mkOutwardErr typ label msg = OutwardResponseError $ OutwardError typ (Just $ ErrorPayload label msg) outward :: (Members '[Remote, Polysemy.Error ServerError, Polysemy.Reader RunSettings] r) => SingleServerT info Outward (Sem r) _ outward = singleService (Mu.method @"call" callOutward) diff --git a/services/federator/src/Federator/Remote.hs b/services/federator/src/Federator/Remote.hs index 9a170a59603..92344111f46 100644 --- a/services/federator/src/Federator/Remote.hs +++ b/services/federator/src/Federator/Remote.hs @@ -24,23 +24,24 @@ import Data.String.Conversions (cs) import Federator.Discovery (DiscoverFederator, LookupError, discoverFederator) import Imports import Mu.GRpc.Client.Optics (GRpcReply) -import Mu.GRpc.Client.Record (GRpcMessageProtocol (MsgProtoBuf), GrpcClient, grpcClientConfigSimple, setupGrpcClient') +import Mu.GRpc.Client.Record (GRpcMessageProtocol (MsgProtoBuf)) import Mu.GRpc.Client.TyApps (gRpcCall) -import Network.HTTP2.Client (ClientError) +import Network.GRPC.Client.Helpers import Polysemy import Polysemy.TinyLog (TinyLog) import qualified Polysemy.TinyLog as Log import qualified System.Logger.Message as Log +import Wire.API.Federation.GRPC.Client import Wire.API.Federation.GRPC.Types import Wire.Network.DNS.SRV (SrvTarget (SrvTarget)) data RemoteError = RemoteErrorDiscoveryFailure LookupError Domain - | RemoteErrorClientFailure ClientError SrvTarget + | RemoteErrorClientFailure GrpcClientErr SrvTarget deriving (Show, Eq) data Remote m a where - DiscoverAndCall :: ValidatedFederatedRequest -> Remote m (Either RemoteError (GRpcReply Response)) + DiscoverAndCall :: ValidatedFederatedRequest -> Remote m (Either RemoteError (GRpcReply InwardResponse)) makeSem ''Remote @@ -62,7 +63,7 @@ interpretRemote = interpret $ \case Right <$> callInward client vRequest Left err -> pure $ Left err -callInward :: MonadIO m => GrpcClient -> Request -> m (GRpcReply Response) +callInward :: MonadIO m => GrpcClient -> Request -> m (GRpcReply InwardResponse) callInward client request = liftIO $ gRpcCall @'MsgProtoBuf @Inward @"Inward" @"call" client request @@ -77,10 +78,7 @@ mkGrpcClient target@(SrvTarget host port) = do -- See https://github.com/haskell-grpc-native/http2-grpc-haskell/issues/47 -- While early testing, this is "convenient" but needs to be fixed! let cfg = grpcClientConfigSimple (cs host) (fromInteger $ toInteger port) True - -- Note: setupGrpcClient' is unsafe and throws exceptions in IO, e.g. when it can't connect. Don't be fooled by the Either, - -- errors appear to never happen in the left side so this is dead code. - -- FUTUREWORK(federation): report setupGrpcClient' buggy behaviour to upstream. - eitherClient <- setupGrpcClient' cfg + eitherClient <- createGrpcClient cfg case eitherClient of Left err -> do Log.debug $ diff --git a/services/federator/test/integration/Test/Federator/InwardSpec.hs b/services/federator/test/integration/Test/Federator/InwardSpec.hs index 298faab98d8..8da1cc171f2 100644 --- a/services/federator/test/integration/Test/Federator/InwardSpec.hs +++ b/services/federator/test/integration/Test/Federator/InwardSpec.hs @@ -39,7 +39,8 @@ import Test.Federator.Util import Test.Hspec import Test.Tasty.HUnit (assertFailure) import Util.Options (Endpoint (Endpoint)) -import Wire.API.Federation.GRPC.Types (Component (..), HTTPMethod (..), HTTPResponse (..), Inward, QueryParam (..), Request (Request), Response (..)) +import Wire.API.Federation.GRPC.Client +import Wire.API.Federation.GRPC.Types (Component (..), HTTPMethod (..), HTTPResponse (..), Inward, InwardResponse (..), QueryParam (..), Request (Request)) import Wire.API.User import Wire.API.User.Auth import Wire.API.User.Handle (UserHandleInfo (UserHandleInfo)) @@ -73,15 +74,18 @@ spec env = _ <- putHandle brig (userId user) hdl Endpoint fedHost fedPort <- federatorExternal . view teOpts <$> ask - Right c <- setupGrpcClient' (grpcClientConfigSimple (Text.unpack fedHost) (fromIntegral fedPort) False) + client <- createGrpcClient (grpcClientConfigSimple (Text.unpack fedHost) (fromIntegral fedPort) False) + c <- case client of + Left (err) -> liftIO $ assertFailure (show err) + Right cli -> pure cli let brigCall = Request Brig (HTTPMethod HTTP.GET) "users/by-handle" [QueryParam "handle" (Text.encodeUtf8 hdl)] mempty res <- liftIO $ gRpcCall @'MsgProtoBuf @Inward @"Inward" @"call" c brigCall liftIO $ case res of - GRpcOk (ResponseHTTPResponse (HTTPResponse sts bdy)) -> do + GRpcOk (InwardResponseHTTPResponse (HTTPResponse sts bdy)) -> do sts `shouldBe` 200 eitherDecodeStrict bdy `shouldBe` Right (UserHandleInfo (userQualifiedId user)) - GRpcOk (ResponseErr err) -> assertFailure $ "Unexpected error response: " <> show err + GRpcOk (InwardResponseErr err) -> assertFailure $ "Unexpected error response: " <> show err x -> assertFailure $ "GRpc call failed unexpectedly: " <> show x -- All the code below is copied from brig-integrration tests diff --git a/services/federator/test/unit/Test/Federator/ExternalServer.hs b/services/federator/test/unit/Test/Federator/ExternalServer.hs index d118a714d99..67c77079d85 100644 --- a/services/federator/test/unit/Test/Federator/ExternalServer.hs +++ b/services/federator/test/unit/Test/Federator/ExternalServer.hs @@ -34,13 +34,13 @@ genMock ''Brig tests :: TestTree tests = - testGroup "InternalServer" $ + testGroup "ExternalServer" $ [ requestBrigSuccess ] requestBrigSuccess :: TestTree requestBrigSuccess = - testCase "should translate response from brig to 'Response'" $ + testCase "should translate response from brig to 'InwardResponse'" $ runM . evalMock @Brig @IO $ do mockBrigCallReturns @IO (\_ _ _ _ -> pure (HTTP.status200, Just "response body")) let request = Request Brig (HTTPMethod HTTP.GET) "/users" [QueryParam "handle" "foo"] mempty @@ -50,4 +50,4 @@ requestBrigSuccess = actualCalls <- mockBrigCallCalls @IO let expectedCall = (HTTP.GET, "/users", [QueryParam "handle" "foo"], mempty) embed $ assertEqual "one call to brig should be made" [expectedCall] actualCalls - embed $ assertEqual "response should be success with correct body" (ResponseHTTPResponse (HTTPResponse 200 "response body")) res + embed $ assertEqual "response should be success with correct body" (InwardResponseHTTPResponse (HTTPResponse 200 "response body")) res diff --git a/services/federator/test/unit/Test/Federator/InternalServer.hs b/services/federator/test/unit/Test/Federator/InternalServer.hs index e910d372118..a701dc9e586 100644 --- a/services/federator/test/unit/Test/Federator/InternalServer.hs +++ b/services/federator/test/unit/Test/Federator/InternalServer.hs @@ -17,11 +17,10 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Test.Federator.InternalServer where +module Test.Federator.InternalServer (tests) where import Data.Domain (Domain (Domain)) -import Data.Either.Validation -import Federator.Discovery (LookupError (LookupErrorSrvNotAvailable)) +import Federator.Discovery (LookupError (LookupErrorDNSError, LookupErrorSrvNotAvailable)) import Federator.InternalServer (callOutward) import Federator.Options (AllowedDomains (..), FederationStrategy (..), RunSettings (..)) import Federator.Remote (Remote, RemoteError (RemoteErrorDiscoveryFailure)) @@ -35,7 +34,7 @@ import qualified Polysemy.Reader as Polysemy import Test.Polysemy.Mock (Mock (mock), evalMock) import Test.Polysemy.Mock.TH (genMock) import Test.Tasty (TestTree, testGroup) -import Test.Tasty.HUnit (assertBool, assertEqual, testCase) +import Test.Tasty.HUnit (assertBool, assertEqual, assertFailure, testCase) import Wire.API.Federation.GRPC.Types genMock ''Remote @@ -52,7 +51,8 @@ tests = federatedRequestFailureTMC, federatedRequestFailureErrCode, federatedRequestFailureErrStr, - federatedRequestFailureErrConn, + federatedRequestFailureNoRemote, + federatedRequestFailureDNS, federatedRequestFailureAllowList ] ] @@ -64,7 +64,7 @@ federatedRequestSuccess :: TestTree federatedRequestSuccess = testCase "should successfully return success response" $ runM . evalMock @Remote @IO $ do - mockDiscoverAndCallReturns @IO (const $ pure (Right (GRpcOk (ResponseHTTPResponse (HTTPResponse 200 "success!"))))) + mockDiscoverAndCallReturns @IO (const $ pure (Right (GRpcOk (InwardResponseHTTPResponse (HTTPResponse 200 "success!"))))) let federatedRequest = FederatedRequest validDomainText (Just validLocalPart) res <- mock @Remote @IO . Polysemy.runReader allowAllSettings $ callOutward federatedRequest @@ -72,7 +72,7 @@ federatedRequestSuccess = actualCalls <- mockDiscoverAndCallCalls @IO let expectedCall = ValidatedFederatedRequest (Domain validDomainText) validLocalPart embed $ assertEqual "one remote call should be made" [expectedCall] actualCalls - embed $ assertEqual "successful response should be returned" (ResponseHTTPResponse (HTTPResponse 200 "success!")) res + embed $ assertEqual "successful response should be returned" (OutwardResponseHTTPResponse (HTTPResponse 200 "success!")) res -- FUTUREWORK(federation): This is probably not ideal, we should figure out what this error -- means and act accordingly. @@ -87,8 +87,9 @@ federatedRequestFailureTMC = actualCalls <- mockDiscoverAndCallCalls @IO let expectedCall = ValidatedFederatedRequest (Domain validDomainText) validLocalPart - embed $ assertEqual "one remote call should be made" [expectedCall] actualCalls - embed $ assertBool "the response should be error" (isResponseError res) + embed $ do + assertEqual "one remote call should be made" [expectedCall] actualCalls + assertResponseErrorWithType RemoteFederatorError res federatedRequestFailureErrCode :: TestTree federatedRequestFailureErrCode = @@ -101,8 +102,9 @@ federatedRequestFailureErrCode = actualCalls <- mockDiscoverAndCallCalls @IO let expectedCall = ValidatedFederatedRequest (Domain validDomainText) validLocalPart - embed $ assertEqual "one remote call should be made" [expectedCall] actualCalls - embed $ assertBool "the response should be error" (isResponseError res) + embed $ do + assertEqual "one remote call should be made" [expectedCall] actualCalls + assertResponseErrorWithType RemoteFederatorError res federatedRequestFailureErrStr :: TestTree federatedRequestFailureErrStr = @@ -115,12 +117,13 @@ federatedRequestFailureErrStr = actualCalls <- mockDiscoverAndCallCalls @IO let expectedCall = ValidatedFederatedRequest (Domain validDomainText) validLocalPart - embed $ assertEqual "one remote call should be made" [expectedCall] actualCalls - embed $ assertBool "the response should have error" (isResponseError res) + embed $ do + assertEqual "one remote call should be made" [expectedCall] actualCalls + assertResponseErrorWithType RemoteFederatorError res -federatedRequestFailureErrConn :: TestTree -federatedRequestFailureErrConn = - testCase "should respond with error when facing RemoteError" $ +federatedRequestFailureNoRemote :: TestTree +federatedRequestFailureNoRemote = + testCase "should respond with error when SRV record is not found" $ runM . evalMock @Remote @IO $ do mockDiscoverAndCallReturns @IO (const $ pure (Left $ RemoteErrorDiscoveryFailure (LookupErrorSrvNotAvailable "_something._tcp.example.com") (Domain "example.com"))) let federatedRequest = FederatedRequest validDomainText (Just validLocalPart) @@ -129,23 +132,38 @@ federatedRequestFailureErrConn = actualCalls <- mockDiscoverAndCallCalls @IO let expectedCall = ValidatedFederatedRequest (Domain validDomainText) validLocalPart - embed $ assertEqual "one remote call should be made" [expectedCall] actualCalls - embed $ assertBool "the response should have error" (isResponseError res) + embed $ do + assertEqual "one remote call should be made" [expectedCall] actualCalls + assertResponseErrorWithType RemoteNotFound res + +federatedRequestFailureDNS :: TestTree +federatedRequestFailureDNS = + testCase "should respond with error when SRV lookup fails due to DNSError" $ + runM . evalMock @Remote @IO $ do + mockDiscoverAndCallReturns @IO (const $ pure (Left $ RemoteErrorDiscoveryFailure (LookupErrorDNSError "No route to 1.1.1.1") (Domain "example.com"))) + let federatedRequest = FederatedRequest validDomainText (Just validLocalPart) + + res <- mock @Remote @IO . Polysemy.runReader allowAllSettings $ callOutward federatedRequest + + actualCalls <- mockDiscoverAndCallCalls @IO + let expectedCall = ValidatedFederatedRequest (Domain validDomainText) validLocalPart + embed $ do + assertEqual "one remote call should be made" [expectedCall] actualCalls + assertResponseErrorWithType DiscoveryFailed res federatedRequestFailureAllowList :: TestTree federatedRequestFailureAllowList = testCase "should not make a call when target domain not in the allowList" $ runM . evalMock @Remote @IO $ do - mockDiscoverAndCallReturns @IO (const $ pure (Left $ RemoteErrorDiscoveryFailure (LookupErrorSrvNotAvailable "_something._tcp.example.com") (Domain "example.com"))) let federatedRequest = FederatedRequest validDomainText (Just validLocalPart) - let allowList = RunSettings (AllowList (AllowedDomains [Domain "hello.world"])) res <- mock @Remote @IO . Polysemy.runReader allowList $ callOutward federatedRequest actualCalls <- mockDiscoverAndCallCalls @IO - embed $ assertEqual "no remote calls should be made" [] actualCalls - embed $ assertBool "the response should have error" (isResponseError res) + embed $ do + assertEqual "no remote calls should be made" [] actualCalls + assertResponseErrorWithType FederationDeniedLocally res federateWithAllowListSuccess :: TestTree federateWithAllowListSuccess = @@ -163,12 +181,13 @@ federateWithAllowListFail = res <- Polysemy.runReader allowList $ federateWith (Domain "hello.world") embed $ assertBool "federating should not be allowed" (not res) -isResponseError :: Response -> Bool -isResponseError (ResponseErr _) = True -isResponseError (ResponseHTTPResponse _) = False - -isRight' :: Validation a b -> Bool -isRight' = isRight . validationToEither +assertResponseErrorWithType :: HasCallStack => OutwardErrorType -> OutwardResponse -> IO () +assertResponseErrorWithType expectedType res = + case res of + OutwardResponseHTTPResponse _ -> + assertFailure $ "Expected response to be error, but it was not: " <> show res + OutwardResponseError (OutwardError actualType _) -> + assertEqual "Unexpected error type" expectedType actualType validLocalPart :: Request validLocalPart = Request Brig (HTTPMethod HTTP.GET) "/users" [QueryParam "handle" "foo"] mempty diff --git a/stack.yaml b/stack.yaml index 636e0f67ec1..282d3eab381 100644 --- a/stack.yaml +++ b/stack.yaml @@ -170,13 +170,19 @@ extra-deps: # Not updated on Stackage yet - QuickCheck-2.14 - splitmix-0.0.4 # needed for QuickCheck -- servant-0.18.2 -- servant-server-0.18.2 - servant-mock-0.8.7 - servant-swagger-ui-0.3.4.3.36.1 - git: https://github.com/wireapp/servant-swagger.git commit: 23e9afafadaade29d21181b935286087457171e3 +# For changes from https://github.com/haskell-servant/servant/pull/1376 +# Not released to hackage yet +- git: https://github.com/haskell-servant/servant.git + commit: 27173c922311112dd153346cf3cd72b9fb0f3551 + subdirs: + - servant + - servant-server + - HsOpenSSL-x509-system-0.1.0.3@sha256:f4958ee0eec555c5c213662eff6764bddefe5665e2afcfd32733ce3801a9b687,1774 # Latest: lts-14.27 - cql-4.0.2@sha256:a0006a5ac13d6f86d5eff28c11be80928246309f217ea6d5f5c8a76a5d16b48b,3157 # Latest: lts-14.27 - cql-io-1.1.1@sha256:897ef0811b227c8b1a269b29b9c1ebfb09c46f00d66834e2e8c6f19ea7f90f7d,4611 # Latest: lts-14.27 diff --git a/stack.yaml.lock b/stack.yaml.lock index b8228581ee0..50add945209 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -505,20 +505,6 @@ packages: sha256: e58892088b95190bfb59a7c0803f7ef65338e57fc9b938d7c166563605003902 original: hackage: splitmix-0.0.4 -- completed: - hackage: servant-0.18.2@sha256:f8c9f0e9891a3ada1337a3c0b369333a3b5a2d0909dd3cd09d79bc26adeaca44,5298 - pantry-tree: - size: 2662 - sha256: e930e814de1aa4d24274bdf18341a50b7ed38604ae4734f730e09238ac5bf7e2 - original: - hackage: servant-0.18.2 -- completed: - hackage: servant-server-0.18.2@sha256:56679af62ab8820a2108da6153d9ae9dde37199e62172365bdaea1458c3f7c2d,5482 - pantry-tree: - size: 2614 - sha256: 3ac7430134439e4b67f0f5333f63b89d0cb7de5e2e07f0af7801c8e223942b9c - original: - hackage: servant-server-0.18.2 - completed: hackage: servant-mock-0.8.7@sha256:64cb3e52bbd51ab6cb25e3f412a99ea712c6c26f1efd117f01a8d1664df49c67,2306 pantry-tree: @@ -544,6 +530,32 @@ packages: original: git: https://github.com/wireapp/servant-swagger.git commit: 23e9afafadaade29d21181b935286087457171e3 +- completed: + subdir: servant + name: servant + version: 0.18.2 + git: https://github.com/haskell-servant/servant.git + pantry-tree: + size: 2809 + sha256: 952540fb295f50de371c8a98222eaf28146ef0e4366b986dd7b601c0ea9a5d00 + commit: 27173c922311112dd153346cf3cd72b9fb0f3551 + original: + subdir: servant + git: https://github.com/haskell-servant/servant.git + commit: 27173c922311112dd153346cf3cd72b9fb0f3551 +- completed: + subdir: servant-server + name: servant-server + version: 0.18.2 + git: https://github.com/haskell-servant/servant.git + pantry-tree: + size: 2727 + sha256: bda5fc5c3e70633dec238284bc0f626bae432d39758ab0905875388d587fc3d0 + commit: 27173c922311112dd153346cf3cd72b9fb0f3551 + original: + subdir: servant-server + git: https://github.com/haskell-servant/servant.git + commit: 27173c922311112dd153346cf3cd72b9fb0f3551 - completed: hackage: HsOpenSSL-x509-system-0.1.0.3@sha256:f4958ee0eec555c5c213662eff6764bddefe5665e2afcfd32733ce3801a9b687,1774 pantry-tree: From 9019b6b304c1617caba80f18c2f393c065ea30f7 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Mon, 22 Mar 2021 14:04:11 +0100 Subject: [PATCH 15/18] Bump nixpkgs for hls-1.0 (#1412) --- nix/sources.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nix/sources.json b/nix/sources.json index 35d75021c90..c3675c36b54 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -17,10 +17,10 @@ "homepage": "https://github.com/NixOS/nixpkgs", "owner": "NixOS", "repo": "nixpkgs", - "rev": "11cd34cd592f917bab5f42e2b378ab329dee3bcf", - "sha256": "1mgga54np22csagzaxfjq5hrgyv8y4igrl3f6z24fb39rvvx236w", + "rev": "8e1891d5b8d0b898db8890ddab73141f0cd3c2bc", + "sha256": "0a767mn0nfp4qnklsvs6bnc0vng4nc3ch566nmrz18ypk67z4zz0", "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/11cd34cd592f917bab5f42e2b378ab329dee3bcf.tar.gz", + "url": "https://github.com/NixOS/nixpkgs/archive/8e1891d5b8d0b898db8890ddab73141f0cd3c2bc.tar.gz", "url_template": "https://github.com///archive/.tar.gz" } } From ae318fdba0ee7795bc0ca36ba1843c48b2eb257e Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Mon, 22 Mar 2021 15:49:58 +0000 Subject: [PATCH 16/18] Add spar-migrate-data to cassandara-migrations helm chart (#1417) * Add chart * galley-migrate-data, spar-migrate-data hook weight --- .../templates/galley-migrate-data.yaml | 1 + .../templates/spar-migrate-data.yaml | 44 +++++++++++++++++++ charts/cassandra-migrations/values.yaml | 1 + 3 files changed, 46 insertions(+) create mode 100644 charts/cassandra-migrations/templates/spar-migrate-data.yaml diff --git a/charts/cassandra-migrations/templates/galley-migrate-data.yaml b/charts/cassandra-migrations/templates/galley-migrate-data.yaml index d7b4c4da8fc..32398803554 100644 --- a/charts/cassandra-migrations/templates/galley-migrate-data.yaml +++ b/charts/cassandra-migrations/templates/galley-migrate-data.yaml @@ -12,6 +12,7 @@ metadata: heritage: "{{ .Release.Service }}" annotations: "helm.sh/hook": post-install,post-upgrade + "helm.sh/hook-weight": "10" "helm.sh/hook-delete-policy": "before-hook-creation" spec: template: diff --git a/charts/cassandra-migrations/templates/spar-migrate-data.yaml b/charts/cassandra-migrations/templates/spar-migrate-data.yaml new file mode 100644 index 00000000000..4a7aee12737 --- /dev/null +++ b/charts/cassandra-migrations/templates/spar-migrate-data.yaml @@ -0,0 +1,44 @@ +# This jobs runs data migrations for the spar DB using the spar-migrate-data tool. +# The source for the tool can be found at services/spar/migrate-data +# +apiVersion: batch/v1 +kind: Job +metadata: + name: spar-migrate-data + labels: + wireService: "cassandra-migrations" + chart: "{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" + annotations: + "helm.sh/hook": post-install,post-upgrade + "helm.sh/hook-weight": "20" + "helm.sh/hook-delete-policy": "before-hook-creation" +spec: + template: + metadata: + name: "{{.Release.Name}}" + labels: + wireService: spar-migrate-data + app: spar-migrate-data + heritage: {{.Release.Service | quote }} + release: {{.Release.Name | quote }} + chart: "{{.Chart.Name}}-{{.Chart.Version}}" + spec: + restartPolicy: OnFailure + containers: + - name: spar-migrate-data + image: "{{ .Values.images.sparMigrateData }}:{{ .Values.images.tag }}" + args: + - --cassandra-host-spar + - "{{ .Values.cassandra.host }}" + - --cassandra-port-spar + - "9042" + - --cassandra-keyspace-spar + - spar + - --cassandra-host-brig + - "{{ .Values.cassandra.host }}" + - --cassandra-port-brig + - "9042" + - --cassandra-keyspace-brig + - brig diff --git a/charts/cassandra-migrations/values.yaml b/charts/cassandra-migrations/values.yaml index d5660d88fe7..0347fac256d 100644 --- a/charts/cassandra-migrations/values.yaml +++ b/charts/cassandra-migrations/values.yaml @@ -5,3 +5,4 @@ images: galley: quay.io/wire/galley-schema spar: quay.io/wire/spar-schema galleyMigrateData: quay.io/wire/galley-migrate-data + sparMigrateData: quay.io/wire/spar-migrate-data From d999301041513f64981b67177e4987d5cd70a0a5 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Tue, 23 Mar 2021 10:01:49 +0000 Subject: [PATCH 17/18] Remove usage of spar.scim_external_ids table (#1418) * Remove usage of spar.scim_external_ids table * Add note about scim_external_ids removal * Revert "Add note about scim_external_ids removal" This reverts commit 259f60a6380ede0a6e1f37c305292a044b591a58. * Update services/spar/src/Spar/App.hs Co-authored-by: fisx Co-authored-by: fisx --- services/spar/src/Spar/App.hs | 30 +++++++++---- services/spar/src/Spar/Data.hs | 42 +++++-------------- services/spar/src/Spar/Scim/User.hs | 24 +++++------ .../test-integration/Test/Spar/APISpec.hs | 22 +++++----- .../Test/Spar/Scim/UserSpec.hs | 2 +- services/spar/test-integration/Util/Core.hs | 6 +-- 6 files changed, 59 insertions(+), 67 deletions(-) diff --git a/services/spar/src/Spar/App.hs b/services/spar/src/Spar/App.hs index cb1b0c80963..1e2e60d5df4 100644 --- a/services/spar/src/Spar/App.hs +++ b/services/spar/src/Spar/App.hs @@ -26,7 +26,8 @@ module Spar.App wrapMonadClientWithEnv, wrapMonadClient, verdictHandler, - getUser, + getUserByUref, + getUserByScimExternalId, insertUser, autoprovisionSamlUser, autoprovisionSamlUserWithId, @@ -84,12 +85,13 @@ import Spar.Error import qualified Spar.Intra.Brig as Intra import qualified Spar.Intra.Galley as Intra import Spar.Orphans () -import Spar.Scim.Types (ValidExternalId (..), runValidExternalId) +import Spar.Scim.Types (ValidExternalId (..)) import Spar.Types import qualified System.Logger as Log import System.Logger.Class (MonadLogger (log)) import URI.ByteString as URI import Web.Cookie (SetCookie, renderSetCookie) +import Wire.API.User.Identity (Email (..)) newtype Spar a = Spar {fromSpar :: ReaderT Env (ExceptT SparError IO) a} deriving (Functor, Applicative, Monad, MonadIO, MonadReader Env, MonadError SparError) @@ -188,11 +190,21 @@ insertUser uref uid = wrapMonadClient $ Data.insertSAMLUser uref uid -- password handshake have not been completed; it's still ok for the user to gain access to -- the team with valid SAML credentials. -- --- ASSUMPTIONS: User creation on brig/galley is idempotent. Any incomplete creation (because of --- brig or galley crashing) will cause the lookup here to yield 'Nothing'. -getUser :: ValidExternalId -> Spar (Maybe UserId) -getUser veid = do - muid <- wrapMonadClient $ runValidExternalId Data.getSAMLUser Data.lookupScimExternalId veid +-- FUTUREWORK: Remove and reinstatate getUser, in AuthID refactoring PR. (in https://github.com/wireapp/wire-server/pull/1410, undo https://github.com/wireapp/wire-server/pull/1418) +getUserByUref :: SAML.UserRef -> Spar (Maybe UserId) +getUserByUref uref = do + muid <- wrapMonadClient $ Data.getSAMLUser uref + case muid of + Nothing -> pure Nothing + Just uid -> do + let withpending = Intra.WithPendingInvitations -- see haddocks above + itis <- isJust <$> Intra.getBrigUserTeam withpending uid + pure $ if itis then Just uid else Nothing + +-- FUTUREWORK: Remove and reinstatate getUser, in AuthID refactoring PR +getUserByScimExternalId :: TeamId -> Email -> Spar (Maybe UserId) +getUserByScimExternalId tid email = do + muid <- wrapMonadClient $ (Data.lookupScimExternalId tid email) case muid of Nothing -> pure Nothing Just uid -> do @@ -376,7 +388,7 @@ findUserWithOldIssuer (SAML.UserRef issuer subject) = do idp <- getIdPConfigByIssuer issuer let tryFind :: Maybe (SAML.UserRef, UserId) -> Issuer -> Spar (Maybe (SAML.UserRef, UserId)) tryFind found@(Just _) _ = pure found - tryFind Nothing oldIssuer = (uref,) <$$> getUser (UrefOnly uref) + tryFind Nothing oldIssuer = (uref,) <$$> getUserByUref uref where uref = SAML.UserRef oldIssuer subject foldM tryFind Nothing (idp ^. idpExtraInfo . wiOldIssuers) @@ -396,7 +408,7 @@ verdictHandlerResultCore bindCky = \case SAML.AccessGranted userref -> do uid :: UserId <- do viaBindCookie <- maybe (pure Nothing) (wrapMonadClient . Data.lookupBindCookie) bindCky - viaSparCassandra <- getUser (UrefOnly userref) + viaSparCassandra <- getUserByUref userref -- race conditions: if the user has been created on spar, but not on brig, 'getUser' -- returns 'Nothing'. this is ok assuming 'createUser', 'bindUser' (called below) are -- idempotent. diff --git a/services/spar/src/Spar/Data.hs b/services/spar/src/Spar/Data.hs index a46e8c675d6..fe38e29c64e 100644 --- a/services/spar/src/Spar/Data.hs +++ b/services/spar/src/Spar/Data.hs @@ -784,42 +784,22 @@ deleteScimUserTimes uid = retry x5 . write del $ params Quorum (Identity uid) -- as a 'Text'.) insertScimExternalId :: (HasCallStack, MonadClient m) => TeamId -> Email -> UserId -> m () insertScimExternalId tid (fromEmail -> email) uid = - retry - x5 - ( batch $ do - setType BatchLogged - setConsistency Quorum - addPrepQuery ins (email, uid) - addPrepQuery insFuture (tid, email, uid) - ) + retry x5 . write insert $ params Quorum (tid, email, uid) where - ins :: PrepQuery W (Text, UserId) () - ins = "INSERT INTO scim_external_ids (external, user) VALUES (?, ?)" - - insFuture :: PrepQuery W (TeamId, Text, UserId) () - insFuture = "INSERT INTO scim_external (team, external_id, user) VALUES (?, ?, ?)" + insert :: PrepQuery W (TeamId, Text, UserId) () + insert = "INSERT INTO scim_external (team, external_id, user) VALUES (?, ?, ?)" -- | The inverse of 'insertScimExternalId'. -lookupScimExternalId :: (HasCallStack, MonadClient m) => Email -> m (Maybe UserId) -lookupScimExternalId (fromEmail -> email) = runIdentity <$$> (retry x1 . query1 sel $ params Quorum (Identity email)) +lookupScimExternalId :: (HasCallStack, MonadClient m) => TeamId -> Email -> m (Maybe UserId) +lookupScimExternalId tid (fromEmail -> email) = runIdentity <$$> (retry x1 . query1 sel $ params Quorum (tid, email)) where - sel :: PrepQuery R (Identity Text) (Identity UserId) - sel = "SELECT user FROM scim_external_ids WHERE external = ?" + sel :: PrepQuery R (TeamId, Text) (Identity UserId) + sel = "SELECT user FROM scim_external WHERE team = ? and external_id = ?" -- | The other inverse of 'insertScimExternalId' :). deleteScimExternalId :: (HasCallStack, MonadClient m) => TeamId -> Email -> m () -deleteScimExternalId team (fromEmail -> email) = - retry - x5 - ( batch $ do - setType BatchLogged - setConsistency Quorum - addPrepQuery del (Identity email) - addPrepQuery delFuture (team, email) - ) +deleteScimExternalId tid (fromEmail -> email) = + retry x5 . write delete $ params Quorum (tid, email) where - del :: PrepQuery W (Identity Text) () - del = "DELETE FROM scim_external_ids WHERE external = ?" - - delFuture :: PrepQuery W (TeamId, Text) () - delFuture = "DELETE FROM scim_external WHERE team = ? and external_id = ?" + delete :: PrepQuery W (TeamId, Text) () + delete = "DELETE FROM scim_external WHERE team = ? and external_id = ?" diff --git a/services/spar/src/Spar/Scim/User.hs b/services/spar/src/Spar/Scim/User.hs index ede938ef153..c1eb1d56575 100644 --- a/services/spar/src/Spar/Scim/User.hs +++ b/services/spar/src/Spar/Scim/User.hs @@ -64,7 +64,7 @@ import qualified Data.UUID.V4 as UUID import Imports import Network.URI (URI, parseURI) import qualified SAML2.WebSSO as SAML -import Spar.App (Spar, getUser, sparCtxOpts, validateEmailIfExists, wrapMonadClient) +import Spar.App (Spar, getUserByScimExternalId, getUserByUref, sparCtxOpts, validateEmailIfExists, wrapMonadClient) import qualified Spar.Data as Data import qualified Spar.Intra.Brig as Brig import Spar.Scim.Auth () @@ -370,7 +370,7 @@ createValidScimUser tokeninfo@ScimTokenInfo {stiTeam} vsu@(ST.ValidScimUser veid $ do -- ensure uniqueness constraints of all affected identifiers. -- {if we crash now, retry POST will just work} - assertExternalIdUnused veid + assertExternalIdUnused stiTeam veid assertHandleUnused handl -- {if we crash now, retry POST will just work, or user gets told the handle -- is already in use and stops POSTing} @@ -444,7 +444,7 @@ updateValidScimUser :: UserId -> ST.ValidScimUser -> m (Scim.StoredUser ST.SparTag) -updateValidScimUser tokinfo uid newValidScimUser = +updateValidScimUser tokinfo@ScimTokenInfo {stiTeam} uid newValidScimUser = logScim ( logFunction "Spar.Scim.User.updateValidScimUser" . logVSU newValidScimUser @@ -459,7 +459,7 @@ updateValidScimUser tokinfo uid newValidScimUser = -- assertions about new valid scim user that cannot be checked in 'validateScimUser' because -- they differ from the ones in 'createValidScimUser'. - assertExternalIdNotUsedElsewhere (newValidScimUser ^. ST.vsuExternalId) uid + assertExternalIdNotUsedElsewhere stiTeam (newValidScimUser ^. ST.vsuExternalId) uid assertHandleNotUsedElsewhere uid (newValidScimUser ^. ST.vsuHandle) if oldValidScimUser == newValidScimUser @@ -471,7 +471,7 @@ updateValidScimUser tokinfo uid newValidScimUser = case ( oldValidScimUser ^. ST.vsuExternalId, newValidScimUser ^. ST.vsuExternalId ) of - (old, new) | old /= new -> updateVsuUref (stiTeam tokinfo) uid old new + (old, new) | old /= new -> updateVsuUref stiTeam uid old new _ -> pure () when (newValidScimUser ^. ST.vsuName /= oldValidScimUser ^. ST.vsuName) $ do @@ -628,9 +628,9 @@ calculateVersion uid usr = Scim.Weak (Text.pack (show h)) -- -- ASSUMPTION: every scim user has a 'SAML.UserRef', and the `SAML.NameID` in it corresponds -- to a single `externalId`. -assertExternalIdUnused :: ST.ValidExternalId -> Scim.ScimHandler Spar () -assertExternalIdUnused veid = do - mExistingUserId <- lift $ getUser veid +assertExternalIdUnused :: TeamId -> ST.ValidExternalId -> Scim.ScimHandler Spar () +assertExternalIdUnused tid veid = do + mExistingUserId <- lift $ ST.runValidExternalId (getUserByUref) (getUserByScimExternalId tid) veid unless (isNothing mExistingUserId) $ throwError Scim.conflict {Scim.detail = Just "externalId is already taken"} @@ -640,9 +640,9 @@ assertExternalIdUnused veid = do -- -- ASSUMPTION: every scim user has a 'SAML.UserRef', and the `SAML.NameID` in it corresponds -- to a single `externalId`. -assertExternalIdNotUsedElsewhere :: ST.ValidExternalId -> UserId -> Scim.ScimHandler Spar () -assertExternalIdNotUsedElsewhere veid wireUserId = do - mExistingUserId <- lift $ getUser veid +assertExternalIdNotUsedElsewhere :: TeamId -> ST.ValidExternalId -> UserId -> Scim.ScimHandler Spar () +assertExternalIdNotUsedElsewhere tid veid wireUserId = do + mExistingUserId <- lift $ ST.runValidExternalId getUserByUref (getUserByScimExternalId tid) veid unless (mExistingUserId `elem` [Nothing, Just wireUserId]) $ do throwError Scim.conflict {Scim.detail = Just "externalId already in use by another Wire user"} @@ -791,7 +791,7 @@ scimFindUserByEmail mIdpConfig stiTeam email = do -- FUTUREWORK: we could also always lookup brig, that's simpler and possibly faster, -- and it never should be visible in spar, but not in brig. inspar, inbrig :: Spar (Maybe UserId) - inspar = wrapMonadClient $ Data.lookupScimExternalId eml + inspar = wrapMonadClient $ Data.lookupScimExternalId stiTeam eml inbrig = userId . accountUser <$$> Brig.getBrigUserByEmail eml logFilter :: Filter -> (Msg -> Msg) diff --git a/services/spar/test-integration/Test/Spar/APISpec.hs b/services/spar/test-integration/Test/Spar/APISpec.hs index 965b1e972d9..da0ad684911 100644 --- a/services/spar/test-integration/Test/Spar/APISpec.hs +++ b/services/spar/test-integration/Test/Spar/APISpec.hs @@ -390,13 +390,13 @@ specBindingUsers = describe "binding existing users to sso identities" $ do context "known IdP, running session with sso user" $ do checkInitiateBind True (registerTestIdPWithMeta >>= \(_, _, idp, (_, privcreds)) -> loginSsoUserFirstTime idp privcreds) describe "POST /sso/finalize-login" $ do - let checkGrantingAuthnResp :: HasCallStack => UserId -> SignedAuthnResponse -> ResponseLBS -> TestSpar () - checkGrantingAuthnResp uid sparrq sparresp = do + let checkGrantingAuthnResp :: HasCallStack => TeamId -> UserId -> SignedAuthnResponse -> ResponseLBS -> TestSpar () + checkGrantingAuthnResp tid uid sparrq sparresp = do checkGrantingAuthnResp' sparresp ssoidViaAuthResp <- getSsoidViaAuthResp sparrq ssoidViaSelf <- getSsoidViaSelf uid liftIO $ ('s', ssoidViaSelf) `shouldBe` ('s', ssoidViaAuthResp) - Just uidViaSpar <- ssoToUidSpar ssoidViaAuthResp + Just uidViaSpar <- ssoToUidSpar tid ssoidViaAuthResp liftIO $ ('u', uidViaSpar) `shouldBe` ('u', uid) checkGrantingAuthnResp' :: HasCallStack => ResponseLBS -> TestSpar () checkGrantingAuthnResp' sparresp = do @@ -458,21 +458,21 @@ specBindingUsers = describe "binding existing users to sso identities" $ do pure (authnResp, sparAuthnResp) context "initial bind" $ do it "allowed" $ do - (uid, _, idp, (_, privcreds)) <- registerTestIdPWithMeta + (uid, tid, idp, (_, privcreds)) <- registerTestIdPWithMeta (_, authnResp, sparAuthnResp) <- initialBind uid idp privcreds - checkGrantingAuthnResp uid authnResp sparAuthnResp + checkGrantingAuthnResp tid uid authnResp sparAuthnResp context "re-bind to same UserRef" $ do it "allowed" $ do - (uid, _, idp, (_, privcreds)) <- registerTestIdPWithMeta + (uid, tid, idp, (_, privcreds)) <- registerTestIdPWithMeta (subj, _, _) <- initialBind uid idp privcreds (sparrq, sparresp) <- reBindSame uid idp privcreds subj - checkGrantingAuthnResp uid sparrq sparresp + checkGrantingAuthnResp tid uid sparrq sparresp context "re-bind to new UserRef from different IdP" $ do it "allowed" $ do - (uid, _, idp, (_, privcreds)) <- registerTestIdPWithMeta + (uid, tid, idp, (_, privcreds)) <- registerTestIdPWithMeta _ <- initialBind uid idp privcreds (sparrq, sparresp) <- reBindDifferent uid - checkGrantingAuthnResp uid sparrq sparresp + checkGrantingAuthnResp tid uid sparrq sparresp context "bind to UserRef in use by other wire user" $ do it "forbidden" $ do env <- ask @@ -496,11 +496,11 @@ specBindingUsers = describe "binding existing users to sso identities" $ do let check :: HasCallStack => (Cky.Cookies -> Maybe Cky.Cookies) -> Bool -> SpecWith TestEnv check tweakcookies bindsucceeds = do it (if bindsucceeds then "binds existing user" else "creates new user") $ do - (uid, _, idp, (_, privcreds)) <- registerTestIdPWithMeta + (uid, tid, idp, (_, privcreds)) <- registerTestIdPWithMeta (subj :: NameID, sparrq, sparresp) <- initialBind' tweakcookies uid idp privcreds checkGrantingAuthnResp' sparresp uid' <- getUserIdViaRef $ UserRef (idp ^. idpMetadata . edIssuer) subj - checkGrantingAuthnResp uid' sparrq sparresp + checkGrantingAuthnResp tid uid' sparrq sparresp liftIO $ (if bindsucceeds then shouldBe else shouldNotBe) uid' uid addAtBeginning :: Cky.SetCookie -> Cky.Cookies -> Cky.Cookies addAtBeginning cky = ((Cky.setCookieName cky, Cky.setCookieValue cky) :) diff --git a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs index fa54134ed02..04e20a52b44 100644 --- a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs @@ -1655,7 +1655,7 @@ testDeletedUsersFreeExternalIdNoIdp = do void $ aFewTimes - (runClient clientState $ lookupScimExternalId email) + (runClient clientState $ lookupScimExternalId tid email) (== Nothing) specSCIMManaged :: SpecWith TestEnv diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index 14a500108f7..bf17c6ec24d 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -1123,13 +1123,13 @@ callDeleteDefaultSsoCode sparreq_ = do -- helpers talking to spar's cassandra directly -- | Look up 'UserId' under 'UserSSOId' on spar's cassandra directly. -ssoToUidSpar :: (HasCallStack, MonadIO m, MonadReader TestEnv m) => Brig.UserSSOId -> m (Maybe UserId) -ssoToUidSpar ssoid = do +ssoToUidSpar :: (HasCallStack, MonadIO m, MonadReader TestEnv m) => TeamId -> Brig.UserSSOId -> m (Maybe UserId) +ssoToUidSpar tid ssoid = do veid <- either (error . ("could not parse brig sso_id: " <>)) pure $ Intra.veidFromUserSSOId ssoid runSparCass @Client $ runValidExternalId Data.getSAMLUser - Data.lookupScimExternalId + (Data.lookupScimExternalId tid) veid runSparCass :: From bb1d4574168d02072896fce167fa249cbcca0349 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Tue, 23 Mar 2021 11:15:32 +0100 Subject: [PATCH 18/18] Update CHANGELOG --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91b231319a3..1e0e5bc5566 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,38 @@ ## Internal changes --> +# [2020-03-23] + +## Release Notes + +Note that you should never skip a release when upgrading Wire. If you are upgrading to this release, make sure you have deployed the previous releases in order beforehand. + +## Features + +* [federation] Handle errors which could happen while talking to remote federator (#1408) +* [federation] Forward grpc traffic to federator via ingress (or nginz for local integration tests) (#1386) +* [federation] Return UserProfile when getting user by qualified handle (#1397) + +## Bug fixes and other updates + +* [SCIM] Fix: Invalid requests raise 5xxs (#1392) +* [SAML] Fix: permissions for IdP CRUD operations. (#1405) + +## Documentation + +* Tweak docs about team search visibility configuration. (#1407) +* Move docs around. (#1399) +* Describe how to look at swagger locally (#1388) + +## Internal changes + +* Optimize /users/list-clients to only fetch required things from DB (#1398) +* [SCIM] Remove usage of spar.scim_external_ids table (#1418) +* Add-license. (#1394) +* Bump nixpkgs for hls-1.0 (#1412) +* stack-deps.nix: Use nixpkgs from niv (#1406) + + # [2020-03-21] ## Release Notes