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 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/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/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/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/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/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/changelog.d/5-internal/helm-test b/changelog.d/5-internal/helm-test new file mode 100644 index 0000000000..2bd90dfe39 --- /dev/null +++ b/changelog.d/5-internal/helm-test @@ -0,0 +1 @@ +charts: Mark all service/secret/configmap test resources to be re-created by defining them as helm hooks (#3037, #3049) 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/charts/brig/templates/tests/brig-integration.yaml b/charts/brig/templates/tests/brig-integration.yaml index 0687604fb2..c8cfa602f1 100644 --- a/charts/brig/templates/tests/brig-integration.yaml +++ b/charts/brig/templates/tests/brig-integration.yaml @@ -2,6 +2,9 @@ apiVersion: v1 kind: Service metadata: name: "brig-integration" + annotations: + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation labels: app: brig-integration chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} @@ -19,7 +22,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..56667e55ed 100644 --- a/charts/brig/templates/tests/configmap.yaml +++ b/charts/brig/templates/tests/configmap.yaml @@ -2,6 +2,9 @@ apiVersion: v1 kind: ConfigMap metadata: name: "brig-integration" + annotations: + "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 c31128667c..6eda016c82 100644 --- a/charts/brig/templates/tests/nginz-service.yaml +++ b/charts/brig/templates/tests/nginz-service.yaml @@ -5,6 +5,9 @@ apiVersion: v1 kind: Service metadata: name: nginz-integration-http + annotations: + "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 bfe877caf7..69ce7e671e 100644 --- a/charts/brig/templates/tests/secret.yaml +++ b/charts/brig/templates/tests/secret.yaml @@ -2,6 +2,9 @@ apiVersion: v1 kind: ConfigMap metadata: name: brig-integration-secrets + annotations: + "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/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..18a5b29b22 100644 --- a/charts/cargohold/templates/tests/configmap.yaml +++ b/charts/cargohold/templates/tests/configmap.yaml @@ -2,6 +2,9 @@ apiVersion: v1 kind: ConfigMap metadata: name: "cargohold-integration" + annotations: + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation data: integration.yaml: | cargohold: 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/charts/federator/templates/tests/configmap.yaml b/charts/federator/templates/tests/configmap.yaml index 910411fe5d..44146840bd 100644 --- a/charts/federator/templates/tests/configmap.yaml +++ b/charts/federator/templates/tests/configmap.yaml @@ -2,6 +2,9 @@ apiVersion: v1 kind: ConfigMap metadata: name: "federator-integration" + annotations: + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation 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/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/configmap.yaml b/charts/galley/templates/tests/configmap.yaml index 10d65d5b65..89d7d589de 100644 --- a/charts/galley/templates/tests/configmap.yaml +++ b/charts/galley/templates/tests/configmap.yaml @@ -2,6 +2,9 @@ apiVersion: v1 kind: ConfigMap metadata: name: "galley-integration" + annotations: + "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 7d6efa6e4b..8c7df48e70 100644 --- a/charts/galley/templates/tests/galley-integration.yaml +++ b/charts/galley/templates/tests/galley-integration.yaml @@ -2,6 +2,9 @@ apiVersion: v1 kind: Service metadata: name: "galley-integration" + annotations: + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation labels: app: galley-integration chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} @@ -19,7 +22,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 }} @@ -36,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/charts/galley/templates/tests/secret.yaml b/charts/galley/templates/tests/secret.yaml index 74f118d1c2..d58a49c360 100644 --- a/charts/galley/templates/tests/secret.yaml +++ b/charts/galley/templates/tests/secret.yaml @@ -2,6 +2,9 @@ apiVersion: v1 kind: ConfigMap metadata: name: galley-integration-secrets + annotations: + "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 5c398d39e1..6829860247 100644 --- a/charts/gundeck/templates/tests/configmap.yaml +++ b/charts/gundeck/templates/tests/configmap.yaml @@ -2,6 +2,9 @@ apiVersion: v1 kind: ConfigMap metadata: name: "gundeck-integration" + annotations: + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation 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/nginz/values.yaml b/charts/nginz/values.yaml index 35c51fdfe2..2625545fd9 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 @@ -500,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/charts/spar/templates/tests/configmap.yaml b/charts/spar/templates/tests/configmap.yaml index e446ee7bdd..2eb1966099 100644 --- a/charts/spar/templates/tests/configmap.yaml +++ b/charts/spar/templates/tests/configmap.yaml @@ -2,6 +2,9 @@ apiVersion: v1 kind: ConfigMap metadata: name: "spar-integration" + annotations: + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation 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/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/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/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/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..b0b24151d3 --- /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](https://linkedin.com/in/theumar9) for reporting this vulnerability. diff --git a/docs/src/understand/searchability.md b/docs/src/understand/searchability.md index 083faa030f..2f37fdcc09 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 `SearchVisibilityInbound` 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: @@ -281,15 +222,53 @@ 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 ``` +### 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 +``` + 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/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/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/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/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' 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/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/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) diff --git a/libs/wire-api/src/Wire/API/OAuth.hs b/libs/wire-api/src/Wire/API/OAuth.hs index 77084a8036..1f2903f0db 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,37 +165,72 @@ instance ToSchema OAuthResponseType where [ element "code" OAuthResponseTypeCode ] +-- | 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 - = ConversationCreate - | ConversationCodeCreate - | SelfRead + = WriteConversation + | WriteConversationCode + | ReadSelf + | ReadFeatureConfigs deriving (Eq, Show, Generic, Ord) + deriving (Arbitrary) via (GenericUniform OAuthScope) 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 + +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. +class IsOAuthScopes scopes where + showOAuthScopeList :: Text + allowOAuthScopeList :: Set.Set OAuthScope -> Bool + +instance IsOAuthScopes '[] where + showOAuthScopeList = mempty + allowOAuthScopeList _ = False + +instance (IsOAuthScope scope, IsOAuthScopes scopes) => IsOAuthScopes (scope ': scopes) where + 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 - ConversationCreate -> "conversation:create" - ConversationCodeCreate -> "conversation-code:create" - SelfRead -> "self:read" + WriteConversation -> "write:conversation" + WriteConversationCode -> "write:conversation_code" + ReadSelf -> "read:self" + ReadFeatureConfigs -> "read:feature_configs" 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 + "read:feature_configs" -> pure ReadFeatureConfigs _ -> fail "invalid scope" newtype OAuthScopes = OAuthScopes {unOAuthScopes :: Set OAuthScope} @@ -426,8 +463,8 @@ hcsSub = >=> preview string >=> either (const Nothing) pure . parseIdFromText -hasScope :: OAuthScope -> OAuthClaimsSet -> Bool -hasScope s claims = s `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/API.hs b/libs/wire-api/src/Wire/API/Routes/API.hs index 607933e2ed..0500983e1b 100644 --- a/libs/wire-api/src/Wire/API/Routes/API.hs +++ b/libs/wire-api/src/Wire/API/Routes/API.hs @@ -17,18 +17,21 @@ module Wire.API.Routes.API ( API, - hoistAPIHandler, hoistAPI, + hoistAPIHandler, mkAPI, mkNamedAPI, + hoistServerWithDomain, + hoistServerWithDomainAndJwk, (<@>), ServerEffect (..), ServerEffects (..), - hoistServerWithDomain, ) where +import Crypto.JOSE (JWK) import Data.Domain +import Data.Kind (Type) import Data.Proxy import Imports import Polysemy @@ -47,15 +50,29 @@ 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 (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 = API . Named . unAPI . mkAPI @r0 @api +mkNamedAPI = mkNamedAPIWithContext @'[Domain, Maybe JWK] + +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 @@ -77,13 +94,22 @@ hoistServerWithDomain :: ServerT api n hoistServerWithDomain = hoistServerWithContext (Proxy @api) (Proxy @'[Domain]) +-- | Like `hoistServerWithDomain`, but with a additional 'Maybe JWK' 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] => + HasServer api '[Domain, Maybe JWK] => (forall x. Sem r x -> n x) -> API api r -> ServerT api n -hoistAPIHandler f = hoistServerWithDomain @api f . unAPI +hoistAPIHandler f = hoistServerWithContext (Proxy @api) (Proxy @'[Domain, Maybe JWK]) f . unAPI hoistAPI :: forall api1 api2 r1 r2. diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index f9873d6efb..08411bd492 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -30,16 +30,20 @@ module Wire.API.Routes.Public ZBot, ZConversation, ZProvider, - ZUserOrOAuth, + + -- * OAuth combinators + ZOauthUser, + ZOAuthLocalUser, -- * Swagger combinators OmitDocs, ) 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) import Data.Domain import Data.Either.Combinators import qualified Data.HashMap.Strict.InsOrd as InsOrdHashMap @@ -50,8 +54,9 @@ 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) +import GHC.TypeLits (KnownSymbol, symbolVal) import Imports hiding (All, exp, head) import Network.Wai import qualified Network.Wai as Wai @@ -166,7 +171,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 +181,54 @@ type InternalAuth ztype opts = (ZHeader ztype) (ZParam ztype) -type ZLocalUser = ZAuthServant 'ZLocalAuthUser InternalAuthDefOpts +type ZLocalUser = ZAuthServant 'ZLocalAuthUser InternalAuthDefOpts 'Nothing + +type ZUser = ZAuthServant 'ZAuthUser InternalAuthDefOpts 'Nothing + +type ZClient = ZAuthServant 'ZAuthClient InternalAuthDefOpts 'Nothing -type ZUser = ZAuthServant 'ZAuthUser InternalAuthDefOpts +type ZConn = ZAuthServant 'ZAuthConn InternalAuthDefOpts 'Nothing -type ZClient = ZAuthServant 'ZAuthClient InternalAuthDefOpts +type ZBot = ZAuthServant 'ZAuthBot InternalAuthDefOpts 'Nothing -type ZConn = ZAuthServant 'ZAuthConn InternalAuthDefOpts +type ZConversation = ZAuthServant 'ZAuthConv InternalAuthDefOpts 'Nothing -type ZBot = ZAuthServant 'ZAuthBot InternalAuthDefOpts +type ZProvider = ZAuthServant 'ZAuthProvider InternalAuthDefOpts 'Nothing -type ZConversation = ZAuthServant 'ZAuthConv InternalAuthDefOpts +type ZOptUser = ZAuthServant 'ZAuthUser '[Servant.Optional, Servant.Strict] 'Nothing -type ZProvider = ZAuthServant 'ZAuthProvider InternalAuthDefOpts +type ZOptClient = ZAuthServant 'ZAuthClient '[Servant.Optional, Servant.Strict] 'Nothing -type ZOptUser = ZAuthServant 'ZAuthUser '[Servant.Optional, Servant.Strict] +type ZOptConn = ZAuthServant 'ZAuthConn '[Servant.Optional, Servant.Strict] 'Nothing -type ZOptClient = ZAuthServant 'ZAuthClient '[Servant.Optional, Servant.Strict] +type ZOAuthLocalUser (scopes :: [OAuthScope]) = ZAuthServant 'ZLocalAuthUser InternalAuthDefOpts ('Just scopes) -type ZOptConn = ZAuthServant 'ZAuthConn '[Servant.Optional, Servant.Strict] +type ZOauthUser (scopes :: [OAuthScope]) = ZAuthServant 'ZAuthUser InternalAuthDefOpts ('Just scopes) -instance HasSwagger api => HasSwagger (ZAuthServant 'ZAuthUser _opts :> api) where +instance + (HasSwagger api, IsOAuthScopes scopes, scopes ~ (s ': ss), Typeable ztype) => + 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) + & security <>~ [SecurityRequirement $ InsOrdHashMap.singleton "OAuth" []] + & addScopeDescription + 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 \\'.\ + \Further reading: https://docs.wire.com/how-to/install/oauth.html" + } + + addScopeDescription :: Swagger -> Swagger + addScopeDescription = allOperations . description %~ Just . (<> "OAuth scope(s): " <> showOAuthScopeList @scopes) . fold + +instance (HasSwagger api, Typeable ztype) => HasSwagger (ZAuthServant (ztype :: ZType) _opts 'Nothing :> api) where toSwagger _ = toSwagger (Proxy @api) & securityDefinitions <>~ SecurityDefinitions (InsOrdHashMap.singleton "ZAuth" secScheme) @@ -205,23 +237,112 @@ instance HasSwagger api => HasSwagger (ZAuthServant 'ZAuthUser _opts :> api) whe 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 \\'.\ + \\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 HasSwagger api => HasSwagger (ZAuthServant 'ZLocalAuthUser opts :> api) where - toSwagger _ = toSwagger (Proxy @(ZAuthServant 'ZAuthUser opts :> 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) +-- | Handle routes that support both ZAuth and OAuth, tried in that order (scopes is Just). instance - {-# OVERLAPPABLE #-} - HasSwagger api => - HasSwagger (ZAuthServant ztype _opts :> api) + ( IsZType ztype ctx, + HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, + HasContextEntry ctx (Maybe JWK), + SBoolI (FoldLenient opts), + SBoolI (FoldRequired opts), + HasServer api ctx, + IsOAuthScopes (scopes :: [OAuthScope]), + ZParam ztype ~ Id a + ) => + HasServer (ZAuthServant ztype opts ('Just scopes) :> api) ctx where - toSwagger _ = toSwagger (Proxy @api) + type + ServerT (ZAuthServant ztype opts ('Just scopes) :> api) m = + 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, + IsOAuthScopes scopes + ) => + Proxy (ZAuthServant ztype opts ('Just scopes) :> api) -> + Context ctx -> + 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 @scopes @ctx ctx (tokenType @ztype)))) + + hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s +checkType' :: + forall ztype scopes ctx a. + ( IsZType ztype ctx, + HasContextEntry ctx (Maybe JWK), + IsOAuthScopes scopes, + ZParam ztype ~ Id a + ) => + Context ctx -> + Maybe ByteString -> + Request -> + 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 + (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 (ZParam ztype) + zauth = maybe (delayedFailFatal error403) pure . fromByteString @(ZParam ztype) + + 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 + where + tryOAuth :: DelayedIO (Either ServerError (ZParam ztype)) + tryOAuth = do + let jwkOrError = maybeToRight jwtError mJwk + let tokenOrError = mapLeft invalidOAuthToken $ parseHeader h + 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 ZAuth, but not OAuth (scopes is Nothing). instance ( IsZType ztype ctx, HasContextEntry (ctx .++ DefaultErrorFormatters) ErrorFormatters, @@ -229,37 +350,48 @@ 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)) ctx ( fmap (. mapRequestArgument @opts (qualifyZParam @ztype ctx)) - (addAcceptCheck subserver (withRequest (checkType (tokenType @ztype)))) + (addAuthCheck (fmap const 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 -instance RoutesToPaths api => RoutesToPaths (ZAuthServant ztype opts :> api) 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 error403 + _ -> pure () + +error403 :: ServerError +error403 = + ServerError + { errHTTPCode = 403, + errReasonPhrase = "Access denied", + errBody = "", + errHeaders = [] + } + +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 @@ -288,63 +420,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) - -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 @@ -356,6 +431,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"} 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..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" - :> ZUserOrOAuth 'SelfRead + :> ZOauthUser '[ 'ReadSelf] :> "self" :> Get '[JSON] SelfProfile ) 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..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 @@ -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,8 +339,8 @@ type ConversationAPI = :> CanThrow OperationDenied :> CanThrow 'MissingLegalholdConsent :> Description "This returns 201 when a new conversation is created, and 200 when the conversation already existed" - :> ZLocalUser - :> ZConn + :> ZOAuthLocalUser '[ 'WriteConversation] + :> ZOptConn :> "conversations" :> VersionedReqBody 'V2 '[Servant.JSON] NewConv :> ConversationV2Verb @@ -357,8 +358,8 @@ type ConversationAPI = :> CanThrow OperationDenied :> CanThrow 'MissingLegalholdConsent :> Description "This returns 201 when a new conversation is created, and 200 when the conversation already existed" - :> ZLocalUser - :> ZConn + :> ZOAuthLocalUser '[ 'WriteConversation] + :> ZOptConn :> "conversations" :> ReqBody '[Servant.JSON] NewConv :> ConversationVerb 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/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/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/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/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..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 @@ -308,6 +310,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 +369,6 @@ in paths = commonTools ++ [ (pkgs.haskell-language-server.override { supportedGhcVersions = [ "92" ]; }) pkgs.ghcid - pkgs.cfssl pkgs.kind pkgs.netcat pkgs.niv 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/docs/swagger.md b/services/brig/docs/swagger.md index db6210b536..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. @@ -37,6 +35,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. 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 b07b7e1d45..c691e95656 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 @@ -52,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 (Domain (..)) import Data.Id (RequestId (..)) import Data.Metrics.AWS (gaugeTokenRemaing) import qualified Data.Metrics.Servant as Metrics @@ -71,7 +69,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) @@ -81,6 +79,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 @@ -147,21 +146,13 @@ mkApp o = do (Proxy @ServantCombinedAPI) (mJwk :. customFormatters :. localDomain :. Servant.EmptyContext) ( docsAPI - :<|> hoistServerWithContext' @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 :<|> 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/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/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") diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index 20e1b57b0c..c7c05d7ccb 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -18,10 +18,10 @@ module API.OAuth where +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 @@ -34,7 +34,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 +52,17 @@ 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)) +import Wire.Sem.Jwk (readJwk) -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, @@ -96,6 +100,19 @@ tests m db b n o = do test m "no token" $ testAccessResourceNoToken b, 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, test m "refresh access token - success" $ testRefreshTokenRetrieveAccessToken o b, @@ -129,7 +146,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 @@ -171,7 +188,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 @@ -200,7 +217,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 @@ -212,7 +229,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 @@ -224,7 +241,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 @@ -236,7 +253,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 @@ -249,7 +266,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 @@ -261,7 +278,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 @@ -301,7 +318,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 @@ -324,7 +341,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 @@ -344,7 +361,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 @@ -354,7 +371,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 @@ -368,7 +385,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 @@ -395,7 +412,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 @@ -413,7 +430,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 @@ -471,7 +488,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 @@ -486,7 +503,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 @@ -503,7 +520,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 @@ -516,7 +533,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 <- @@ -535,7 +552,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 @@ -549,7 +566,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 @@ -563,7 +580,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 @@ -578,7 +595,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 @@ -592,7 +609,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 @@ -643,9 +660,74 @@ testRevokeApplicationAccountAccess brig = do liftIO $ assertEqual "apps" 0 (length apps) _ -> liftIO $ assertFailure "unexpected number of apps" +testWriteConversationSuccessInternal :: Brig -> Galley -> Http () +testWriteConversationSuccessInternal brig galley = do + (uid, tid) <- Team.createUserWithTeam brig + accessToken <- getAccessTokenForScope brig uid WriteConversation + createTeamConv galley zOAuthHeader (oatAccessToken accessToken) tid "oauth test group" !!! do + const 201 === statusCode + +testWriteConversationSuccessNginz :: Brig -> Nginz -> Http () +testWriteConversationSuccessNginz brig nginz = do + (uid, tid) <- Team.createUserWithTeam brig + 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) -> + 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 + ) + +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/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/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 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 diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 04be5d47a9..4ff0a6c702 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -227,6 +227,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..3aa993b83b 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/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 :: 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 diff --git a/services/galley/src/Galley/Options.hs b/services/galley/src/Galley/Options.hs index 844ca39064..60511985eb 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..5978492bbc 100644 --- a/services/galley/src/Galley/Run.hs +++ b/services/galley/src/Galley/Run.hs @@ -64,6 +64,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 @@ -89,7 +90,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 +103,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 + ( hoistAPIHandler @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/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. 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"} diff --git a/services/nginz/integration-test/conf/nginz/nginx.conf b/services/nginz/integration-test/conf/nginz/nginx.conf index 4f7e4728b8..9803fede07 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; } @@ -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; } 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