From fa2bdda2e2483bdbea50e6abdcb0ca25db5c0ce5 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Tue, 31 Jan 2023 12:09:53 +0100 Subject: [PATCH 01/56] Fix flaky tests, add debugging to others (#3041) --- services/brig/test/integration/API/Federation.hs | 4 +++- services/brig/test/integration/API/MLS.hs | 4 ++-- services/galley/test/integration/API/Util.hs | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/services/brig/test/integration/API/Federation.hs b/services/brig/test/integration/API/Federation.hs index a3685ad4da..af65db356b 100644 --- a/services/brig/test/integration/API/Federation.hs +++ b/services/brig/test/integration/API/Federation.hs @@ -40,6 +40,7 @@ import Data.Timeout import qualified Data.UUID.V4 as UUIDv4 import Federation.Util (generateClientPrekeys) import Imports +import qualified Network.Wai.Test as WaiTest import Test.QuickCheck hiding ((===)) import Test.Tasty import qualified Test.Tasty.Cannon as WS @@ -196,7 +197,8 @@ testSearchRestrictions opts brig = do Opt.FederationDomainConfig domainFullSearch FullSearch ] - let expectSearch domain squery expectedUsers expectedSearchPolicy = do + let expectSearch :: HasCallStack => Domain -> Text -> [Qualified UserId] -> FederatedUserSearchPolicy -> WaiTest.Session () + expectSearch domain squery expectedUsers expectedSearchPolicy = do searchResponse <- runWaiTestFedClient domain $ createWaiTestFedClient @"search-users" @'Brig (SearchRequest squery) diff --git a/services/brig/test/integration/API/MLS.hs b/services/brig/test/integration/API/MLS.hs index 93c118cf17..440da8e28d 100644 --- a/services/brig/test/integration/API/MLS.hs +++ b/services/brig/test/integration/API/MLS.hs @@ -84,7 +84,7 @@ testKeyPackageZeroCount brig = do testKeyPackageExpired :: Brig -> Http () testKeyPackageExpired brig = do u <- userQualifiedId <$> randomUser brig - let lifetime = 2 # Second + let lifetime = 3 # Second [c1, c2] <- for [(0, Just lifetime), (1, Nothing)] $ \(i, lt) -> do c <- createClient brig u i -- upload 1 key package for each client @@ -95,7 +95,7 @@ testKeyPackageExpired brig = do count <- getKeyPackageCount brig u cid liftIO $ count @?= expectedCount -- wait for c1's key package to expire - threadDelay (fromIntegral ((lifetime + 3 # Second) #> MicroSecond)) + threadDelay (fromIntegral ((lifetime + 4 # Second) #> MicroSecond)) -- c1's key package has expired by now for_ [(c1, 0), (c2, 1)] $ \(cid, expectedCount) -> do diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index a2ca4c3f48..329097b95c 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -2741,7 +2741,7 @@ checkConvMemberLeaveEvent cid usr w = WS.assertMatch_ checkTimeout w $ \notif -> other -> assertFailure $ "Unexpected event data: " <> show other checkTimeout :: WS.Timeout -checkTimeout = 3 # Second +checkTimeout = 4 # Second -- | The function is used in conjuction with 'withTempMockFederator' to mock -- responses by Brig on the mocked side of federation. From f959352a9a43a1b55ad19e98d5329f6ac54aec31 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 31 Jan 2023 14:08:50 +0100 Subject: [PATCH 02/56] Streamline team feature ExposeInvitationURLsToTeamAdminConfig implementation (#3039) --- libs/wire-api/src/Wire/API/Team/Feature.hs | 3 ++- .../galley/src/Galley/API/Teams/Features.hs | 27 +++++-------------- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 5fe2776909..885e6c07dd 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -136,7 +136,8 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -- -- 5. Implement 'GetFeatureConfig' and 'SetFeatureConfig' in -- Galley.API.Teams.Features which defines the main business logic for getting --- and setting (with side-effects). +-- and setting (with side-effects). Note that we don't have to check the lockstatus inside 'setConfigForTeam' +-- because the lockstatus is checked in 'setFeatureStatus' before which is the public API for setting the feature status. -- -- 6. Add public routes to Wire.API.Routes.Public.Galley.Feature: 'FeatureStatusGet', -- 'FeatureStatusPut' (optional) and by by user: 'FeatureConfigGet'. Then diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index 04b8eda219..3da18e4848 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -108,6 +108,11 @@ class GetFeatureConfig (db :: Type) cfg where getConfigForServer :: Members '[Input Opts] r => Sem r (WithStatus cfg) + -- only override if there is additional business logic for getting the feature config + -- and/or if the feature flag is configured for the backend in 'FeatureFlags' for galley in 'Galley.Types.Teams' + -- otherwise this will return the default config from wire-api + default getConfigForServer :: (IsFeatureConfig cfg) => Sem r (WithStatus cfg) + getConfigForServer = pure defFeatureStatus getConfigForTeam :: GetConfigForTeamConstraints db cfg r => @@ -674,10 +679,7 @@ instance GetFeatureConfig db ValidateSAMLEmailsConfig where instance SetFeatureConfig db ValidateSAMLEmailsConfig where setConfigForTeam tid wsnl = persistAndPushEvent @db tid wsnl -instance GetFeatureConfig db DigitalSignaturesConfig where - -- FUTUREWORK: we may also want to get a default from the server config file here, like for - -- sso, and team search visibility. - getConfigForServer = pure defFeatureStatus +instance GetFeatureConfig db DigitalSignaturesConfig instance SetFeatureConfig db DigitalSignaturesConfig where setConfigForTeam tid wsnl = persistAndPushEvent @db tid wsnl @@ -703,8 +705,6 @@ instance GetFeatureConfig db LegalholdConfig where r ) - getConfigForServer = pure defFeatureStatus - getConfigForTeam tid = do status <- isLegalHoldEnabledForTeam @db tid <&> \case @@ -866,15 +866,6 @@ instance SetFeatureConfig db MLSConfig where persistAndPushEvent @db tid wsnl instance GetFeatureConfig db ExposeInvitationURLsToTeamAdminConfig where - getConfigForServer = - -- we could look at the galley settings, but we don't have a team here, so there is not much else we can say. - pure $ - withStatus - FeatureStatusDisabled - LockStatusLocked - ExposeInvitationURLsToTeamAdminConfig - FeatureTTLUnlimited - getConfigForTeam tid = do allowList <- input <&> view (optSettings . setExposeInvitationURLsTeamAllowlist . to (fromMaybe [])) mbOldStatus <- TeamFeatures.getFeatureConfig @db (Proxy @ExposeInvitationURLsToTeamAdminConfig) tid <&> fmap wssStatus @@ -897,11 +888,7 @@ instance GetFeatureConfig db ExposeInvitationURLsToTeamAdminConfig where instance SetFeatureConfig db ExposeInvitationURLsToTeamAdminConfig where type SetConfigForTeamConstraints db ExposeInvitationURLsToTeamAdminConfig (r :: EffectRow) = (Member (ErrorS OperationDenied) r) - setConfigForTeam tid wsnl = do - lockStatus <- getConfigForTeam @db @ExposeInvitationURLsToTeamAdminConfig tid <&> wsLockStatus - case lockStatus of - LockStatusLocked -> throwS @OperationDenied - LockStatusUnlocked -> persistAndPushEvent @db tid wsnl + setConfigForTeam tid wsnl = persistAndPushEvent @db tid wsnl instance SetFeatureConfig db OutlookCalIntegrationConfig where setConfigForTeam tid wsnl = persistAndPushEvent @db tid wsnl From 0a8d0a49d99fb85d1cdf9a73239207fb6bb7affc Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 31 Jan 2023 15:26:12 +0100 Subject: [PATCH 03/56] charts: Mark all test resources to be only created while running tests (#3037) * charts: Mark all test resources to be only created while running tests * Use patched helm to ensure it doesn't try to get logs of configmaps * ciImage,devSetup: Add awk * ciImage: Add cfssl --- changelog.d/5-internal/helm-test | 1 + .../templates/tests/brig-integration.yaml | 4 +- charts/brig/templates/tests/configmap.yaml | 2 + .../brig/templates/tests/nginz-service.yaml | 2 + charts/brig/templates/tests/secret.yaml | 2 + .../tests/cargohold-integration.yaml | 2 +- .../cargohold/templates/tests/configmap.yaml | 2 + .../federator/templates/tests/configmap.yaml | 2 + .../tests/federator-integration.yaml | 2 +- charts/galley/templates/tests/configmap.yaml | 2 + .../templates/tests/galley-integration.yaml | 4 +- charts/galley/templates/tests/secret.yaml | 2 + charts/gundeck/templates/tests/configmap.yaml | 2 + .../templates/tests/gundeck-integration.yaml | 2 +- charts/spar/templates/tests/configmap.yaml | 2 + .../templates/tests/spar-integration.yaml | 2 +- nix/overlay.nix | 13 +---- nix/pkgs/helm/default.nix | 52 +++++++++++++++++++ nix/wire-server.nix | 3 +- 19 files changed, 84 insertions(+), 19 deletions(-) create mode 100644 changelog.d/5-internal/helm-test create mode 100644 nix/pkgs/helm/default.nix diff --git a/changelog.d/5-internal/helm-test b/changelog.d/5-internal/helm-test new file mode 100644 index 0000000000..14bfeb22a3 --- /dev/null +++ b/changelog.d/5-internal/helm-test @@ -0,0 +1 @@ +charts: Mark all test resources to be only created while running tests \ No newline at end of file diff --git a/charts/brig/templates/tests/brig-integration.yaml b/charts/brig/templates/tests/brig-integration.yaml index 0687604fb2..81020f9d00 100644 --- a/charts/brig/templates/tests/brig-integration.yaml +++ b/charts/brig/templates/tests/brig-integration.yaml @@ -2,6 +2,8 @@ apiVersion: v1 kind: Service metadata: name: "brig-integration" + annotations: + "helm.sh/hook": test labels: app: brig-integration chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} @@ -19,7 +21,7 @@ kind: Pod metadata: name: "{{ .Release.Name }}-brig-integration" annotations: - "helm.sh/hook": test-success + "helm.sh/hook": test labels: app: brig-integration release: {{ .Release.Name }} diff --git a/charts/brig/templates/tests/configmap.yaml b/charts/brig/templates/tests/configmap.yaml index 01721ebf18..4d409eb59f 100644 --- a/charts/brig/templates/tests/configmap.yaml +++ b/charts/brig/templates/tests/configmap.yaml @@ -2,6 +2,8 @@ apiVersion: v1 kind: ConfigMap metadata: name: "brig-integration" + annotations: + "helm.sh/hook": test data: integration.yaml: | brig: diff --git a/charts/brig/templates/tests/nginz-service.yaml b/charts/brig/templates/tests/nginz-service.yaml index c31128667c..ec1b22284b 100644 --- a/charts/brig/templates/tests/nginz-service.yaml +++ b/charts/brig/templates/tests/nginz-service.yaml @@ -5,6 +5,8 @@ apiVersion: v1 kind: Service metadata: name: nginz-integration-http + annotations: + "helm.sh/hook": test spec: type: ClusterIP ports: diff --git a/charts/brig/templates/tests/secret.yaml b/charts/brig/templates/tests/secret.yaml index bfe877caf7..bda96d4f3f 100644 --- a/charts/brig/templates/tests/secret.yaml +++ b/charts/brig/templates/tests/secret.yaml @@ -2,6 +2,8 @@ apiVersion: v1 kind: ConfigMap metadata: name: brig-integration-secrets + annotations: + "helm.sh/hook": test data: # These "secrets" are only used in tests and are therefore safe to be stored unencrypted provider-privatekey.pem: | diff --git a/charts/cargohold/templates/tests/cargohold-integration.yaml b/charts/cargohold/templates/tests/cargohold-integration.yaml index 6decd33e47..722d138637 100644 --- a/charts/cargohold/templates/tests/cargohold-integration.yaml +++ b/charts/cargohold/templates/tests/cargohold-integration.yaml @@ -3,7 +3,7 @@ kind: Pod metadata: name: "{{ .Release.Name }}-cargohold-integration" annotations: - "helm.sh/hook": test-success + "helm.sh/hook": test spec: volumes: - name: "cargohold-integration" diff --git a/charts/cargohold/templates/tests/configmap.yaml b/charts/cargohold/templates/tests/configmap.yaml index bb0ff67c8f..1a52056adb 100644 --- a/charts/cargohold/templates/tests/configmap.yaml +++ b/charts/cargohold/templates/tests/configmap.yaml @@ -2,6 +2,8 @@ apiVersion: v1 kind: ConfigMap metadata: name: "cargohold-integration" + annotations: + "helm.sh/hook": test data: integration.yaml: | cargohold: diff --git a/charts/federator/templates/tests/configmap.yaml b/charts/federator/templates/tests/configmap.yaml index 910411fe5d..b845f3d401 100644 --- a/charts/federator/templates/tests/configmap.yaml +++ b/charts/federator/templates/tests/configmap.yaml @@ -2,6 +2,8 @@ apiVersion: v1 kind: ConfigMap metadata: name: "federator-integration" + annotations: + "helm.sh/hook": test data: integration.yaml: | federatorInternal: diff --git a/charts/federator/templates/tests/federator-integration.yaml b/charts/federator/templates/tests/federator-integration.yaml index 32e6eef09e..3341cf4173 100644 --- a/charts/federator/templates/tests/federator-integration.yaml +++ b/charts/federator/templates/tests/federator-integration.yaml @@ -3,7 +3,7 @@ kind: Pod metadata: name: "{{ .Release.Name }}-federator-integration" annotations: - "helm.sh/hook": test-success + "helm.sh/hook": test spec: volumes: - name: "federator-integration" diff --git a/charts/galley/templates/tests/configmap.yaml b/charts/galley/templates/tests/configmap.yaml index 10d65d5b65..20036f2f15 100644 --- a/charts/galley/templates/tests/configmap.yaml +++ b/charts/galley/templates/tests/configmap.yaml @@ -2,6 +2,8 @@ apiVersion: v1 kind: ConfigMap metadata: name: "galley-integration" + annotations: + "helm.sh/hook": test data: integration.yaml: | galley: diff --git a/charts/galley/templates/tests/galley-integration.yaml b/charts/galley/templates/tests/galley-integration.yaml index 7d6efa6e4b..d7b64bf5ba 100644 --- a/charts/galley/templates/tests/galley-integration.yaml +++ b/charts/galley/templates/tests/galley-integration.yaml @@ -2,6 +2,8 @@ apiVersion: v1 kind: Service metadata: name: "galley-integration" + annotations: + "helm.sh/hook": test labels: app: galley-integration chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} @@ -19,7 +21,7 @@ kind: Pod metadata: name: "{{ .Release.Name }}-galley-integration" annotations: - "helm.sh/hook": test-success + "helm.sh/hook": test labels: app: galley-integration release: {{ .Release.Name }} diff --git a/charts/galley/templates/tests/secret.yaml b/charts/galley/templates/tests/secret.yaml index 74f118d1c2..ec0e5624b2 100644 --- a/charts/galley/templates/tests/secret.yaml +++ b/charts/galley/templates/tests/secret.yaml @@ -2,6 +2,8 @@ apiVersion: v1 kind: ConfigMap metadata: name: galley-integration-secrets + annotations: + "helm.sh/hook": test data: # These "secrets" are only used in tests and are therefore safe to be stored unencrypted provider-privatekey.pem: | diff --git a/charts/gundeck/templates/tests/configmap.yaml b/charts/gundeck/templates/tests/configmap.yaml index 5c398d39e1..004d58f060 100644 --- a/charts/gundeck/templates/tests/configmap.yaml +++ b/charts/gundeck/templates/tests/configmap.yaml @@ -2,6 +2,8 @@ apiVersion: v1 kind: ConfigMap metadata: name: "gundeck-integration" + annotations: + "helm.sh/hook": test data: integration.yaml: | gundeck: diff --git a/charts/gundeck/templates/tests/gundeck-integration.yaml b/charts/gundeck/templates/tests/gundeck-integration.yaml index 8424fd3770..4f81fa2c22 100644 --- a/charts/gundeck/templates/tests/gundeck-integration.yaml +++ b/charts/gundeck/templates/tests/gundeck-integration.yaml @@ -3,7 +3,7 @@ kind: Pod metadata: name: "{{ .Release.Name }}-gundeck-integration" annotations: - "helm.sh/hook": test-success + "helm.sh/hook": test spec: volumes: - name: "gundeck-integration" diff --git a/charts/spar/templates/tests/configmap.yaml b/charts/spar/templates/tests/configmap.yaml index e446ee7bdd..8773895447 100644 --- a/charts/spar/templates/tests/configmap.yaml +++ b/charts/spar/templates/tests/configmap.yaml @@ -2,6 +2,8 @@ apiVersion: v1 kind: ConfigMap metadata: name: "spar-integration" + annotations: + "helm.sh/hook": test data: integration.yaml: | brig: diff --git a/charts/spar/templates/tests/spar-integration.yaml b/charts/spar/templates/tests/spar-integration.yaml index c4735ffd15..7063bbb745 100644 --- a/charts/spar/templates/tests/spar-integration.yaml +++ b/charts/spar/templates/tests/spar-integration.yaml @@ -3,7 +3,7 @@ kind: Pod metadata: name: "{{ .Release.Name }}-spar-integration" annotations: - "helm.sh/hook": test-success + "helm.sh/hook": test labels: app: spar-integration release: {{ .Release.Name }} diff --git a/nix/overlay.nix b/nix/overlay.nix index d63b121432..1f20335dbb 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -87,18 +87,7 @@ self: super: { inherit (super) stdenv fetchurl; }; - helm = staticBinaryInTarball { - pname = "helm"; - version = "3.6.3"; - - darwinAmd64Url = "https://get.helm.sh/helm-v3.6.3-darwin-amd64.tar.gz"; - darwinAmd64Sha256 = "0djjvgla8cw27h8s4y6jby19f74j58byb2vfv590cd03vlbzz8c4"; - - linuxAmd64Url = "https://get.helm.sh/helm-v3.6.3-linux-amd64.tar.gz"; - linuxAmd64Sha256 = "0qp28fq137b07haz4vsdbc5biagh60dcs29jj70ksqi5k6201h87"; - - inherit (super) stdenv fetchurl; - }; + helm = super.callPackage ./pkgs/helm {}; helmfile = staticBinary { pname = "helmfile"; diff --git a/nix/pkgs/helm/default.nix b/nix/pkgs/helm/default.nix new file mode 100644 index 0000000000..8b68403913 --- /dev/null +++ b/nix/pkgs/helm/default.nix @@ -0,0 +1,52 @@ +# Copied from nixpkgs and modified because it seems too complicated to override +# buildGoModule packages. +{ lib, stdenv, buildGoModule, fetchFromGitHub, installShellFiles }: + +buildGoModule rec { + pname = "kubernetes-helm"; + version = "3.11.0-patched"; + + src = fetchFromGitHub { + owner = "wireapp"; + repo = "helm"; + rev = "949de3195be5b3d21ed707da18ee3bcb2a9a2af8"; + sha256 = "sha256-alyR6+gm7WEvFfJxHl9a0jpC3+457Kg6aRHcidA0RZg="; + }; + vendorSha256 = "sha256-LRMDrBSl5EGQqQt5FUU4JJHqdwfYt5qsVpe76jUQBVI="; + + subPackages = [ "cmd/helm" ]; + ldflags = [ + "-w" + "-s" + "-X helm.sh/helm/v3/internal/version.version=v${version}" + "-X helm.sh/helm/v3/internal/version.gitCommit=${src.rev}" + ]; + + preCheck = '' + # skipping version tests because they require dot git directory + substituteInPlace cmd/helm/version_test.go \ + --replace "TestVersion" "SkipVersion" + '' + lib.optionalString stdenv.isLinux '' + # skipping plugin tests on linux + substituteInPlace cmd/helm/plugin_test.go \ + --replace "TestPluginDynamicCompletion" "SkipPluginDynamicCompletion" \ + --replace "TestLoadPlugins" "SkipLoadPlugins" + substituteInPlace cmd/helm/helm_test.go \ + --replace "TestPluginExitCode" "SkipPluginExitCode" + ''; + + nativeBuildInputs = [ installShellFiles ]; + postInstall = '' + $out/bin/helm completion bash > helm.bash + $out/bin/helm completion zsh > helm.zsh + installShellCompletion helm.{bash,zsh} + ''; + + meta = with lib; { + homepage = "https://github.com/kubernetes/helm"; + description = "A package manager for kubernetes"; + mainProgram = "helm"; + license = licenses.asl20; + maintainers = with maintainers; [ rlupton20 edude03 saschagrunert Frostman Chili-Man techknowlogick ]; + }; +} diff --git a/nix/wire-server.nix b/nix/wire-server.nix index 99dc0caaaf..33320d327d 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -308,6 +308,8 @@ let pkgs.ormolu pkgs.shellcheck pkgs.treefmt + pkgs.gawk + pkgs.cfssl (hlib.justStaticExecutables pkgs.haskellPackages.cabal-fmt) ] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.skopeo @@ -365,7 +367,6 @@ in paths = commonTools ++ [ (pkgs.haskell-language-server.override { supportedGhcVersions = [ "92" ]; }) pkgs.ghcid - pkgs.cfssl pkgs.kind pkgs.netcat pkgs.niv From 17b6e3a4dffb8d0c758989f2a3667742542899af Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Wed, 1 Feb 2023 14:41:52 +0100 Subject: [PATCH 04/56] Update searchability documentation (#3044) --- docs/src/understand/searchability.md | 189 ++++++++++++--------------- 1 file changed, 84 insertions(+), 105 deletions(-) diff --git a/docs/src/understand/searchability.md b/docs/src/understand/searchability.md index 083faa030f..625e6754ac 100644 --- a/docs/src/understand/searchability.md +++ b/docs/src/understand/searchability.md @@ -5,7 +5,7 @@ You can configure how search is limited or not based on user membership in a giv There are two types of searches based on the direction of search: - **Inbound** searches mean that somebody is searching for you. Configuring the inbound search visibility means that you (or some admin) can configure whether others can find you or not. -- **Outbound** searches mean that you are searching for somebody. Configuring the outbound search visibility means that some admin can configure whether you can find other users or not. +- **Out-Bound** searches mean that you are searching for somebody. Configuring the out-bound search visibility means that some admin can configure whether you can find other users or not. There are different types of matches: @@ -14,9 +14,13 @@ There are different types of matches: ## Searching users on the same backend +```{note} +For configuring searching accross federated backends this section is irrelevant. +``` + Search visibility is controlled by three parameters on the backend: -- A team outbound configuration flag, `TeamSearchVisibility` with possible values `SearchVisibilityStandard`, `SearchVisibilityNoNameOutsideTeam` +- A team out-bound configuration flag, `TeamSearchVisibility` with possible values `SearchVisibilityStandard`, `SearchVisibilityNoNameOutsideTeam` - `SearchVisibilityStandard` means that the user can find other people outside of the team, if the searched-person inbound search allows it - `SearchVisibilityNoNameOutsideTeam` means that the user can not find any user outside the team by full text search (but exact handle search still works) @@ -28,7 +32,7 @@ Search visibility is controlled by three parameters on the backend: - A server configuration flag `searchSameTeamOnly` with possible values true, false. - - `Note`: For the same backend, this affects inbound and outbound searches (simply because all teams will be subject to this behavior) + - `Note`: For the same backend, this affects inbound and out-bound searches (simply because all teams will be subject to this behavior) - Setting this to `true` means that the all teams on that backend can only find users that belong to their team These flag are set on the backend and the clients do not need to be aware of them. @@ -45,13 +49,13 @@ The flags will influence the behavior of the search API endpoint; clients will o +------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ | Yes, `tA` | Yes, the same team `tA` | Irrelevant | Irrelevant | Irrelevant | Found | Found | +------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ -| **Outbound search unrestricted** | +| **Out-Bound search unrestricted** | +------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ | Yes, `tA` | Yes, another team tB | false | `SearchVisibilityStandard` | `SearchableByAllTeams` | Found | Found | +------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ | Yes, `tA` | Yes, another team tB | false | `SearchVisibilityStandard` | `SearchableByOwnTeam` | Found | Not found | +------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ -| **Outbound search restricted** | +| **Out-Bound search restricted** | +------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ | Yes, `tA` | Yes, another team tB | true | Irrelevant | Irrelevant | Not found | Not found | +------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ @@ -95,45 +99,38 @@ galley: This default value applies to all teams for which no explicit configuration of the `TeamSearchVisibility` has been set. -## Searching users on another (federated) backend - -For federated search the table above does not apply, see following table. - -```{note} -Incoming federated searches (i.e. searches from one backend to another) are considered always as being performed from a team user, even if they are performed from a personal user. - -This is because the incoming search request does not carry the information whether the user performing the search was in a team or not. +## Searching users on another federated backend -So we have to make one assumption, and we assume that they were in a team. -``` Allowing search is done at the backend configuration level by the sysadmin: -- Outbound search restrictions (`searchSameTeamOnly`, `TeamSearchVisibility`) do not apply to federated searches - - A configuration setting `FederatedUserSearchPolicy` per federating domain with these possible values: - `no_search` The federating backend is not allowed to search any users (either by exact handle or full-text). - `exact_handle_search` The federating backend may only search by exact handle - `full_search` The federating backend may search users by full text search on display name and handle. The search search results are additionally affected by `SearchVisibilityInbound` setting of each team on the backend. + The configuration value `FederatedUserSearchPolicy` is per federated domain, e.g. in the values of the wire-server chart: + + ```yaml + brig: + config: + optSettings: + setFederationDomainConfigs: + - domain: a.example.com + search_policy: no_search + - domain: a.example.com + search_policy: full_search + ``` + - The `SearchVisibilityInbound` setting applies. Since the default value for teams is `SearchableByOwnTeam` this means that for a team to be full-text searchable by users on a federating backend both - `FederatedUserSearchPolicy` needs to be set to to full_search for the federating backend - Any team that wants to be full-text searchable needs to be set to `SearchableByAllTeams` -The configuration value `FederatedUserSearchPolicy` is per federated domain, e.g. in the values of the wire-server chart: +- Out-Bound search restrictions (`searchSameTeamOnly`, `TeamSearchVisibility`) do not apply to federated searches + -```yaml -brig: - config: - optSettings: - setFederationDomainConfigs: - - domain: a.example.com - search_policy: no_search - - domain: a.example.com - search_policy: full_search -``` ### Table of possible outcomes @@ -152,95 +149,39 @@ It’s worth nothing that if two users are on two separate backend, they are als ## Changing the settings for a given team -If you need to change searchabilility for a specific team (rather than the entire backend, as above), you need to make specific calls to the API. - -### Team searchVisibility - -The team flag `searchVisibility` affects the outbound search of user searches. - -If it is set to `no-name-outside-team` for a team then all users of that team will no longer be able to find users that are not part of their team when searching. - -This also includes finding other users by by providing their exact handle. By default it is set to `standard`, which doesn't put any additional restrictions to outbound searches. - -The setting can be changed via endpoint (for more details on how to make the API calls with `curl`, read further): - -``` -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" -``` +### TeamFeature searchVisibilityInbound -The team feature flag `teamSearchVisibility` determines whether it is allowed to change the `searchVisibility` setting or not. +The team feature flag `searchVisibilityInbound` affects whether the team's users are searchable by users from other teams. -The default is `disabled-by-default`. +The default setting is `searchable-by-own-team` which hides users from search +results by users from other teams. If it is set to `searchable-by-all-teams` +then users of this team may be included in the results of search queries by +other users. -```{note} -Whenever this feature setting is disabled the `searchVisibility` will be reset to standard. -``` -The default setting that applies to all teams on the instance can be defined at configuration +The default setting that applies to all teams on the instance can be defined at configuration. ```yaml -settings: - featureFlags: - teamSearchVisibility: disabled-by-default # or enabled-by-default +galley: + config: + settings: + featureFlags: + searchVisibilityInbound: + defaults: + status: enabled # or "disabled" (default is "disabled") ``` -### TeamFeature searchVisibilityInbound - -The team feature flag `searchVisibilityInbound` affects if the team's users are searchable by users from other teams. - -The default setting is `searchable-by-own-team` which hides users from search results by users from other teams. - -If it is set to `searchable-by-all-teams` then users of this team may be included in the results of search queries by other users. - ```{note} -The configuration of this flag does not affect search results when the search query matches the handle exactly. - -If the handle is provdided then any user on the instance can find users. -``` - -This team feature flag can only by toggled by site-administrators with direct access to the galley instance (for more details on how to make the API calls with `curl`, read further): - -``` -PUT /i/teams/{tid}/features/search-visibility-inbound -``` - -With JSON body: - -```json -{"status": "enabled"} +Changing this setting in the instance configuration doesn't affect any users that have already been created. To affect these users please toggle the setting on a per-team basis (see below). Switching between "enabled" and "disabled" setting for the team causes a re-indexing of all the users of the team, thereby making the setting effective, e.g. changing to a "disabled" setting first, followed by changing to an "enabled" setting (or vice versa). ``` -or +#### Overriding the default setting -```json -{"status": "disabled"} -``` +Individual teams can overwrite the default setting with API calls: -Where `enabled` is equivalent to `searchable-by-all-teams` and `disabled` is equivalent to `searchable-by-own-team`. +To make API calls to set an explicit configuration for ` TeamSearchVisibilityInbound` per team, you first need to know the Team ID, which can be found in the team settings app. -The default setting that applies to all teams on the instance can be defined at configuration. - -```yaml -searchVisibilityInbound: - defaults: - status: enabled # OR disabled -``` - -Individual teams can overwrite the default setting with API calls as per above. - -### Making the API calls - -To make API calls to set an explicit configuration for\` TeamSearchVisibilityInbound\` per team, you first need to know the Team ID, which can be found in the team settings app. - -It is an `UUID` which has format like this `dcbedf9a-af2a-4f43-9fd5-525953a919e1`. +It is an [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier) which has format like this `dcbedf9a-af2a-4f43-9fd5-525953a919e1`. In the following we will be using this Team ID as an example, please replace it with your own team id. @@ -267,9 +208,9 @@ Next, set up a port-forwarding from your local machine's port `9000` to the gall kubectl port-forward -n wire galley-5f4787fdc7-9l64n 9000:8080 ``` -Keep this command running until the end of these instuctions. +Keep this command running until the end of these instructions. -Please run the following commands in a seperate terminal while keeping the terminal which establishes the port-forwarding open. +Please run the following commands in a separate terminal while keeping the terminal which establishes the port-forwarding open. To see team's current setting run: @@ -293,3 +234,41 @@ To change the TeamSearchVisibilityInbound to SearchableByOwnTeam for the team ru curl -XPUT -H 'Content-Type: application/json' -d "{\"status\": \"disabled\"}" http://localhost:9000/i/teams/dcbedf9a-af2a-4f43-9fd5-525953a919e1/features/searchVisibilityInbound ``` +### Team searchVisibility + +The team flag `searchVisibility` affects the out-bound search of user searches on the same backend. Federated searches are not affected by its setting. + +If it is set to `no-name-outside-team` for a team then all users of that team will no longer be able to find users that are not part of their team when searching. + +This also includes finding other users by providing their exact handle. By default it is set to `standard`, which doesn't put any additional restrictions to out-bound searches. + +The setting can be changed via endpoint (for more details on how to make the API calls with `curl`, read further): + +``` +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" +``` + +The team feature flag `teamSearchVisibility` determines whether it is allowed to change the `searchVisibility` setting or not. + +The default is `disabled-by-default`. + +```{note} +Whenever this feature setting is disabled the `searchVisibility` will be reset to standard. +``` + +The default setting that applies to all teams on the instance can be defined at configuration + +```yaml +settings: + featureFlags: + teamSearchVisibility: disabled-by-default # or enabled-by-default +``` + From a54578ab8e517aaeb8fd3ce58330007284edb1b3 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 1 Feb 2023 16:22:45 +0000 Subject: [PATCH 05/56] play with hasserver instance --- libs/wire-api/src/Wire/API/OAuth.hs | 4 +- libs/wire-api/src/Wire/API/Routes/Public.hs | 196 ++++++++++++------ .../src/Wire/API/Routes/Public/Brig.hs | 2 +- 3 files changed, 130 insertions(+), 72 deletions(-) diff --git a/libs/wire-api/src/Wire/API/OAuth.hs b/libs/wire-api/src/Wire/API/OAuth.hs index 77084a8036..a6d2c3f344 100644 --- a/libs/wire-api/src/Wire/API/OAuth.hs +++ b/libs/wire-api/src/Wire/API/OAuth.hs @@ -426,8 +426,8 @@ hcsSub = >=> preview string >=> either (const Nothing) pure . parseIdFromText -hasScope :: OAuthScope -> OAuthClaimsSet -> Bool -hasScope s claims = s `Set.member` unOAuthScopes (scope claims) +hasScope :: forall scope. IsOAuthScope scope => OAuthClaimsSet -> Bool +hasScope claims = (toOAuthScope @scope) `Set.member` unOAuthScopes (scope claims) -- | Verify a JWT and return the claims set. Use this function if you have a custom claims set. verify :: JWK -> SignedJWT -> IO (Either JWTError OAuthClaimsSet) diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index f9873d6efb..6bc9136301 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -30,7 +30,7 @@ module Wire.API.Routes.Public ZBot, ZConversation, ZProvider, - ZUserOrOAuth, + ZOauthUser, -- * Swagger combinators OmitDocs, @@ -166,7 +166,7 @@ instance IsZType 'ZAuthProvider ctx where instance HasTokenType 'ZAuthProvider where tokenType = Just "provider" -data ZAuthServant (ztype :: ZType) (opts :: [Type]) +data ZAuthServant (ztype :: ZType) (opts :: [Type]) (scopes :: Maybe OAuthScope) type InternalAuthDefOpts = '[Servant.Required, Servant.Strict] @@ -176,27 +176,30 @@ type InternalAuth ztype opts = (ZHeader ztype) (ZParam ztype) -type ZLocalUser = ZAuthServant 'ZLocalAuthUser InternalAuthDefOpts +type ZLocalUser = ZAuthServant 'ZLocalAuthUser InternalAuthDefOpts 'Nothing -type ZUser = ZAuthServant 'ZAuthUser InternalAuthDefOpts +type ZUser = ZAuthServant 'ZAuthUser InternalAuthDefOpts 'Nothing -type ZClient = ZAuthServant 'ZAuthClient InternalAuthDefOpts +type ZOauthUser scopes = ZAuthServant 'ZAuthUser InternalAuthDefOpts ('Just scopes) -type ZConn = ZAuthServant 'ZAuthConn InternalAuthDefOpts +type ZClient = ZAuthServant 'ZAuthClient InternalAuthDefOpts 'Nothing -type ZBot = ZAuthServant 'ZAuthBot InternalAuthDefOpts +type ZConn = ZAuthServant 'ZAuthConn InternalAuthDefOpts 'Nothing -type ZConversation = ZAuthServant 'ZAuthConv InternalAuthDefOpts +type ZBot = ZAuthServant 'ZAuthBot InternalAuthDefOpts 'Nothing -type ZProvider = ZAuthServant 'ZAuthProvider InternalAuthDefOpts +type ZConversation = ZAuthServant 'ZAuthConv InternalAuthDefOpts 'Nothing -type ZOptUser = ZAuthServant 'ZAuthUser '[Servant.Optional, Servant.Strict] +type ZProvider = ZAuthServant 'ZAuthProvider InternalAuthDefOpts 'Nothing -type ZOptClient = ZAuthServant 'ZAuthClient '[Servant.Optional, Servant.Strict] +type ZOptUser = ZAuthServant 'ZAuthUser '[Servant.Optional, Servant.Strict] 'Nothing -type ZOptConn = ZAuthServant 'ZAuthConn '[Servant.Optional, Servant.Strict] +type ZOptClient = ZAuthServant 'ZAuthClient '[Servant.Optional, Servant.Strict] 'Nothing -instance HasSwagger api => HasSwagger (ZAuthServant 'ZAuthUser _opts :> api) where +type ZOptConn = ZAuthServant 'ZAuthConn '[Servant.Optional, Servant.Strict] 'Nothing + +-- TODO(leif): doc for scopes (also other instances) +instance HasSwagger api => HasSwagger (ZAuthServant 'ZAuthUser _opts scopes :> api) where toSwagger _ = toSwagger (Proxy @api) & securityDefinitions <>~ SecurityDefinitions (InsOrdHashMap.singleton "ZAuth" secScheme) @@ -208,20 +211,112 @@ instance HasSwagger api => HasSwagger (ZAuthServant 'ZAuthUser _opts :> api) whe _securitySchemeDescription = Just "Must be a token retrieved by calling 'POST /login' or 'POST /access'. It must be presented in this format: 'Bearer \\'." } -instance HasSwagger api => HasSwagger (ZAuthServant 'ZLocalAuthUser opts :> api) where - toSwagger _ = toSwagger (Proxy @(ZAuthServant 'ZAuthUser opts :> api)) +instance HasSwagger api => HasSwagger (ZAuthServant 'ZLocalAuthUser opts scopes :> api) where + toSwagger _ = toSwagger (Proxy @(ZAuthServant 'ZAuthUser opts scopes :> api)) -instance HasLink endpoint => HasLink (ZAuthServant usr opts :> endpoint) where - type MkLink (ZAuthServant _ _ :> endpoint) a = MkLink endpoint a +instance HasLink endpoint => HasLink (ZAuthServant usr opts scopes :> endpoint) where + type MkLink (ZAuthServant _ _ _ :> endpoint) a = MkLink endpoint a toLink toA _ = toLink toA (Proxy @endpoint) instance {-# OVERLAPPABLE #-} HasSwagger api => - HasSwagger (ZAuthServant ztype _opts :> api) + HasSwagger (ZAuthServant ztype _opts scopes :> api) where toSwagger _ = toSwagger (Proxy @api) +_routeZAuth :: + forall ztype opts scopes api env ctx a. + ( IsZType ztype ctx, + HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, + HasContextEntry ctx (Maybe JWK), + opts ~ InternalAuthDefOpts, + HasServer api ctx, + IsOAuthScope scopes, + ZQualifiedParam ztype ~ Id a + ) => + Proxy (ZAuthServant ztype opts ('Just scopes) :> api) -> + Context ctx -> + Delayed env (Server (ZAuthServant ztype opts ('Just scopes) :> api)) -> + Router env +_routeZAuth _ ctx subserver = route (Proxy @(ZAuthServant ztype opts 'Nothing :> api)) ctx subserver + +_routeOAuth :: + forall ztype opts scopes api env ctx a. + ( IsZType ztype ctx, + HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, + HasContextEntry ctx (Maybe JWK), + opts ~ InternalAuthDefOpts, + HasServer api ctx, + IsOAuthScope scopes, + ZQualifiedParam ztype ~ Id a + ) => + Proxy (ZAuthServant ztype opts ('Just scopes) :> api) -> + Context ctx -> + Delayed env (Server (ZAuthServant ztype opts ('Just scopes) :> api)) -> + Router env +_routeOAuth _ ctx subserver = route (Proxy @api) ctx (addAuthCheck subserver (withRequest checkAuth)) + where + -- todo(leif): add sig and toplevel + + checkAuth :: Request -> DelayedIO (ZQualifiedParam ztype) + checkAuth = doOAuth (getContextEntry ctx) >=> either delayedFailFatal pure + + doOAuth :: Maybe JWK -> Request -> DelayedIO (Either ServerError (ZQualifiedParam ztype)) + doOAuth mJwk req = tryOAuth + where + tryOAuth :: DelayedIO (Either ServerError (ZQualifiedParam ztype)) + tryOAuth = do + let headerOrError = maybeToRight oauthTokenMissing $ lookup "Z-OAuth" (requestHeaders req) + let jwkOrError = maybeToRight jwtError mJwk + let tokenOrError = headerOrError >>= mapLeft invalidOAuthToken . parseHeader + either (pure . Left) verifyOAuthToken $ (,) <$> tokenOrError <*> jwkOrError + + verifyOAuthToken :: (Bearer OAuthAccessToken, JWK) -> DelayedIO (Either ServerError (ZQualifiedParam ztype)) + verifyOAuthToken (token, key) = do + verifiedOrError <- mapLeft (invalidOAuthToken . cs . show) <$> liftIO (verify key (unOAuthToken . unBearer $ token)) + pure $ + verifiedOrError >>= \claimSet -> + if hasScope @scopes claimSet + then maybeToRight (invalidOAuthToken "Invalid token: Missing or invalid sub claim") (hcsSub claimSet) + else Left insufficientScope + +instance + ( IsZType ztype ctx, + HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, + HasContextEntry ctx (Maybe JWK), + opts ~ InternalAuthDefOpts, + HasServer api ctx, + IsOAuthScope scopes, + ZQualifiedParam ztype ~ Id a + ) => + HasServer (ZAuthServant ztype opts ('Just scopes) :> api) ctx + where + type + ServerT (ZAuthServant ztype opts ('Just scopes) :> api) m = + RequestArgument opts (ZQualifiedParam ztype) -> ServerT api m + + route :: + ( IsZType ztype ctx, + HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, + HasContextEntry ctx (Maybe JWK), + SBoolI (FoldLenient opts), + SBoolI (FoldRequired opts), + HasServer api ctx, + IsOAuthScope scopes + ) => + Proxy (ZAuthServant ztype opts ('Just scopes) :> api) -> + Context ctx -> + Delayed env (Server (ZAuthServant ztype opts ('Just scopes) :> api)) -> + Router env + route _ _ctx _subserver = + -- withRequest $ \req -> maybe routeOAuth (const routeZAuth) $ lookup "Z-Type" (requestHeaders req) + undefined + + hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s + +-- TODO(leif): remove Nothing instance + instance ( IsZType ztype ctx, HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, @@ -229,12 +324,23 @@ instance SBoolI (FoldRequired opts), HasServer api ctx ) => - HasServer (ZAuthServant ztype opts :> api) ctx + HasServer (ZAuthServant ztype opts 'Nothing :> api) ctx where type - ServerT (ZAuthServant ztype opts :> api) m = + ServerT (ZAuthServant ztype opts 'Nothing :> api) m = RequestArgument opts (ZQualifiedParam ztype) -> ServerT api m + route :: + ( IsZType ztype ctx, + HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, + SBoolI (FoldLenient opts), + SBoolI (FoldRequired opts), + HasServer api ctx + ) => + Proxy (ZAuthServant ztype opts 'Nothing :> api) -> + Context ctx -> + Delayed env (Server (ZAuthServant ztype opts 'Nothing :> api)) -> + Router env route _ ctx subserver = do Servant.route (Proxy @(InternalAuth ztype opts :> api)) @@ -259,7 +365,7 @@ instance hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s -instance RoutesToPaths api => RoutesToPaths (ZAuthServant ztype opts :> api) where +instance RoutesToPaths api => RoutesToPaths (ZAuthServant ztype opts scopes :> api) where getRoutes = getRoutes @api -- FUTUREWORK: Make a PR to the servant-swagger package with this instance @@ -297,54 +403,6 @@ data ZUserOrOAuth (scope :: OAuthScope) instance HasSwagger api => HasSwagger (ZUserOrOAuth scope :> api) where toSwagger _ = toSwagger (Proxy @api) -checkZAuthOrOAuth :: OAuthScope -> Maybe JWK -> Request -> DelayedIO (Either ServerError UserId) -checkZAuthOrOAuth oauthScope mJwk req = maybe tryOAuth (pure . Right) tryZUserAuth - where - tryZUserAuth :: Maybe UserId - tryZUserAuth = lookup "Z-User" (requestHeaders req) >>= (either (const Nothing) pure . parseHeader) - - tryOAuth :: DelayedIO (Either ServerError UserId) - tryOAuth = do - let headerOrError = maybeToRight oauthTokenMissing $ lookup "Z-OAuth" (requestHeaders req) - let jwkOrError = maybeToRight jwtError mJwk - let tokenOrError = headerOrError >>= mapLeft invalidOAuthToken . parseHeader - either (pure . Left) verifyOAuthToken $ (,) <$> tokenOrError <*> jwkOrError - - verifyOAuthToken :: (Bearer OAuthAccessToken, JWK) -> DelayedIO (Either ServerError UserId) - verifyOAuthToken (token, key) = do - verifiedOrError <- mapLeft (invalidOAuthToken . cs . show) <$> liftIO (verify key (unOAuthToken . unBearer $ token)) - pure $ - verifiedOrError >>= \claimSet -> - if hasScope oauthScope claimSet - then maybeToRight (invalidOAuthToken "Invalid token: Missing or invalid sub claim") (hcsSub claimSet) - else Left insufficientScope - -instance (HasServer api context, HasContextEntry context (Maybe JWK), IsOAuthScope scope) => HasServer (ZUserOrOAuth scope :> api) context where - type ServerT (ZUserOrOAuth scope :> api) m = UserId -> ServerT api m - - route :: - (HasServer api context, HasContextEntry context (Maybe JWK)) => - Proxy (ZUserOrOAuth scope :> api) -> - Context context -> - Delayed env (Server (ZUserOrOAuth scope :> api)) -> - Router env - route _ ctx svr = route (Proxy @api) ctx (addAuthCheck svr (withRequest checkAuth)) - where - checkAuth :: Request -> DelayedIO UserId - checkAuth = checkZAuthOrOAuth (toOAuthScope @scope) (getContextEntry ctx) >=> either delayedFailFatal pure - - hoistServerWithContext :: - (HasServer api context, HasContextEntry context (Maybe JWK)) => - Proxy (ZUserOrOAuth scope :> api) -> - Proxy context -> - (forall x. m x -> n x) -> - ServerT (ZUserOrOAuth scope :> api) m -> - ServerT (ZUserOrOAuth scope :> api) n - hoistServerWithContext _ pc f s = hoistServerWithContext (Proxy :: Proxy api) pc f . s - -instance RoutesToPaths api => RoutesToPaths (ZUserOrOAuth scope :> api) where - getRoutes = getRoutes @api - -------------------------------------------------------------------------------- -- Util diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index 88f970b79c..4c1d56f2a6 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -262,7 +262,7 @@ type SelfAPI = Named "get-self" ( Summary "Get your own profile" - :> ZUserOrOAuth 'SelfRead + :> ZOauthUser 'SelfRead :> "self" :> Get '[JSON] SelfProfile ) From b30fe2d3a3ee5a241ee1bb60fdec9fb7a60e14ca Mon Sep 17 00:00:00 2001 From: Jonas Schmitz Date: Wed, 1 Feb 2023 17:32:22 +0100 Subject: [PATCH 06/56] Add security response about wire.com DoS and HTML injection --- .../2023-01-04_website_outage.md | 21 +++++++++++++++++++ .../2023-01-19_html_injection.md | 18 ++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 docs/src/security-responses/2023-01-04_website_outage.md create mode 100644 docs/src/security-responses/2023-01-19_html_injection.md diff --git a/docs/src/security-responses/2023-01-04_website_outage.md b/docs/src/security-responses/2023-01-04_website_outage.md new file mode 100644 index 0000000000..ce396c3291 --- /dev/null +++ b/docs/src/security-responses/2023-01-04_website_outage.md @@ -0,0 +1,21 @@ +# 2023-01-04 - Outage of wire.com caused by a DoS attack + +Last updated: 2023-01-19 + +## What happened? +On Tuesday, 2023-01-04, the Wire website wire.com was affected by an outage caused by a Denial-of-Service attack. This outage only concerns the wire.com website and none of the services provided by Wire. + +## What was the impact identified? +Several outages of short periods (7min, 2min, 3min, 4min) have been identified beginning from UTC 05:13. + +## Are Wire installations affected? +Wire/wire-server was not affected by the wire.com website outage. + +## Timeline + +*2023-01-04 05:13*: The website monitor triggered an alert on a server issue.\ +*2023-01-04 05:16*: The responsible team of the service provider responded and saw an outage for periods of 7 minutes beginning from UTC 05:13, 2 minutes (UTC 05:29), 3 minutes (UTC 05:36) and 4 minutes (UTC 05:43).\ +*2023-01-04/11:34*: Wire was informed about an attempted DoS attack on wire.com by the service provider which manually blocked the IP address in the process.\ +*2023-01-04 09:15*: Wire started an internal investigation to check whether other systems have been affected, which was not the case.\ +*2023-01-04 14:30*: Wire contacted the service provider for more details on the incident as well as the corresponding log files.\ +*2023-01-18 09:53*: Wire received the incident report from the service provider.\ diff --git a/docs/src/security-responses/2023-01-19_html_injection.md b/docs/src/security-responses/2023-01-19_html_injection.md new file mode 100644 index 0000000000..1152061355 --- /dev/null +++ b/docs/src/security-responses/2023-01-19_html_injection.md @@ -0,0 +1,18 @@ +# 2023-01-19 - Security Advisory: HTML Injection in wire.com + +Last updated: 2023-01-31 + +## Introduction +On the 16st January, 2023, we were informed about a possible vulnerability in our website wire.com. A get-parameter on the page https://wire.com/en/pricing/ was vulnerable to HTML injection. As the website wire.com is not directly maintained by Wire, the service provider was directly informed about the disclosed issue. The patch that fixed that vulnerability was rolled out on the 18th of January. + +## Impact +An adversary would have been able to inject arbitrary HTML code through the parameter and send that link to someone else which would show them e.g., a defaced website or potentially inject JavaScript. + +## Are Wire installations affected? +Wire/wire-server is not affected by this vulnerability as our website wire.com is completely separated from the Wire backend. + +## Are Wire clients affected? +Wire clients are not affected by this vulnerability. + +## Credits +We thank [Umar Ahmed](http://linkedin.com/in/theumar9) for reporting this vulnerability. From 3373794fa6fadec72a5e221c7a88ee750894724f Mon Sep 17 00:00:00 2001 From: sanojwr <97119542+sanojwr@users.noreply.github.com> Date: Wed, 1 Feb 2023 18:57:03 +0100 Subject: [PATCH 07/56] Update docs/src/security-responses/2023-01-19_html_injection.md Co-authored-by: Sebastian Willenborg --- docs/src/security-responses/2023-01-19_html_injection.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/security-responses/2023-01-19_html_injection.md b/docs/src/security-responses/2023-01-19_html_injection.md index 1152061355..8870ec381c 100644 --- a/docs/src/security-responses/2023-01-19_html_injection.md +++ b/docs/src/security-responses/2023-01-19_html_injection.md @@ -3,7 +3,7 @@ Last updated: 2023-01-31 ## Introduction -On the 16st January, 2023, we were informed about a possible vulnerability in our website wire.com. A get-parameter on the page https://wire.com/en/pricing/ was vulnerable to HTML injection. As the website wire.com is not directly maintained by Wire, the service provider was directly informed about the disclosed issue. The patch that fixed that vulnerability was rolled out on the 18th of January. +On the 16st January, 2023, we were informed about a possible vulnerability in our website wire.com. A get-parameter on the page https://wire.com/en/pricing/ was vulnerable to HTML injection. As the website wire.com is not directly maintained by Wire, the service provider was directly informed about the disclosed issue. The patch that fixed that vulnerability was rolled out on the 18th of January. ## Impact An adversary would have been able to inject arbitrary HTML code through the parameter and send that link to someone else which would show them e.g., a defaced website or potentially inject JavaScript. From d01cbb24ce862767ce164406f3b1b8501528a051 Mon Sep 17 00:00:00 2001 From: sanojwr <97119542+sanojwr@users.noreply.github.com> Date: Wed, 1 Feb 2023 18:57:09 +0100 Subject: [PATCH 08/56] Update docs/src/security-responses/2023-01-19_html_injection.md Co-authored-by: Sebastian Willenborg --- docs/src/security-responses/2023-01-19_html_injection.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/security-responses/2023-01-19_html_injection.md b/docs/src/security-responses/2023-01-19_html_injection.md index 8870ec381c..b0b24151d3 100644 --- a/docs/src/security-responses/2023-01-19_html_injection.md +++ b/docs/src/security-responses/2023-01-19_html_injection.md @@ -15,4 +15,4 @@ Wire/wire-server is not affected by this vulnerability as our website wire.com i Wire clients are not affected by this vulnerability. ## Credits -We thank [Umar Ahmed](http://linkedin.com/in/theumar9) for reporting this vulnerability. +We thank [Umar Ahmed](https://linkedin.com/in/theumar9) for reporting this vulnerability. From e8408bf7145475fd2ae741fe07df9a554fce230c Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Thu, 2 Feb 2023 10:06:15 +0100 Subject: [PATCH 09/56] .gitignore: Add third party modules of nginz --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 413128240b..9b827937eb 100644 --- a/.gitignore +++ b/.gitignore @@ -112,3 +112,6 @@ result-* /integration-ca-key.pem /integration-ca.pem + +services/nginz/third_party/headers-more-nginx-module +services/nginz/third_party/nginx-module-vts From 70aa9a7cfc7fdac248b7365554fbbac2e6a49717 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Thu, 2 Feb 2023 10:09:22 +0100 Subject: [PATCH 10/56] ... --- libs/wire-api/src/Wire/API/Routes/Public.hs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index 6bc9136301..afb2b1c126 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -225,22 +225,6 @@ instance where toSwagger _ = toSwagger (Proxy @api) -_routeZAuth :: - forall ztype opts scopes api env ctx a. - ( IsZType ztype ctx, - HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, - HasContextEntry ctx (Maybe JWK), - opts ~ InternalAuthDefOpts, - HasServer api ctx, - IsOAuthScope scopes, - ZQualifiedParam ztype ~ Id a - ) => - Proxy (ZAuthServant ztype opts ('Just scopes) :> api) -> - Context ctx -> - Delayed env (Server (ZAuthServant ztype opts ('Just scopes) :> api)) -> - Router env -_routeZAuth _ ctx subserver = route (Proxy @(ZAuthServant ztype opts 'Nothing :> api)) ctx subserver - _routeOAuth :: forall ztype opts scopes api env ctx a. ( IsZType ztype ctx, @@ -315,8 +299,7 @@ instance hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s --- TODO(leif): remove Nothing instance - +-- | Handle routes that support ZAuth, but not OAuth (scopes is Nothing). instance ( IsZType ztype ctx, HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, From afdbe5c3f2ef3028dd7452710a43144d0db175a8 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Thu, 2 Feb 2023 10:20:16 +0100 Subject: [PATCH 11/56] ... --- libs/wire-api/src/Wire/API/Routes/Public.hs | 23 ++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index afb2b1c126..afbe7da1c7 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -239,13 +239,21 @@ _routeOAuth :: Context ctx -> Delayed env (Server (ZAuthServant ztype opts ('Just scopes) :> api)) -> Router env -_routeOAuth _ ctx subserver = route (Proxy @api) ctx (addAuthCheck subserver (withRequest checkAuth)) - where - -- todo(leif): add sig and toplevel - - checkAuth :: Request -> DelayedIO (ZQualifiedParam ztype) - checkAuth = doOAuth (getContextEntry ctx) >=> either delayedFailFatal pure +_routeOAuth _ ctx subserver = route (Proxy @api) ctx (addAuthCheck subserver (withRequest (checkAuth @ztype @scopes @ctx @a ctx))) +checkAuth :: + forall ztype scopes ctx a. + ( IsZType ztype ctx, + HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, + HasContextEntry ctx (Maybe JWK), + IsOAuthScope scopes, + ZQualifiedParam ztype ~ Id a + ) => + Context ctx -> + Request -> + DelayedIO (ZQualifiedParam ztype) +checkAuth ctx = doOAuth (getContextEntry ctx) >=> either delayedFailFatal pure + where doOAuth :: Maybe JWK -> Request -> DelayedIO (Either ServerError (ZQualifiedParam ztype)) doOAuth mJwk req = tryOAuth where @@ -265,11 +273,12 @@ _routeOAuth _ ctx subserver = route (Proxy @api) ctx (addAuthCheck subserver (wi then maybeToRight (invalidOAuthToken "Invalid token: Missing or invalid sub claim") (hcsSub claimSet) else Left insufficientScope +-- | Handle routes that support both ZAuth and OAuth, tried in that order (scopes is Just). instance ( IsZType ztype ctx, HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, HasContextEntry ctx (Maybe JWK), - opts ~ InternalAuthDefOpts, + opts ~ InternalAuthDefOpts, -- oauth is never optional. HasServer api ctx, IsOAuthScope scopes, ZQualifiedParam ztype ~ Id a From d58d78afaff6112be927f4e660e12b8ba9b935c2 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Thu, 2 Feb 2023 10:25:19 +0100 Subject: [PATCH 12/56] ... --- libs/wire-api/src/Wire/API/Routes/Public.hs | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index afbe7da1c7..eed5574501 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -225,22 +225,6 @@ instance where toSwagger _ = toSwagger (Proxy @api) -_routeOAuth :: - forall ztype opts scopes api env ctx a. - ( IsZType ztype ctx, - HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, - HasContextEntry ctx (Maybe JWK), - opts ~ InternalAuthDefOpts, - HasServer api ctx, - IsOAuthScope scopes, - ZQualifiedParam ztype ~ Id a - ) => - Proxy (ZAuthServant ztype opts ('Just scopes) :> api) -> - Context ctx -> - Delayed env (Server (ZAuthServant ztype opts ('Just scopes) :> api)) -> - Router env -_routeOAuth _ ctx subserver = route (Proxy @api) ctx (addAuthCheck subserver (withRequest (checkAuth @ztype @scopes @ctx @a ctx))) - checkAuth :: forall ztype scopes ctx a. ( IsZType ztype ctx, @@ -302,9 +286,10 @@ instance Context ctx -> Delayed env (Server (ZAuthServant ztype opts ('Just scopes) :> api)) -> Router env - route _ _ctx _subserver = + route _ ctx subserver = -- withRequest $ \req -> maybe routeOAuth (const routeZAuth) $ lookup "Z-Type" (requestHeaders req) - undefined + + route (Proxy @api) ctx (addAuthCheck subserver (withRequest (checkAuth @ztype @scopes @ctx @a ctx))) hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s From 5087280a201a19bb46abd0b7633e87c4d8a277ad Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Thu, 2 Feb 2023 10:37:08 +0100 Subject: [PATCH 13/56] ... --- libs/wire-api/src/Wire/API/Routes/Public.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index eed5574501..6b508f759f 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -225,6 +225,7 @@ instance where toSwagger _ = toSwagger (Proxy @api) +-- TODO(fisx): move to a where block in the instance where it's called? checkAuth :: forall ztype scopes ctx a. ( IsZType ztype ctx, From a721f44e699f698eb936342b992df316895f063e Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Thu, 2 Feb 2023 10:43:58 +0100 Subject: [PATCH 14/56] ... --- libs/wire-api/src/Wire/API/Routes/Public.hs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index 6b508f759f..44890feb5c 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -278,8 +278,7 @@ instance ( IsZType ztype ctx, HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, HasContextEntry ctx (Maybe JWK), - SBoolI (FoldLenient opts), - SBoolI (FoldRequired opts), + opts ~ InternalAuthDefOpts, HasServer api ctx, IsOAuthScope scopes ) => From 633cfaea18f8ba3e92e82afb703b63484292059c Mon Sep 17 00:00:00 2001 From: jschaul Date: Thu, 2 Feb 2023 11:03:26 +0100 Subject: [PATCH 15/56] spar tests deflake: do an ES refresh, not reindex (#3048) --- services/spar/test-integration/Test/Spar/Scim/UserSpec.hs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs index 342fbbb5cc..7a7f9a9964 100644 --- a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs @@ -736,9 +736,7 @@ testCreateUserNoIdP = do -- | ES is only refreshed occasionally; we don't want to wait for that in tests. refreshIndex :: BrigReq -> TestSpar () refreshIndex brig = do - call $ void $ post (brig . path "/i/index/reindex" . expect2xx) - -- wait for async reindexing to complete (hopefully) - lift $ threadDelay 3_000_000 + call $ void $ post (brig . path "/i/index/refresh" . expect2xx) testCreateUserNoIdPNoEmail :: TestSpar () testCreateUserNoIdPNoEmail = do From 443f7417da1949fb6bcbce8f501f8888677e3abf Mon Sep 17 00:00:00 2001 From: jschaul Date: Thu, 2 Feb 2023 11:03:36 +0100 Subject: [PATCH 16/56] Helm hook type (#3049) * change helm hook type of test resources which are not Pods * changelog adjustment --- changelog.d/5-internal/helm-test | 2 +- charts/brig/templates/tests/brig-integration.yaml | 3 ++- charts/brig/templates/tests/configmap.yaml | 3 ++- charts/brig/templates/tests/nginz-service.yaml | 3 ++- charts/brig/templates/tests/secret.yaml | 3 ++- charts/cargohold/templates/tests/configmap.yaml | 3 ++- charts/federator/templates/tests/configmap.yaml | 3 ++- charts/galley/templates/tests/configmap.yaml | 3 ++- charts/galley/templates/tests/galley-integration.yaml | 3 ++- charts/galley/templates/tests/secret.yaml | 3 ++- charts/gundeck/templates/tests/configmap.yaml | 3 ++- charts/spar/templates/tests/configmap.yaml | 3 ++- 12 files changed, 23 insertions(+), 12 deletions(-) diff --git a/changelog.d/5-internal/helm-test b/changelog.d/5-internal/helm-test index 14bfeb22a3..2bd90dfe39 100644 --- a/changelog.d/5-internal/helm-test +++ b/changelog.d/5-internal/helm-test @@ -1 +1 @@ -charts: Mark all test resources to be only created while running tests \ No newline at end of file +charts: Mark all service/secret/configmap test resources to be re-created by defining them as helm hooks (#3037, #3049) diff --git a/charts/brig/templates/tests/brig-integration.yaml b/charts/brig/templates/tests/brig-integration.yaml index 81020f9d00..c8cfa602f1 100644 --- a/charts/brig/templates/tests/brig-integration.yaml +++ b/charts/brig/templates/tests/brig-integration.yaml @@ -3,7 +3,8 @@ kind: Service metadata: name: "brig-integration" annotations: - "helm.sh/hook": test + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation labels: app: brig-integration chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} diff --git a/charts/brig/templates/tests/configmap.yaml b/charts/brig/templates/tests/configmap.yaml index 4d409eb59f..56667e55ed 100644 --- a/charts/brig/templates/tests/configmap.yaml +++ b/charts/brig/templates/tests/configmap.yaml @@ -3,7 +3,8 @@ kind: ConfigMap metadata: name: "brig-integration" annotations: - "helm.sh/hook": test + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation data: integration.yaml: | brig: diff --git a/charts/brig/templates/tests/nginz-service.yaml b/charts/brig/templates/tests/nginz-service.yaml index ec1b22284b..6eda016c82 100644 --- a/charts/brig/templates/tests/nginz-service.yaml +++ b/charts/brig/templates/tests/nginz-service.yaml @@ -6,7 +6,8 @@ kind: Service metadata: name: nginz-integration-http annotations: - "helm.sh/hook": test + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation spec: type: ClusterIP ports: diff --git a/charts/brig/templates/tests/secret.yaml b/charts/brig/templates/tests/secret.yaml index bda96d4f3f..69ce7e671e 100644 --- a/charts/brig/templates/tests/secret.yaml +++ b/charts/brig/templates/tests/secret.yaml @@ -3,7 +3,8 @@ kind: ConfigMap metadata: name: brig-integration-secrets annotations: - "helm.sh/hook": test + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation data: # These "secrets" are only used in tests and are therefore safe to be stored unencrypted provider-privatekey.pem: | diff --git a/charts/cargohold/templates/tests/configmap.yaml b/charts/cargohold/templates/tests/configmap.yaml index 1a52056adb..18a5b29b22 100644 --- a/charts/cargohold/templates/tests/configmap.yaml +++ b/charts/cargohold/templates/tests/configmap.yaml @@ -3,7 +3,8 @@ kind: ConfigMap metadata: name: "cargohold-integration" annotations: - "helm.sh/hook": test + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation data: integration.yaml: | cargohold: diff --git a/charts/federator/templates/tests/configmap.yaml b/charts/federator/templates/tests/configmap.yaml index b845f3d401..44146840bd 100644 --- a/charts/federator/templates/tests/configmap.yaml +++ b/charts/federator/templates/tests/configmap.yaml @@ -3,7 +3,8 @@ kind: ConfigMap metadata: name: "federator-integration" annotations: - "helm.sh/hook": test + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation data: integration.yaml: | federatorInternal: diff --git a/charts/galley/templates/tests/configmap.yaml b/charts/galley/templates/tests/configmap.yaml index 20036f2f15..89d7d589de 100644 --- a/charts/galley/templates/tests/configmap.yaml +++ b/charts/galley/templates/tests/configmap.yaml @@ -3,7 +3,8 @@ kind: ConfigMap metadata: name: "galley-integration" annotations: - "helm.sh/hook": test + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation data: integration.yaml: | galley: diff --git a/charts/galley/templates/tests/galley-integration.yaml b/charts/galley/templates/tests/galley-integration.yaml index d7b64bf5ba..c543eb2048 100644 --- a/charts/galley/templates/tests/galley-integration.yaml +++ b/charts/galley/templates/tests/galley-integration.yaml @@ -3,7 +3,8 @@ kind: Service metadata: name: "galley-integration" annotations: - "helm.sh/hook": test + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation labels: app: galley-integration chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} diff --git a/charts/galley/templates/tests/secret.yaml b/charts/galley/templates/tests/secret.yaml index ec0e5624b2..d58a49c360 100644 --- a/charts/galley/templates/tests/secret.yaml +++ b/charts/galley/templates/tests/secret.yaml @@ -3,7 +3,8 @@ kind: ConfigMap metadata: name: galley-integration-secrets annotations: - "helm.sh/hook": test + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation data: # These "secrets" are only used in tests and are therefore safe to be stored unencrypted provider-privatekey.pem: | diff --git a/charts/gundeck/templates/tests/configmap.yaml b/charts/gundeck/templates/tests/configmap.yaml index 004d58f060..6829860247 100644 --- a/charts/gundeck/templates/tests/configmap.yaml +++ b/charts/gundeck/templates/tests/configmap.yaml @@ -3,7 +3,8 @@ kind: ConfigMap metadata: name: "gundeck-integration" annotations: - "helm.sh/hook": test + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation data: integration.yaml: | gundeck: diff --git a/charts/spar/templates/tests/configmap.yaml b/charts/spar/templates/tests/configmap.yaml index 8773895447..2eb1966099 100644 --- a/charts/spar/templates/tests/configmap.yaml +++ b/charts/spar/templates/tests/configmap.yaml @@ -3,7 +3,8 @@ kind: ConfigMap metadata: name: "spar-integration" annotations: - "helm.sh/hook": test + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation data: integration.yaml: | brig: From d182942b7cbcfa44701fd1fed665bde9615dba9c Mon Sep 17 00:00:00 2001 From: jschaul Date: Thu, 2 Feb 2023 11:03:58 +0100 Subject: [PATCH 17/56] helmfile sync: speedup (#3052) - change liveness and readyness probes to start querying more quickly to see if cassandra is up. Instead of 90 - 120 seconds, if cassandra is up earlier that should manifest itself in the setup time of 'make kube-integration-setup' - change helmfile for wire-server to wait for databases-ephemeral to be up before launching pods: cassandra-migration needs to have a working cassandra anyway - the crashloop-backoff strategy leads to a lot of waiting in between restarts; so it should be faster to wait for cassandra to be up before attempting schema migrations --- changelog.d/5-internal/helm-setup | 1 + charts/cassandra-ephemeral/values.yaml | 11 +++++++++++ hack/helmfile.yaml | 4 ++++ 3 files changed, 16 insertions(+) create mode 100644 changelog.d/5-internal/helm-setup diff --git a/changelog.d/5-internal/helm-setup b/changelog.d/5-internal/helm-setup new file mode 100644 index 0000000000..1374d5bd51 --- /dev/null +++ b/changelog.d/5-internal/helm-setup @@ -0,0 +1 @@ +CI integration setup time should be reduced: tweak the way cassandra-ephemeral is started diff --git a/charts/cassandra-ephemeral/values.yaml b/charts/cassandra-ephemeral/values.yaml index e28bc2a529..611cf827a9 100644 --- a/charts/cassandra-ephemeral/values.yaml +++ b/charts/cassandra-ephemeral/values.yaml @@ -22,3 +22,14 @@ cassandra-ephemeral: seed_size: 1 max_heap_size: 2048M heap_new_size: 1024M + + livenessProbe: + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 15 + readinessProbe: + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 15 diff --git a/hack/helmfile.yaml b/hack/helmfile.yaml index de8a1bc340..6392d64a43 100644 --- a/hack/helmfile.yaml +++ b/hack/helmfile.yaml @@ -129,6 +129,8 @@ releases: value: {{ .Values.federationDomain }} - name: brig.config.optSettings.setFederationDomainConfigs[0].domain value: {{ .Values.federationDomainFed2 }} + needs: + - '{{ .Values.namespace }}-databases-ephemeral' - name: '{{ .Values.namespace }}-wire-server-2' namespace: '{{ .Values.namespaceFed2 }}' @@ -145,3 +147,5 @@ releases: value: {{ .Values.federationDomainFed2 }} - name: brig.config.optSettings.setFederationDomainConfigs[0].domain value: {{ .Values.federationDomain }} + needs: + - '{{ .Values.namespace }}-databases-ephemeral-2' From 11e41678224ad77ceb3e812f63692a77054c9e88 Mon Sep 17 00:00:00 2001 From: jschaul Date: Thu, 2 Feb 2023 11:34:27 +0100 Subject: [PATCH 18/56] Deflake metrics test (#3053) example case where this test failed: https://concourse.ops.zinfra.io/teams/main/pipelines/staging/jobs/test/builds/342 output of failing test: ``` metrics prometheus: OK (0.02s) work: FAIL (1.06s) Error message: /login was called twice expected: 2 but got: 3 CallStack (from HasCallStack): assertFailure, called at ./Test/Tasty/HUnit/Orig.hs:86:32 in tasty-hunit-0.10.0.3-KJER1RJhmod6e0raY4U8z6:Test.Tasty.HUnit.Orig assertEqual, called at test/integration/API/Metrics.hs:78:12 in main:API.Metrics Use -p '(!/turn/&&!/user.auth.cookies.limit/)&&/metrics.work/' to rerun this test only. ``` --- changelog.d/5-internal/deflake-metrics | 1 + services/brig/test/integration/API/Metrics.hs | 9 +++------ 2 files changed, 4 insertions(+), 6 deletions(-) create mode 100644 changelog.d/5-internal/deflake-metrics diff --git a/changelog.d/5-internal/deflake-metrics b/changelog.d/5-internal/deflake-metrics new file mode 100644 index 0000000000..e923cbb5ef --- /dev/null +++ b/changelog.d/5-internal/deflake-metrics @@ -0,0 +1 @@ +Deflake integration test: metrics diff --git a/services/brig/test/integration/API/Metrics.hs b/services/brig/test/integration/API/Metrics.hs index 836efebeab..22dffa10f9 100644 --- a/services/brig/test/integration/API/Metrics.hs +++ b/services/brig/test/integration/API/Metrics.hs @@ -51,9 +51,6 @@ testPrometheusMetrics brig = do -- Should contain the request duration metric in its output const (Just "TYPE http_request_duration_seconds histogram") =~= responseBody --- | This test runs in `withSettingsOverrides` to ensure that only this test is --- accessing brig, if we target the real brig, some other test running --- in-parallel could make this test fail. testMetricsEndpoint :: Opt.Opts -> Brig -> Http () testMetricsEndpoint opts brig0 = withSettingsOverrides opts $ do let brig = apiVersion "v1" . brig0 @@ -71,11 +68,11 @@ testMetricsEndpoint opts brig0 = withSettingsOverrides opts $ do _ <- post (brig . path p3 . contentJson . queryItem "persist" "true" . json (defEmailLogin email) . expect2xx) _ <- post (brig . path p3 . contentJson . queryItem "persist" "true" . json (defEmailLogin email) . expect2xx) countSelf <- getCount "/self" "GET" - liftIO $ assertEqual "/self was called once" (beforeSelf + 1) countSelf + liftIO $ assertBool "/self was called at least once" ((beforeSelf + 1) <= countSelf) countClients <- getCount "/users/:uid/clients" "GET" - liftIO $ assertEqual "/users/:uid/clients was called twice" (beforeClients + 2) countClients + liftIO $ assertBool "/users/:uid/clients was called at least twice" ((beforeClients + 2) <= countClients) countProperties <- getCount "/login" "POST" - liftIO $ assertEqual "/login was called twice" (beforeProperties + 2) countProperties + liftIO $ assertBool "/login was called at least twice" ((beforeProperties + 2) <= countProperties) where getCount endpoint m = do rsp <- responseBody <$> get (brig0 . path "i/metrics") From 6ac782bde5c4c91091c6400c099276b76f16e4d6 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 2 Feb 2023 12:19:44 +0100 Subject: [PATCH 19/56] WIP: attempt to use checkType in OAuth logic --- libs/wire-api/src/Wire/API/Routes/Public.hs | 88 +++++++++++++++------ 1 file changed, 64 insertions(+), 24 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index 44890feb5c..0d91f04778 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -226,30 +226,31 @@ instance toSwagger _ = toSwagger (Proxy @api) -- TODO(fisx): move to a where block in the instance where it's called? +-- TODO: consider passing just the key and not the whole context checkAuth :: forall ztype scopes ctx a. ( IsZType ztype ctx, HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, HasContextEntry ctx (Maybe JWK), IsOAuthScope scopes, - ZQualifiedParam ztype ~ Id a + ZParam ztype ~ Id a ) => Context ctx -> Request -> - DelayedIO (ZQualifiedParam ztype) + DelayedIO (ZParam ztype) checkAuth ctx = doOAuth (getContextEntry ctx) >=> either delayedFailFatal pure where - doOAuth :: Maybe JWK -> Request -> DelayedIO (Either ServerError (ZQualifiedParam ztype)) + doOAuth :: Maybe JWK -> Request -> DelayedIO (Either ServerError (ZParam ztype)) doOAuth mJwk req = tryOAuth where - tryOAuth :: DelayedIO (Either ServerError (ZQualifiedParam ztype)) + tryOAuth :: DelayedIO (Either ServerError (ZParam ztype)) tryOAuth = do let headerOrError = maybeToRight oauthTokenMissing $ lookup "Z-OAuth" (requestHeaders req) let jwkOrError = maybeToRight jwtError mJwk let tokenOrError = headerOrError >>= mapLeft invalidOAuthToken . parseHeader either (pure . Left) verifyOAuthToken $ (,) <$> tokenOrError <*> jwkOrError - verifyOAuthToken :: (Bearer OAuthAccessToken, JWK) -> DelayedIO (Either ServerError (ZQualifiedParam ztype)) + verifyOAuthToken :: (Bearer OAuthAccessToken, JWK) -> DelayedIO (Either ServerError (ZParam ztype)) verifyOAuthToken (token, key) = do verifiedOrError <- mapLeft (invalidOAuthToken . cs . show) <$> liftIO (verify key (unOAuthToken . unBearer $ token)) pure $ @@ -265,14 +266,16 @@ instance HasContextEntry ctx (Maybe JWK), opts ~ InternalAuthDefOpts, -- oauth is never optional. HasServer api ctx, - IsOAuthScope scopes, - ZQualifiedParam ztype ~ Id a + IsOAuthScope (scopes :: OAuthScope), + ZParam ztype ~ Id a ) => HasServer (ZAuthServant ztype opts ('Just scopes) :> api) ctx where type + -- ServerT (ZAuthServant ztype opts ('Just scopes) :> api) m = + -- RequestArgument opts (ZQualifiedParam ztype) -> ServerT api m ServerT (ZAuthServant ztype opts ('Just scopes) :> api) m = - RequestArgument opts (ZQualifiedParam ztype) -> ServerT api m + ZParam ztype -> ServerT api m route :: ( IsZType ztype ctx, @@ -288,11 +291,49 @@ instance Router env route _ ctx subserver = -- withRequest $ \req -> maybe routeOAuth (const routeZAuth) $ lookup "Z-Type" (requestHeaders req) + -- route (Proxy @api) ctx (addAuthCheck subserver (withRequest (checkAuth @ztype @scopes @ctx @a ctx))) + + Servant.route + (Proxy @api) + ctx + (addAuthCheck subserver (withRequest (checkType' @ztype @scopes @ctx @a ctx (tokenType @ztype)))) - route (Proxy @api) ctx (addAuthCheck subserver (withRequest (checkAuth @ztype @scopes @ctx @a ctx))) + -- ( fmap + -- (. fmap (qualifyZParam @ztype ctx)) + -- ) hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s +checkType' :: + forall ztype scopes ctx a. + ( IsZType ztype ctx, + HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, + HasContextEntry ctx (Maybe JWK), + IsOAuthScope scopes, + ZParam ztype ~ Id a + ) => + Context ctx -> + Maybe ByteString -> + Wai.Request -> + DelayedIO (ZParam ztype) +checkType' ctx tokenType req = case (lookup "Z-Type" (Wai.requestHeaders req), lookup "Z-User" (Wai.requestHeaders req)) of + -- TODO: replace "Z-User" + (Nothing, _) -> checkAuth @ztype @scopes @ctx @a ctx req + (ztype, mHeaderValue) -> case mHeaderValue of + Just headerValue | ztype == tokenType -> error "deserialise" + _ -> error "return an error" + +-- (Just t, value) +-- | value /= Just t -> +-- delayedFail +-- ServerError +-- { errHTTPCode = 403, +-- errReasonPhrase = "Access denied", +-- errBody = "", +-- errHeaders = [] +-- } +-- _ -> pure () + -- | Handle routes that support ZAuth, but not OAuth (scopes is Nothing). instance ( IsZType ztype ctx, @@ -324,24 +365,23 @@ instance ctx ( fmap (. mapRequestArgument @opts (qualifyZParam @ztype ctx)) - (addAcceptCheck subserver (withRequest (checkType (tokenType @ztype)))) + (addAuthCheck (fmap (\x _ -> x) subserver) (withRequest (checkType (tokenType @ztype)))) ) - where - checkType :: Maybe ByteString -> Wai.Request -> DelayedIO () - checkType token req = case (token, lookup "Z-Type" (Wai.requestHeaders req)) of - (Just t, value) - | value /= Just t -> - delayedFail - ServerError - { errHTTPCode = 403, - errReasonPhrase = "Access denied", - errBody = "", - errHeaders = [] - } - _ -> pure () - hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s +checkType :: Maybe ByteString -> Wai.Request -> DelayedIO () +checkType token req = case (token, lookup "Z-Type" (Wai.requestHeaders req)) of + (Just t, value) + | value /= Just t -> + delayedFail + ServerError + { errHTTPCode = 403, + errReasonPhrase = "Access denied", + errBody = "", + errHeaders = [] + } + _ -> pure () + instance RoutesToPaths api => RoutesToPaths (ZAuthServant ztype opts scopes :> api) where getRoutes = getRoutes @api From 58b2955a80c9f81067f1d866d9db584ea5adc244 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Thu, 2 Feb 2023 12:36:02 +0100 Subject: [PATCH 20/56] ... --- libs/wire-api/src/Wire/API/Routes/Public.hs | 24 ++++++++------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index 0d91f04778..e0bd243d7f 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -316,23 +316,17 @@ checkType' :: Maybe ByteString -> Wai.Request -> DelayedIO (ZParam ztype) -checkType' ctx tokenType req = case (lookup "Z-Type" (Wai.requestHeaders req), lookup "Z-User" (Wai.requestHeaders req)) of +checkType' ctx tokenType req = case (lookup "Z-Type" (Wai.requestHeaders req), lookup (ZHeader @ztype) (Wai.requestHeaders req)) of -- TODO: replace "Z-User" (Nothing, _) -> checkAuth @ztype @scopes @ctx @a ctx req - (ztype, mHeaderValue) -> case mHeaderValue of - Just headerValue | ztype == tokenType -> error "deserialise" - _ -> error "return an error" - --- (Just t, value) --- | value /= Just t -> --- delayedFail --- ServerError --- { errHTTPCode = 403, --- errReasonPhrase = "Access denied", --- errBody = "", --- errHeaders = [] --- } --- _ -> pure () + (ztype, mHeaderValue) -> + -- or maybe call this route instance in this line instead?: https://hoogle.zinfra.io/file/nix/store/v71izsswrazdr55yzscdzgnvxghkzrc0-servant-server-0.19.1-doc/share/doc/servant-server-0.19.1/html/src/Servant.Server.Internal.html#line-406 + case mHeaderValue of + Just headerValue + | ztype == tokenType -> + let parseWhateverId = _ -- whatever the `Header` instance did before + in parseWhateverId headerValue >>= fromMaybe crash + _ -> error "return an error" -- | Handle routes that support ZAuth, but not OAuth (scopes is Nothing). instance From be524fac7eb46d5419c319d5f8161f0c518a7f69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Thu, 2 Feb 2023 14:36:22 +0100 Subject: [PATCH 21/56] [FS-1075] Extend the Swagger documentation for federation error types (#3045) * Extend the docs on the federation error type --- changelog.d/4-docs/federation-error-type | 1 + services/brig/docs/swagger.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog.d/4-docs/federation-error-type diff --git a/changelog.d/4-docs/federation-error-type b/changelog.d/4-docs/federation-error-type new file mode 100644 index 0000000000..cafb3fc6a2 --- /dev/null +++ b/changelog.d/4-docs/federation-error-type @@ -0,0 +1 @@ +Extend the docs on the federation error type diff --git a/services/brig/docs/swagger.md b/services/brig/docs/swagger.md index db6210b536..5c92ab8aac 100644 --- a/services/brig/docs/swagger.md +++ b/services/brig/docs/swagger.md @@ -37,6 +37,7 @@ For errors that are more likely to be transient, we suggest clients to retry wha **Note**: when a failure occurs as a result of making a federated RPC to another backend, the error response contains the following extra fields: + - `type`: "federation" (just the literal string in quotes, which can be used as an error type identifier when parsing errors) - `domain`: the target backend of the RPC that failed; - `path`: the path of the RPC that failed. From e7724d7b965ea88b6ab959dabc3d10fcf873822f Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 2 Feb 2023 15:49:16 +0000 Subject: [PATCH 22/56] wip --- libs/wire-api/src/Wire/API/Routes/Public.hs | 199 ++++++++++---------- 1 file changed, 102 insertions(+), 97 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index e0bd243d7f..7a54c60cca 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -1,5 +1,7 @@ {-# LANGUAGE DerivingVia #-} {-# OPTIONS_GHC -Wno-orphans #-} +{-# OPTIONS_GHC -Wno-unused-imports #-} +{-# OPTIONS_GHC -Wno-unused-top-binds #-} -- This file is part of the Wire Server implementation. -- @@ -30,7 +32,10 @@ module Wire.API.Routes.Public ZBot, ZConversation, ZProvider, + + -- * OAuth combinators ZOauthUser, + ZOAuthLocalUser, -- * Swagger combinators OmitDocs, @@ -40,6 +45,8 @@ where import Control.Lens ((<>~)) import Control.Monad.Except import Crypto.JWT hiding (Context, params, uri, verify) +import Data.ByteString.Conversion (fromByteString) +import Data.Data (typeRep) import Data.Domain import Data.Either.Combinators import qualified Data.HashMap.Strict.InsOrd as InsOrdHashMap @@ -50,8 +57,9 @@ import Data.Qualified import Data.SOP import Data.String.Conversions (cs) import Data.Swagger +import qualified Data.Text as T import GHC.Base (Symbol) -import GHC.TypeLits (KnownSymbol) +import GHC.TypeLits (KnownSymbol, symbolVal, symbolVal') import Imports hiding (All, exp, head) import Network.Wai import qualified Network.Wai as Wai @@ -59,6 +67,7 @@ import Servant hiding (Handler, JSON, Tagged, addHeader, respond) import Servant.API.Modifiers import Servant.Server.Internal.Delayed import Servant.Server.Internal.DelayedIO +import Servant.Server.Internal.ErrorFormatter (mkContextWithErrorFormatter) import Servant.Server.Internal.Router import Servant.Swagger (HasSwagger (toSwagger)) import Servant.Swagger.Internal.Orphans () @@ -166,7 +175,7 @@ instance IsZType 'ZAuthProvider ctx where instance HasTokenType 'ZAuthProvider where tokenType = Just "provider" -data ZAuthServant (ztype :: ZType) (opts :: [Type]) (scopes :: Maybe OAuthScope) +data ZAuthServant (ztype :: ZType) (opts :: [Type]) (scope :: Maybe OAuthScope) type InternalAuthDefOpts = '[Servant.Required, Servant.Strict] @@ -178,9 +187,11 @@ type InternalAuth ztype opts = type ZLocalUser = ZAuthServant 'ZLocalAuthUser InternalAuthDefOpts 'Nothing +type ZOAuthLocalUser scope = ZAuthServant 'ZLocalAuthUser InternalAuthDefOpts ('Just scope) + type ZUser = ZAuthServant 'ZAuthUser InternalAuthDefOpts 'Nothing -type ZOauthUser scopes = ZAuthServant 'ZAuthUser InternalAuthDefOpts ('Just scopes) +type ZOauthUser scope = ZAuthServant 'ZAuthUser InternalAuthDefOpts ('Just scope) type ZClient = ZAuthServant 'ZAuthClient InternalAuthDefOpts 'Nothing @@ -198,8 +209,8 @@ type ZOptClient = ZAuthServant 'ZAuthClient '[Servant.Optional, Servant.Strict] type ZOptConn = ZAuthServant 'ZAuthConn '[Servant.Optional, Servant.Strict] 'Nothing --- TODO(leif): doc for scopes (also other instances) -instance HasSwagger api => HasSwagger (ZAuthServant 'ZAuthUser _opts scopes :> api) where +-- TODO(leif): doc for scope (also other instances) +instance HasSwagger api => HasSwagger (ZAuthServant 'ZAuthUser _opts scope :> api) where toSwagger _ = toSwagger (Proxy @api) & securityDefinitions <>~ SecurityDefinitions (InsOrdHashMap.singleton "ZAuth" secScheme) @@ -211,71 +222,35 @@ instance HasSwagger api => HasSwagger (ZAuthServant 'ZAuthUser _opts scopes :> a _securitySchemeDescription = Just "Must be a token retrieved by calling 'POST /login' or 'POST /access'. It must be presented in this format: 'Bearer \\'." } -instance HasSwagger api => HasSwagger (ZAuthServant 'ZLocalAuthUser opts scopes :> api) where - toSwagger _ = toSwagger (Proxy @(ZAuthServant 'ZAuthUser opts scopes :> api)) +instance HasSwagger api => HasSwagger (ZAuthServant 'ZLocalAuthUser opts scope :> api) where + toSwagger _ = toSwagger (Proxy @(ZAuthServant 'ZAuthUser opts scope :> api)) -instance HasLink endpoint => HasLink (ZAuthServant usr opts scopes :> endpoint) where +instance HasLink endpoint => HasLink (ZAuthServant usr opts scope :> endpoint) where type MkLink (ZAuthServant _ _ _ :> endpoint) a = MkLink endpoint a toLink toA _ = toLink toA (Proxy @endpoint) instance {-# OVERLAPPABLE #-} HasSwagger api => - HasSwagger (ZAuthServant ztype _opts scopes :> api) + HasSwagger (ZAuthServant ztype _opts scope :> api) where toSwagger _ = toSwagger (Proxy @api) --- TODO(fisx): move to a where block in the instance where it's called? --- TODO: consider passing just the key and not the whole context -checkAuth :: - forall ztype scopes ctx a. - ( IsZType ztype ctx, - HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, - HasContextEntry ctx (Maybe JWK), - IsOAuthScope scopes, - ZParam ztype ~ Id a - ) => - Context ctx -> - Request -> - DelayedIO (ZParam ztype) -checkAuth ctx = doOAuth (getContextEntry ctx) >=> either delayedFailFatal pure - where - doOAuth :: Maybe JWK -> Request -> DelayedIO (Either ServerError (ZParam ztype)) - doOAuth mJwk req = tryOAuth - where - tryOAuth :: DelayedIO (Either ServerError (ZParam ztype)) - tryOAuth = do - let headerOrError = maybeToRight oauthTokenMissing $ lookup "Z-OAuth" (requestHeaders req) - let jwkOrError = maybeToRight jwtError mJwk - let tokenOrError = headerOrError >>= mapLeft invalidOAuthToken . parseHeader - either (pure . Left) verifyOAuthToken $ (,) <$> tokenOrError <*> jwkOrError - - verifyOAuthToken :: (Bearer OAuthAccessToken, JWK) -> DelayedIO (Either ServerError (ZParam ztype)) - verifyOAuthToken (token, key) = do - verifiedOrError <- mapLeft (invalidOAuthToken . cs . show) <$> liftIO (verify key (unOAuthToken . unBearer $ token)) - pure $ - verifiedOrError >>= \claimSet -> - if hasScope @scopes claimSet - then maybeToRight (invalidOAuthToken "Invalid token: Missing or invalid sub claim") (hcsSub claimSet) - else Left insufficientScope - --- | Handle routes that support both ZAuth and OAuth, tried in that order (scopes is Just). +-- | Handle routes that support both ZAuth and OAuth, tried in that order (scope is Just). instance ( IsZType ztype ctx, HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, HasContextEntry ctx (Maybe JWK), opts ~ InternalAuthDefOpts, -- oauth is never optional. HasServer api ctx, - IsOAuthScope (scopes :: OAuthScope), + IsOAuthScope (scope :: OAuthScope), ZParam ztype ~ Id a ) => - HasServer (ZAuthServant ztype opts ('Just scopes) :> api) ctx + HasServer (ZAuthServant ztype opts ('Just scope) :> api) ctx where type - -- ServerT (ZAuthServant ztype opts ('Just scopes) :> api) m = - -- RequestArgument opts (ZQualifiedParam ztype) -> ServerT api m - ServerT (ZAuthServant ztype opts ('Just scopes) :> api) m = - ZParam ztype -> ServerT api m + ServerT (ZAuthServant ztype opts ('Just scope) :> api) m = + ZQualifiedParam ztype -> ServerT api m route :: ( IsZType ztype ctx, @@ -283,52 +258,90 @@ instance HasContextEntry ctx (Maybe JWK), opts ~ InternalAuthDefOpts, HasServer api ctx, - IsOAuthScope scopes + IsOAuthScope scope ) => - Proxy (ZAuthServant ztype opts ('Just scopes) :> api) -> + Proxy (ZAuthServant ztype opts ('Just scope) :> api) -> Context ctx -> - Delayed env (Server (ZAuthServant ztype opts ('Just scopes) :> api)) -> + Delayed env (Server (ZAuthServant ztype opts ('Just scope) :> api)) -> Router env route _ ctx subserver = - -- withRequest $ \req -> maybe routeOAuth (const routeZAuth) $ lookup "Z-Type" (requestHeaders req) - -- route (Proxy @api) ctx (addAuthCheck subserver (withRequest (checkAuth @ztype @scopes @ctx @a ctx))) - Servant.route (Proxy @api) ctx - (addAuthCheck subserver (withRequest (checkType' @ztype @scopes @ctx @a ctx (tokenType @ztype)))) - - -- ( fmap - -- (. fmap (qualifyZParam @ztype ctx)) - -- ) + (addAuthCheck subserver (withRequest (checkType' @ztype @scope @ctx ctx (tokenType @ztype)))) hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s checkType' :: - forall ztype scopes ctx a. + forall ztype scope ctx opts a. ( IsZType ztype ctx, HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, HasContextEntry ctx (Maybe JWK), - IsOAuthScope scopes, + opts ~ InternalAuthDefOpts, + IsOAuthScope scope, ZParam ztype ~ Id a ) => Context ctx -> Maybe ByteString -> Wai.Request -> - DelayedIO (ZParam ztype) -checkType' ctx tokenType req = case (lookup "Z-Type" (Wai.requestHeaders req), lookup (ZHeader @ztype) (Wai.requestHeaders req)) of - -- TODO: replace "Z-User" - (Nothing, _) -> checkAuth @ztype @scopes @ctx @a ctx req - (ztype, mHeaderValue) -> - -- or maybe call this route instance in this line instead?: https://hoogle.zinfra.io/file/nix/store/v71izsswrazdr55yzscdzgnvxghkzrc0-servant-server-0.19.1-doc/share/doc/servant-server-0.19.1/html/src/Servant.Server.Internal.html#line-406 - case mHeaderValue of - Just headerValue - | ztype == tokenType -> - let parseWhateverId = _ -- whatever the `Header` instance did before - in parseWhateverId headerValue >>= fromMaybe crash - _ -> error "return an error" - --- | Handle routes that support ZAuth, but not OAuth (scopes is Nothing). + DelayedIO (ZQualifiedParam ztype) +checkType' ctx mTokenType req = case mTokenType of + -- if the token type is given, the request must have the correct Z-Type header, otherwise access is denied. + Just t -> case lookup "Z-Type" (requestHeaders req) of + Just t' + | t == t' -> + case lookup headerName (requestHeaders req) of + -- a Z- Header exists, try ZAuth or fail + Just a -> zauth a + Nothing -> delayedFail error403 + -- the Z-Type header is not correct, so access is denied. + _ -> + delayedFail error403 + -- if the token type is not given, we try to authenticate with ZAuth first, then fall back to OAuth. + Nothing -> case lookup headerName (requestHeaders req) of + -- a Z- Header exists, try ZAuth or fail + Just a -> zauth a + -- no Z header exists, so try OAuth or fail + Nothing -> oauth ctx req + where + headerName :: IsString n => n + headerName = fromString $ symbolVal (Proxy @(ZHeader ztype)) + + zauth :: ByteString -> DelayedIO (ZQualifiedParam ztype) + zauth bs = case fromByteString @(ZParam ztype) bs of + Just a -> pure $ qualifyZParam @ztype ctx a + Nothing -> delayedFail error403 + + oauth :: Context ctx -> Request -> DelayedIO (ZQualifiedParam ztype) + oauth c r = fmap (qualifyZParam @ztype ctx) (checkAuth c r) + + -- TODO: consider passing just the key and not the whole context + checkAuth :: + Context ctx -> + Request -> + DelayedIO (ZParam ztype) + checkAuth c = doOAuth (getContextEntry c) >=> either delayedFailFatal pure + where + doOAuth :: Maybe JWK -> Request -> DelayedIO (Either ServerError (ZParam ztype)) + doOAuth mJwk r = tryOAuth + where + tryOAuth :: DelayedIO (Either ServerError (ZParam ztype)) + tryOAuth = do + let headerOrError = maybeToRight oauthTokenMissing $ lookup "Z-OAuth" (requestHeaders r) + let jwkOrError = maybeToRight jwtError mJwk + let tokenOrError = headerOrError >>= mapLeft invalidOAuthToken . parseHeader + either (pure . Left) verifyOAuthToken $ (,) <$> tokenOrError <*> jwkOrError + + verifyOAuthToken :: (Bearer OAuthAccessToken, JWK) -> DelayedIO (Either ServerError (ZParam ztype)) + verifyOAuthToken (token, key) = do + verifiedOrError <- mapLeft (invalidOAuthToken . cs . show) <$> liftIO (verify key (unOAuthToken . unBearer $ token)) + pure $ + verifiedOrError >>= \claimSet -> + if hasScope @scope claimSet + then maybeToRight (invalidOAuthToken "Invalid token: Missing or invalid sub claim") (hcsSub claimSet) + else Left insufficientScope + +-- | Handle routes that support ZAuth, but not OAuth (scope is Nothing). instance ( IsZType ztype ctx, HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, @@ -359,24 +372,25 @@ instance ctx ( fmap (. mapRequestArgument @opts (qualifyZParam @ztype ctx)) - (addAuthCheck (fmap (\x _ -> x) subserver) (withRequest (checkType (tokenType @ztype)))) + (addAuthCheck (fmap const subserver) (withRequest (checkType (tokenType @ztype)))) ) hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s checkType :: Maybe ByteString -> Wai.Request -> DelayedIO () checkType token req = case (token, lookup "Z-Type" (Wai.requestHeaders req)) of - (Just t, value) - | value /= Just t -> - delayedFail - ServerError - { errHTTPCode = 403, - errReasonPhrase = "Access denied", - errBody = "", - errHeaders = [] - } + (Just t, value) | value /= Just t -> delayedFail error403 _ -> pure () -instance RoutesToPaths api => RoutesToPaths (ZAuthServant ztype opts scopes :> api) where +error403 :: ServerError +error403 = + ServerError + { errHTTPCode = 403, + errReasonPhrase = "Access denied", + errBody = "", + errHeaders = [] + } + +instance RoutesToPaths api => RoutesToPaths (ZAuthServant ztype opts scope :> api) where getRoutes = getRoutes @api -- FUTUREWORK: Make a PR to the servant-swagger package with this instance @@ -405,15 +419,6 @@ instance HasServer api ctx => HasServer (OmitDocs :> api) ctx where instance RoutesToPaths api => RoutesToPaths (OmitDocs :> api) where getRoutes = getRoutes @api --------------------------------------------------------------------------------- --- Z-OAuth - --- FUTUREWORK: it would be nice to have unit tests for this and the instances (esp. `HasServer`, but we cover this with integration tests in brig et al. for now.) -data ZUserOrOAuth (scope :: OAuthScope) - -instance HasSwagger api => HasSwagger (ZUserOrOAuth scope :> api) where - toSwagger _ = toSwagger (Proxy @api) - -------------------------------------------------------------------------------- -- Util From cb6222489229b76aebb1f8dd870ef55040486a28 Mon Sep 17 00:00:00 2001 From: jschaul Date: Thu, 2 Feb 2023 17:09:06 +0100 Subject: [PATCH 23/56] Test helper SQSWatcher: use purgeQueue (#3054) * Test helper SQSWatcher: use purgeQueue The previous logic of emptying the queue by reading all messages and deleting them assumes there is no other process writing anything into the queue, which might not be the case (in case of parallel brig/galley/spar tests). Instead, use purgeQueue to empty the queue, which should be faster and more reliable. * Hi CI --- libs/types-common-aws/src/Util/Test/SQS.hs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/libs/types-common-aws/src/Util/Test/SQS.hs b/libs/types-common-aws/src/Util/Test/SQS.hs index 61349f1b42..9f6368edd1 100644 --- a/libs/types-common-aws/src/Util/Test/SQS.hs +++ b/libs/types-common-aws/src/Util/Test/SQS.hs @@ -50,6 +50,8 @@ data SQSWatcher a = SQSWatcher -- This function drops everything in the queue before starting the async loop. -- This helps test run faster and makes sure that initial tests don't timeout if -- the queue has too many things in it before the tests start. +-- Note that the purgeQueue command is not guaranteed to be instant (can take up to 60 seconds) +-- Hopefully, the fake-aws implementation used during tests is fast enough. watchSQSQueue :: Message a => AWS.Env -> Text -> IO (SQSWatcher a) watchSQSQueue env queueUrl = do eventsRef <- newIORef [] @@ -73,18 +75,7 @@ watchSQSQueue env queueUrl = do recieveLoop ref ensureEmpty :: IO () - ensureEmpty = do - let rcvReq = - SQS.newReceiveMessage queueUrl - & set SQS.receiveMessage_waitTimeSeconds (Just 1) - . set SQS.receiveMessage_maxNumberOfMessages (Just 10) -- 10 is maximum allowed by AWS - . set SQS.receiveMessage_visibilityTimeout (Just 1) - rcvRes <- execute env $ sendEnv rcvReq - case fromMaybe [] $ view SQS.receiveMessageResponse_messages rcvRes of - [] -> pure () - ms -> do - execute env $ mapM_ (deleteMessage queueUrl) ms - ensureEmpty + ensureEmpty = void $ execute env $ sendEnv (SQS.newPurgeQueue queueUrl) -- | Waits for a message matching a predicate for a given number of seconds. waitForMessage :: (MonadUnliftIO m, Eq a, Show a) => SQSWatcher a -> Int -> (a -> Bool) -> m (Maybe a) From 7d1504b209c856e2b873b4eded4ff264ccb88e04 Mon Sep 17 00:00:00 2001 From: jschaul Date: Thu, 2 Feb 2023 17:18:42 +0100 Subject: [PATCH 24/56] Improve helm test output; and provide the means (even if disabled due to flaky tests) for parallel helm test executions. (#3040) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Allow running helm tests in parallel if desired, using `HELM_PARALLELISM=6` (disabled for now until we have fixed some flaky tests which fail more often when tests run in parallel) 2. rework integration test output: logs from test runs will only show if there are any failed tests. Also, the bottom of the output will have a summary of what failed and what didn't; as well as only the failed test lines with a context of +- 10 lines. This should hopefully make it easier to see what went wrong: just scroll to the bottom. The summary looks like this: ``` === tail cargohold: === All 21 tests passed (8.45s) === tail gundeck: === All 33 tests passed (56.60s) === tail federator: === Finished in 0.6576 seconds 9 examples, 0 failures === tail spar: === Finished in 397.2779 seconds 553 examples, 0 failures, 65 pending === tail brig: === 2 out of 449 tests failed (123.07s) === tail galley: === 1 out of 414 tests failed (136.33s) cargohold-integration passed ✅. gundeck-integration passed ✅. federator-integration passed ✅. spar-integration passed ✅. brig-integration FAILED ❌. pfff... galley-integration FAILED ❌. pfff... Tests failed. ``` --- Makefile | 7 +- changelog.d/5-internal/parallel-helm-tests | 1 + hack/bin/integration-logs-relevant-bits.sh | 54 +++++++++++++ hack/bin/integration-setup-federation.sh | 6 +- hack/bin/integration-setup.sh | 6 +- hack/bin/integration-test.sh | 88 +++++++++++++++++++++- nix/wire-server.nix | 2 + 7 files changed, 153 insertions(+), 11 deletions(-) create mode 100644 changelog.d/5-internal/parallel-helm-tests create mode 100755 hack/bin/integration-logs-relevant-bits.sh diff --git a/Makefile b/Makefile index 28ba398972..986013600f 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,7 @@ elasticsearch-ephemeral minio-external cassandra-external \ nginx-ingress-controller nginx-ingress-services reaper sftd restund coturn \ inbucket k8ssandra-test-cluster KIND_CLUSTER_NAME := wire-server +HELM_PARALLELISM ?= 1 # 1 for sequential tests; 6 for all-parallel tests package ?= all EXE_SCHEMA := ./dist/$(package)-schema @@ -315,15 +316,15 @@ kube-integration: kube-integration-setup kube-integration-test .PHONY: kube-integration-setup kube-integration-setup: charts-integration - export NAMESPACE=$(NAMESPACE); ./hack/bin/integration-setup-federation.sh + export NAMESPACE=$(NAMESPACE); export HELM_PARALLELISM=$(HELM_PARALLELISM); ./hack/bin/integration-setup-federation.sh .PHONY: kube-integration-test kube-integration-test: - export NAMESPACE=$(NAMESPACE); ./hack/bin/integration-test.sh + export NAMESPACE=$(NAMESPACE); export HELM_PARALLELISM=$(HELM_PARALLELISM); ./hack/bin/integration-test.sh .PHONY: kube-integration-teardown kube-integration-teardown: - export NAMESPACE=$(NAMESPACE); ./hack/bin/integration-teardown-federation.sh + export NAMESPACE=$(NAMESPACE); export HELM_PARALLELISM=$(HELM_PARALLELISM); ./hack/bin/integration-teardown-federation.sh .PHONY: kube-integration-e2e-telepresence kube-integration-e2e-telepresence: diff --git a/changelog.d/5-internal/parallel-helm-tests b/changelog.d/5-internal/parallel-helm-tests new file mode 100644 index 0000000000..3f4909c3b7 --- /dev/null +++ b/changelog.d/5-internal/parallel-helm-tests @@ -0,0 +1 @@ +Add config to allow to run helm tests for different services in parallel; improve integration test output logs diff --git a/hack/bin/integration-logs-relevant-bits.sh b/hack/bin/integration-logs-relevant-bits.sh new file mode 100755 index 0000000000..96b836ca90 --- /dev/null +++ b/hack/bin/integration-logs-relevant-bits.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -euo pipefail + +USAGE=" +Filter out garbage from logs (but keep colors and highlight problems). + +(Adapted from logfilter.sh by Stefan Matting) + +To use it pipe log output from integration-test.sh into this tool. + +Usage: logs | $0 [options] +" + +red_color="\x1b\[;1m\x1b\[31m" +problem_markers="Failures:|FAILED|ExitFailure""\ +|\^+""\ +|FAIL""\ +|tests failed""\ +|Test suite failure""\ +|""$red_color""error:""\ +|""$red_color""\ +|Test suite .+ failed" + +exit_usage() { + echo "$USAGE" + exit 1 +} + +# remove debug/info logs +# often this is just noise like connection to cassandra. +excludeLogEntries() { + grep -v '^{".*Info"' | + grep -v '^{".*Debug"' | + grep -v '^20.*, D, .*socket: [0-9]\+>$' +} + +cleanup() { + # replace backspaces with newlines + # remove "Progress" lines + # Remove blank lines + # add newline between interleaved test name and log output lines + sed 's/\x08\+/\n/g' | + sed '/^Progress [0-9]\+/d' | + sed '/^\s\+$/d' | + sed 's/:\s\+{/:\n{/g' +} + +grepper() { + # print 10 lines before/after for context + rg "$problem_markers" --color=always -A 10 -B 10 + echo -e "\033[0m" +} + +cleanup | excludeLogEntries | grepper diff --git a/hack/bin/integration-setup-federation.sh b/hack/bin/integration-setup-federation.sh index 7795226bab..f569928239 100755 --- a/hack/bin/integration-setup-federation.sh +++ b/hack/bin/integration-setup-federation.sh @@ -7,6 +7,7 @@ TOP_LEVEL="$DIR/../.." export NAMESPACE=${NAMESPACE:-test-integration} HELMFILE_ENV=${HELMFILE_ENV:-default} CHARTS_DIR="${TOP_LEVEL}/.local/charts" +HELM_PARALLELISM=${HELM_PARALLELISM:-1} . "$DIR/helm_overrides.sh" ${DIR}/integration-cleanup.sh @@ -20,9 +21,8 @@ ${DIR}/integration-cleanup.sh # (e.g. cassandra from underneath databases-ephemeral) echo "updating recursive dependencies ..." charts=(fake-aws databases-ephemeral redis-cluster wire-server nginx-ingress-controller nginx-ingress-services) -for chart in "${charts[@]}"; do - "$DIR/update.sh" "$CHARTS_DIR/$chart" -done +mkdir -p ~/.parallel && touch ~/.parallel/will-cite +printf '%s\n' "${charts[@]}" | parallel -P "${HELM_PARALLELISM}" "$DIR/update.sh" "$CHARTS_DIR/{}" # FUTUREWORK: use helm functions instead, see https://wearezeta.atlassian.net/browse/SQPIT-723 echo "Generating self-signed certificates..." diff --git a/hack/bin/integration-setup.sh b/hack/bin/integration-setup.sh index 634cc3a49f..59cf0e4f84 100755 --- a/hack/bin/integration-setup.sh +++ b/hack/bin/integration-setup.sh @@ -7,6 +7,7 @@ TOP_LEVEL="$DIR/../.." export NAMESPACE=${NAMESPACE:-test-integration} HELMFILE_ENV=${HELMFILE_ENV:-default} CHARTS_DIR="${TOP_LEVEL}/.local/charts" +HELM_PARALLELISM=${HELM_PARALLELISM:-1} . "$DIR/helm_overrides.sh" @@ -14,9 +15,8 @@ CHARTS_DIR="${TOP_LEVEL}/.local/charts" echo "updating recursive dependencies ..." charts=(fake-aws databases-ephemeral redis-cluster wire-server nginx-ingress-controller nginx-ingress-services) -for chart in "${charts[@]}"; do - "$DIR/update.sh" "$CHARTS_DIR/$chart" -done +mkdir -p ~/.parallel && touch ~/.parallel/will-cite +printf '%s\n' "${charts[@]}" | parallel -P "${HELM_PARALLELISM}" "$DIR/update.sh" "$CHARTS_DIR/{}" echo "Generating self-signed certificates..." export FEDERATION_DOMAIN_BASE="$NAMESPACE.svc.cluster.local" diff --git a/hack/bin/integration-test.sh b/hack/bin/integration-test.sh index 8ffb21d8c7..47c5db9c3d 100755 --- a/hack/bin/integration-test.sh +++ b/hack/bin/integration-test.sh @@ -1,9 +1,93 @@ #!/usr/bin/env bash set -euo pipefail +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" NAMESPACE=${NAMESPACE:-test-integration} +# set to 1 to disable running helm tests in parallel +HELM_PARALLELISM=${HELM_PARALLELISM:-1} +CLEANUP_LOCAL_FILES=${CLEANUP_LOCAL_FILES:-1} # set to 0 to keep files -echo "Running integration tests on wire-server" +echo "Running integration tests on wire-server with parallelism=${HELM_PARALLELISM} ..." CHART=wire-server -helm test --logs -n "${NAMESPACE}" "${NAMESPACE}-${CHART}" --timeout 900s +tests=(galley cargohold gundeck federator spar brig) + +cleanup() { + if (( CLEANUP_LOCAL_FILES > 0 )); then + for t in "${tests[@]}"; do + rm -f "stat-$t" + rm -f "logs-$t" + done + fi +} + +summary() { + echo "===============" + echo "=== summary ===" + echo "===============" + printf '%s\n' "${tests[@]}" | parallel echo "=== tail {}: ===" ';' tail -2 logs-{} + + for t in "${tests[@]}"; do + x=$(cat "stat-$t") + if ((x > 0)); then + echo "$t-integration FAILED ❌. pfff..." + else + echo "$t-integration passed ✅." + fi + done +} + +# Run tests in parallel using GNU parallel (see https://www.gnu.org/software/parallel/) +# The below commands are a little convoluted, but we wish to: +# - run integration tests. If they fail, keep track of this, but still go and get logs, so we see what failed +# - run all tests. Perhaps multiple flaky tests in multiple services exist, if so, we wish to see all problems +mkdir -p ~/.parallel && touch ~/.parallel/will-cite +printf '%s\n' "${tests[@]}" | parallel echo "Running helm tests for {}..." +printf '%s\n' "${tests[@]}" | parallel -P "${HELM_PARALLELISM}" \ + helm test -n "${NAMESPACE}" "${NAMESPACE}-${CHART}" --timeout 900s --filter name="${NAMESPACE}-${CHART}-{}-integration" '> logs-{};' \ + echo '$? > stat-{};' \ + echo "==== Done testing {}. ====" '};' \ + kubectl -n "${NAMESPACE}" logs "${NAMESPACE}-${CHART}-{}-integration" '>> logs-{};' + +summary + +# in case any integration test suite failed, exit this script with an error. +exit_code=0 +for t in "${tests[@]}"; do + x=$(cat "stat-$t") + if ((x > 0)); then + exit_code=1 + fi +done + +if ((exit_code > 0)); then + echo "=======================" + echo "=== failed job logs ===" + echo "=======================" + # in case a integration test suite failed, print relevant logs + for t in "${tests[@]}"; do + x=$(cat "stat-$t") + if ((x > 0)); then + echo "=== logs for failed $t-integration ===" + cat "logs-$t" + fi + done + summary + for t in "${tests[@]}"; do + x=$(cat "stat-$t") + if ((x > 0)); then + echo "=== (relevant) logs for failed $t-integration ===" + "$DIR/integration-logs-relevant-bits.sh" < "logs-$t" + fi + done + summary +fi + +cleanup + +if ((exit_code > 0)); then + echo "Tests failed." + exit 1 +else + echo "All integration tests passed ✅." +fi diff --git a/nix/wire-server.nix b/nix/wire-server.nix index 33320d327d..c215298f8b 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -298,6 +298,8 @@ let pkgs.cabal2nix pkgs.gnumake pkgs.gnused + pkgs.parallel + pkgs.ripgrep pkgs.helm pkgs.helmfile pkgs.hlint From 3d7d9b2e2cdf219bca74cad0a617ed671a8e8135 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 3 Feb 2023 07:30:37 +0000 Subject: [PATCH 25/56] clean up --- libs/wire-api/src/Wire/API/Routes/Public.hs | 82 +++++++++------------ 1 file changed, 34 insertions(+), 48 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index 7a54c60cca..ae438899ec 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -1,7 +1,5 @@ {-# LANGUAGE DerivingVia #-} {-# OPTIONS_GHC -Wno-orphans #-} -{-# OPTIONS_GHC -Wno-unused-imports #-} -{-# OPTIONS_GHC -Wno-unused-top-binds #-} -- This file is part of the Wire Server implementation. -- @@ -46,7 +44,6 @@ import Control.Lens ((<>~)) import Control.Monad.Except import Crypto.JWT hiding (Context, params, uri, verify) import Data.ByteString.Conversion (fromByteString) -import Data.Data (typeRep) import Data.Domain import Data.Either.Combinators import qualified Data.HashMap.Strict.InsOrd as InsOrdHashMap @@ -57,9 +54,8 @@ import Data.Qualified import Data.SOP import Data.String.Conversions (cs) import Data.Swagger -import qualified Data.Text as T import GHC.Base (Symbol) -import GHC.TypeLits (KnownSymbol, symbolVal, symbolVal') +import GHC.TypeLits (KnownSymbol, symbolVal) import Imports hiding (All, exp, head) import Network.Wai import qualified Network.Wai as Wai @@ -67,7 +63,6 @@ import Servant hiding (Handler, JSON, Tagged, addHeader, respond) import Servant.API.Modifiers import Servant.Server.Internal.Delayed import Servant.Server.Internal.DelayedIO -import Servant.Server.Internal.ErrorFormatter (mkContextWithErrorFormatter) import Servant.Server.Internal.Router import Servant.Swagger (HasSwagger (toSwagger)) import Servant.Swagger.Internal.Orphans () @@ -187,12 +182,8 @@ type InternalAuth ztype opts = type ZLocalUser = ZAuthServant 'ZLocalAuthUser InternalAuthDefOpts 'Nothing -type ZOAuthLocalUser scope = ZAuthServant 'ZLocalAuthUser InternalAuthDefOpts ('Just scope) - type ZUser = ZAuthServant 'ZAuthUser InternalAuthDefOpts 'Nothing -type ZOauthUser scope = ZAuthServant 'ZAuthUser InternalAuthDefOpts ('Just scope) - type ZClient = ZAuthServant 'ZAuthClient InternalAuthDefOpts 'Nothing type ZConn = ZAuthServant 'ZAuthConn InternalAuthDefOpts 'Nothing @@ -209,6 +200,10 @@ type ZOptClient = ZAuthServant 'ZAuthClient '[Servant.Optional, Servant.Strict] type ZOptConn = ZAuthServant 'ZAuthConn '[Servant.Optional, Servant.Strict] 'Nothing +type ZOAuthLocalUser scope = ZAuthServant 'ZLocalAuthUser InternalAuthDefOpts ('Just scope) + +type ZOauthUser scope = ZAuthServant 'ZAuthUser InternalAuthDefOpts ('Just scope) + -- TODO(leif): doc for scope (also other instances) instance HasSwagger api => HasSwagger (ZAuthServant 'ZAuthUser _opts scope :> api) where toSwagger _ = @@ -283,26 +278,19 @@ checkType' :: ) => Context ctx -> Maybe ByteString -> - Wai.Request -> + Request -> DelayedIO (ZQualifiedParam ztype) checkType' ctx mTokenType req = case mTokenType of -- if the token type is given, the request must have the correct Z-Type header, otherwise access is denied. Just t -> case lookup "Z-Type" (requestHeaders req) of - Just t' - | t == t' -> - case lookup headerName (requestHeaders req) of - -- a Z- Header exists, try ZAuth or fail - Just a -> zauth a - Nothing -> delayedFail error403 + Just t' | t == t' -> case lookup headerName (requestHeaders req) of + -- a Z- Header exists, try ZAuth or fail + Just a -> zauth a + Nothing -> delayedFail error403 -- the Z-Type header is not correct, so access is denied. - _ -> - delayedFail error403 + _ -> delayedFail error403 -- if the token type is not given, we try to authenticate with ZAuth first, then fall back to OAuth. - Nothing -> case lookup headerName (requestHeaders req) of - -- a Z- Header exists, try ZAuth or fail - Just a -> zauth a - -- no Z header exists, so try OAuth or fail - Nothing -> oauth ctx req + Nothing -> maybe oauth zauth $ lookup headerName (requestHeaders req) where headerName :: IsString n => n headerName = fromString $ symbolVal (Proxy @(ZHeader ztype)) @@ -312,34 +300,32 @@ checkType' ctx mTokenType req = case mTokenType of Just a -> pure $ qualifyZParam @ztype ctx a Nothing -> delayedFail error403 - oauth :: Context ctx -> Request -> DelayedIO (ZQualifiedParam ztype) - oauth c r = fmap (qualifyZParam @ztype ctx) (checkAuth c r) + oauth :: DelayedIO (ZQualifiedParam ztype) + oauth = fmap (qualifyZParam @ztype ctx) (doOAuthOrFail req) - -- TODO: consider passing just the key and not the whole context - checkAuth :: - Context ctx -> + doOAuthOrFail :: Request -> DelayedIO (ZParam ztype) - checkAuth c = doOAuth (getContextEntry c) >=> either delayedFailFatal pure + doOAuthOrFail = doOAuth (getContextEntry ctx) >=> either delayedFailFatal pure + + doOAuth :: Maybe JWK -> Request -> DelayedIO (Either ServerError (ZParam ztype)) + doOAuth mJwk r = tryOAuth where - doOAuth :: Maybe JWK -> Request -> DelayedIO (Either ServerError (ZParam ztype)) - doOAuth mJwk r = tryOAuth - where - tryOAuth :: DelayedIO (Either ServerError (ZParam ztype)) - tryOAuth = do - let headerOrError = maybeToRight oauthTokenMissing $ lookup "Z-OAuth" (requestHeaders r) - let jwkOrError = maybeToRight jwtError mJwk - let tokenOrError = headerOrError >>= mapLeft invalidOAuthToken . parseHeader - either (pure . Left) verifyOAuthToken $ (,) <$> tokenOrError <*> jwkOrError - - verifyOAuthToken :: (Bearer OAuthAccessToken, JWK) -> DelayedIO (Either ServerError (ZParam ztype)) - verifyOAuthToken (token, key) = do - verifiedOrError <- mapLeft (invalidOAuthToken . cs . show) <$> liftIO (verify key (unOAuthToken . unBearer $ token)) - pure $ - verifiedOrError >>= \claimSet -> - if hasScope @scope claimSet - then maybeToRight (invalidOAuthToken "Invalid token: Missing or invalid sub claim") (hcsSub claimSet) - else Left insufficientScope + tryOAuth :: DelayedIO (Either ServerError (ZParam ztype)) + tryOAuth = do + let headerOrError = maybeToRight oauthTokenMissing $ lookup "Z-OAuth" (requestHeaders r) + let jwkOrError = maybeToRight jwtError mJwk + let tokenOrError = headerOrError >>= mapLeft invalidOAuthToken . parseHeader + either (pure . Left) verifyOAuthToken $ (,) <$> tokenOrError <*> jwkOrError + + verifyOAuthToken :: (Bearer OAuthAccessToken, JWK) -> DelayedIO (Either ServerError (ZParam ztype)) + verifyOAuthToken (token, key) = do + verifiedOrError <- mapLeft (invalidOAuthToken . cs . show) <$> liftIO (verify key (unOAuthToken . unBearer $ token)) + pure $ + verifiedOrError >>= \claimSet -> + if hasScope @scope claimSet + then maybeToRight (invalidOAuthToken "Invalid token: Missing or invalid sub claim") (hcsSub claimSet) + else Left insufficientScope -- | Handle routes that support ZAuth, but not OAuth (scope is Nothing). instance From 95dde995453c48cef0f0c06572f7253915dec354 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 3 Feb 2023 07:43:59 +0000 Subject: [PATCH 26/56] more clear comments --- libs/wire-api/src/Wire/API/Routes/Public.hs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index ae438899ec..796af9f9c8 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -281,15 +281,18 @@ checkType' :: Request -> DelayedIO (ZQualifiedParam ztype) checkType' ctx mTokenType req = case mTokenType of - -- if the token type is given, the request must have the correct Z-Type header, otherwise access is denied. Just t -> case lookup "Z-Type" (requestHeaders req) of + -- if the token type is given, the request must have the correct Z-Type header, otherwise access is denied. Just t' | t == t' -> case lookup headerName (requestHeaders req) of - -- a Z- Header exists, try ZAuth or fail + -- a Z header (e.g. 'Z-Provider') header exists, so we try ZAuth or fail Just a -> zauth a + -- the 'Z-Type' header is correct, but no Z header exists, so access is denied. Nothing -> delayedFail error403 - -- the Z-Type header is not correct, so access is denied. + -- the 'Z-Type' header is either not set or not correct, access is denied. _ -> delayedFail error403 - -- if the token type is not given, we try to authenticate with ZAuth first, then fall back to OAuth. + -- if the token type is not given, we check the Z header (e.g. 'Z-User') + -- and if it exists we do ZAuth or fail, + -- or if it doesn't exist we fall back to OAuth Nothing -> maybe oauth zauth $ lookup headerName (requestHeaders req) where headerName :: IsString n => n From e4b69e81ba2aa42d7f63025d7394f182616eaf5d Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 3 Feb 2023 10:29:57 +0000 Subject: [PATCH 27/56] clean up naming --- libs/wire-api/src/Wire/API/Routes/Public.hs | 47 ++++++++++----------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index 796af9f9c8..9f95d7ea23 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -280,45 +280,45 @@ checkType' :: Maybe ByteString -> Request -> DelayedIO (ZQualifiedParam ztype) -checkType' ctx mTokenType req = case mTokenType of - Just t -> case lookup "Z-Type" (requestHeaders req) of - -- if the token type is given, the request must have the correct Z-Type header, otherwise access is denied. - Just t' | t == t' -> case lookup headerName (requestHeaders req) of - -- a Z header (e.g. 'Z-Provider') header exists, so we try ZAuth or fail - Just a -> zauth a - -- the 'Z-Type' header is correct, but no Z header exists, so access is denied. - Nothing -> delayedFail error403 - -- the 'Z-Type' header is either not set or not correct, access is denied. - _ -> delayedFail error403 - -- if the token type is not given, we check the Z header (e.g. 'Z-User') - -- and if it exists we do ZAuth or fail, - -- or if it doesn't exist we fall back to OAuth - Nothing -> maybe oauth zauth $ lookup headerName (requestHeaders req) +checkType' ctx mTokenType req = + case lookupHeaders of + -- if the ztype requires a Z-Type header (instance of 'HasTokenType' returns a Just ...), we expect it to match the type we're looking for + (Just expType, Just actType, Just t, Nothing) | expType == actType -> zauth t + -- auth fails if the ztype doesn't match + (Just _, _, _, _) -> delayedFailFatal error403 + -- if the ztype does not require a Z-Type header, we just care for the ZParam ('Z-User' etc.) header + (Nothing, _, Just t, Nothing) -> zauth t + -- if the 'Z-Oauth' header is present, we try to authenticate with OAuth + (Nothing, Nothing, Nothing, Just t) -> oauth t + -- any other case should fail + (Nothing, _, _, _) -> delayedFailFatal error403 where + lookupHeaders :: (Maybe ByteString, Maybe ByteString, Maybe ByteString, Maybe ByteString) + lookupHeaders = (mTokenType, lookup "Z-Type" (requestHeaders req), lookup headerName (requestHeaders req), lookup "Z-OAuth" (requestHeaders req)) + headerName :: IsString n => n headerName = fromString $ symbolVal (Proxy @(ZHeader ztype)) zauth :: ByteString -> DelayedIO (ZQualifiedParam ztype) zauth bs = case fromByteString @(ZParam ztype) bs of Just a -> pure $ qualifyZParam @ztype ctx a - Nothing -> delayedFail error403 + Nothing -> delayedFailFatal error403 - oauth :: DelayedIO (ZQualifiedParam ztype) - oauth = fmap (qualifyZParam @ztype ctx) (doOAuthOrFail req) + oauth :: ByteString -> DelayedIO (ZQualifiedParam ztype) + oauth h = fmap (qualifyZParam @ztype ctx) (doOAuthOrFail h) doOAuthOrFail :: - Request -> + ByteString -> DelayedIO (ZParam ztype) doOAuthOrFail = doOAuth (getContextEntry ctx) >=> either delayedFailFatal pure - doOAuth :: Maybe JWK -> Request -> DelayedIO (Either ServerError (ZParam ztype)) - doOAuth mJwk r = tryOAuth + doOAuth :: Maybe JWK -> ByteString -> DelayedIO (Either ServerError (ZParam ztype)) + doOAuth mJwk h = tryOAuth where tryOAuth :: DelayedIO (Either ServerError (ZParam ztype)) tryOAuth = do - let headerOrError = maybeToRight oauthTokenMissing $ lookup "Z-OAuth" (requestHeaders r) let jwkOrError = maybeToRight jwtError mJwk - let tokenOrError = headerOrError >>= mapLeft invalidOAuthToken . parseHeader + let tokenOrError = mapLeft invalidOAuthToken $ parseHeader h either (pure . Left) verifyOAuthToken $ (,) <$> tokenOrError <*> jwkOrError verifyOAuthToken :: (Bearer OAuthAccessToken, JWK) -> DelayedIO (Either ServerError (ZParam ztype)) @@ -419,6 +419,3 @@ jwtError = err500 {errReasonPhrase = "jwt-error", errBody = "Internal error whil invalidOAuthToken :: Text -> ServerError invalidOAuthToken t = err403 {errReasonPhrase = "Access denied", errBody = "Invalid token: " <> cs t} - -oauthTokenMissing :: ServerError -oauthTokenMissing = err403 {errReasonPhrase = "Access denied"} From f37c319519b613258eb5895aedc27d363325b088 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 3 Feb 2023 10:44:41 +0000 Subject: [PATCH 28/56] another cleanup --- libs/wire-api/src/Wire/API/Routes/Public.hs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index 9f95d7ea23..4f94ee5ccd 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -263,7 +263,7 @@ instance Servant.route (Proxy @api) ctx - (addAuthCheck subserver (withRequest (checkType' @ztype @scope @ctx ctx (tokenType @ztype)))) + (addAuthCheck subserver (withRequest (fmap (qualifyZParam @ztype ctx) . checkType' @ztype @scope @ctx ctx (tokenType @ztype)))) hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s @@ -279,7 +279,7 @@ checkType' :: Context ctx -> Maybe ByteString -> Request -> - DelayedIO (ZQualifiedParam ztype) + DelayedIO (ZParam ztype) checkType' ctx mTokenType req = case lookupHeaders of -- if the ztype requires a Z-Type header (instance of 'HasTokenType' returns a Just ...), we expect it to match the type we're looking for @@ -299,18 +299,11 @@ checkType' ctx mTokenType req = headerName :: IsString n => n headerName = fromString $ symbolVal (Proxy @(ZHeader ztype)) - zauth :: ByteString -> DelayedIO (ZQualifiedParam ztype) - zauth bs = case fromByteString @(ZParam ztype) bs of - Just a -> pure $ qualifyZParam @ztype ctx a - Nothing -> delayedFailFatal error403 + zauth :: ByteString -> DelayedIO (ZParam ztype) + zauth = maybe (delayedFailFatal error403) pure . fromByteString @(ZParam ztype) - oauth :: ByteString -> DelayedIO (ZQualifiedParam ztype) - oauth h = fmap (qualifyZParam @ztype ctx) (doOAuthOrFail h) - - doOAuthOrFail :: - ByteString -> - DelayedIO (ZParam ztype) - doOAuthOrFail = doOAuth (getContextEntry ctx) >=> either delayedFailFatal pure + oauth :: ByteString -> DelayedIO (ZParam ztype) + oauth = doOAuth (getContextEntry ctx) >=> either delayedFailFatal pure doOAuth :: Maybe JWK -> ByteString -> DelayedIO (Either ServerError (ZParam ztype)) doOAuth mJwk h = tryOAuth From 716b5f89985401c93d05e2738fd1924015b61b8f Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Fri, 3 Feb 2023 12:17:35 +0100 Subject: [PATCH 29/56] Small fixes to documentation (#3060) --- docs/src/how-to/install/post-install.md | 1 + docs/src/understand/searchability.md | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/src/how-to/install/post-install.md b/docs/src/how-to/install/post-install.md index 6a513f0ece..f04d9157a8 100644 --- a/docs/src/how-to/install/post-install.md +++ b/docs/src/how-to/install/post-install.md @@ -1,3 +1,4 @@ +(checks)= # Verifying your installation After a successful installation of wire-server and its components, there are some useful checks to be run to ensure the proper functioning of the system. Here's a non-exhaustive list of checks to run on the hosts: diff --git a/docs/src/understand/searchability.md b/docs/src/understand/searchability.md index 625e6754ac..2f37fdcc09 100644 --- a/docs/src/understand/searchability.md +++ b/docs/src/understand/searchability.md @@ -49,13 +49,13 @@ The flags will influence the behavior of the search API endpoint; clients will o +------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ | Yes, `tA` | Yes, the same team `tA` | Irrelevant | Irrelevant | Irrelevant | Found | Found | +------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ -| **Out-Bound search unrestricted** | +| **Out-Bound search unrestricted** | +------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ | Yes, `tA` | Yes, another team tB | false | `SearchVisibilityStandard` | `SearchableByAllTeams` | Found | Found | +------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ | Yes, `tA` | Yes, another team tB | false | `SearchVisibilityStandard` | `SearchableByOwnTeam` | Found | Not found | +------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ -| **Out-Bound search restricted** | +| **Out-Bound search restricted** | +------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ | Yes, `tA` | Yes, another team tB | true | Irrelevant | Irrelevant | Not found | Not found | +------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ @@ -179,7 +179,7 @@ Changing this setting in the instance configuration doesn't affect any users tha Individual teams can overwrite the default setting with API calls: -To make API calls to set an explicit configuration for ` TeamSearchVisibilityInbound` per team, you first need to know the Team ID, which can be found in the team settings app. +To make API calls to set an explicit configuration for `SearchVisibilityInbound` per team, you first need to know the Team ID, which can be found in the team settings app. It is an [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier) which has format like this `dcbedf9a-af2a-4f43-9fd5-525953a919e1`. @@ -222,13 +222,13 @@ curl -XGET http://localhost:9000/i/teams/dcbedf9a-af2a-4f43-9fd5-525953a919e1/fe Where `disabled` corresponds to `SearchableByOwnTeam` and enabled corresponds to `SearchableByAllTeams`. -To change the `TeamSearchVisibilityInbound` to `SearchableByAllTeams` for the team run: +To change the `SearchVisibilityInbound` to `SearchableByAllTeams` for the team run: ```sh curl -XPUT -H 'Content-Type: application/json' -d "{\"status\": \"enabled\"}" http://localhost:9000/i/teams/dcbedf9a-af2a-4f43-9fd5-525953a919e1/features/searchVisibilityInbound ``` -To change the TeamSearchVisibilityInbound to SearchableByOwnTeam for the team run: +To change the `SearchVisibilityInbound` to `SearchableByOwnTeam` for the team run: ```sh curl -XPUT -H 'Content-Type: application/json' -d "{\"status\": \"disabled\"}" http://localhost:9000/i/teams/dcbedf9a-af2a-4f43-9fd5-525953a919e1/features/searchVisibilityInbound From a0549ee82a9aa0634bfaf7586ec6383c5048bf37 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 3 Feb 2023 11:19:22 +0000 Subject: [PATCH 30/56] conn id optional --- .../src/Wire/API/Routes/Public/Galley/Conversation.hs | 4 ++-- services/galley/src/Galley/API/Create.hs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index d7b08b83cb..f623be75d8 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -339,7 +339,7 @@ type ConversationAPI = :> CanThrow 'MissingLegalholdConsent :> Description "This returns 201 when a new conversation is created, and 200 when the conversation already existed" :> ZLocalUser - :> ZConn + :> ZOptConn :> "conversations" :> VersionedReqBody 'V2 '[Servant.JSON] NewConv :> ConversationV2Verb @@ -358,7 +358,7 @@ type ConversationAPI = :> CanThrow 'MissingLegalholdConsent :> Description "This returns 201 when a new conversation is created, and 200 when the conversation already existed" :> ZLocalUser - :> ZConn + :> ZOptConn :> "conversations" :> ReqBody '[Servant.JSON] NewConv :> ConversationVerb diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index 2995ca3f07..3254759b03 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -111,10 +111,10 @@ createGroupConversation :: CallsFed 'Galley "on-conversation-created" ) => Local UserId -> - ConnId -> + Maybe ConnId -> NewConv -> Sem r ConversationResponse -createGroupConversation lusr conn newConv = do +createGroupConversation lusr mConn newConv = do (nc, fromConvSize -> allUsers) <- newRegularConversation lusr newConv let tinfo = newConvTeam newConv checkCreateConvPermissions lusr newConv tinfo allUsers @@ -141,7 +141,7 @@ createGroupConversation lusr conn newConv = do now <- input -- NOTE: We only send (conversation) events to members of the conversation - notifyCreatedConversation (Just now) lusr (Just conn) conv + notifyCreatedConversation (Just now) lusr mConn conv conversationCreated lusr conv ensureNoLegalholdConflicts :: From f6e46c347eae3ee25d5dfe77d89b120c375c95c7 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Fri, 3 Feb 2023 15:04:27 +0100 Subject: [PATCH 31/56] Lower the log level of federator inotify (#3056) * Lower the log level of federator inotify --------- Co-authored-by: jschaul --- changelog.d/5-internal/federator-log | 1 + services/federator/src/Federator/Monitor/Internal.hs | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 changelog.d/5-internal/federator-log diff --git a/changelog.d/5-internal/federator-log b/changelog.d/5-internal/federator-log new file mode 100644 index 0000000000..ae5f62c403 --- /dev/null +++ b/changelog.d/5-internal/federator-log @@ -0,0 +1 @@ +Lower the log level of federator inotify diff --git a/services/federator/src/Federator/Monitor/Internal.hs b/services/federator/src/Federator/Monitor/Internal.hs index 4d38328bdc..b1a050e0f6 100644 --- a/services/federator/src/Federator/Monitor/Internal.hs +++ b/services/federator/src/Federator/Monitor/Internal.hs @@ -139,7 +139,7 @@ delMonitor monitor = Polysemy.resourceToIOFinal stop (wd, _) = do -- ignore exceptions when removing watches embed . void . try @IOException $ removeWatch wd - Log.debug $ + Log.trace $ Log.msg ("stopped watching file" :: Text) . Log.field "descriptor" (show wd) @@ -153,7 +153,7 @@ mkMonitor :: Sem r Monitor mkMonitor runSem tlsVar rs = do inotify <- embed initINotify - Log.debug $ + Log.trace $ Log.msg ("inotify initialized" :: Text) . Log.field "inotify" (show inotify) @@ -244,7 +244,7 @@ addWatchedFile monitor wpath = do let pathText = Text.decodeUtf8With Text.lenientDecode (watchedPath wpath) case r of Right w -> - Log.debug $ + Log.trace $ Log.msg ("watching file" :: Text) . Log.field "descriptor" (show w) . Log.field "path" pathText From 80511386abc11118c6a869b8aed18920a34df52a Mon Sep 17 00:00:00 2001 From: fisx Date: Fri, 3 Feb 2023 15:07:19 +0100 Subject: [PATCH 32/56] Update spar docs (#3038) Co-authored-by: Leif Battermann --- changelog.d/4-docs/pr-3038 | 1 + .../src/developer/reference/spar-braindump.md | 125 ----------- docs/src/understand/single-sign-on/index.md | 5 +- .../single-sign-on/trouble-shooting.md | 204 ++++++++++++------ .../src/Wire/API/Routes/Public/Spar.hs | 12 +- services/brig/docs/swagger.md | 12 +- 6 files changed, 153 insertions(+), 206 deletions(-) create mode 100644 changelog.d/4-docs/pr-3038 diff --git a/changelog.d/4-docs/pr-3038 b/changelog.d/4-docs/pr-3038 new file mode 100644 index 0000000000..becf001f5c --- /dev/null +++ b/changelog.d/4-docs/pr-3038 @@ -0,0 +1 @@ +Update SAML/SCIM docs \ No newline at end of file diff --git a/docs/src/developer/reference/spar-braindump.md b/docs/src/developer/reference/spar-braindump.md index 05735e98c6..a5ed0a78b0 100644 --- a/docs/src/developer/reference/spar-braindump.md +++ b/docs/src/developer/reference/spar-braindump.md @@ -26,25 +26,6 @@ documentation answering your questions, look here! - if you want to work on our saml/scim implementation and do not have access to [https://github.com/zinfra/backend-issues/issues?q=is%3Aissue+is%3Aopen+label%3Aspar] and [https://github.com/wireapp/design-specs/tree/master/Single%20Sign%20On], please get in touch with us. -## design considerations - -### SCIM without SAML. - -Before https://github.com/wireapp/wire-server/pull/1200, scim tokens could only be added to teams that already had exactly one SAML IdP. Now, we also allow SAML-less teams to have SCIM provisioning. This is an alternative to onboarding via team-settings and produces user accounts that are authenticated with email and password. (Phone may or may not work, but is not officially supported.) - -The way this works is different from team-settings: we don't send invites, but we create active users immediately the moment the SCIM user post is processed. The new thing is that the created user has neither email nor phone nor a SAML identity, nor a password. - -How does this work? - -**email:** If no SAML IdP is present, SCIM user posts must contain an externalId that is an email address. This email address is not added to the newly created user, because it has not been validated. Instead, the flow for changing an email address is triggered in brig: an email is sent to the address containing a validation key, and once the user completes the flow, brig will add the email address to the user. We had to add very little code for this in this PR, it's all an old feature. - -When SCIM user gets are processed, in order to reconstruct the externalId from the user spar is retrieving from brig, we introduce a new json object for the `sso_id` field that looks like this: `{'scim_external_id': 'me@example.com'}`. - -In order to find users that have email addresses pending validation, we introduce a new table in spar's cassandra called `scim_external_ids`, in analogy to `user`. We have tried to use brig's internal `GET /i/user&email=...`, but that also finds pending email addresses, and there are corner cases when changing email addresses and waiting for the new address to be validated and the old to be removed... that made this approach seem infeasible. - -**password:** once the user has validated their email address, they need to trigger the "forgot password" flow -- also old code. - - ## operations ### enabling / disabling the sso feature for a team @@ -226,35 +207,6 @@ This entry gets removed automatically when the corresponding idp is deleted. You Clients can then ask for the default SSO code on `/sso/settings` and use it to initiate single sign-on. -### troubleshooting - -#### gathering information - -- find metadata for team in table `spar.idp_raw_metadata` via cqlsh - (since https://github.com/wireapp/wire-server/pull/872) - -- ask user for screenshots of the error message, or even better, for - the text. the error message contains lots of strings that you can - grep for in the spar sources. - - -#### making spar work with a new IdP - -often, new IdPs work out of the box, because there appears to be some -consensus about what minimum feature set everybody should support. - -if there are problems: collect the metadata xml and an authentication -response xml (either from the browser http logs via a more technically -savvy customer; FUTUREWORK: it would be nice to log all saml response -xml files that spar receives in prod and cannot process). - -https://github.com/wireapp/saml2-web-sso supports writing [unit vendor -compatibility -tests](https://github.com/wireapp/saml2-web-sso/blob/ff9b9f445475809d1fa31ef7f2932caa0ed31613/test/Test/SAML2/WebSSO/APISpec.hs#L266-L329) -against that response value. once that test passes, it should all -work fine. - - ### common misconceptions @@ -325,80 +277,3 @@ TODO (probably little difference between this and "user deletes herself"?) #### delete via scim TODO - - -## using the same IdP (same entityID, or Issuer) with different teams - -Some SAML IdP vendors do not allow to set up fresh entityIDs (issuers) -for fresh apps; instead, all apps controlled by the IdP are receiving -SAML credentials from the same issuer. - -In the past, wire has used the a tuple of IdP issuer and 'NameID' -(Haskell type 'UserRef') to uniquely identity users (tables -`spar.user_v2` and `spar.issuer_idp`). - -In order to allow one IdP to serve more than one team, this has been -changed: we now allow to identity an IdP by a combination of -entityID/issuer and wire `TeamId`. The necessary tweaks to the -protocol are listed here. - -For everybody using IdPs that do not have this limitation, we have -taken great care to not change the behavior. - - -### what you need to know when operating a team or an instance - -No instance-level configuration is required. - -If your IdP supports different entityID / issuer for different apps, -you don't need to change anything. We hope to deprecate the old -flavor of the SAML protocol eventually, but we will keep you posted in -the release notes, and give you time to react. - -If your IdP does not support different entityID / issuer for different -apps, keep reading. At the time of writing this section, there is no -support for multi-team IdP issuers in team-settings, so you have two -options: (1) use the rest API directly; or (2) contact our customer -support and send them the link to this section. - -If you feel up to calling the rest API, try the following: - -- Use the above end-point `GET /sso/metadata/:tid` with your `TeamId` - for pulling the SP metadata. -- When calling `POST /identity-provider`, make sure to add - `?api_version=v2`. (`?api_version=v1` or no omission of the query - param both invoke the old behavior.) - -NB: Neither version of the API allows you to provision a user with the -same Issuer and same NamdID. RATIONALE: this allows us to implement -'getSAMLUser' without adding 'TeamId' to 'UserRef', which in turn -would break the (admittedly leaky) abstarctions of saml2-web-sso. - - -### API changes in more detail - -- New query param `api_version=` for `POST - /identity-providers`. The version is stored in `spar.idp` together - with the rest of the IdP setup, and is used by `GET - /sso/initiate-login` (see below). -- `GET /sso/initiate-login` sends audience based on api_version stored - in `spar.idp`: for v1, the audience is `/sso/finalize-login`; for - v2, it's `/sso/finalize-login/:tid`. -- New end-point `POST /sso/finalize-login/:tid` that behaves - indistinguishable from `POST /sso/finalize-login`, except when more - than one IdP with the same issuer, but different teams are - registered. In that case, this end-point can process the - credentials by discriminating on the `TeamId`. -- `POST /sso/finalize-login/:tid` remains unchanged. -- New end-point `GET /sso/metadata/:tid` returns the same SP metadata as - `GET /sso/metadata`, with the exception that it lists - `"/sso/finalize-login/:tid"` as the path of the - `AssertionConsumerService` (rather than `"/sso/finalize-login"` as - before). -- `GET /sso/metadata` remains unchanged, and still returns the old SP - metadata, without the `TeamId` in the paths. - - -### database schema changes - -[V15](https://github.com/wireapp/wire-server/blob/b97439756cfe0721164934db1f80658b60de1e5e/services/spar/schema/src/V15.hs#L29-L43) diff --git a/docs/src/understand/single-sign-on/index.md b/docs/src/understand/single-sign-on/index.md index 01317c99b5..915c8b8ee3 100644 --- a/docs/src/understand/single-sign-on/index.md +++ b/docs/src/understand/single-sign-on/index.md @@ -7,11 +7,12 @@ :glob: true :maxdepth: 1 -Single sign-on and user provisioning +Single sign-on and user provisioning: the user manual +Trouble shooting and FAQ Generic setup SSO integration with ADFS SSO integration with Azure SSO integration with Centrify SSO integration with Okta -* +Internals for the intensely curious ``` diff --git a/docs/src/understand/single-sign-on/trouble-shooting.md b/docs/src/understand/single-sign-on/trouble-shooting.md index cdc7e1204a..53b1e76fbe 100644 --- a/docs/src/understand/single-sign-on/trouble-shooting.md +++ b/docs/src/understand/single-sign-on/trouble-shooting.md @@ -1,5 +1,9 @@ (trouble-shooting-faq)= +```{contents} +:depth: 2 +``` + # Trouble shooting & FAQ ## Reporting a problem with user provisioning or SSO authentication @@ -7,75 +11,156 @@ In order for us to analyse and understand your problem, we need at least the following information up-front: - Have you followed the following instructions? - : - {ref}`FAQ ` (This document) + - {ref}`FAQ ` (This document) - [Howtos](https://docs.wire.com/how-to/single-sign-on/index.html) for supported vendors - [General documentation on the setup flow](https://support.wire.com/hc/en-us/articles/360001285718-Set-up-SSO-externally) -- Vendor information (octa, azure, centrica, other (which one)?) -- Team ID (looks like eg. `2e9a9c9c-6f83-11eb-a118-3342c6f16f4e`, can be found in team settings) +- Which vendor (or product) are you using (octa, azure, centrica, other (which one)?) +- Team ID (looks like eg. `2e9a9c9c-6f83-11eb-a118-3342c6f16f4e`, can be found in the team management app) +- User ID of the account that has the problem (alternatively: handle, email address) - What do you expect to happen? - : - eg.: "I enter login code, authenticate successfully against IdP, get redirected, and see the wire landing page." + - e.g.: "I enter login code, authenticate successfully against IdP, get redirected, and see the wire landing page." - What does happen instead? - : - Screenshots + - Screenshots - Copy the text into your report where applicable in addition to screenshots (for automatic processing). - - eg.: "instead of being logged into wire, I see the following error page: ..." + - e.g.: "instead of being logged into wire, I see the following error page: ..." - Screenshots of the Configuration (both SAML and SCIM, as applicable), including, but not limited to: - : - If you are using SAML: SAML IdP metadata file + - If you are using SAML: SAML IdP metadata file - If you are using SCIM for provisioning: Which attributes in the User schema are mapped? How? +- If you have successfully authenticated on your IdP and are + redirected into wire, and then see a white page with an error + message that contains a lot of machine-readable info: copy the full + message to the clipboard and insert it into your report. We do not + log this information for privacy reasons, but we can use it to + investigate your problem. (Hint, if you want to investigate + yourself: it's base64 encoded!) + +### notes for wire support / development + +Not officially supported IdP vendors may work out of the box, as we +are requiring a minimum amount of SAML features. + +If there are problems: collect the metadata xml and an authentication +response xml (either from the browser http logs via a more technically +savvy customer; or from the "white page with an error" mentioned +above. + +https://github.com/wireapp/saml2-web-sso supports writing [unit vendor +compatibility +tests](https://github.com/wireapp/saml2-web-sso/blob/ff9b9f445475809d1fa31ef7f2932caa0ed31613/test/Test/SAML2/WebSSO/APISpec.hs#L266-L329) +against that response value. Once that test passes, it should all +work fine. + + +## Can I use SCIM without SAML? + +Yes. Scim is a technology for onboarding alternative to the team management app, and can produce both user accounts authenticated via SAML or via email and password. (Phone may or may not work, but is not officially supported.) + +How does it work? Make sure your team has no SAML IdPs registered. Set up your SCIM peer to provision users with valid email addresses as `externalIds`. Newly provisioned users will be created in status `PendingInvitation`, and an invitation email will be sent. From here on out, the flow is exactly the same as if you had added the user to your team in the team management app. + +Upcoming features: +- support for the `emails` field in the scim user record (so you can choose non-email `externalId` values). +- flexible mapping between any number of SAML IdPs and any number of SCIM tokens in team management. + +## Can I use SAML without SCIM? + +Yes, but this is not recommended. User (de-)provisioning requires more manual work without SCIM, and some of the account information cannot be provisioned at all via SAML. + ## Can I use the same SSO login code for multiple teams? -No, but there is a good reason for it and a work-around. +Most SAML IdP products allow you to register arbitrary many apps for +arbitrary many teams, by using a different entity Id for each app/team. This is +currently supported out of the box. -Reason: we *could* implement this, but that would require that we -disable implicit user creation for those teams. Implicit user -creation means that a person who has never logged onto wire before can -use her credentials for the IdP to get access to wire, and create a -new user based on those credentials. In order for this to work, the -IdP must uniquely determine the team. +If you don't have this option, i.e. you need to serve two teams with an +IdP that has only one entity ID, please keep reading and/or contact +customer support. -Work-around: on your IdP dashboard, you can set up a separate app for -every wire team you own. Each IdP will get a different metadata file, -and can be registered with its target team only. This way, users from -different teams have different SSO logins, but the IdP operators can -still use the same user base for all teams. This has the extra -advantage that a user can be part of two teams with the same -credentials, which would be impossible even with the hypothetical fix. +### The long answer -## Can an existing user without IdP (or with a different IdP) be bound to a new IdP? +Some SAML IdP vendors do not allow to set up fresh entity IDs (issuers) +for fresh apps; instead, all apps controlled by the IdP are receiving +SAML credentials from the same issuer. -No. This is a feature we never fully implemented. Details / latest -updates: +In the past, wire has used a tuple of IdP issuer and 'NameID' +(Haskell type 'UserRef') to uniquely identity users (tables +`spar.user_v2` and `spar.issuer_idp`). -## Can the SSO feature be disabled for a team? +In order to allow one IdP to serve more than one team, this has been +changed: we now allow to identify an IdP by a combination of +entityID/issuer and wire `TeamId`. The necessary tweaks to the +protocol are listed here. + +**This extension is currently (as of 2023-02-03) not supported by the +team management app. If you need this, please contact customer +support.** + +#### what you need to know when operating a team or an instance -No, this is [not implemented](https://github.com/wireapp/wire-server/blob/7a97cb5a944ae593c729341b6f28dfa1dabc28e5/services/galley/src/Galley/API/Error.hs#L215). +No instance-level configuration is required. -## Can you remove a SAML connection? +If your IdP supports different entityID / issuer for different apps, +you don't need to change anything. -It is not possible to delete a SAML connection in the Team Settings app, however it can be overwritten with a new connection. -It is possible do delete a SAML connection directly via the API endpoint `DELETE /identity-providers/{id}`. However deleting a SAML connection also requires deleting all users that can log in with this SAML connection. To prevent accidental deletion of users this functionality is not available directly from Team Settings. +If your IdP does not support different entityID / issuer for different +apps, keep reading. At the time of writing this section, there is no +support for multi-team IdP issuers in the team management app, so you have two +options: (1) use the rest API directly; or (2) contact our customer +support and send them the link to this section. -## If you get an error when returning from your IdP +If you feel up to calling the rest API, try the following: -`Symptoms:` +- Use the above end-point `GET /sso/metadata/:tid` with your `TeamId` + for pulling the SP metadata. +- When calling `POST /identity-provider`, make sure to add + `?api_version=v2`. (`?api_version=v1` or no omission of the query + param both invoke the old behavior.) -You have successfully authenticated on your IdP and are -redirected into wire. Wire shows a white page with an error message -that contains a lot of machine-readable info. +NB: Neither version of the API allows you to provision a user with the +same Issuer and same NameID. The pair of Issuer and NameID must +always be globally unique. -`What we need from you:` +#### API changes in even more detail -- Your SSO metadata file -- The SSO login code (eg. `wire-3f61d2ce-525c-11ea-b8da-cf641a7b716a`; - you can find it in the team settings where you registered your IdP) -- The full browser page with the error message (copy it into your - clipboard and insert it into an email to us, or save the page as an - html file and send that to us) +- New query param `api_version=` for `POST + /identity-providers`. The version is stored in `spar.idp` together + with the rest of the IdP setup, and is used by `GET + /sso/initiate-login` (see below). +- `GET /sso/initiate-login` sends audience based on api_version stored + in `spar.idp`: for v1, the audience is `/sso/finalize-login`; for + v2, it's `/sso/finalize-login/:tid`. +- New end-point `POST /sso/finalize-login/:tid` that behaves + indistinguishable from `POST /sso/finalize-login`, except when more + than one IdP with the same issuer, but different teams are + registered. In that case, this end-point can process the + credentials by discriminating on the `TeamId`. +- `POST /sso/finalize-login/` remains unchanged. +- New end-point `GET /sso/metadata/:tid` returns the same SP metadata as + `GET /sso/metadata`, with the exception that it lists + `"/sso/finalize-login/:tid"` as the path of the + `AssertionConsumerService` (rather than `"/sso/finalize-login"` as + before). +- `GET /sso/metadata` remains unchanged, and still returns the old SP + metadata, without the `TeamId` in the paths. -With all this information, please get in touch with our customer -support. +#### database schema changes -## Do I need any firewall settings? +[V15](https://github.com/wireapp/wire-server/blob/b97439756cfe0721164934db1f80658b60de1e5e/services/spar/schema/src/V15.hs#L29-L43) + + +## Can an existing user without IdP (or with a different IdP) be bound to a new IdP? + +Yes, you can, by updating the user via SCIM. (If you use SAML without +SCIM, there is a way in theory, but there are no plans to implement +it.) + + +## Can the SSO feature be disabled for a team? + +No, this is [not implemented](https://github.com/wireapp/wire-server/blob/7a97cb5a944ae593c729341b6f28dfa1dabc28e5/services/galley/src/Galley/API/Error.hs#L215). But the team admin can remove all IdPs, which will effectively disable all SAML logins. + + +## Do I need to change any firewall settings in order to use SAML? No. @@ -83,6 +168,7 @@ There is nothing to be done here. There is no internet traffic between your SAML IdP and the wire service. All communication happens via the browser or app. + ## Why does the team owner have to keep using password? The user who creates the team cannot be authenticated via SSO. There @@ -92,7 +178,7 @@ that's the team owner with their password. (It is also unwise to bind that owner to SAML once it's installed. If there is ever any issue with SAML authentication that can only be -resolved by updating the IdP metadata in team settings, the owner must +resolved by updating the IdP metadata in the team management app, the owner must still have a way to authenticate in order to do that.) There is a good workaround, though: you can create a team with user A @@ -158,7 +244,7 @@ minimal example that still works, we'd be love to take a look. ## Why does the auth response not contain a reference to an auth request? (Also: can i use IdP-initiated login?) -tl;dr: Wire only supports SP-initiated login, where the user selects +**tl;dr:** Wire only supports SP-initiated login, where the user selects the auth method from inside the app's login screen. It does not support IdP-initiated login, where the user enters the app from a list of applications in the IdP UI. @@ -225,7 +311,7 @@ in your wire team: `unspecified`. 2. If email/password authentication is used, SCIM's `externalId` is mapped on wire's email address, and provisioning works like in - team settings with invitation emails. + the team management app with invitation emails. This means that if you use email/password authentication, you **must** map an email address to `externalId` on your side. With `userName` @@ -241,8 +327,8 @@ contact customer support if this causes any issues. Users may find it awkward to copy and paste the login code into the form. If they are using the webapp, an alternative is to give them -the following URL (fill in the login code that you can find in your -team settings): +the following URL (fill in the login code that you can find in the +team management app): ```bash https://wire-webapp-dev.zinfra.io/auth#sso/3c4f050a-f073-11eb-b4c9-931bceeed13e @@ -281,23 +367,3 @@ clash. Do not rely on case sensitivity of `IssuerID` or `NameID`, or on `NameID` qualifiers for distinguishing user identifiers. - -## How to report problems - -If you have a problem you cannot resolve by yourself, please get in touch. Add as much of the following details to your report as possible: - -- Are you on cloud or on-prem? (If on-prem: which instance?) -- XML IdP metadata -- SSL Login code or IdP Issuer EntityID -- NameID of the account that has the problem -- SP metadata - -Problem description, including, but not limited to: - -- what happened? -- what did you want to happen? -- what does your idp config in the wire team management app look like? -- what does your wire config in your IdP management app look like? -- Please include screenshots *and* copied text (for cut&paste when we investigate) *and* further description and comments where feasible. - -(If you can't produce some of this information of course please get in touch anyway! It'll merely be harder for us to resolve your issue quickly, and we may need to make a few extra rounds of data gathering together with you.) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs index 26bd5205d0..e92fb9b14b 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs @@ -51,8 +51,13 @@ type API = :<|> "scim" :> APIScim :<|> OmitDocs :> "i" :> APIINTERNAL +type DeprecateSSOAPIV1 = + Description + "DEPRECATED! use /sso/metadata/:tid instead! \ + \Details: https://docs.wire.com/understand/single-sign-on/trouble-shooting.html#can-i-use-the-same-sso-login-code-for-multiple-teams" + type APISSO = - "metadata" :> SAML.APIMeta + DeprecateSSOAPIV1 :> "metadata" :> SAML.APIMeta :<|> "metadata" :> Capture "team" TeamId :> SAML.APIMeta :<|> "initiate-login" :> APIAuthReqPrecheck :<|> "initiate-login" :> APIAuthReq @@ -76,7 +81,8 @@ type APIAuthReq = :> Get '[SAML.HTML] (SAML.FormRedirect SAML.AuthnRequest) type APIAuthRespLegacy = - "finalize-login" + DeprecateSSOAPIV1 + :> "finalize-login" -- (SAML.APIAuthResp from here on, except for response) :> MultipartForm Mem SAML.AuthnResponseBody :> Post '[PlainText] Void @@ -106,7 +112,7 @@ type IdpGetAll = Get '[JSON] IdPList type IdpCreate = ReqBodyCustomError '[RawXML, JSON] "wai-error" IdPMetadataInfo :> QueryParam' '[Optional, Strict] "replaces" SAML.IdPId - :> QueryParam' '[Optional, Strict] "api_version" WireIdPAPIVersion + :> QueryParam' '[Optional, Strict] "api_version" WireIdPAPIVersion -- see also: 'DeprecateSSOAPIV1' -- FUTUREWORK: The handle is restricted to 32 characters. Can we find a more reasonable upper bound and create a type for it? Also see `IdpUpdate`. :> QueryParam' '[Optional, Strict] "handle" (Range 1 32 Text) :> PostCreated '[JSON] IdP diff --git a/services/brig/docs/swagger.md b/services/brig/docs/swagger.md index 5c92ab8aac..28f214eae9 100644 --- a/services/brig/docs/swagger.md +++ b/services/brig/docs/swagger.md @@ -1,10 +1,8 @@ ## General -**NOTE**: only a few endpoints are visible here at the moment, more will come as we migrate them to Swagger 2.0. In the meantime please also look at the old swagger docs link for the not-yet-migrated endpoints. See https://docs.wire.com/understand/api-client-perspective/swagger.html for the old endpoints. +### SSO Endpoints -## SSO Endpoints - -### Overview +#### Overview `/sso/metadata` will be requested by the IdPs to learn how to talk to wire. @@ -13,11 +11,11 @@ `/identity-providers` end-points are for use in the team settings page when IdPs are registered. They talk json. -### Configuring IdPs +#### Configuring IdPs IdPs usually allow you to copy the metadata into your clipboard. That should contain all the details you need to post the idp in your team under `/identity-providers`. (Team id is derived from the authorization credentials of the request.) -#### okta.com +##### okta.com Okta will ask you to provide two URLs when you set it up for talking to wireapp: @@ -25,7 +23,7 @@ Okta will ask you to provide two URLs when you set it up for talking to wireapp: 2. The `Audience URI`. You can find this in the metadata returned by the `/sso/metadata` end-point. It is the contents of the `md:OrganizationURL` element. -#### centrify.com +##### centrify.com Centrify allows you to upload the metadata xml document that you get from the `/sso/metadata` end-point. You can also enter the metadata url and have centrify retrieve the xml, but to guarantee integrity of the setup, the metadata should be copied from the team settings page and pasted into the centrify setup page without any URL indirections. From ed226cdd07d8045d9beee1fd897cc3d672163b71 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Fri, 3 Feb 2023 15:28:40 +0100 Subject: [PATCH 33/56] Allow single scopes and lists of scopes in routes. --- libs/wire-api/src/Wire/API/OAuth.hs | 19 +++++++++++++++++-- libs/wire-api/src/Wire/API/Routes/Public.hs | 6 +++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/libs/wire-api/src/Wire/API/OAuth.hs b/libs/wire-api/src/Wire/API/OAuth.hs index a6d2c3f344..9fb3e00268 100644 --- a/libs/wire-api/src/Wire/API/OAuth.hs +++ b/libs/wire-api/src/Wire/API/OAuth.hs @@ -181,6 +181,21 @@ instance IsOAuthScope 'ConversationCodeCreate where instance IsOAuthScope 'SelfRead where toOAuthScope = SelfRead +-- | Given a type-level collection of scopes X, this class gives you a function that tests if +-- a list of scopes from a token intersects with X, ie., if a token grants access to the route +-- with scopes X. Instances for single scopes and (nested) lists are available. +class IsOAuthScopes scopes where + allowOAuthScopeList :: Set.Set OAuthScope -> Bool + +instance IsOAuthScope (scope :: OAuthScope) => IsOAuthScopes scope where + allowOAuthScopeList = ((toOAuthScope @scope) `Set.member`) + +instance IsOAuthScopes scope => IsOAuthScopes '[scope] where + allowOAuthScopeList = allowOAuthScopeList @scope + +instance (IsOAuthScopes scope, IsOAuthScopes scopes) => IsOAuthScopes (scope ': scopes) where + allowOAuthScopeList scopes = (allowOAuthScopeList @scope scopes) || (allowOAuthScopeList @scopes scopes) + instance ToByteString OAuthScope where builder = \case ConversationCreate -> "conversation:create" @@ -426,8 +441,8 @@ hcsSub = >=> preview string >=> either (const Nothing) pure . parseIdFromText -hasScope :: forall scope. IsOAuthScope scope => OAuthClaimsSet -> Bool -hasScope claims = (toOAuthScope @scope) `Set.member` unOAuthScopes (scope claims) +hasScope :: forall scopes. IsOAuthScopes scopes => OAuthClaimsSet -> Bool +hasScope = allowOAuthScopeList @scopes . unOAuthScopes . scope -- | Verify a JWT and return the claims set. Use this function if you have a custom claims set. verify :: JWK -> SignedJWT -> IO (Either JWTError OAuthClaimsSet) diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index 796af9f9c8..91c8da3a96 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -238,7 +238,7 @@ instance HasContextEntry ctx (Maybe JWK), opts ~ InternalAuthDefOpts, -- oauth is never optional. HasServer api ctx, - IsOAuthScope (scope :: OAuthScope), + IsOAuthScopes (scope :: OAuthScope), ZParam ztype ~ Id a ) => HasServer (ZAuthServant ztype opts ('Just scope) :> api) ctx @@ -253,7 +253,7 @@ instance HasContextEntry ctx (Maybe JWK), opts ~ InternalAuthDefOpts, HasServer api ctx, - IsOAuthScope scope + IsOAuthScopes scope ) => Proxy (ZAuthServant ztype opts ('Just scope) :> api) -> Context ctx -> @@ -273,7 +273,7 @@ checkType' :: HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, HasContextEntry ctx (Maybe JWK), opts ~ InternalAuthDefOpts, - IsOAuthScope scope, + IsOAuthScopes scope, ZParam ztype ~ Id a ) => Context ctx -> From a7e4cbf9c72a91fdad553380ebe2253deca75f5a Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Fri, 3 Feb 2023 16:32:01 +0100 Subject: [PATCH 34/56] instance HasSwagger (ZAuthServant ...) --- libs/wire-api/src/Wire/API/OAuth.hs | 18 ++--- libs/wire-api/src/Wire/API/Routes/Public.hs | 73 +++++++++++-------- .../src/Wire/API/Routes/Public/Brig.hs | 2 +- 3 files changed, 53 insertions(+), 40 deletions(-) diff --git a/libs/wire-api/src/Wire/API/OAuth.hs b/libs/wire-api/src/Wire/API/OAuth.hs index 9fb3e00268..3abbbe19ee 100644 --- a/libs/wire-api/src/Wire/API/OAuth.hs +++ b/libs/wire-api/src/Wire/API/OAuth.hs @@ -181,20 +181,20 @@ instance IsOAuthScope 'ConversationCodeCreate where instance IsOAuthScope 'SelfRead where toOAuthScope = SelfRead --- | Given a type-level collection of scopes X, this class gives you a function that tests if +-- | Given a type-level list of scopes X, this class gives you a function that tests if -- a list of scopes from a token intersects with X, ie., if a token grants access to the route --- with scopes X. Instances for single scopes and (nested) lists are available. +-- with scopes X. class IsOAuthScopes scopes where + showOAuthScopeList :: Text allowOAuthScopeList :: Set.Set OAuthScope -> Bool -instance IsOAuthScope (scope :: OAuthScope) => IsOAuthScopes scope where - allowOAuthScopeList = ((toOAuthScope @scope) `Set.member`) +instance IsOAuthScopes '[] where + showOAuthScopeList = mempty + allowOAuthScopeList _ = False -instance IsOAuthScopes scope => IsOAuthScopes '[scope] where - allowOAuthScopeList = allowOAuthScopeList @scope - -instance (IsOAuthScopes scope, IsOAuthScopes scopes) => IsOAuthScopes (scope ': scopes) where - allowOAuthScopeList scopes = (allowOAuthScopeList @scope scopes) || (allowOAuthScopeList @scopes scopes) +instance (IsOAuthScope scope, IsOAuthScopes scopes) => IsOAuthScopes (scope ': scopes) where + showOAuthScopeList = T.unwords [cs $ show (toOAuthScope @scope), showOAuthScopeList @scopes] + allowOAuthScopeList scopes = ((toOAuthScope @scope) `Set.member` scopes) || (allowOAuthScopeList @scopes scopes) instance ToByteString OAuthScope where builder = \case diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index f3427b41bb..dcdc9d6371 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -170,7 +170,7 @@ instance IsZType 'ZAuthProvider ctx where instance HasTokenType 'ZAuthProvider where tokenType = Just "provider" -data ZAuthServant (ztype :: ZType) (opts :: [Type]) (scope :: Maybe OAuthScope) +data ZAuthServant (ztype :: ZType) (opts :: [Type]) (scopes :: Maybe [OAuthScope]) type InternalAuthDefOpts = '[Servant.Required, Servant.Strict] @@ -200,12 +200,32 @@ type ZOptClient = ZAuthServant 'ZAuthClient '[Servant.Optional, Servant.Strict] type ZOptConn = ZAuthServant 'ZAuthConn '[Servant.Optional, Servant.Strict] 'Nothing -type ZOAuthLocalUser scope = ZAuthServant 'ZLocalAuthUser InternalAuthDefOpts ('Just scope) +type ZOAuthLocalUser (scopes :: [OAuthScope]) = ZAuthServant 'ZLocalAuthUser InternalAuthDefOpts ('Just scopes) -type ZOauthUser scope = ZAuthServant 'ZAuthUser InternalAuthDefOpts ('Just scope) +type ZOauthUser (scopes :: [OAuthScope]) = ZAuthServant 'ZAuthUser InternalAuthDefOpts ('Just scopes) --- TODO(leif): doc for scope (also other instances) -instance HasSwagger api => HasSwagger (ZAuthServant 'ZAuthUser _opts scope :> api) where +instance + (HasSwagger api, IsOAuthScopes scopes, scopes ~ (s ': ss)) => + HasSwagger (ZAuthServant (ztype :: ZType) _opts ('Just scopes) :> api) + where + toSwagger _ = + toSwagger (Proxy @(ZAuthServant ztype _opts ('Nothing :: Maybe [OAuthScope]) :> api)) + & securityDefinitions <>~ SecurityDefinitions (InsOrdHashMap.singleton "OAuth" secScheme) + where + secScheme = + SecurityScheme + { _securitySchemeType = SecuritySchemeApiKey (ApiKeyParams "Authorization" ApiKeyHeader), + _securitySchemeDescription = + Just $ + "Must be a token retrieved with an oauth handshake. It must be presented in this \ + \format: 'Bearer \\'.\ + \\ + \Allowed oauth scopes: " + <> (showOAuthScopeList @scopes) + <> "\nfurther reading: https://docs.wire.com/how-to/install/oauth.html" + } + +instance HasSwagger api => HasSwagger (ZAuthServant (ztype :: ZType) _opts 'Nothing :> api) where toSwagger _ = toSwagger (Proxy @api) & securityDefinitions <>~ SecurityDefinitions (InsOrdHashMap.singleton "ZAuth" secScheme) @@ -214,37 +234,30 @@ instance HasSwagger api => HasSwagger (ZAuthServant 'ZAuthUser _opts scope :> ap secScheme = SecurityScheme { _securitySchemeType = SecuritySchemeApiKey (ApiKeyParams "Authorization" ApiKeyHeader), - _securitySchemeDescription = Just "Must be a token retrieved by calling 'POST /login' or 'POST /access'. It must be presented in this format: 'Bearer \\'." + _securitySchemeDescription = + Just + "Must be a token retrieved by calling 'POST /login' or 'POST /access'. It must be \ + \presented in this format: 'Bearer \\'." } -instance HasSwagger api => HasSwagger (ZAuthServant 'ZLocalAuthUser opts scope :> api) where - toSwagger _ = toSwagger (Proxy @(ZAuthServant 'ZAuthUser opts scope :> api)) - -instance HasLink endpoint => HasLink (ZAuthServant usr opts scope :> endpoint) where +instance HasLink endpoint => HasLink (ZAuthServant usr opts scopes :> endpoint) where type MkLink (ZAuthServant _ _ _ :> endpoint) a = MkLink endpoint a toLink toA _ = toLink toA (Proxy @endpoint) -instance - {-# OVERLAPPABLE #-} - HasSwagger api => - HasSwagger (ZAuthServant ztype _opts scope :> api) - where - toSwagger _ = toSwagger (Proxy @api) - --- | Handle routes that support both ZAuth and OAuth, tried in that order (scope is Just). +-- | Handle routes that support both ZAuth and OAuth, tried in that order (scopes is Just). instance ( IsZType ztype ctx, HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, HasContextEntry ctx (Maybe JWK), opts ~ InternalAuthDefOpts, -- oauth is never optional. HasServer api ctx, - IsOAuthScopes (scope :: OAuthScope), + IsOAuthScopes (scopes :: [OAuthScope]), ZParam ztype ~ Id a ) => - HasServer (ZAuthServant ztype opts ('Just scope) :> api) ctx + HasServer (ZAuthServant ztype opts ('Just scopes) :> api) ctx where type - ServerT (ZAuthServant ztype opts ('Just scope) :> api) m = + ServerT (ZAuthServant ztype opts ('Just scopes) :> api) m = ZQualifiedParam ztype -> ServerT api m route :: @@ -253,27 +266,27 @@ instance HasContextEntry ctx (Maybe JWK), opts ~ InternalAuthDefOpts, HasServer api ctx, - IsOAuthScopes scope + IsOAuthScopes scopes ) => - Proxy (ZAuthServant ztype opts ('Just scope) :> api) -> + Proxy (ZAuthServant ztype opts ('Just scopes) :> api) -> Context ctx -> - Delayed env (Server (ZAuthServant ztype opts ('Just scope) :> api)) -> + Delayed env (Server (ZAuthServant ztype opts ('Just scopes) :> api)) -> Router env route _ ctx subserver = Servant.route (Proxy @api) ctx - (addAuthCheck subserver (withRequest (fmap (qualifyZParam @ztype ctx) . checkType' @ztype @scope @ctx ctx (tokenType @ztype)))) + (addAuthCheck subserver (withRequest (fmap (qualifyZParam @ztype ctx) . checkType' @ztype @scopes @ctx ctx (tokenType @ztype)))) hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s checkType' :: - forall ztype scope ctx opts a. + forall ztype scopes ctx opts a. ( IsZType ztype ctx, HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, HasContextEntry ctx (Maybe JWK), opts ~ InternalAuthDefOpts, - IsOAuthScopes scope, + IsOAuthScopes scopes, ZParam ztype ~ Id a ) => Context ctx -> @@ -319,11 +332,11 @@ checkType' ctx mTokenType req = verifiedOrError <- mapLeft (invalidOAuthToken . cs . show) <$> liftIO (verify key (unOAuthToken . unBearer $ token)) pure $ verifiedOrError >>= \claimSet -> - if hasScope @scope claimSet + if hasScope @scopes claimSet then maybeToRight (invalidOAuthToken "Invalid token: Missing or invalid sub claim") (hcsSub claimSet) else Left insufficientScope --- | Handle routes that support ZAuth, but not OAuth (scope is Nothing). +-- | Handle routes that support ZAuth, but not OAuth (scopes is Nothing). instance ( IsZType ztype ctx, HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, @@ -372,7 +385,7 @@ error403 = errHeaders = [] } -instance RoutesToPaths api => RoutesToPaths (ZAuthServant ztype opts scope :> api) where +instance RoutesToPaths api => RoutesToPaths (ZAuthServant ztype opts scopes :> api) where getRoutes = getRoutes @api -- FUTUREWORK: Make a PR to the servant-swagger package with this instance diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index 4c1d56f2a6..fa13ec1edd 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -262,7 +262,7 @@ type SelfAPI = Named "get-self" ( Summary "Get your own profile" - :> ZOauthUser 'SelfRead + :> ZOauthUser '[ 'SelfRead] :> "self" :> Get '[JSON] SelfProfile ) From 70d1f477c086d32a3aa6285eb9b163fefc71ff8b Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Fri, 3 Feb 2023 16:49:24 +0100 Subject: [PATCH 35/56] Fixup --- libs/wire-api/src/Wire/API/Routes/Public.hs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index dcdc9d6371..942e888b24 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -54,6 +54,7 @@ import Data.Qualified import Data.SOP import Data.String.Conversions (cs) import Data.Swagger +import Data.Typeable (typeRep) import GHC.Base (Symbol) import GHC.TypeLits (KnownSymbol, symbolVal) import Imports hiding (All, exp, head) @@ -205,7 +206,7 @@ type ZOAuthLocalUser (scopes :: [OAuthScope]) = ZAuthServant 'ZLocalAuthUser Int type ZOauthUser (scopes :: [OAuthScope]) = ZAuthServant 'ZAuthUser InternalAuthDefOpts ('Just scopes) instance - (HasSwagger api, IsOAuthScopes scopes, scopes ~ (s ': ss)) => + (HasSwagger api, IsOAuthScopes scopes, scopes ~ (s ': ss), Typeable ztype) => HasSwagger (ZAuthServant (ztype :: ZType) _opts ('Just scopes) :> api) where toSwagger _ = @@ -225,7 +226,7 @@ instance <> "\nfurther reading: https://docs.wire.com/how-to/install/oauth.html" } -instance HasSwagger api => HasSwagger (ZAuthServant (ztype :: ZType) _opts 'Nothing :> api) where +instance (HasSwagger api, Typeable ztype) => HasSwagger (ZAuthServant (ztype :: ZType) _opts 'Nothing :> api) where toSwagger _ = toSwagger (Proxy @api) & securityDefinitions <>~ SecurityDefinitions (InsOrdHashMap.singleton "ZAuth" secScheme) @@ -235,9 +236,12 @@ instance HasSwagger api => HasSwagger (ZAuthServant (ztype :: ZType) _opts 'Noth SecurityScheme { _securitySchemeType = SecuritySchemeApiKey (ApiKeyParams "Authorization" ApiKeyHeader), _securitySchemeDescription = - Just + Just $ "Must be a token retrieved by calling 'POST /login' or 'POST /access'. It must be \ - \presented in this format: 'Bearer \\'." + \presented in this format: 'Bearer \\'.\ + \\ + \Expected token type: " + <> (cs . show . typeRep $ (Proxy @ztype)) } instance HasLink endpoint => HasLink (ZAuthServant usr opts scopes :> endpoint) where From d93bb7daa5d022937f0e781596655604276314a1 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Mon, 6 Feb 2023 08:57:49 +0100 Subject: [PATCH 36/56] Tweak HasSwagger docs. --- libs/wire-api/src/Wire/API/Routes/Public.hs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index 942e888b24..f3769945c3 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -220,10 +220,9 @@ instance Just $ "Must be a token retrieved with an oauth handshake. It must be presented in this \ \format: 'Bearer \\'.\ - \\ - \Allowed oauth scopes: " + \\nAllowed oauth scopes: " <> (showOAuthScopeList @scopes) - <> "\nfurther reading: https://docs.wire.com/how-to/install/oauth.html" + <> "\nFurther reading: https://docs.wire.com/how-to/install/oauth.html" } instance (HasSwagger api, Typeable ztype) => HasSwagger (ZAuthServant (ztype :: ZType) _opts 'Nothing :> api) where @@ -239,9 +238,9 @@ instance (HasSwagger api, Typeable ztype) => HasSwagger (ZAuthServant (ztype :: Just $ "Must be a token retrieved by calling 'POST /login' or 'POST /access'. It must be \ \presented in this format: 'Bearer \\'.\ - \\ - \Expected token type: " + \\nExpected token type: " <> (cs . show . typeRep $ (Proxy @ztype)) + <> "\nFurther reading: https://github.com/wireapp/wire-server/blob/develop/libs/wire-api/src/Wire/API/Routes/Public.hs (search for HasSwagger instances)" } instance HasLink endpoint => HasLink (ZAuthServant usr opts scopes :> endpoint) where From 3407a2f241a5a1e676fb1e318837a15cbdc05f6b Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Mon, 6 Feb 2023 09:29:12 +0100 Subject: [PATCH 37/56] support lenient, optional zoauth combinators. --- libs/wire-api/src/Wire/API/Routes/Public.hs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index f3769945c3..77bf4d5812 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -252,7 +252,8 @@ instance ( IsZType ztype ctx, HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, HasContextEntry ctx (Maybe JWK), - opts ~ InternalAuthDefOpts, -- oauth is never optional. + SBoolI (FoldLenient opts), + SBoolI (FoldRequired opts), HasServer api ctx, IsOAuthScopes (scopes :: [OAuthScope]), ZParam ztype ~ Id a @@ -267,7 +268,8 @@ instance ( IsZType ztype ctx, HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, HasContextEntry ctx (Maybe JWK), - opts ~ InternalAuthDefOpts, + SBoolI (FoldLenient opts), + SBoolI (FoldRequired opts), HasServer api ctx, IsOAuthScopes scopes ) => @@ -279,7 +281,7 @@ instance Servant.route (Proxy @api) ctx - (addAuthCheck subserver (withRequest (fmap (qualifyZParam @ztype ctx) . checkType' @ztype @scopes @ctx ctx (tokenType @ztype)))) + (addAuthCheck subserver (withRequest (fmap (qualifyZParam @ztype ctx) . checkType' @ztype @scopes @ctx @opts ctx (tokenType @ztype)))) hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s @@ -288,7 +290,8 @@ checkType' :: ( IsZType ztype ctx, HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, HasContextEntry ctx (Maybe JWK), - opts ~ InternalAuthDefOpts, + SBoolI (FoldLenient opts), + SBoolI (FoldRequired opts), IsOAuthScopes scopes, ZParam ztype ~ Id a ) => From fea0a27154c9c3249262b91617d75ef08697cd6a Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Mon, 6 Feb 2023 11:27:59 +0100 Subject: [PATCH 38/56] Fixup --- libs/wire-api/src/Wire/API/Routes/Public.hs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index 77bf4d5812..e6e1b6e631 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -281,17 +281,14 @@ instance Servant.route (Proxy @api) ctx - (addAuthCheck subserver (withRequest (fmap (qualifyZParam @ztype ctx) . checkType' @ztype @scopes @ctx @opts ctx (tokenType @ztype)))) + (addAuthCheck subserver (withRequest (fmap (qualifyZParam @ztype ctx) . checkType' @ztype @scopes @ctx ctx (tokenType @ztype)))) hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s checkType' :: - forall ztype scopes ctx opts a. + forall ztype scopes ctx a. ( IsZType ztype ctx, - HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, HasContextEntry ctx (Maybe JWK), - SBoolI (FoldLenient opts), - SBoolI (FoldRequired opts), IsOAuthScopes scopes, ZParam ztype ~ Id a ) => From 8ad2b3f77621aa924ca432b6d0eb9bde11cb2092 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Mon, 6 Feb 2023 11:20:12 +0000 Subject: [PATCH 39/56] make oauth work with swagger --- libs/wire-api/src/Wire/API/Routes/Public.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index 77bf4d5812..26fcc1ebab 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -212,6 +212,7 @@ instance toSwagger _ = toSwagger (Proxy @(ZAuthServant ztype _opts ('Nothing :: Maybe [OAuthScope]) :> api)) & securityDefinitions <>~ SecurityDefinitions (InsOrdHashMap.singleton "OAuth" secScheme) + & security <>~ [SecurityRequirement $ InsOrdHashMap.singleton "OAuth" []] where secScheme = SecurityScheme From 15015d019b8fd5573a779502358d255a3c8c0a41 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Mon, 6 Feb 2023 12:25:27 +0000 Subject: [PATCH 40/56] show scope(s) for each endpoint in swagger --- libs/wire-api/src/Wire/API/OAuth.hs | 4 ++-- libs/wire-api/src/Wire/API/Routes/Public.hs | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/libs/wire-api/src/Wire/API/OAuth.hs b/libs/wire-api/src/Wire/API/OAuth.hs index 3abbbe19ee..22aa2a093d 100644 --- a/libs/wire-api/src/Wire/API/OAuth.hs +++ b/libs/wire-api/src/Wire/API/OAuth.hs @@ -193,8 +193,8 @@ instance IsOAuthScopes '[] where allowOAuthScopeList _ = False instance (IsOAuthScope scope, IsOAuthScopes scopes) => IsOAuthScopes (scope ': scopes) where - showOAuthScopeList = T.unwords [cs $ show (toOAuthScope @scope), showOAuthScopeList @scopes] - allowOAuthScopeList scopes = ((toOAuthScope @scope) `Set.member` scopes) || (allowOAuthScopeList @scopes scopes) + showOAuthScopeList = T.unwords [cs $ toByteString (toOAuthScope @scope), showOAuthScopeList @scopes] + allowOAuthScopeList scopes = ((toOAuthScope @scope) `Set.member` scopes) || allowOAuthScopeList @scopes scopes instance ToByteString OAuthScope where builder = \case diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index 26fcc1ebab..82b136ad03 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -40,7 +40,7 @@ module Wire.API.Routes.Public ) where -import Control.Lens ((<>~)) +import Control.Lens hiding (Context) import Control.Monad.Except import Crypto.JWT hiding (Context, params, uri, verify) import Data.ByteString.Conversion (fromByteString) @@ -213,6 +213,7 @@ instance toSwagger (Proxy @(ZAuthServant ztype _opts ('Nothing :: Maybe [OAuthScope]) :> api)) & securityDefinitions <>~ SecurityDefinitions (InsOrdHashMap.singleton "OAuth" secScheme) & security <>~ [SecurityRequirement $ InsOrdHashMap.singleton "OAuth" []] + & addScopeDescription @scopes where secScheme = SecurityScheme @@ -221,11 +222,12 @@ instance Just $ "Must be a token retrieved with an oauth handshake. It must be presented in this \ \format: 'Bearer \\'.\ - \\nAllowed oauth scopes: " - <> (showOAuthScopeList @scopes) - <> "\nFurther reading: https://docs.wire.com/how-to/install/oauth.html" + \Further reading: https://docs.wire.com/how-to/install/oauth.html" } + addScopeDescription :: forall scopes. (IsOAuthScopes scopes) => Swagger -> Swagger + addScopeDescription = allOperations . description %~ Just . (<> "oauth scope: " <> showOAuthScopeList @scopes) . fold + instance (HasSwagger api, Typeable ztype) => HasSwagger (ZAuthServant (ztype :: ZType) _opts 'Nothing :> api) where toSwagger _ = toSwagger (Proxy @api) From 8192d7e8ae95efe5ea35d446bf7b3e71ffd28cbf Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Mon, 6 Feb 2023 12:27:17 +0000 Subject: [PATCH 41/56] fix --- libs/wire-api/src/Wire/API/Routes/Public.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index 1865054ebd..18a8d6cd9d 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -225,7 +225,7 @@ instance \Further reading: https://docs.wire.com/how-to/install/oauth.html" } - addScopeDescription :: forall scopes. (IsOAuthScopes scopes) => Swagger -> Swagger + addScopeDescription :: Swagger -> Swagger addScopeDescription = allOperations . description %~ Just . (<> "oauth scope: " <> showOAuthScopeList @scopes) . fold instance (HasSwagger api, Typeable ztype) => HasSwagger (ZAuthServant (ztype :: ZType) _opts 'Nothing :> api) where From 00032559dcc7525f77abbfc5af39b63611e6e5e5 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Mon, 6 Feb 2023 12:28:37 +0000 Subject: [PATCH 42/56] fix 2 --- libs/wire-api/src/Wire/API/Routes/Public.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index 18a8d6cd9d..c7c6b61b24 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -213,7 +213,7 @@ instance toSwagger (Proxy @(ZAuthServant ztype _opts ('Nothing :: Maybe [OAuthScope]) :> api)) & securityDefinitions <>~ SecurityDefinitions (InsOrdHashMap.singleton "OAuth" secScheme) & security <>~ [SecurityRequirement $ InsOrdHashMap.singleton "OAuth" []] - & addScopeDescription @scopes + & addScopeDescription where secScheme = SecurityScheme From a9f4d3eb17e5938a62a39f6e1ccc79b1f1d6cd19 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Mon, 6 Feb 2023 14:17:06 +0000 Subject: [PATCH 43/56] grammar --- libs/wire-api/src/Wire/API/Routes/Public.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index c7c6b61b24..08411bd492 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -226,7 +226,7 @@ instance } addScopeDescription :: Swagger -> Swagger - addScopeDescription = allOperations . description %~ Just . (<> "oauth scope: " <> showOAuthScopeList @scopes) . fold + addScopeDescription = allOperations . description %~ Just . (<> "OAuth scope(s): " <> showOAuthScopeList @scopes) . fold instance (HasSwagger api, Typeable ztype) => HasSwagger (ZAuthServant (ztype :: ZType) _opts 'Nothing :> api) where toSwagger _ = From 389fa39070d99236fee8a3aece6023cbb7012a83 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 7 Feb 2023 09:27:38 +0000 Subject: [PATCH 44/56] make JWK available in galley --- libs/wire-api/src/Wire/API/Routes/API.hs | 41 +++++++++++++++++-- .../API/Routes/Public/Galley/Conversation.hs | 5 ++- services/brig/src/Brig/Run.hs | 14 ++----- services/galley/galley.cabal | 2 + services/galley/galley.integration.yaml | 1 + .../src/Galley/API/Public/Conversation.hs | 7 +++- services/galley/src/Galley/Effects/Jwk.hs | 23 +++++++++++ services/galley/src/Galley/Options.hs | 4 +- services/galley/src/Galley/Run.hs | 14 ++++--- .../resources/oauth/ed25519_public_jwk.json | 1 + 10 files changed, 88 insertions(+), 24 deletions(-) create mode 100644 services/galley/src/Galley/Effects/Jwk.hs create mode 100644 services/galley/test/resources/oauth/ed25519_public_jwk.json diff --git a/libs/wire-api/src/Wire/API/Routes/API.hs b/libs/wire-api/src/Wire/API/Routes/API.hs index 607933e2ed..6c6ddd4934 100644 --- a/libs/wire-api/src/Wire/API/Routes/API.hs +++ b/libs/wire-api/src/Wire/API/Routes/API.hs @@ -18,17 +18,21 @@ module Wire.API.Routes.API ( API, hoistAPIHandler, + hoistAPIHandlerWithContext, hoistAPI, mkAPI, mkNamedAPI, + mkNamedAPIWithContext, (<@>), ServerEffect (..), ServerEffects (..), hoistServerWithDomain, + hoistServerWithContext', ) where import Data.Domain +import Data.Kind (Type) import Data.Proxy import Imports import Polysemy @@ -47,7 +51,14 @@ mkAPI :: (HasServer api '[Domain], ServerEffects (DeclaredErrorEffects api) r0) => ServerT api (Sem (Append (DeclaredErrorEffects api) r0)) -> API api r0 -mkAPI h = API $ hoistServerWithDomain @api (interpretServerEffects @(DeclaredErrorEffects api) @r0) h +mkAPI = mkAPIWithContext @'[Domain] + +mkAPIWithContext :: + forall (context :: [Type]) r0 api. + (HasServer api context, ServerEffects (DeclaredErrorEffects api) r0) => + ServerT api (Sem (Append (DeclaredErrorEffects api) r0)) -> + API api r0 +mkAPIWithContext h = API $ hoistServerWithContext' @context @api (interpretServerEffects @(DeclaredErrorEffects api) @r0) h -- | Convert a polysemy handler to a named 'API' value. mkNamedAPI :: @@ -55,7 +66,14 @@ mkNamedAPI :: (HasServer api '[Domain], ServerEffects (DeclaredErrorEffects api) r0) => ServerT api (Sem (Append (DeclaredErrorEffects api) r0)) -> API (Named name api) r0 -mkNamedAPI = API . Named . unAPI . mkAPI @r0 @api +mkNamedAPI = mkNamedAPIWithContext @'[Domain] + +mkNamedAPIWithContext :: + forall (context :: [Type]) name r0 api. + (HasServer api context, ServerEffects (DeclaredErrorEffects api) r0) => + ServerT api (Sem (Append (DeclaredErrorEffects api) r0)) -> + API (Named name api) r0 +mkNamedAPIWithContext = API . Named . unAPI . mkAPIWithContext @context @r0 @api -- | Combine APIs. (<@>) :: API api1 r -> API api2 r -> API (api1 :<|> api2) r @@ -69,6 +87,15 @@ infixr 3 <@> -- but unfortunately the 'hoistServerWithContext' function is also part of the -- 'HasServer' typeclass, even though it cannot possibly make use of its @context@ -- type argument. +hoistServerWithContext' :: + forall (context :: [Type]) api m n. + HasServer api context => + (forall x. m x -> n x) -> + ServerT api m -> + ServerT api n +hoistServerWithContext' = hoistServerWithContext (Proxy @api) (Proxy @context) + +-- | Like `hoistServerWithContext'`, but with a 'Domain' context. hoistServerWithDomain :: forall api m n. HasServer api '[Domain] => @@ -83,7 +110,15 @@ hoistAPIHandler :: (forall x. Sem r x -> n x) -> API api r -> ServerT api n -hoistAPIHandler f = hoistServerWithDomain @api f . unAPI +hoistAPIHandler f = hoistServerWithContext' @'[Domain] @api f . unAPI + +hoistAPIHandlerWithContext :: + forall (context :: [Type]) api r n. + HasServer api context => + (forall x. Sem r x -> n x) -> + API api r -> + ServerT api n +hoistAPIHandlerWithContext f = hoistServerWithContext' @context @api f . unAPI hoistAPI :: forall api1 api2 r1 r2. diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index f623be75d8..218690ff62 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -33,6 +33,7 @@ import Wire.API.Event.Conversation import Wire.API.MLS.PublicGroupState import Wire.API.MLS.Servant import Wire.API.MakesFederatedCall +import Wire.API.OAuth import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.Routes.Public @@ -338,7 +339,7 @@ type ConversationAPI = :> CanThrow OperationDenied :> CanThrow 'MissingLegalholdConsent :> Description "This returns 201 when a new conversation is created, and 200 when the conversation already existed" - :> ZLocalUser + :> ZOAuthLocalUser '[ 'ConversationCreate] :> ZOptConn :> "conversations" :> VersionedReqBody 'V2 '[Servant.JSON] NewConv @@ -357,7 +358,7 @@ type ConversationAPI = :> CanThrow OperationDenied :> CanThrow 'MissingLegalholdConsent :> Description "This returns 201 when a new conversation is created, and 200 when the conversation already existed" - :> ZLocalUser + :> ZOAuthLocalUser '[ 'ConversationCreate] :> ZOptConn :> "conversations" :> ReqBody '[Servant.JSON] NewConv diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index b07b7e1d45..b3c811996c 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -52,7 +52,7 @@ import Control.Monad.Random (randomRIO) import Crypto.JWT import qualified Data.Aeson as Aeson import Data.Default (Default (def)) -import Data.Domain (Domain (..)) +import Data.Domain import Data.Id (RequestId (..)) import Data.Metrics.AWS (gaugeTokenRemaing) import qualified Data.Metrics.Servant as Metrics @@ -71,7 +71,7 @@ import Network.Wai.Utilities (lookupRequestId) import Network.Wai.Utilities.Server import qualified Network.Wai.Utilities.Server as Server import Polysemy (Members) -import Servant (Context ((:.)), HasServer (hoistServerWithContext), ServerT, (:<|>) (..)) +import Servant (Context ((:.)), (:<|>) (..)) import qualified Servant import System.Logger (msg, val, (.=), (~~)) import System.Logger.Class (MonadLogger, err) @@ -147,21 +147,13 @@ mkApp o = do (Proxy @ServantCombinedAPI) (mJwk :. customFormatters :. localDomain :. Servant.EmptyContext) ( docsAPI - :<|> hoistServerWithContext' @BrigAPI (toServantHandler e) servantSitemap + :<|> hoistServerWithContext' @'[Domain, Maybe JWK] @BrigAPI (toServantHandler e) servantSitemap :<|> hoistServerWithDomain @IAPI.API (toServantHandler e) IAPI.servantSitemap :<|> hoistServerWithDomain @FederationAPI (toServantHandler e) federationSitemap :<|> hoistServerWithDomain @VersionAPI (toServantHandler e) versionAPI :<|> Servant.Tagged (app e) ) -hoistServerWithContext' :: - forall api m n. - HasServer api '[Domain, Maybe JWK] => - (forall x. m x -> n x) -> - ServerT api m -> - ServerT api n -hoistServerWithContext' = hoistServerWithContext (Proxy @api) (Proxy @'[Domain, Maybe JWK]) - type ServantCombinedAPI = ( DocsAPI :<|> BrigAPI diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 04be5d47a9..eeb163765b 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -99,6 +99,7 @@ library Galley.Effects.FederatorAccess Galley.Effects.FireAndForget Galley.Effects.GundeckAccess + Galley.Effects.Jwk Galley.Effects.LegalHoldStore Galley.Effects.ListItems Galley.Effects.MemberStore @@ -227,6 +228,7 @@ library , http-types >=0.8 , imports , insert-ordered-containers + , jose , kan-extensions , lens >=4.4 , memory diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index 6a90f1b58e..2ae13e344b 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -46,6 +46,7 @@ settings: mlsPrivateKeyPaths: removal: ed25519: test/resources/ed25519.pem + OAuthPublicJwk: test/resources/oauth/ed25519_public_jwk.json featureFlags: # see #RefConfigOptions in `/docs/reference` sso: disabled-by-default diff --git a/services/galley/src/Galley/API/Public/Conversation.hs b/services/galley/src/Galley/API/Public/Conversation.hs index 800c9f4654..9db9f38c0c 100644 --- a/services/galley/src/Galley/API/Public/Conversation.hs +++ b/services/galley/src/Galley/API/Public/Conversation.hs @@ -17,6 +17,9 @@ module Galley.API.Public.Conversation where +import Crypto.JOSE.JWK +import Data.Domain (Domain) +import Data.Maybe import Galley.API.Create import Galley.API.MLS.GroupInfo import Galley.API.MLS.Types @@ -44,8 +47,8 @@ conversationAPI = <@> mkNamedAPI @"list-conversations@v2" (callsFed listConversations) <@> mkNamedAPI @"list-conversations" (callsFed listConversations) <@> mkNamedAPI @"get-conversation-by-reusable-code" (getConversationByReusableCode @Cassandra) - <@> mkNamedAPI @"create-group-conversation@v2" (callsFed createGroupConversation) - <@> mkNamedAPI @"create-group-conversation" (callsFed createGroupConversation) + <@> mkNamedAPIWithContext @'[Domain, Maybe JWK] @"create-group-conversation@v2" (callsFed createGroupConversation) + <@> mkNamedAPIWithContext @'[Domain, Maybe JWK] @"create-group-conversation" (callsFed createGroupConversation) <@> mkNamedAPI @"create-self-conversation@v2" createProteusSelfConversation <@> mkNamedAPI @"create-self-conversation" createProteusSelfConversation <@> mkNamedAPI @"get-mls-self-conversation" getMLSSelfConversationWithError diff --git a/services/galley/src/Galley/Effects/Jwk.hs b/services/galley/src/Galley/Effects/Jwk.hs new file mode 100644 index 0000000000..220d15affe --- /dev/null +++ b/services/galley/src/Galley/Effects/Jwk.hs @@ -0,0 +1,23 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Galley.Effects.Jwk where + +import Control.Exception +import Crypto.JOSE.JWK +import Data.Aeson +import qualified Data.ByteString as BS +import Data.String.Conversions (cs) +import Imports +import Polysemy + +-- todo(leif): extract to a shared place and reuse in Brig +data Jwk m a where + Get :: FilePath -> Jwk m (Maybe JWK) + +makeSem ''Jwk + +interpretJwk :: Members '[Embed IO] r => Sem (Jwk ': r) a -> Sem r a +interpretJwk = interpret $ \(Get fp) -> liftIO $ readJwk fp + +readJwk :: FilePath -> IO (Maybe JWK) +readJwk fp = try @IOException (BS.readFile fp) <&> either (const Nothing) (decode . cs) diff --git a/services/galley/src/Galley/Options.hs b/services/galley/src/Galley/Options.hs index 844ca39064..72ffb8cc04 100644 --- a/services/galley/src/Galley/Options.hs +++ b/services/galley/src/Galley/Options.hs @@ -36,6 +36,7 @@ module Galley.Options defConcurrentDeletionEvents, defDeleteConvThrottleMillis, defFanoutLimit, + setOAuthPublicJwk, JournalOpts (JournalOpts), awsQueueName, awsEndpoint, @@ -116,7 +117,8 @@ data Settings = Settings _setMlsPrivateKeyPaths :: !(Maybe MLSPrivateKeyPaths), -- | FUTUREWORK: 'setFeatureFlags' should be renamed to 'setFeatureConfigs' in all types. _setFeatureFlags :: !FeatureFlags, - _setDisabledAPIVersions :: Maybe (Set Version) + _setDisabledAPIVersions :: Maybe (Set Version), + _setOAuthPublicJwk :: !(Maybe FilePath) } deriving (Show, Generic) diff --git a/services/galley/src/Galley/Run.hs b/services/galley/src/Galley/Run.hs index b528a6c054..e02bb2a18f 100644 --- a/services/galley/src/Galley/Run.hs +++ b/services/galley/src/Galley/Run.hs @@ -31,8 +31,10 @@ import qualified Control.Concurrent.Async as Async import Control.Exception (finally) import Control.Lens (view, (.~), (^.)) import Control.Monad.Codensity +import Crypto.JOSE.JWK import qualified Data.Aeson as Aeson import Data.Default +import Data.Domain (Domain) import Data.Id import Data.Metrics (Metrics) import Data.Metrics.AWS (gaugeTokenRemaing) @@ -48,6 +50,7 @@ import Galley.App import qualified Galley.App as App import Galley.Aws (awsEnv) import Galley.Cassandra +import Galley.Effects.Jwk (readJwk) import Galley.Monad import Galley.Options import qualified Galley.Queue as Q @@ -89,7 +92,7 @@ mkApp opts = metrics <- lift $ M.metrics env <- lift $ App.createEnv metrics opts lift $ runClient (env ^. cstate) $ versionCheck schemaVersion - + mJwk <- lift $ join <$> forM (opts ^. optSettings . setOAuthPublicJwk) readJwk let logger = env ^. App.applog let middlewares = @@ -102,20 +105,21 @@ mkApp opts = Log.info logger $ Log.msg @Text "Galley application finished." Log.flush logger Log.close logger - pure (middlewares $ servantApp env, env) + pure (middlewares $ servantApp env mJwk, env) where rtree = compile API.sitemap runGalley e r k = evalGalleyToIO e (route rtree r k) -- the servant API wraps the one defined using wai-routing - servantApp e0 r = + servantApp e0 mJwk r = let e = reqId .~ lookupReqId r $ e0 in Servant.serveWithContext (Proxy @CombinedAPI) - ( view (options . optSettings . setFederationDomain) e + ( mJwk + :. view (options . optSettings . setFederationDomain) e :. customFormatters :. Servant.EmptyContext ) - ( hoistAPIHandler (toServantHandler e) API.servantSitemap + ( hoistAPIHandlerWithContext @'[Domain, Maybe JWK] @GalleyAPI.ServantAPI (toServantHandler e) API.servantSitemap :<|> hoistAPIHandler (toServantHandler e) internalAPI :<|> hoistServerWithDomain @FederationAPI (toServantHandler e) federationSitemap :<|> Servant.Tagged (runGalley e) diff --git a/services/galley/test/resources/oauth/ed25519_public_jwk.json b/services/galley/test/resources/oauth/ed25519_public_jwk.json new file mode 100644 index 0000000000..9ef33c1ce2 --- /dev/null +++ b/services/galley/test/resources/oauth/ed25519_public_jwk.json @@ -0,0 +1 @@ +{"kty":"OKP","crv":"Ed25519","x":"mhP-NgFw3ifIXGZqJVB0kemt9L3BtD5P8q4Gah4Iklc"} From fa47d2119866ced3a84c04cb68b4e13db017df00 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 7 Feb 2023 11:06:26 +0000 Subject: [PATCH 45/56] oauth access to conversation create works --- services/brig/test/integration/API/OAuth.hs | 53 +++++++++++++++++-- services/brig/test/integration/Main.hs | 2 +- services/galley/galley.integration.yaml | 2 +- .../integration-test/conf/nginz/nginx.conf | 2 +- 4 files changed, 52 insertions(+), 7 deletions(-) diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index 20e1b57b0c..0cab54bc60 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -18,6 +18,7 @@ module API.OAuth where +import qualified API.Team.Util as Team import Bilge import Bilge.Assert import Brig.API.OAuth hiding (verifyRefreshToken) @@ -34,7 +35,7 @@ import qualified Data.ByteString.Char8 as BS import Data.ByteString.Conversion (fromByteString, toByteString') import Data.Domain (domainText) import Data.Id -import Data.Range (unsafeRange) +import Data.Range import Data.Set as Set hiding (delete, null, (\\)) import Data.String.Conversions (cs) import Data.Text.Ascii (encodeBase16) @@ -52,13 +53,16 @@ import Text.RawString.QQ import URI.ByteString import Util import Web.FormUrlEncoded +import qualified Wire.API.Conversation as Conv +import Wire.API.Conversation.Protocol (ProtocolTag (ProtocolProteusTag)) +import qualified Wire.API.Conversation.Role as Role import Wire.API.OAuth import Wire.API.Routes.Bearer (Bearer (Bearer, unBearer)) import Wire.API.User (SelfProfile, User (userId), userEmail) import Wire.API.User.Auth (CookieType (PersistentCookie)) -tests :: Manager -> C.ClientState -> Brig -> Nginz -> Opts -> TestTree -tests m db b n o = do +tests :: Manager -> C.ClientState -> Brig -> Galley -> Nginz -> Opts -> TestTree +tests m db b g n o = do testGroup "oauth" [ test m "register new oauth client" $ testRegisterNewOAuthClient b, @@ -94,7 +98,9 @@ tests m db b n o = do test m "expired token" $ testAccessResourceExpiredToken o b, test m "nonsense token" $ testAccessResourceNonsenseToken b, test m "no token" $ testAccessResourceNoToken b, - test m "invalid signature" $ testAccessResourceInvalidSignature o b + test m "invalid signature" $ testAccessResourceInvalidSignature o b, + test m "create conversation (internal)" $ testCreateConversationSuccessInternal b g, + test m "create conversation (nginz)" $ testCreateConversationSuccessNginz b n ], testGroup "refresh tokens" $ [ test m "max active tokens" $ testRefreshTokenMaxActiveTokens o db b, @@ -643,9 +649,48 @@ testRevokeApplicationAccountAccess brig = do liftIO $ assertEqual "apps" 0 (length apps) _ -> liftIO $ assertFailure "unexpected number of apps" +testCreateConversationSuccessInternal :: Brig -> Galley -> Http () +testCreateConversationSuccessInternal brig galley = do + (uid, tid) <- Team.createUserWithTeam brig + let redirectUrl = mkUrl "https://example.com" + let scopes = OAuthScopes $ Set.fromList [ConversationCreate] + (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + accessToken <- createOAuthAccessToken brig accessTokenRequest + createTeamConv galley zOAuthHeader (oatAccessToken accessToken) tid "oauth test group" !!! do + const 201 === statusCode + +testCreateConversationSuccessNginz :: Brig -> Nginz -> Http () +testCreateConversationSuccessNginz brig nginz = do + (uid, tid) <- Team.createUserWithTeam brig + let redirectUrl = mkUrl "https://example.com" + let scopes = OAuthScopes $ Set.fromList [ConversationCreate] + (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + accessToken <- createOAuthAccessToken brig accessTokenRequest + createTeamConv nginz authHeader (oatAccessToken accessToken) tid "oauth test group" !!! do + const 201 === statusCode + ------------------------------------------------------------------------------- -- Util +createTeamConv :: + (Request -> Request) -> + (OAuthAccessToken -> Request -> Request) -> + OAuthAccessToken -> + TeamId -> + Text -> + Http ResponseLBS +createTeamConv svc mkHeader token tid name = do + let tinfo = Conv.ConvTeamInfo tid + let conv = Conv.NewConv [] [] (checked name) mempty Nothing (Just tinfo) Nothing Nothing Role.roleNameWireAdmin ProtocolProteusTag Nothing + post + ( svc + . path "conversations" + . mkHeader token + . json conv + ) + createOAuthApplicationWithAccountAccess :: Brig -> UserId -> Http OAuthAccessTokenResponse createOAuthApplicationWithAccountAccess brig uid = do let redirectUrl = mkUrl "https://example.com" diff --git a/services/brig/test/integration/Main.hs b/services/brig/test/integration/Main.hs index 91b2f15e0b..cb51eefd06 100644 --- a/services/brig/test/integration/Main.hs +++ b/services/brig/test/integration/Main.hs @@ -160,7 +160,7 @@ runTests iConf brigOpts otherArgs = do let smtp = SMTP.tests mg lg versionApi = API.Version.tests mg brigOpts b mlsApi = MLS.tests mg b brigOpts - oauthAPI = API.OAuth.tests mg db b n brigOpts + oauthAPI = API.OAuth.tests mg db b g n brigOpts withArgs otherArgs . defaultMain $ testGroup diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index 2ae13e344b..faa199757b 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -46,7 +46,7 @@ settings: mlsPrivateKeyPaths: removal: ed25519: test/resources/ed25519.pem - OAuthPublicJwk: test/resources/oauth/ed25519_public_jwk.json + oAuthPublicJwk: test/resources/oauth/ed25519_public_jwk.json featureFlags: # see #RefConfigOptions in `/docs/reference` sso: disabled-by-default diff --git a/services/nginz/integration-test/conf/nginz/nginx.conf b/services/nginz/integration-test/conf/nginz/nginx.conf index 4f7e4728b8..c4801bfaf7 100644 --- a/services/nginz/integration-test/conf/nginz/nginx.conf +++ b/services/nginz/integration-test/conf/nginz/nginx.conf @@ -346,7 +346,7 @@ http { } location ~* ^(/v[0-9]+)?/conversations.* { - include common_response_with_zauth.conf; + include common_response_with_zauth_oauth.conf; proxy_pass http://galley; } From 26e75c515efe625e01a14d8e57ec8cae1828599f Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 7 Feb 2023 12:38:51 +0000 Subject: [PATCH 46/56] jwk effect shared --- .../polysemy-wire-zoo/polysemy-wire-zoo.cabal | 7 +++++- .../polysemy-wire-zoo/src/Wire/Sem}/Jwk.hs | 2 +- services/brig/brig.cabal | 1 - services/brig/src/Brig/API/OAuth.hs | 4 ++-- services/brig/src/Brig/API/Public.hs | 2 +- .../brig/src/Brig/CanonicalInterpreter.hs | 2 +- services/brig/src/Brig/Run.hs | 2 +- services/galley/galley.cabal | 1 - services/galley/src/Galley/Effects/Jwk.hs | 23 ------------------- services/galley/src/Galley/Run.hs | 2 +- 10 files changed, 13 insertions(+), 33 deletions(-) rename {services/brig/src/Brig/Effects => libs/polysemy-wire-zoo/src/Wire/Sem}/Jwk.hs (94%) delete mode 100644 services/galley/src/Galley/Effects/Jwk.hs diff --git a/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal b/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal index 56bec80f2f..50e297d544 100644 --- a/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal +++ b/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal @@ -18,6 +18,7 @@ library Wire.Sem.Concurrency.IO Wire.Sem.Concurrency.Sequential Wire.Sem.FromUTC + Wire.Sem.Jwk Wire.Sem.Logger Wire.Sem.Logger.Level Wire.Sem.Logger.TinyLog @@ -77,16 +78,20 @@ library -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path build-depends: - base >=4.6 && <5.0 + aeson + , base >=4.6 && <5.0 + , bytestring , cassandra-util , HsOpenSSL , hspec , imports + , jose , polysemy , polysemy-check , polysemy-plugin , QuickCheck , saml2-web-sso + , string-conversions , time , tinylog , types-common diff --git a/services/brig/src/Brig/Effects/Jwk.hs b/libs/polysemy-wire-zoo/src/Wire/Sem/Jwk.hs similarity index 94% rename from services/brig/src/Brig/Effects/Jwk.hs rename to libs/polysemy-wire-zoo/src/Wire/Sem/Jwk.hs index fc6581c60f..183e427f42 100644 --- a/services/brig/src/Brig/Effects/Jwk.hs +++ b/libs/polysemy-wire-zoo/src/Wire/Sem/Jwk.hs @@ -1,6 +1,6 @@ {-# LANGUAGE TemplateHaskell #-} -module Brig.Effects.Jwk where +module Wire.Sem.Jwk where import Control.Exception import Crypto.JOSE.JWK diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index f2e3890c5d..ef33a2cb47 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -67,7 +67,6 @@ library Brig.Effects.Delay Brig.Effects.GalleyProvider Brig.Effects.GalleyProvider.RPC - Brig.Effects.Jwk Brig.Effects.JwtTools Brig.Effects.PasswordResetStore Brig.Effects.PasswordResetStore.CodeStore diff --git a/services/brig/src/Brig/API/OAuth.hs b/services/brig/src/Brig/API/OAuth.hs index f29749e7bc..0aee2410e4 100644 --- a/services/brig/src/Brig/API/OAuth.hs +++ b/services/brig/src/Brig/API/OAuth.hs @@ -20,8 +20,6 @@ module Brig.API.OAuth where import Brig.API.Error (throwStd) import Brig.API.Handler (Handler) import Brig.App -import Brig.Effects.Jwk -import qualified Brig.Effects.Jwk as Jwk import qualified Brig.Options as Opt import Brig.Password (Password, mkSafePassword, verifyPassword) import Cassandra hiding (Set) @@ -47,6 +45,8 @@ import Wire.API.OAuth as OAuth import qualified Wire.API.Routes.Internal.Brig.OAuth as I import Wire.API.Routes.Named (Named (..)) import Wire.API.Routes.Public.Brig.OAuth +import Wire.Sem.Jwk +import qualified Wire.Sem.Jwk as Jwk import Wire.Sem.Now (Now) import qualified Wire.Sem.Now as Now diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 1787e7c5a5..06ccdd8c8d 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -52,7 +52,6 @@ import Brig.Effects.BlacklistStore (BlacklistStore) import Brig.Effects.CodeStore (CodeStore) import Brig.Effects.GalleyProvider (GalleyProvider) import qualified Brig.Effects.GalleyProvider as GalleyProvider -import Brig.Effects.Jwk (Jwk) import Brig.Effects.JwtTools (JwtTools) import Brig.Effects.PasswordResetStore (PasswordResetStore) import Brig.Effects.PublicKeyBundle (PublicKeyBundle) @@ -145,6 +144,7 @@ import qualified Wire.API.User.RichInfo as Public import qualified Wire.API.UserMap as Public import qualified Wire.API.Wrapped as Public import Wire.Sem.Concurrency +import Wire.Sem.Jwk (Jwk) import Wire.Sem.Now (Now) -- User API ----------------------------------------------------------- diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index 3cea580063..d380b6d229 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -9,7 +9,6 @@ import Brig.Effects.CodeStore (CodeStore) import Brig.Effects.CodeStore.Cassandra (codeStoreToCassandra, interpretClientToIO) import Brig.Effects.GalleyProvider (GalleyProvider) import Brig.Effects.GalleyProvider.RPC (interpretGalleyProviderToRPC) -import Brig.Effects.Jwk import Brig.Effects.JwtTools import Brig.Effects.PasswordResetStore (PasswordResetStore) import Brig.Effects.PasswordResetStore.CodeStore (passwordResetStoreToCodeStore) @@ -30,6 +29,7 @@ import Polysemy.Error (Error, mapError, runError) import Polysemy.TinyLog (TinyLog) import Wire.Sem.Concurrency import Wire.Sem.Concurrency.IO +import Wire.Sem.Jwk import Wire.Sem.Logger.TinyLog (loggerToTinyLog) import Wire.Sem.Now (Now) import Wire.Sem.Now.IO (nowToIOAction) diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index b3c811996c..c455160833 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -36,7 +36,6 @@ import qualified Brig.AWS.SesNotification as SesNotification import Brig.App import qualified Brig.Calling as Calling import Brig.CanonicalInterpreter -import Brig.Effects.Jwk (readJwk) import Brig.Effects.UserPendingActivationStore (UserPendingActivation (UserPendingActivation), UserPendingActivationStore) import qualified Brig.Effects.UserPendingActivationStore as UsersPendingActivationStore import qualified Brig.InternalEvent.Process as Internal @@ -81,6 +80,7 @@ import Wire.API.Routes.API import Wire.API.Routes.Public.Brig import Wire.API.Routes.Version import Wire.API.Routes.Version.Wai +import Wire.Sem.Jwk (readJwk) import qualified Wire.Sem.Paging as P -- FUTUREWORK: If any of these async threads die, we will have no clue about it diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index eeb163765b..4ff0a6c702 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -99,7 +99,6 @@ library Galley.Effects.FederatorAccess Galley.Effects.FireAndForget Galley.Effects.GundeckAccess - Galley.Effects.Jwk Galley.Effects.LegalHoldStore Galley.Effects.ListItems Galley.Effects.MemberStore diff --git a/services/galley/src/Galley/Effects/Jwk.hs b/services/galley/src/Galley/Effects/Jwk.hs deleted file mode 100644 index 220d15affe..0000000000 --- a/services/galley/src/Galley/Effects/Jwk.hs +++ /dev/null @@ -1,23 +0,0 @@ -{-# LANGUAGE TemplateHaskell #-} - -module Galley.Effects.Jwk where - -import Control.Exception -import Crypto.JOSE.JWK -import Data.Aeson -import qualified Data.ByteString as BS -import Data.String.Conversions (cs) -import Imports -import Polysemy - --- todo(leif): extract to a shared place and reuse in Brig -data Jwk m a where - Get :: FilePath -> Jwk m (Maybe JWK) - -makeSem ''Jwk - -interpretJwk :: Members '[Embed IO] r => Sem (Jwk ': r) a -> Sem r a -interpretJwk = interpret $ \(Get fp) -> liftIO $ readJwk fp - -readJwk :: FilePath -> IO (Maybe JWK) -readJwk fp = try @IOException (BS.readFile fp) <&> either (const Nothing) (decode . cs) diff --git a/services/galley/src/Galley/Run.hs b/services/galley/src/Galley/Run.hs index e02bb2a18f..b418e8e912 100644 --- a/services/galley/src/Galley/Run.hs +++ b/services/galley/src/Galley/Run.hs @@ -50,7 +50,6 @@ import Galley.App import qualified Galley.App as App import Galley.Aws (awsEnv) import Galley.Cassandra -import Galley.Effects.Jwk (readJwk) import Galley.Monad import Galley.Options import qualified Galley.Queue as Q @@ -67,6 +66,7 @@ import Util.Options import Wire.API.Routes.API import qualified Wire.API.Routes.Public.Galley as GalleyAPI import Wire.API.Routes.Version.Wai +import Wire.Sem.Jwk (readJwk) run :: Opts -> IO () run opts = lowerCodensity $ do From 54a7cb2387a0e7904217016a991f8bef32ff1526 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 7 Feb 2023 13:44:51 +0000 Subject: [PATCH 47/56] generated local nix packages --- libs/polysemy-wire-zoo/default.nix | 8 ++++++++ services/galley/default.nix | 2 ++ 2 files changed, 10 insertions(+) diff --git a/libs/polysemy-wire-zoo/default.nix b/libs/polysemy-wire-zoo/default.nix index ea3e21fdf0..8762faf572 100644 --- a/libs/polysemy-wire-zoo/default.nix +++ b/libs/polysemy-wire-zoo/default.nix @@ -3,7 +3,9 @@ # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. { mkDerivation +, aeson , base +, bytestring , cassandra-util , containers , gitignoreSource @@ -11,12 +13,14 @@ , hspec , hspec-discover , imports +, jose , lib , polysemy , polysemy-check , polysemy-plugin , QuickCheck , saml2-web-sso +, string-conversions , time , tinylog , types-common @@ -29,16 +33,20 @@ mkDerivation { version = "0.1.0"; src = gitignoreSource ./.; libraryHaskellDepends = [ + aeson base + bytestring cassandra-util HsOpenSSL hspec imports + jose polysemy polysemy-check polysemy-plugin QuickCheck saml2-web-sso + string-conversions time tinylog types-common diff --git a/services/galley/default.nix b/services/galley/default.nix index bc14fd14a4..185b1263f3 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -54,6 +54,7 @@ , http-types , imports , insert-ordered-containers +, jose , kan-extensions , lens , lens-aeson @@ -181,6 +182,7 @@ mkDerivation { http-types imports insert-ordered-containers + jose kan-extensions lens memory From 501a96fef76c26c28f4e3caa79bfef8b5249751e Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 7 Feb 2023 13:16:09 +0000 Subject: [PATCH 48/56] setup config for secret for staging --- charts/galley/templates/configmap.yaml | 3 +++ charts/galley/templates/deployment.yaml | 4 ++-- .../galley/templates/{mls-secret.yaml => secret.yaml} | 5 ++++- charts/galley/templates/tests/galley-integration.yaml | 2 +- docs/src/developer/reference/config-options.md | 10 ++++++++++ hack/helm_vars/wire-server/values.yaml.gotmpl | 6 ++++++ services/galley/galley.integration.yaml | 2 +- services/galley/src/Galley/Options.hs | 4 ++-- services/galley/src/Galley/Run.hs | 2 +- 9 files changed, 30 insertions(+), 8 deletions(-) rename charts/galley/templates/{mls-secret.yaml => secret.yaml} (73%) diff --git a/charts/galley/templates/configmap.yaml b/charts/galley/templates/configmap.yaml index 2e783e5bdf..124b9a5233 100644 --- a/charts/galley/templates/configmap.yaml +++ b/charts/galley/templates/configmap.yaml @@ -72,6 +72,9 @@ data: {{- if .settings.disabledAPIVersions }} disabledAPIVersions: {{ .settings.disabledAPIVersions }} {{- end }} + {{- if $.Values.secrets.oauthPublicJwk }} + oauthPublicJwk: /etc/wire/galley/secrets/public_jwk_oauth.json + {{- end }} {{- if .settings.featureFlags }} featureFlags: sso: {{ .settings.featureFlags.sso }} diff --git a/charts/galley/templates/deployment.yaml b/charts/galley/templates/deployment.yaml index c5f1b9ee25..c1594f9996 100644 --- a/charts/galley/templates/deployment.yaml +++ b/charts/galley/templates/deployment.yaml @@ -26,7 +26,7 @@ spec: # An annotation of the configmap checksum ensures changes to the configmap cause a redeployment upon `helm upgrade` checksum/configmap: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum }} checksum/aws-secret: {{ include (print .Template.BasePath "/aws-secret.yaml") . | sha256sum }} - checksum/mls-secret: {{ include (print .Template.BasePath "/mls-secret.yaml") . | sha256sum }} + checksum/secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }} spec: serviceAccountName: {{ .Values.serviceAccount.name }} volumes: @@ -35,7 +35,7 @@ spec: name: "galley" - name: "galley-secrets" secret: - secretName: "galley-mls" + secretName: "galley" containers: - name: galley image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" diff --git a/charts/galley/templates/mls-secret.yaml b/charts/galley/templates/secret.yaml similarity index 73% rename from charts/galley/templates/mls-secret.yaml rename to charts/galley/templates/secret.yaml index 1b77c325a9..92b1c59b49 100644 --- a/charts/galley/templates/mls-secret.yaml +++ b/charts/galley/templates/secret.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Secret metadata: - name: galley-mls + name: galley labels: app: galley chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" @@ -14,3 +14,6 @@ data: removal_ed25519.pem: {{ .Values.secrets.mlsPrivateKeys.removal.ed25519 | b64enc | quote }} {{- end -}} {{- end -}} + {{- if .Values.secrets.oauthPublicJwk }} + public_jwk_oauth.json: {{ .Values.secrets.oauthPublicJwk | b64enc | quote }} + {{- end -}} diff --git a/charts/galley/templates/tests/galley-integration.yaml b/charts/galley/templates/tests/galley-integration.yaml index c543eb2048..8c7df48e70 100644 --- a/charts/galley/templates/tests/galley-integration.yaml +++ b/charts/galley/templates/tests/galley-integration.yaml @@ -39,7 +39,7 @@ spec: name: "galley-integration-secrets" - name: "galley-secrets" secret: - secretName: "galley-mls" + secretName: "galley" containers: - name: integration image: "{{ .Values.image.repository }}-integration:{{ .Values.image.tag }}" diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index d0500bd600..90c00a6bc6 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -54,6 +54,16 @@ certificate, is to run the following command: openssl req -nodes -newkey ed25519 -keyout ed25519.pem -out /dev/null -subj / ``` +### Public JWK for OAuth + +Set the path to the public JWK key for OAuth like this: + +```yml +# [galley.yaml] +settings: + oauthPublicJwk: test/resources/oauth/ed25519_public_jwk.json +``` + ## Feature flags > Also see [Wire docs](https://docs.wire.com/how-to/install/team-feature-settings.html) where some of the feature flags are documented from an operations point of view. diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index f9521fb2b8..4e42674cc3 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -193,6 +193,12 @@ galley: -----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEIAocCDXsKIAjb65gOUn5vEF0RIKnVJkKR4ebQzuZ709c -----END PRIVATE KEY----- + oauthPublicJwk: | + { + "kty": "OKP", + "crv": "Ed25519", + "x": "mhP-NgFw3ifIXGZqJVB0kemt9L3BtD5P8q4Gah4Iklc" + } gundeck: replicaCount: 1 diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index faa199757b..3aa993b83b 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -46,7 +46,7 @@ settings: mlsPrivateKeyPaths: removal: ed25519: test/resources/ed25519.pem - oAuthPublicJwk: test/resources/oauth/ed25519_public_jwk.json + oauthPublicJwk: test/resources/oauth/ed25519_public_jwk.json featureFlags: # see #RefConfigOptions in `/docs/reference` sso: disabled-by-default diff --git a/services/galley/src/Galley/Options.hs b/services/galley/src/Galley/Options.hs index 72ffb8cc04..60511985eb 100644 --- a/services/galley/src/Galley/Options.hs +++ b/services/galley/src/Galley/Options.hs @@ -36,7 +36,7 @@ module Galley.Options defConcurrentDeletionEvents, defDeleteConvThrottleMillis, defFanoutLimit, - setOAuthPublicJwk, + setOauthPublicJwk, JournalOpts (JournalOpts), awsQueueName, awsEndpoint, @@ -118,7 +118,7 @@ data Settings = Settings -- | FUTUREWORK: 'setFeatureFlags' should be renamed to 'setFeatureConfigs' in all types. _setFeatureFlags :: !FeatureFlags, _setDisabledAPIVersions :: Maybe (Set Version), - _setOAuthPublicJwk :: !(Maybe FilePath) + _setOauthPublicJwk :: !(Maybe FilePath) } deriving (Show, Generic) diff --git a/services/galley/src/Galley/Run.hs b/services/galley/src/Galley/Run.hs index b418e8e912..92b883f0c9 100644 --- a/services/galley/src/Galley/Run.hs +++ b/services/galley/src/Galley/Run.hs @@ -92,7 +92,7 @@ mkApp opts = metrics <- lift $ M.metrics env <- lift $ App.createEnv metrics opts lift $ runClient (env ^. cstate) $ versionCheck schemaVersion - mJwk <- lift $ join <$> forM (opts ^. optSettings . setOAuthPublicJwk) readJwk + mJwk <- lift $ join <$> forM (opts ^. optSettings . setOauthPublicJwk) readJwk let logger = env ^. App.applog let middlewares = From 62da19949e9c689982dabffb0073a5ffccedc022 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 7 Feb 2023 14:22:01 +0000 Subject: [PATCH 49/56] fix tests --- services/brig/test/integration/API/OAuth.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index 0cab54bc60..1952c05225 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -22,7 +22,6 @@ import qualified API.Team.Util as Team import Bilge import Bilge.Assert import Brig.API.OAuth hiding (verifyRefreshToken) -import Brig.Effects.Jwk (readJwk) import Brig.Options import qualified Brig.Options as Opt import qualified Cassandra as C @@ -60,6 +59,7 @@ import Wire.API.OAuth import Wire.API.Routes.Bearer (Bearer (Bearer, unBearer)) import Wire.API.User (SelfProfile, User (userId), userEmail) import Wire.API.User.Auth (CookieType (PersistentCookie)) +import Wire.Sem.Jwk (readJwk) tests :: Manager -> C.ClientState -> Brig -> Galley -> Nginz -> Opts -> TestTree tests m db b g n o = do From 87e6dd8a7482156f52a5c56a241c95f8817acd95 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 7 Feb 2023 14:58:15 +0000 Subject: [PATCH 50/56] enable oauth for post conversations --- charts/nginz/values.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/charts/nginz/values.yaml b/charts/nginz/values.yaml index 35c51fdfe2..3f382d44f9 100644 --- a/charts/nginz/values.yaml +++ b/charts/nginz/values.yaml @@ -429,6 +429,7 @@ nginx_conf: envs: - all doc: true + enable_oauth: true - path: /legalhold/conversations/(.*) envs: - all From 9d5f5e3948a94ea714d66fb00d145361dcc20940 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 7 Feb 2023 14:59:37 +0000 Subject: [PATCH 51/56] renaming --- hack/bin/oauth_test.sh | 2 +- libs/wire-api/src/Wire/API/OAuth.hs | 30 +++++------ .../src/Wire/API/Routes/Public/Brig.hs | 2 +- services/brig/test/integration/API/OAuth.hs | 52 +++++++++---------- 4 files changed, 43 insertions(+), 43 deletions(-) diff --git a/hack/bin/oauth_test.sh b/hack/bin/oauth_test.sh index d2c891c245..fa1d395869 100755 --- a/hack/bin/oauth_test.sh +++ b/hack/bin/oauth_test.sh @@ -36,7 +36,7 @@ if [ -z "$USER" ]; then exit 1 fi -SCOPE="self:read" +SCOPE="read:self" CLIENT=$( curl -s -X POST localhost:8082/i/oauth/clients \ diff --git a/libs/wire-api/src/Wire/API/OAuth.hs b/libs/wire-api/src/Wire/API/OAuth.hs index 22aa2a093d..22e1ad603c 100644 --- a/libs/wire-api/src/Wire/API/OAuth.hs +++ b/libs/wire-api/src/Wire/API/OAuth.hs @@ -164,22 +164,22 @@ instance ToSchema OAuthResponseType where ] data OAuthScope - = ConversationCreate - | ConversationCodeCreate - | SelfRead + = WriteConversation + | WriteConversationCode + | ReadSelf deriving (Eq, Show, Generic, Ord) class IsOAuthScope scope where toOAuthScope :: OAuthScope -instance IsOAuthScope 'ConversationCreate where - toOAuthScope = ConversationCreate +instance IsOAuthScope 'WriteConversation where + toOAuthScope = WriteConversation -instance IsOAuthScope 'ConversationCodeCreate where - toOAuthScope = ConversationCodeCreate +instance IsOAuthScope 'WriteConversationCode where + toOAuthScope = WriteConversationCode -instance IsOAuthScope 'SelfRead where - toOAuthScope = SelfRead +instance IsOAuthScope 'ReadSelf where + toOAuthScope = ReadSelf -- | Given a type-level list of scopes X, this class gives you a function that tests if -- a list of scopes from a token intersects with X, ie., if a token grants access to the route @@ -198,17 +198,17 @@ instance (IsOAuthScope scope, IsOAuthScopes scopes) => IsOAuthScopes (scope ': s instance ToByteString OAuthScope where builder = \case - ConversationCreate -> "conversation:create" - ConversationCodeCreate -> "conversation-code:create" - SelfRead -> "self:read" + WriteConversation -> "write:conversation" + WriteConversationCode -> "conversation_code:create" + ReadSelf -> "read:self" instance FromByteString OAuthScope where parser = do s <- parser case s & T.toLower of - "conversation:create" -> pure ConversationCreate - "conversation-code:create" -> pure ConversationCodeCreate - "self:read" -> pure SelfRead + "write:conversation" -> pure WriteConversation + "write:conversation_code" -> pure WriteConversationCode + "read:self" -> pure ReadSelf _ -> fail "invalid scope" newtype OAuthScopes = OAuthScopes {unOAuthScopes :: Set OAuthScope} diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index fa13ec1edd..f445954e49 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -262,7 +262,7 @@ type SelfAPI = Named "get-self" ( Summary "Get your own profile" - :> ZOauthUser '[ 'SelfRead] + :> ZOauthUser '[ 'ReadSelf] :> "self" :> Get '[JSON] SelfProfile ) diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index 1952c05225..1ebe1acb33 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -135,7 +135,7 @@ testCreateOAuthCodeSuccess brig = do let newOAuthClient@(NewOAuthClient _ redirectUrl) = newOAuthClientRequestBody "E Corp" "https://example.com" cid <- occClientId <$> registerNewOAuthClient brig newOAuthClient uid <- randomId - let scope = OAuthScopes $ Set.fromList [ConversationCreate, ConversationCodeCreate] + let scope = OAuthScopes $ Set.fromList [WriteConversation, WriteConversationCode] state <- UUID.toText <$> liftIO nextRandom createOAuthCode brig uid (NewOAuthAuthCode cid scope OAuthResponseTypeCode redirectUrl state) !!! do const 302 === statusCode @@ -177,7 +177,7 @@ testCreateAccessTokenSuccess opts brig = do now <- liftIO getCurrentTime uid <- userId <$> createUser "alice" brig let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.singleton SelfRead + let scopes = OAuthScopes $ Set.singleton ReadSelf (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl accessToken <- createOAuthAccessToken brig accessTokenRequest @@ -206,7 +206,7 @@ testCreateAccessTokenWrongClientId :: Brig -> Http () testCreateAccessTokenWrongClientId brig = do uid <- randomId let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [ConversationCreate, ConversationCodeCreate] + let scopes = OAuthScopes $ Set.fromList [WriteConversation, WriteConversationCode] (_, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl cid <- randomId let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl @@ -218,7 +218,7 @@ testCreateAccessTokenWrongClientSecret :: Brig -> Http () testCreateAccessTokenWrongClientSecret brig = do uid <- randomId let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [ConversationCreate, ConversationCodeCreate] + let scopes = OAuthScopes $ Set.fromList [WriteConversation, WriteConversationCode] (cid, _, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl let secret = OAuthClientPlainTextSecret $ encodeBase16 "ee2316e304f5c318e4607d86748018eb9c66dc4f391c31bcccd9291d24b4c7e" let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl @@ -230,7 +230,7 @@ testCreateAccessTokenWrongAuthCode :: Brig -> Http () testCreateAccessTokenWrongAuthCode brig = do uid <- randomId let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [ConversationCreate, ConversationCodeCreate] + let scopes = OAuthScopes $ Set.fromList [WriteConversation, WriteConversationCode] (cid, secret, _) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl let code = OAuthAuthCode $ encodeBase16 "eb32eb9e2aa36c081c89067dddf81bce83c1c57e0b74cfb14c9f026f145f2b1f" let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl @@ -242,7 +242,7 @@ testCreateAccessTokenWrongUrl :: Brig -> Http () testCreateAccessTokenWrongUrl brig = do uid <- randomId let redirectUrl = mkUrl "https://wire.com" - let scopes = OAuthScopes $ Set.fromList [ConversationCreate, ConversationCodeCreate] + let scopes = OAuthScopes $ Set.fromList [WriteConversation, WriteConversationCode] (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl let wrongUrl = mkUrl "https://example.com" let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code wrongUrl @@ -255,7 +255,7 @@ testCreateAccessTokenExpiredCode opts brig = withSettingsOverrides (opts & Opt.optionSettings . Opt.oauthAuthCodeExpirationTimeSecsInternal ?~ 1) $ do uid <- randomId let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [ConversationCreate, ConversationCodeCreate] + let scopes = OAuthScopes $ Set.fromList [WriteConversation, WriteConversationCode] (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl liftIO $ threadDelay (1 * 1200 * 1000) let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl @@ -267,7 +267,7 @@ testCreateAccessTokenWrongGrantType :: Brig -> Http () testCreateAccessTokenWrongGrantType brig = do uid <- randomId let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [ConversationCreate, ConversationCodeCreate] + let scopes = OAuthScopes $ Set.fromList [WriteConversation, WriteConversationCode] (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeRefreshToken cid secret code redirectUrl createOAuthAccessToken' brig accessTokenRequest !!! assertAccessDenied @@ -307,7 +307,7 @@ testRefreshAccessTokenAccessDeniedWhenDisabled :: Opt.Opts -> Brig -> Http () testRefreshAccessTokenAccessDeniedWhenDisabled opts brig = do uid <- randomId let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [SelfRead] + let scopes = OAuthScopes $ Set.fromList [ReadSelf] (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl accessToken <- createOAuthAccessToken brig accessTokenRequest @@ -330,7 +330,7 @@ testAccessResourceSuccessInternal :: Brig -> Http () testAccessResourceSuccessInternal brig = do uid <- userId <$> createUser "alice" brig let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [SelfRead] + let scopes = OAuthScopes $ Set.fromList [ReadSelf] (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl accessToken <- createOAuthAccessToken brig accessTokenRequest @@ -350,7 +350,7 @@ testAccessResourceSuccessNginz brig nginz = do -- with Authorization header containing an OAuth bearer token let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [SelfRead] + let scopes = OAuthScopes $ Set.fromList [ReadSelf] (cid, secret, code) <- generateOAuthClientAndAuthCode brig (userId user) scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl oauthToken <- oatAccessToken <$> createOAuthAccessToken brig accessTokenRequest @@ -360,7 +360,7 @@ testAccessResourceInsufficientScope :: Brig -> Http () testAccessResourceInsufficientScope brig = do uid <- userId <$> createUser "alice" brig let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [ConversationCreate] + let scopes = OAuthScopes $ Set.fromList [WriteConversation] (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl accessToken <- createOAuthAccessToken brig accessTokenRequest @@ -374,7 +374,7 @@ testAccessResourceExpiredToken opts brig = withSettingsOverrides (opts & Opt.optionSettings . Opt.oauthAccessTokenExpirationTimeSecsInternal ?~ 1) $ do uid <- userId <$> createUser "alice" brig let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [SelfRead] + let scopes = OAuthScopes $ Set.fromList [ReadSelf] (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl accessToken <- createOAuthAccessToken brig accessTokenRequest @@ -401,7 +401,7 @@ testAccessResourceInvalidSignature :: Opt.Opts -> Brig -> Http () testAccessResourceInvalidSignature opts brig = do uid <- userId <$> createUser "alice" brig let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [SelfRead] + let scopes = OAuthScopes $ Set.fromList [ReadSelf] (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl accessToken <- createOAuthAccessToken brig accessTokenRequest @@ -419,7 +419,7 @@ testRefreshTokenMaxActiveTokens opts db brig = uid <- randomId jwk <- liftIO $ readJwk (fromMaybe "path to jwk not set" (Opt.setOAuthJwkKeyPair $ Opt.optSettings opts)) <&> fromMaybe (error "invalid key") let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [ConversationCreate, ConversationCodeCreate] + let scopes = OAuthScopes $ Set.fromList [WriteConversation, WriteConversationCode] let delayOneSec = -- we have to wait ~1 sec before we create the next token, to make sure it is created with a different timestamp -- this is due to the interpreter of the `Now` effect which auto-updates every second @@ -477,7 +477,7 @@ testRefreshTokenRetrieveAccessToken opts brig = withSettingsOverrides (opts & Opt.optionSettings . Opt.oauthAccessTokenExpirationTimeSecsInternal ?~ 2) $ do uid <- userId <$> createUser "alice" brig let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [SelfRead] + let scopes = OAuthScopes $ Set.fromList [ReadSelf] (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl accessToken <- createOAuthAccessToken brig accessTokenRequest @@ -492,7 +492,7 @@ testRefreshTokenWrongSignature :: Opts -> Brig -> Http () testRefreshTokenWrongSignature opts brig = do uid <- userId <$> createUser "alice" brig let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [SelfRead] + let scopes = OAuthScopes $ Set.fromList [ReadSelf] (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl accessToken <- createOAuthAccessToken brig accessTokenRequest @@ -509,7 +509,7 @@ testRefreshTokenNoTokenId :: Opts -> Brig -> Http () testRefreshTokenNoTokenId opts brig = do uid <- userId <$> createUser "alice" brig let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [SelfRead] + let scopes = OAuthScopes $ Set.fromList [ReadSelf] (cid, secret, _) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl key <- liftIO $ readJwk (fromMaybe "path to jwk not set" (Opt.setOAuthJwkKeyPair $ Opt.optSettings opts)) <&> fromMaybe (error "invalid key") badRefreshToken <- liftIO $ OAuthToken <$> signRefreshToken key emptyClaimsSet @@ -522,7 +522,7 @@ testRefreshTokenNonExistingId :: Opts -> Brig -> Http () testRefreshTokenNonExistingId opts brig = do uid <- userId <$> createUser "alice" brig let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [SelfRead] + let scopes = OAuthScopes $ Set.fromList [ReadSelf] (cid, secret, _) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl key <- liftIO $ readJwk (fromMaybe "path to jwk not set" (Opt.setOAuthJwkKeyPair $ Opt.optSettings opts)) <&> fromMaybe (error "invalid key") badRefreshToken <- @@ -541,7 +541,7 @@ testRefreshTokenWrongClientId :: Brig -> Http () testRefreshTokenWrongClientId brig = do uid <- userId <$> createUser "alice" brig let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [SelfRead] + let scopes = OAuthScopes $ Set.fromList [ReadSelf] (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl accessToken <- createOAuthAccessToken brig accessTokenRequest @@ -555,7 +555,7 @@ testRefreshTokenWrongClientSecret :: Brig -> Http () testRefreshTokenWrongClientSecret brig = do uid <- userId <$> createUser "alice" brig let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [SelfRead] + let scopes = OAuthScopes $ Set.fromList [ReadSelf] (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl accessToken <- createOAuthAccessToken brig accessTokenRequest @@ -569,7 +569,7 @@ testRefreshTokenWrongGrantType :: Brig -> Http () testRefreshTokenWrongGrantType brig = do uid <- userId <$> createUser "alice" brig let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [SelfRead] + let scopes = OAuthScopes $ Set.fromList [ReadSelf] (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl accessToken <- createOAuthAccessToken brig accessTokenRequest @@ -584,7 +584,7 @@ testRefreshTokenExpiredToken opts brig = withSettingsOverrides (opts & Opt.optionSettings . Opt.oauthRefreshTokenExpirationTimeSecsInternal ?~ 2) $ do uid <- userId <$> createUser "alice" brig let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [SelfRead] + let scopes = OAuthScopes $ Set.fromList [ReadSelf] (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl accessToken <- createOAuthAccessToken brig accessTokenRequest @@ -598,7 +598,7 @@ testRefreshTokenRevokedToken :: Brig -> Http () testRefreshTokenRevokedToken brig = do uid <- userId <$> createUser "alice" brig let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [SelfRead] + let scopes = OAuthScopes $ Set.fromList [ReadSelf] (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl accessToken <- createOAuthAccessToken brig accessTokenRequest @@ -653,7 +653,7 @@ testCreateConversationSuccessInternal :: Brig -> Galley -> Http () testCreateConversationSuccessInternal brig galley = do (uid, tid) <- Team.createUserWithTeam brig let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [ConversationCreate] + let scopes = OAuthScopes $ Set.fromList [WriteConversation] (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl accessToken <- createOAuthAccessToken brig accessTokenRequest @@ -664,7 +664,7 @@ testCreateConversationSuccessNginz :: Brig -> Nginz -> Http () testCreateConversationSuccessNginz brig nginz = do (uid, tid) <- Team.createUserWithTeam brig let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [ConversationCreate] + let scopes = OAuthScopes $ Set.fromList [WriteConversation] (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl accessToken <- createOAuthAccessToken brig accessTokenRequest From 737ba2b6a1d886ce2494363aa17096f59003b776 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 7 Feb 2023 15:05:33 +0000 Subject: [PATCH 52/56] fix --- .../src/Wire/API/Routes/Public/Galley/Conversation.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index 218690ff62..d9e12df326 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -339,7 +339,7 @@ type ConversationAPI = :> CanThrow OperationDenied :> CanThrow 'MissingLegalholdConsent :> Description "This returns 201 when a new conversation is created, and 200 when the conversation already existed" - :> ZOAuthLocalUser '[ 'ConversationCreate] + :> ZOAuthLocalUser '[ 'WriteConversation] :> ZOptConn :> "conversations" :> VersionedReqBody 'V2 '[Servant.JSON] NewConv @@ -358,7 +358,7 @@ type ConversationAPI = :> CanThrow OperationDenied :> CanThrow 'MissingLegalholdConsent :> Description "This returns 201 when a new conversation is created, and 200 when the conversation already existed" - :> ZOAuthLocalUser '[ 'ConversationCreate] + :> ZOAuthLocalUser '[ 'WriteConversation] :> ZOptConn :> "conversations" :> ReqBody '[Servant.JSON] NewConv From 8d61c297e9c62ad1a2d94f9ffa4c2de35be962b6 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 8 Feb 2023 09:23:52 +0000 Subject: [PATCH 53/56] oauth scope for feature-configs --- charts/nginz/values.yaml | 1 + libs/wire-api/src/Wire/API/OAuth.hs | 22 +++++- .../Wire/API/Routes/Public/Galley/Feature.hs | 3 +- .../Test/Wire/API/Roundtrip/ByteString.hs | 4 +- services/brig/test/integration/API/OAuth.hs | 71 ++++++++++++++----- .../galley/src/Galley/API/Public/Feature.hs | 5 +- .../integration-test/conf/nginz/nginx.conf | 4 +- 7 files changed, 87 insertions(+), 23 deletions(-) diff --git a/charts/nginz/values.yaml b/charts/nginz/values.yaml index 3f382d44f9..2625545fd9 100644 --- a/charts/nginz/values.yaml +++ b/charts/nginz/values.yaml @@ -501,6 +501,7 @@ nginx_conf: - path: /feature-configs(.*) envs: - all + enable_oauth: true - path: /galley-api/swagger-ui disable_zauth: true envs: diff --git a/libs/wire-api/src/Wire/API/OAuth.hs b/libs/wire-api/src/Wire/API/OAuth.hs index 22e1ad603c..d23df37150 100644 --- a/libs/wire-api/src/Wire/API/OAuth.hs +++ b/libs/wire-api/src/Wire/API/OAuth.hs @@ -42,9 +42,11 @@ import Data.Time import Imports hiding (exp, head) import Servant hiding (Handler, JSON, Tagged, addHeader, respond) import Servant.Swagger.Internal.Orphans () +import Test.QuickCheck (Arbitrary) import URI.ByteString import Web.FormUrlEncoded (Form (..), FromForm (..), ToForm (..), parseUnique) import Wire.API.Error +import Wire.Arbitrary (GenericUniform (..)) -------------------------------------------------------------------------------- -- Types @@ -163,11 +165,24 @@ instance ToSchema OAuthResponseType where [ element "code" OAuthResponseTypeCode ] +-- | add oauth scope checklist: +-- - add to the list below +-- - update ToByteString and FromByteString instance of OAuthScope (run unit tests) +-- - implement IsOAuthScope instance for the new scope +-- - for the endpoints that require the new scope, replace: +-- - `ZUser` with `ZOauthUser '[ 'NewScope]` +-- - or `ZLocalUser` with `ZOauthLocalUser '[ 'NewScope]` +-- - update `services/nginz/integration-test/conf/nginz/nginx.conf`: +-- - in location settings for the endpoint in question, replace +-- `include common_response_with_zauth.conf` with `common_response_with_zauth_oauth.conf` +-- - update `charts/nginz/values.yaml` and add `enable_oauth: true` to the endpoint in question data OAuthScope = WriteConversation | WriteConversationCode | ReadSelf + | ReadFeatureConfigs deriving (Eq, Show, Generic, Ord) + deriving (Arbitrary) via (GenericUniform OAuthScope) class IsOAuthScope scope where toOAuthScope :: OAuthScope @@ -181,6 +196,9 @@ instance IsOAuthScope 'WriteConversationCode where instance IsOAuthScope 'ReadSelf where toOAuthScope = ReadSelf +instance IsOAuthScope 'ReadFeatureConfigs where + toOAuthScope = ReadFeatureConfigs + -- | Given a type-level list of scopes X, this class gives you a function that tests if -- a list of scopes from a token intersects with X, ie., if a token grants access to the route -- with scopes X. @@ -199,8 +217,9 @@ instance (IsOAuthScope scope, IsOAuthScopes scopes) => IsOAuthScopes (scope ': s instance ToByteString OAuthScope where builder = \case WriteConversation -> "write:conversation" - WriteConversationCode -> "conversation_code:create" + WriteConversationCode -> "write:conversation_code" ReadSelf -> "read:self" + ReadFeatureConfigs -> "read:feature_configs" instance FromByteString OAuthScope where parser = do @@ -209,6 +228,7 @@ instance FromByteString OAuthScope where "write:conversation" -> pure WriteConversation "write:conversation_code" -> pure WriteConversationCode "read:self" -> pure ReadSelf + "read:feature_configs" -> pure ReadFeatureConfigs _ -> fail "invalid scope" newtype OAuthScopes = OAuthScopes {unOAuthScopes :: Set OAuthScope} diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs index a07a09fdfb..8f8bd140d9 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs @@ -26,6 +26,7 @@ import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley import Wire.API.MakesFederatedCall +import Wire.API.OAuth import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.Routes.Public @@ -214,7 +215,7 @@ type AllFeatureConfigsUserGet = :> Description "Gets feature configs for a user. If the user is a member of a team and has the required permissions, this will return the team's feature configs.\ \If the user is not a member of a team, this will return the personal feature configs (the server defaults)." - :> ZUser + :> ZOauthUser '[ 'ReadFeatureConfigs] :> CanThrow 'NotATeamMember :> CanThrow OperationDenied :> CanThrow 'TeamNotFound diff --git a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/ByteString.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/ByteString.hs index 467a837b58..9c4eb5b9e1 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/ByteString.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/ByteString.hs @@ -26,6 +26,7 @@ import qualified Wire.API.Asset as Asset import qualified Wire.API.Call.Config as Call.Config import qualified Wire.API.Conversation.Code as Conversation.Code import qualified Wire.API.Conversation.Role as Conversation.Role +import qualified Wire.API.OAuth as OAuth import qualified Wire.API.Properties as Properties import qualified Wire.API.Provider as Provider import qualified Wire.API.Provider.Service as Provider.Service @@ -81,7 +82,8 @@ tests = testRoundTrip @User.Search.TeamUserSearchSortBy, testRoundTrip @User.Search.TeamUserSearchSortOrder, testRoundTrip @User.Search.RoleFilter, - testRoundTrip @User.IdentityProvider.WireIdPAPIVersion + testRoundTrip @User.IdentityProvider.WireIdPAPIVersion, + testRoundTrip @OAuth.OAuthScope -- FUTUREWORK: -- testCase "Call.Config.TurnUsername (doesn't have FromByteString)" ... -- testCase "User.Activation.ActivationTarget (doesn't have FromByteString)" ... diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index 1ebe1acb33..c7c05d7ccb 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -98,9 +98,20 @@ tests m db b g n o = do test m "expired token" $ testAccessResourceExpiredToken o b, test m "nonsense token" $ testAccessResourceNonsenseToken b, test m "no token" $ testAccessResourceNoToken b, - test m "invalid signature" $ testAccessResourceInvalidSignature o b, - test m "create conversation (internal)" $ testCreateConversationSuccessInternal b g, - test m "create conversation (nginz)" $ testCreateConversationSuccessNginz b n + test m "invalid signature" $ testAccessResourceInvalidSignature o b + ], + testGroup + "accessing resources" + [ testGroup + "internal" + [ test m "write:conversation" $ testWriteConversationSuccessInternal b g, + test m "read:feature_configs" $ testReadFeatureConfigsSuccessInternal b g + ], + testGroup + "nginz" + [ test m "write:conversation" $ testWriteConversationSuccessNginz b n, + test m "read:feature_configs" $ testReadFeatureConfigsSuccessNginz b n + ] ], testGroup "refresh tokens" $ [ test m "max active tokens" $ testRefreshTokenMaxActiveTokens o db b, @@ -649,31 +660,45 @@ testRevokeApplicationAccountAccess brig = do liftIO $ assertEqual "apps" 0 (length apps) _ -> liftIO $ assertFailure "unexpected number of apps" -testCreateConversationSuccessInternal :: Brig -> Galley -> Http () -testCreateConversationSuccessInternal brig galley = do +testWriteConversationSuccessInternal :: Brig -> Galley -> Http () +testWriteConversationSuccessInternal brig galley = do (uid, tid) <- Team.createUserWithTeam brig - let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [WriteConversation] - (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl - accessToken <- createOAuthAccessToken brig accessTokenRequest + accessToken <- getAccessTokenForScope brig uid WriteConversation createTeamConv galley zOAuthHeader (oatAccessToken accessToken) tid "oauth test group" !!! do const 201 === statusCode -testCreateConversationSuccessNginz :: Brig -> Nginz -> Http () -testCreateConversationSuccessNginz brig nginz = do +testWriteConversationSuccessNginz :: Brig -> Nginz -> Http () +testWriteConversationSuccessNginz brig nginz = do (uid, tid) <- Team.createUserWithTeam brig - let redirectUrl = mkUrl "https://example.com" - let scopes = OAuthScopes $ Set.fromList [WriteConversation] - (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl - let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl - accessToken <- createOAuthAccessToken brig accessTokenRequest + accessToken <- getAccessTokenForScope brig uid WriteConversation createTeamConv nginz authHeader (oatAccessToken accessToken) tid "oauth test group" !!! do const 201 === statusCode +testReadFeatureConfigsSuccessInternal :: Brig -> Galley -> Http () +testReadFeatureConfigsSuccessInternal brig galley = do + (uid, _) <- Team.createUserWithTeam brig + accessToken <- getAccessTokenForScope brig uid ReadFeatureConfigs + getFeatureConfigs galley zOAuthHeader (oatAccessToken accessToken) !!! do + const 200 === statusCode + +testReadFeatureConfigsSuccessNginz :: Brig -> Nginz -> Http () +testReadFeatureConfigsSuccessNginz brig nginz = do + (uid, _) <- Team.createUserWithTeam brig + accessToken <- getAccessTokenForScope brig uid ReadFeatureConfigs + getFeatureConfigs nginz authHeader (oatAccessToken accessToken) !!! do + const 200 === statusCode + ------------------------------------------------------------------------------- -- Util +getAccessTokenForScope :: Brig -> UserId -> OAuthScope -> Http OAuthAccessTokenResponse +getAccessTokenForScope brig uid scope = do + let redirectUrl = mkUrl "https://example.com" + let scopes = OAuthScopes $ Set.fromList [scope] + (cid, secret, code) <- generateOAuthClientAndAuthCode brig uid scopes redirectUrl + let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid secret code redirectUrl + createOAuthAccessToken brig accessTokenRequest + createTeamConv :: (Request -> Request) -> (OAuthAccessToken -> Request -> Request) -> @@ -691,6 +716,18 @@ createTeamConv svc mkHeader token tid name = do . json conv ) +getFeatureConfigs :: + (Request -> Request) -> + (OAuthAccessToken -> Request -> Request) -> + OAuthAccessToken -> + Http ResponseLBS +getFeatureConfigs svc mkHeader token = do + get + ( svc + . path "feature-configs" + . mkHeader token + ) + createOAuthApplicationWithAccountAccess :: Brig -> UserId -> Http OAuthAccessTokenResponse createOAuthApplicationWithAccountAccess brig uid = do let redirectUrl = mkUrl "https://example.com" diff --git a/services/galley/src/Galley/API/Public/Feature.hs b/services/galley/src/Galley/API/Public/Feature.hs index a515aa1a96..1db2ad70f0 100644 --- a/services/galley/src/Galley/API/Public/Feature.hs +++ b/services/galley/src/Galley/API/Public/Feature.hs @@ -17,6 +17,9 @@ module Galley.API.Public.Feature where +import Crypto.JOSE.JWK +import Data.Domain (Domain) +import Data.Maybe import Galley.API.Teams import Galley.API.Teams.Features import Galley.App @@ -62,7 +65,7 @@ featureAPI = <@> mkNamedAPI @'("put", SearchVisibilityInboundConfig) (setFeatureStatus @Cassandra . DoAuth) <@> mkNamedAPI @'("get", OutlookCalIntegrationConfig) (getFeatureStatus @Cassandra . DoAuth) <@> mkNamedAPI @'("put", OutlookCalIntegrationConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @"get-all-feature-configs-for-user" (getAllFeatureConfigsForUser @Cassandra) + <@> mkNamedAPIWithContext @'[Domain, Maybe JWK] @"get-all-feature-configs-for-user" (getAllFeatureConfigsForUser @Cassandra) <@> mkNamedAPI @"get-all-feature-configs-for-team" (getAllFeatureConfigsForTeam @Cassandra) <@> mkNamedAPI @'("get-config", LegalholdConfig) (getFeatureStatusForUser @Cassandra) <@> mkNamedAPI @'("get-config", SSOConfig) (getFeatureStatusForUser @Cassandra) diff --git a/services/nginz/integration-test/conf/nginz/nginx.conf b/services/nginz/integration-test/conf/nginz/nginx.conf index c4801bfaf7..9803fede07 100644 --- a/services/nginz/integration-test/conf/nginz/nginx.conf +++ b/services/nginz/integration-test/conf/nginz/nginx.conf @@ -405,8 +405,8 @@ http { proxy_pass http://galley; } - location ~* ^/feature-configs(.*) { - include common_response_with_zauth.conf; + location ~* ^(/v[0-9]+)?/feature-configs(.*) { + include common_response_with_zauth_oauth.conf; proxy_pass http://galley; } From 5047395f55a21f9de85c88cb5107134c9982c6cb Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 8 Feb 2023 09:43:06 +0000 Subject: [PATCH 54/56] wip --- libs/wire-api/src/Wire/API/Routes/API.hs | 29 +++++- services/brig/src/Brig/Run.hs | 3 +- .../galley/src/Galley/API/Public/Feature.hs | 97 +++++++++---------- services/galley/src/Galley/Run.hs | 4 +- 4 files changed, 73 insertions(+), 60 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Routes/API.hs b/libs/wire-api/src/Wire/API/Routes/API.hs index 6c6ddd4934..761108e869 100644 --- a/libs/wire-api/src/Wire/API/Routes/API.hs +++ b/libs/wire-api/src/Wire/API/Routes/API.hs @@ -18,7 +18,7 @@ module Wire.API.Routes.API ( API, hoistAPIHandler, - hoistAPIHandlerWithContext, + hoistAPIHandlerWithDomainAndJwk, hoistAPI, mkAPI, mkNamedAPI, @@ -28,9 +28,12 @@ module Wire.API.Routes.API ServerEffects (..), hoistServerWithDomain, hoistServerWithContext', + mkNamedAPIWithDomainAndJwk, + hoistServerWithDomainAndJwk, ) where +import Crypto.JOSE (JWK) import Data.Domain import Data.Kind (Type) import Data.Proxy @@ -75,6 +78,13 @@ mkNamedAPIWithContext :: API (Named name api) r0 mkNamedAPIWithContext = API . Named . unAPI . mkAPIWithContext @context @r0 @api +mkNamedAPIWithDomainAndJwk :: + forall name r0 api. + (HasServer api '[Domain, Maybe JWK], ServerEffects (DeclaredErrorEffects api) r0) => + ServerT api (Sem (Append (DeclaredErrorEffects api) r0)) -> + API (Named name api) r0 +mkNamedAPIWithDomainAndJwk = mkNamedAPIWithContext @'[Domain, Maybe JWK] + -- | Combine APIs. (<@>) :: API api1 r -> API api2 r -> API (api1 :<|> api2) r (<@>) (API h1) (API h2) = API (h1 :<|> h2) @@ -104,6 +114,15 @@ hoistServerWithDomain :: ServerT api n hoistServerWithDomain = hoistServerWithContext (Proxy @api) (Proxy @'[Domain]) +-- | Like `hoistServerWithContext'`, but with a 'Domain' context. +hoistServerWithDomainAndJwk :: + forall api m n. + HasServer api '[Domain, Maybe JWK] => + (forall x. m x -> n x) -> + ServerT api m -> + ServerT api n +hoistServerWithDomainAndJwk = hoistServerWithContext (Proxy @api) (Proxy @'[Domain, Maybe JWK]) + hoistAPIHandler :: forall api r n. HasServer api '[Domain] => @@ -112,13 +131,13 @@ hoistAPIHandler :: ServerT api n hoistAPIHandler f = hoistServerWithContext' @'[Domain] @api f . unAPI -hoistAPIHandlerWithContext :: - forall (context :: [Type]) api r n. - HasServer api context => +hoistAPIHandlerWithDomainAndJwk :: + forall api r n. + HasServer api '[Domain, Maybe JWK] => (forall x. Sem r x -> n x) -> API api r -> ServerT api n -hoistAPIHandlerWithContext f = hoistServerWithContext' @context @api f . unAPI +hoistAPIHandlerWithDomainAndJwk f = hoistServerWithContext' @'[Domain, Maybe JWK] @api f . unAPI hoistAPI :: forall api1 api2 r1 r2. diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index c455160833..c691e95656 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -51,7 +51,6 @@ import Control.Monad.Random (randomRIO) import Crypto.JWT import qualified Data.Aeson as Aeson import Data.Default (Default (def)) -import Data.Domain import Data.Id (RequestId (..)) import Data.Metrics.AWS (gaugeTokenRemaing) import qualified Data.Metrics.Servant as Metrics @@ -147,7 +146,7 @@ mkApp o = do (Proxy @ServantCombinedAPI) (mJwk :. customFormatters :. localDomain :. Servant.EmptyContext) ( docsAPI - :<|> hoistServerWithContext' @'[Domain, Maybe JWK] @BrigAPI (toServantHandler e) servantSitemap + :<|> hoistServerWithDomainAndJwk @BrigAPI (toServantHandler e) servantSitemap :<|> hoistServerWithDomain @IAPI.API (toServantHandler e) IAPI.servantSitemap :<|> hoistServerWithDomain @FederationAPI (toServantHandler e) federationSitemap :<|> hoistServerWithDomain @VersionAPI (toServantHandler e) versionAPI diff --git a/services/galley/src/Galley/API/Public/Feature.hs b/services/galley/src/Galley/API/Public/Feature.hs index 1db2ad70f0..d2e7c7b0cf 100644 --- a/services/galley/src/Galley/API/Public/Feature.hs +++ b/services/galley/src/Galley/API/Public/Feature.hs @@ -17,9 +17,6 @@ module Galley.API.Public.Feature where -import Crypto.JOSE.JWK -import Data.Domain (Domain) -import Data.Maybe import Galley.API.Teams import Galley.API.Teams.Features import Galley.App @@ -33,50 +30,50 @@ import Wire.API.Team.Feature featureAPI :: API FeatureAPI GalleyEffects featureAPI = mkNamedAPI @'("get", SSOConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get", LegalholdConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("put", LegalholdConfig) (callsFed (setFeatureStatus @Cassandra . DoAuth)) - <@> mkNamedAPI @'("get", SearchVisibilityAvailableConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("put", SearchVisibilityAvailableConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get-deprecated", SearchVisibilityAvailableConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("put-deprecated", SearchVisibilityAvailableConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @"get-search-visibility" getSearchVisibility - <@> mkNamedAPI @"set-search-visibility" (setSearchVisibility @Cassandra (featureEnabledForTeam @Cassandra @SearchVisibilityAvailableConfig)) - <@> mkNamedAPI @'("get", ValidateSAMLEmailsConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get-deprecated", ValidateSAMLEmailsConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get", DigitalSignaturesConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get-deprecated", DigitalSignaturesConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get", AppLockConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("put", AppLockConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get", FileSharingConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("put", FileSharingConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get", ClassifiedDomainsConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get", ConferenceCallingConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get", SelfDeletingMessagesConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("put", SelfDeletingMessagesConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get", GuestLinksConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("put", GuestLinksConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get", SndFactorPasswordChallengeConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("put", SndFactorPasswordChallengeConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get", MLSConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("put", MLSConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get", ExposeInvitationURLsToTeamAdminConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("put", ExposeInvitationURLsToTeamAdminConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get", SearchVisibilityInboundConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("put", SearchVisibilityInboundConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get", OutlookCalIntegrationConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("put", OutlookCalIntegrationConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithContext @'[Domain, Maybe JWK] @"get-all-feature-configs-for-user" (getAllFeatureConfigsForUser @Cassandra) - <@> mkNamedAPI @"get-all-feature-configs-for-team" (getAllFeatureConfigsForTeam @Cassandra) - <@> mkNamedAPI @'("get-config", LegalholdConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPI @'("get-config", SSOConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPI @'("get-config", SearchVisibilityAvailableConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPI @'("get-config", ValidateSAMLEmailsConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPI @'("get-config", DigitalSignaturesConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPI @'("get-config", AppLockConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPI @'("get-config", FileSharingConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPI @'("get-config", ClassifiedDomainsConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPI @'("get-config", ConferenceCallingConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPI @'("get-config", SelfDeletingMessagesConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPI @'("get-config", GuestLinksConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPI @'("get-config", SndFactorPasswordChallengeConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPI @'("get-config", MLSConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPIWithDomainAndJwk @'("get", LegalholdConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("put", LegalholdConfig) (callsFed (setFeatureStatus @Cassandra . DoAuth)) + <@> mkNamedAPIWithDomainAndJwk @'("get", SearchVisibilityAvailableConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("put", SearchVisibilityAvailableConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("get-deprecated", SearchVisibilityAvailableConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("put-deprecated", SearchVisibilityAvailableConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @"get-search-visibility" getSearchVisibility + <@> mkNamedAPIWithDomainAndJwk @"set-search-visibility" (setSearchVisibility @Cassandra (featureEnabledForTeam @Cassandra @SearchVisibilityAvailableConfig)) + <@> mkNamedAPIWithDomainAndJwk @'("get", ValidateSAMLEmailsConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("get-deprecated", ValidateSAMLEmailsConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("get", DigitalSignaturesConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("get-deprecated", DigitalSignaturesConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("get", AppLockConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("put", AppLockConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("get", FileSharingConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("put", FileSharingConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("get", ClassifiedDomainsConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("get", ConferenceCallingConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("get", SelfDeletingMessagesConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("put", SelfDeletingMessagesConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("get", GuestLinksConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("put", GuestLinksConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("get", SndFactorPasswordChallengeConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("put", SndFactorPasswordChallengeConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("get", MLSConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("put", MLSConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("get", ExposeInvitationURLsToTeamAdminConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("put", ExposeInvitationURLsToTeamAdminConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("get", SearchVisibilityInboundConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("put", SearchVisibilityInboundConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("get", OutlookCalIntegrationConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @'("put", OutlookCalIntegrationConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPIWithDomainAndJwk @"get-all-feature-configs-for-user" (getAllFeatureConfigsForUser @Cassandra) + <@> mkNamedAPIWithDomainAndJwk @"get-all-feature-configs-for-team" (getAllFeatureConfigsForTeam @Cassandra) + <@> mkNamedAPIWithDomainAndJwk @'("get-config", LegalholdConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPIWithDomainAndJwk @'("get-config", SSOConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPIWithDomainAndJwk @'("get-config", SearchVisibilityAvailableConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPIWithDomainAndJwk @'("get-config", ValidateSAMLEmailsConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPIWithDomainAndJwk @'("get-config", DigitalSignaturesConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPIWithDomainAndJwk @'("get-config", AppLockConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPIWithDomainAndJwk @'("get-config", FileSharingConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPIWithDomainAndJwk @'("get-config", ClassifiedDomainsConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPIWithDomainAndJwk @'("get-config", ConferenceCallingConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPIWithDomainAndJwk @'("get-config", SelfDeletingMessagesConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPIWithDomainAndJwk @'("get-config", GuestLinksConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPIWithDomainAndJwk @'("get-config", SndFactorPasswordChallengeConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPIWithDomainAndJwk @'("get-config", MLSConfig) (getFeatureStatusForUser @Cassandra) diff --git a/services/galley/src/Galley/Run.hs b/services/galley/src/Galley/Run.hs index 92b883f0c9..d72e8eb6b3 100644 --- a/services/galley/src/Galley/Run.hs +++ b/services/galley/src/Galley/Run.hs @@ -31,10 +31,8 @@ import qualified Control.Concurrent.Async as Async import Control.Exception (finally) import Control.Lens (view, (.~), (^.)) import Control.Monad.Codensity -import Crypto.JOSE.JWK import qualified Data.Aeson as Aeson import Data.Default -import Data.Domain (Domain) import Data.Id import Data.Metrics (Metrics) import Data.Metrics.AWS (gaugeTokenRemaing) @@ -119,7 +117,7 @@ mkApp opts = :. customFormatters :. Servant.EmptyContext ) - ( hoistAPIHandlerWithContext @'[Domain, Maybe JWK] @GalleyAPI.ServantAPI (toServantHandler e) API.servantSitemap + ( hoistAPIHandlerWithDomainAndJwk @GalleyAPI.ServantAPI (toServantHandler e) API.servantSitemap :<|> hoistAPIHandler (toServantHandler e) internalAPI :<|> hoistServerWithDomain @FederationAPI (toServantHandler e) federationSitemap :<|> Servant.Tagged (runGalley e) From fdff6a6ad85d0c457e0c281bba77caf9662d656e Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 8 Feb 2023 10:16:30 +0000 Subject: [PATCH 55/56] clean up --- libs/wire-api/src/Wire/API/Routes/API.hs | 44 ++------- .../src/Galley/API/Public/Conversation.hs | 7 +- .../galley/src/Galley/API/Public/Feature.hs | 94 +++++++++---------- services/galley/src/Galley/Run.hs | 2 +- 4 files changed, 58 insertions(+), 89 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Routes/API.hs b/libs/wire-api/src/Wire/API/Routes/API.hs index 761108e869..0500983e1b 100644 --- a/libs/wire-api/src/Wire/API/Routes/API.hs +++ b/libs/wire-api/src/Wire/API/Routes/API.hs @@ -17,19 +17,15 @@ module Wire.API.Routes.API ( API, - hoistAPIHandler, - hoistAPIHandlerWithDomainAndJwk, hoistAPI, + hoistAPIHandler, mkAPI, mkNamedAPI, - mkNamedAPIWithContext, + hoistServerWithDomain, + hoistServerWithDomainAndJwk, (<@>), ServerEffect (..), ServerEffects (..), - hoistServerWithDomain, - hoistServerWithContext', - mkNamedAPIWithDomainAndJwk, - hoistServerWithDomainAndJwk, ) where @@ -61,15 +57,15 @@ mkAPIWithContext :: (HasServer api context, ServerEffects (DeclaredErrorEffects api) r0) => ServerT api (Sem (Append (DeclaredErrorEffects api) r0)) -> API api r0 -mkAPIWithContext h = API $ hoistServerWithContext' @context @api (interpretServerEffects @(DeclaredErrorEffects api) @r0) h +mkAPIWithContext h = API $ hoistServerWithContext (Proxy @api) (Proxy @context) (interpretServerEffects @(DeclaredErrorEffects api) @r0) h -- | Convert a polysemy handler to a named 'API' value. mkNamedAPI :: forall name r0 api. - (HasServer api '[Domain], ServerEffects (DeclaredErrorEffects api) r0) => + (HasServer api '[Domain, Maybe JWK], ServerEffects (DeclaredErrorEffects api) r0) => ServerT api (Sem (Append (DeclaredErrorEffects api) r0)) -> API (Named name api) r0 -mkNamedAPI = mkNamedAPIWithContext @'[Domain] +mkNamedAPI = mkNamedAPIWithContext @'[Domain, Maybe JWK] mkNamedAPIWithContext :: forall (context :: [Type]) name r0 api. @@ -78,13 +74,6 @@ mkNamedAPIWithContext :: API (Named name api) r0 mkNamedAPIWithContext = API . Named . unAPI . mkAPIWithContext @context @r0 @api -mkNamedAPIWithDomainAndJwk :: - forall name r0 api. - (HasServer api '[Domain, Maybe JWK], ServerEffects (DeclaredErrorEffects api) r0) => - ServerT api (Sem (Append (DeclaredErrorEffects api) r0)) -> - API (Named name api) r0 -mkNamedAPIWithDomainAndJwk = mkNamedAPIWithContext @'[Domain, Maybe JWK] - -- | Combine APIs. (<@>) :: API api1 r -> API api2 r -> API (api1 :<|> api2) r (<@>) (API h1) (API h2) = API (h1 :<|> h2) @@ -97,15 +86,6 @@ infixr 3 <@> -- but unfortunately the 'hoistServerWithContext' function is also part of the -- 'HasServer' typeclass, even though it cannot possibly make use of its @context@ -- type argument. -hoistServerWithContext' :: - forall (context :: [Type]) api m n. - HasServer api context => - (forall x. m x -> n x) -> - ServerT api m -> - ServerT api n -hoistServerWithContext' = hoistServerWithContext (Proxy @api) (Proxy @context) - --- | Like `hoistServerWithContext'`, but with a 'Domain' context. hoistServerWithDomain :: forall api m n. HasServer api '[Domain] => @@ -114,7 +94,7 @@ hoistServerWithDomain :: ServerT api n hoistServerWithDomain = hoistServerWithContext (Proxy @api) (Proxy @'[Domain]) --- | Like `hoistServerWithContext'`, but with a 'Domain' context. +-- | Like `hoistServerWithDomain`, but with a additional 'Maybe JWK' context. hoistServerWithDomainAndJwk :: forall api m n. HasServer api '[Domain, Maybe JWK] => @@ -124,20 +104,12 @@ hoistServerWithDomainAndJwk :: hoistServerWithDomainAndJwk = hoistServerWithContext (Proxy @api) (Proxy @'[Domain, Maybe JWK]) hoistAPIHandler :: - forall api r n. - HasServer api '[Domain] => - (forall x. Sem r x -> n x) -> - API api r -> - ServerT api n -hoistAPIHandler f = hoistServerWithContext' @'[Domain] @api f . unAPI - -hoistAPIHandlerWithDomainAndJwk :: forall api r n. HasServer api '[Domain, Maybe JWK] => (forall x. Sem r x -> n x) -> API api r -> ServerT api n -hoistAPIHandlerWithDomainAndJwk f = hoistServerWithContext' @'[Domain, Maybe JWK] @api f . unAPI +hoistAPIHandler f = hoistServerWithContext (Proxy @api) (Proxy @'[Domain, Maybe JWK]) f . unAPI hoistAPI :: forall api1 api2 r1 r2. diff --git a/services/galley/src/Galley/API/Public/Conversation.hs b/services/galley/src/Galley/API/Public/Conversation.hs index 9db9f38c0c..800c9f4654 100644 --- a/services/galley/src/Galley/API/Public/Conversation.hs +++ b/services/galley/src/Galley/API/Public/Conversation.hs @@ -17,9 +17,6 @@ module Galley.API.Public.Conversation where -import Crypto.JOSE.JWK -import Data.Domain (Domain) -import Data.Maybe import Galley.API.Create import Galley.API.MLS.GroupInfo import Galley.API.MLS.Types @@ -47,8 +44,8 @@ conversationAPI = <@> mkNamedAPI @"list-conversations@v2" (callsFed listConversations) <@> mkNamedAPI @"list-conversations" (callsFed listConversations) <@> mkNamedAPI @"get-conversation-by-reusable-code" (getConversationByReusableCode @Cassandra) - <@> mkNamedAPIWithContext @'[Domain, Maybe JWK] @"create-group-conversation@v2" (callsFed createGroupConversation) - <@> mkNamedAPIWithContext @'[Domain, Maybe JWK] @"create-group-conversation" (callsFed createGroupConversation) + <@> mkNamedAPI @"create-group-conversation@v2" (callsFed createGroupConversation) + <@> mkNamedAPI @"create-group-conversation" (callsFed createGroupConversation) <@> mkNamedAPI @"create-self-conversation@v2" createProteusSelfConversation <@> mkNamedAPI @"create-self-conversation" createProteusSelfConversation <@> mkNamedAPI @"get-mls-self-conversation" getMLSSelfConversationWithError diff --git a/services/galley/src/Galley/API/Public/Feature.hs b/services/galley/src/Galley/API/Public/Feature.hs index d2e7c7b0cf..a515aa1a96 100644 --- a/services/galley/src/Galley/API/Public/Feature.hs +++ b/services/galley/src/Galley/API/Public/Feature.hs @@ -30,50 +30,50 @@ import Wire.API.Team.Feature featureAPI :: API FeatureAPI GalleyEffects featureAPI = mkNamedAPI @'("get", SSOConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("get", LegalholdConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("put", LegalholdConfig) (callsFed (setFeatureStatus @Cassandra . DoAuth)) - <@> mkNamedAPIWithDomainAndJwk @'("get", SearchVisibilityAvailableConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("put", SearchVisibilityAvailableConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("get-deprecated", SearchVisibilityAvailableConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("put-deprecated", SearchVisibilityAvailableConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @"get-search-visibility" getSearchVisibility - <@> mkNamedAPIWithDomainAndJwk @"set-search-visibility" (setSearchVisibility @Cassandra (featureEnabledForTeam @Cassandra @SearchVisibilityAvailableConfig)) - <@> mkNamedAPIWithDomainAndJwk @'("get", ValidateSAMLEmailsConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("get-deprecated", ValidateSAMLEmailsConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("get", DigitalSignaturesConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("get-deprecated", DigitalSignaturesConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("get", AppLockConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("put", AppLockConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("get", FileSharingConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("put", FileSharingConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("get", ClassifiedDomainsConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("get", ConferenceCallingConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("get", SelfDeletingMessagesConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("put", SelfDeletingMessagesConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("get", GuestLinksConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("put", GuestLinksConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("get", SndFactorPasswordChallengeConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("put", SndFactorPasswordChallengeConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("get", MLSConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("put", MLSConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("get", ExposeInvitationURLsToTeamAdminConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("put", ExposeInvitationURLsToTeamAdminConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("get", SearchVisibilityInboundConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("put", SearchVisibilityInboundConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("get", OutlookCalIntegrationConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @'("put", OutlookCalIntegrationConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPIWithDomainAndJwk @"get-all-feature-configs-for-user" (getAllFeatureConfigsForUser @Cassandra) - <@> mkNamedAPIWithDomainAndJwk @"get-all-feature-configs-for-team" (getAllFeatureConfigsForTeam @Cassandra) - <@> mkNamedAPIWithDomainAndJwk @'("get-config", LegalholdConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPIWithDomainAndJwk @'("get-config", SSOConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPIWithDomainAndJwk @'("get-config", SearchVisibilityAvailableConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPIWithDomainAndJwk @'("get-config", ValidateSAMLEmailsConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPIWithDomainAndJwk @'("get-config", DigitalSignaturesConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPIWithDomainAndJwk @'("get-config", AppLockConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPIWithDomainAndJwk @'("get-config", FileSharingConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPIWithDomainAndJwk @'("get-config", ClassifiedDomainsConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPIWithDomainAndJwk @'("get-config", ConferenceCallingConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPIWithDomainAndJwk @'("get-config", SelfDeletingMessagesConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPIWithDomainAndJwk @'("get-config", GuestLinksConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPIWithDomainAndJwk @'("get-config", SndFactorPasswordChallengeConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPIWithDomainAndJwk @'("get-config", MLSConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPI @'("get", LegalholdConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("put", LegalholdConfig) (callsFed (setFeatureStatus @Cassandra . DoAuth)) + <@> mkNamedAPI @'("get", SearchVisibilityAvailableConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("put", SearchVisibilityAvailableConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get-deprecated", SearchVisibilityAvailableConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("put-deprecated", SearchVisibilityAvailableConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @"get-search-visibility" getSearchVisibility + <@> mkNamedAPI @"set-search-visibility" (setSearchVisibility @Cassandra (featureEnabledForTeam @Cassandra @SearchVisibilityAvailableConfig)) + <@> mkNamedAPI @'("get", ValidateSAMLEmailsConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get-deprecated", ValidateSAMLEmailsConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", DigitalSignaturesConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get-deprecated", DigitalSignaturesConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", AppLockConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("put", AppLockConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", FileSharingConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("put", FileSharingConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", ClassifiedDomainsConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", ConferenceCallingConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", SelfDeletingMessagesConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("put", SelfDeletingMessagesConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", GuestLinksConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("put", GuestLinksConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", SndFactorPasswordChallengeConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("put", SndFactorPasswordChallengeConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", MLSConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("put", MLSConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", ExposeInvitationURLsToTeamAdminConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("put", ExposeInvitationURLsToTeamAdminConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", SearchVisibilityInboundConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("put", SearchVisibilityInboundConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", OutlookCalIntegrationConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("put", OutlookCalIntegrationConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @"get-all-feature-configs-for-user" (getAllFeatureConfigsForUser @Cassandra) + <@> mkNamedAPI @"get-all-feature-configs-for-team" (getAllFeatureConfigsForTeam @Cassandra) + <@> mkNamedAPI @'("get-config", LegalholdConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPI @'("get-config", SSOConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPI @'("get-config", SearchVisibilityAvailableConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPI @'("get-config", ValidateSAMLEmailsConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPI @'("get-config", DigitalSignaturesConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPI @'("get-config", AppLockConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPI @'("get-config", FileSharingConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPI @'("get-config", ClassifiedDomainsConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPI @'("get-config", ConferenceCallingConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPI @'("get-config", SelfDeletingMessagesConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPI @'("get-config", GuestLinksConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPI @'("get-config", SndFactorPasswordChallengeConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPI @'("get-config", MLSConfig) (getFeatureStatusForUser @Cassandra) diff --git a/services/galley/src/Galley/Run.hs b/services/galley/src/Galley/Run.hs index d72e8eb6b3..5978492bbc 100644 --- a/services/galley/src/Galley/Run.hs +++ b/services/galley/src/Galley/Run.hs @@ -117,7 +117,7 @@ mkApp opts = :. customFormatters :. Servant.EmptyContext ) - ( hoistAPIHandlerWithDomainAndJwk @GalleyAPI.ServantAPI (toServantHandler e) API.servantSitemap + ( hoistAPIHandler @GalleyAPI.ServantAPI (toServantHandler e) API.servantSitemap :<|> hoistAPIHandler (toServantHandler e) internalAPI :<|> hoistServerWithDomain @FederationAPI (toServantHandler e) federationSitemap :<|> Servant.Tagged (runGalley e) From 0c14ccad8d857fdc08944b7caa8219cc0b722d11 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 8 Feb 2023 10:16:49 +0000 Subject: [PATCH 56/56] update comment --- libs/wire-api/src/Wire/API/OAuth.hs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/libs/wire-api/src/Wire/API/OAuth.hs b/libs/wire-api/src/Wire/API/OAuth.hs index d23df37150..1f2903f0db 100644 --- a/libs/wire-api/src/Wire/API/OAuth.hs +++ b/libs/wire-api/src/Wire/API/OAuth.hs @@ -165,17 +165,19 @@ instance ToSchema OAuthResponseType where [ element "code" OAuthResponseTypeCode ] --- | add oauth scope checklist: --- - add to the list below --- - update ToByteString and FromByteString instance of OAuthScope (run unit tests) --- - implement IsOAuthScope instance for the new scope --- - for the endpoints that require the new scope, replace: --- - `ZUser` with `ZOauthUser '[ 'NewScope]` --- - or `ZLocalUser` with `ZOauthLocalUser '[ 'NewScope]` --- - update `services/nginz/integration-test/conf/nginz/nginx.conf`: --- - in location settings for the endpoint in question, replace --- `include common_response_with_zauth.conf` with `common_response_with_zauth_oauth.conf` --- - update `charts/nginz/values.yaml` and add `enable_oauth: true` to the endpoint in question +-- | This types represents all valid OAuth scopes +-- If you want to add an OAuth scope, you may want to go through this checklist: +-- - Add the new scope to the list below +-- - Update `ToByteString` and `FromByteString` instance of `OAuthScope` (run unit tests) +-- - Implement an `IsOAuthScope` instance for the new scope +-- - For the endpoint(s) that require the new scope, replace: +-- - `ZUser` with `ZOauthUser '[ ']` +-- - or `ZLocalUser` with `ZOauthLocalUser '[ ']` +-- - Update `services/nginz/integration-test/conf/nginz/nginx.conf` and replace +-- `include common_response_with_zauth.conf` with `include common_response_with_zauth_oauth.conf` +-- in location settings for the endpoint(s) in question +-- - Update `charts/nginz/values.yaml` and add `enable_oauth: true` to the endpoint in question +-- - Consider writing an integration test data OAuthScope = WriteConversation | WriteConversationCode