diff --git a/CHANGELOG.md b/CHANGELOG.md index d56150a7ff..c7c54567f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,62 @@ +# [2022-07-19] (Chart Release 4.21.0) + +## Release notes + + +* Users of the (currently alpha) coturn Helm chart must **manually update + their configuration** due to changes in how the chart handles authentication + secrets. Please see below for further details. (#2553) + + +## API changes + + +* The response to POST /mls/messages adds a timestamp (#2560) + + +## Features + + +* charts/wire-server: default log format everywhere to StructuredJSON format (introduced in #1951 and #1959) (#2559) + +* The coturn chart now supports multiple authentication secrets, which permits + multiple backend instances to use the same TURN servers without needing to + share authentication secrets between the backend instances. + + Correspondingly, the `.Values.secrets.zrestSecret` configuration option, which + took a single authentication secret as its argument, has been replaced with the + option `.Values.secrets.zrestSecrets` (note spelling!), which instead takes a + *list* of authentication secrets as its argument. (#2553) + +* Add support for bare MLS proposals (#2436) + + +## Bug fixes and other updates + + +* Fix a bug in charts/cannon. It's now possible to use a custom TLS certificate when enabling cannon's nginz sidecar container. (Previously only letsencrypt certificates worked, and were tested) (#2558) + +* Minor fixes in helmcharts: + - charts/nginz: Rate limit SSO endpoints less + - charts/nginz: Ensure rate limiting isn't commented out + - charts/galley: Honour .setttings.httpPoolSize + - charts/galley: Fix typo in settings.featureFlags.validateSAMLEmails + - charts/gundeck: Remove aws.connectionLimit + - charts/brig: Fix default brandLabelUrl and remove brandLabel (#2563) + + +## Internal changes + + +* Port brig UserHandle API to servant (#2556) + +* Bump timeout for integration tests to 15 minutes (from 10 minutes), as 10 minutes is no longer enough. (#2570) + +* Internal endpoints to `PATCH` feature status (#2555) + +* Change the proposal hold time to 28 days (#2568) + + # [2022-07-12] (Chart Release 4.19.0) ## Release notes diff --git a/cassandra-schema.cql b/cassandra-schema.cql index 7f4fb97cbf..59b10823e8 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -35,22 +35,13 @@ CREATE TABLE galley_test.meta ( AND read_repair_chance = 0.0 AND speculative_retry = '99PERCENTILE'; -CREATE TABLE galley_test.conversation ( - conv uuid PRIMARY KEY, - access set, - access_role int, - access_roles_v2 set, - creator uuid, - deleted boolean, - epoch bigint, - group_id blob, - message_timer bigint, - name text, - protocol int, - receipt_mode int, +CREATE TABLE galley_test.team_conv ( team uuid, - type int -) WITH bloom_filter_fp_chance = 0.1 + conv uuid, + managed boolean, + PRIMARY KEY (team, conv) +) WITH CLUSTERING ORDER BY (conv ASC) + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} @@ -456,13 +447,23 @@ CREATE TABLE galley_test.clients ( AND read_repair_chance = 0.0 AND speculative_retry = '99PERCENTILE'; -CREATE TABLE galley_test.team_conv ( +CREATE TABLE galley_test.conversation ( + conv uuid PRIMARY KEY, + access set, + access_role int, + access_roles_v2 set, + cipher_suite int, + creator uuid, + deleted boolean, + epoch bigint, + group_id blob, + message_timer bigint, + name text, + protocol int, + receipt_mode int, team uuid, - conv uuid, - managed boolean, - PRIMARY KEY (team, conv) -) WITH CLUSTERING ORDER BY (conv ASC) - AND bloom_filter_fp_chance = 0.1 + type int +) WITH bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} @@ -543,6 +544,28 @@ CREATE TABLE galley_test.billing_team_member ( AND read_repair_chance = 0.0 AND speculative_retry = '99PERCENTILE'; +CREATE TABLE galley_test.mls_proposal_refs ( + group_id blob, + epoch bigint, + ref blob, + proposal blob, + PRIMARY KEY (group_id, epoch, ref) +) WITH CLUSTERING ORDER BY (epoch ASC, ref ASC) + AND bloom_filter_fp_chance = 0.01 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + CREATE KEYSPACE gundeck_test WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = true; CREATE TABLE gundeck_test.push ( diff --git a/charts/brig/templates/configmap.yaml b/charts/brig/templates/configmap.yaml index 1b7a529d59..5ba1ea5e0f 100644 --- a/charts/brig/templates/configmap.yaml +++ b/charts/brig/templates/configmap.yaml @@ -10,8 +10,7 @@ metadata: data: {{- with .Values.config }} brig.yaml: | - logNetStrings: True # log using netstrings encoding: - # http://cr.yp.to/proto/netstrings.txt + logNetStrings: {{ .logNetStrings }} logFormat: {{ .logFormat }} logLevel: {{ .logLevel }} diff --git a/charts/brig/values.yaml b/charts/brig/values.yaml index 2e07ad7090..26dd629905 100644 --- a/charts/brig/values.yaml +++ b/charts/brig/values.yaml @@ -17,7 +17,8 @@ metrics: enable: false config: logLevel: Info - logFormat: JSON + logFormat: StructuredJSON + logNetStrings: false cassandra: host: aws-cassandra elasticsearch: @@ -38,8 +39,7 @@ config: templateBranding: brand: Wire brandUrl: https://wire.com - brandLabel: wire.com - brandLabelUrl: https://wire.com + brandLabelUrl: wire.com brandLogoUrl: https://wire.com/p/img/email/logo-email-black.png brandService: Wire Service Provider copyright: © WIRE SWISS GmbH diff --git a/charts/cannon/templates/conf/_nginx.conf.tpl b/charts/cannon/templates/conf/_nginx.conf.tpl index 2b20ba84be..c6a51b83a5 100644 --- a/charts/cannon/templates/conf/_nginx.conf.tpl +++ b/charts/cannon/templates/conf/_nginx.conf.tpl @@ -142,8 +142,8 @@ http { # Rate Limiting # - limit_req_zone $rate_limited_by_zuser zone=reqs_per_user:12m rate=10r/s; - limit_req_zone $rate_limited_by_addr zone=reqs_per_addr:12m rate=5r/m; + limit_req_zone $rate_limited_by_zuser zone=reqs_per_user:12m rate={{ .Values.nginx_conf.rate_limit_reqs_per_user }}; + limit_req_zone $rate_limited_by_addr zone=reqs_per_addr:12m rate={{ .Values.nginx_conf.rate_limit_reqs_per_addr }}; {{- range $limit := .Values.nginx_conf.user_rate_limit_request_zones }} {{ $limit }} diff --git a/charts/cannon/templates/configmap.yaml b/charts/cannon/templates/configmap.yaml index 17a00a5c7e..256dae79e4 100644 --- a/charts/cannon/templates/configmap.yaml +++ b/charts/cannon/templates/configmap.yaml @@ -1,8 +1,9 @@ apiVersion: v1 data: cannon.yaml: | - logFormat: StructuredJSON + logFormat: {{ .Values.config.logFormat }} logLevel: {{ .Values.config.logLevel }} + logNetStrings: {{ .Values.config.logNetStrings }} cannon: host: 0.0.0.0 diff --git a/charts/cannon/templates/nginz-certificate-secret.yaml b/charts/cannon/templates/nginz-certificate-secret.yaml index 8394ebd8c0..05e552ce94 100644 --- a/charts/cannon/templates/nginz-certificate-secret.yaml +++ b/charts/cannon/templates/nginz-certificate-secret.yaml @@ -10,6 +10,6 @@ metadata: heritage: "{{ .Release.Service }}" type: kubernetes.io/tls data: - tls.crt: {{ .Values.secrets.nginz.tls.crt }} - tls.key: {{ .Values.secrets.nginz.tls.key }} + tls.crt: {{ .Values.secrets.nginz.tls.crt | b64enc | quote }} + tls.key: {{ .Values.secrets.nginz.tls.key | b64enc | quote }} {{- end }} diff --git a/charts/cannon/values.yaml b/charts/cannon/values.yaml index 16c77ee347..f84dba0de2 100644 --- a/charts/cannon/values.yaml +++ b/charts/cannon/values.yaml @@ -9,6 +9,8 @@ nginzImage: pullPolicy: IfNotPresent config: logLevel: Info + logFormat: StructuredJSON + logNetStrings: false # See also the section 'Controlling the speed of websocket draining during # cannon pod replacement' in docs/how-to/install/configuration-options.rst @@ -33,6 +35,8 @@ nginx_conf: worker_rlimit_nofile: 131072 worker_connections: 65536 disabled_paths: [] + rate_limit_reqs_per_user: "10r/s" + rate_limit_reqs_per_addr: "5r/m" user_rate_limit_request_zones: [] tls: diff --git a/charts/cargohold/templates/configmap.yaml b/charts/cargohold/templates/configmap.yaml index 22390d9a0b..5ceadf367c 100644 --- a/charts/cargohold/templates/configmap.yaml +++ b/charts/cargohold/templates/configmap.yaml @@ -4,8 +4,9 @@ metadata: name: "cargohold" data: cargohold.yaml: | - logNetStrings: True # log using netstrings encoding: http://cr.yp.to/proto/netstrings.txt + logFormat: {{ .Values.config.logFormat }} logLevel: {{ .Values.config.logLevel }} + logNetStrings: {{ .Values.config.logNetStrings }} cargohold: host: 0.0.0.0 diff --git a/charts/cargohold/values.yaml b/charts/cargohold/values.yaml index b9cc40ff5c..77198b778d 100644 --- a/charts/cargohold/values.yaml +++ b/charts/cargohold/values.yaml @@ -17,6 +17,8 @@ resources: cpu: "500m" config: logLevel: Info + logFormat: StructuredJSON + logNetStrings: false enableFederator: false # keep enableFederator default in sync with brig and galley chart's config.enableFederator as well as wire-server chart's tag.federator aws: region: "eu-west-1" diff --git a/charts/coturn/templates/configmap-coturn-conf-template.yaml b/charts/coturn/templates/configmap-coturn-conf-template.yaml index 61f5209e62..6d10d7718b 100644 --- a/charts/coturn/templates/configmap-coturn-conf-template.yaml +++ b/charts/coturn/templates/configmap-coturn-conf-template.yaml @@ -15,11 +15,6 @@ data: ## don't turn on coturn's cli. no-cli - ## authentication setup - # FUTUREWORK: enable support for multiple secrets? - zrest - static-auth-secret=__COTURN_SECRET__ - ## turn, stun. listening-ip=__COTURN_EXT_IP__ listening-port={{ .Values.coturnTurnListenPort }} @@ -73,3 +68,8 @@ data: denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff # FUTUREWORK: expose customisable access control settings. + + ## authentication setup + zrest + ## static authentication secrets will be added below this line when the + ## runtime configuration is generated. diff --git a/charts/coturn/templates/secret.yaml b/charts/coturn/templates/secret.yaml index 0980cf7487..f85a13153a 100644 --- a/charts/coturn/templates/secret.yaml +++ b/charts/coturn/templates/secret.yaml @@ -1,3 +1,8 @@ +{{- if or (not .Values.secrets) (not .Values.secrets.zrestSecrets) }} +{{- fail "Secrets are not defined" }} +{{- else if eq (len .Values.secrets.zrestSecrets) 0 }} +{{- fail "At least one authentication secret must be defined" }} +{{- else }} apiVersion: v1 kind: Secret metadata: @@ -7,6 +12,8 @@ metadata: release: "{{ .Release.Name }}" heritage: "{{ .Release.Service }}" type: Opaque -data: - zrest_secret.txt: {{ .Values.secrets.zrestSecret | b64enc | quote }} - +stringData: + zrest_secret.txt: | + {{- range .Values.secrets.zrestSecrets }}{{ . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/coturn/templates/statefulset.yaml b/charts/coturn/templates/statefulset.yaml index 02cdd12205..a2a4fe1198 100644 --- a/charts/coturn/templates/statefulset.yaml +++ b/charts/coturn/templates/statefulset.yaml @@ -90,8 +90,8 @@ spec: - | set -e EXTERNAL_IP=$(cat /external-ip/ip) - ZREST_SECRET="$(cat /secrets/zrest_secret.txt)" - sed -Ee "s;__COTURN_EXT_IP__;$EXTERNAL_IP;g" -e "s;__COTURN_POD_IP__;$POD_IP;g" -e "s;__COTURN_SECRET__;$ZREST_SECRET;" /coturn-template/coturn.conf.template > /coturn-config/turnserver.conf + sed -Ee "s;__COTURN_EXT_IP__;$EXTERNAL_IP;g" -e "s;__COTURN_POD_IP__;$POD_IP;g" /coturn-template/coturn.conf.template > /coturn-config/turnserver.conf + sed -Ee 's/^/static-auth-secret=/' /secrets/zrest_secret.txt >> /coturn-config/turnserver.conf exec /usr/bin/turnserver -c /coturn-config/turnserver.conf {{- if .Values.coturnGracefulTermination }} lifecycle: diff --git a/charts/federator/templates/configmap.yaml b/charts/federator/templates/configmap.yaml index 287e4a9ac7..cbfd53e9e7 100644 --- a/charts/federator/templates/configmap.yaml +++ b/charts/federator/templates/configmap.yaml @@ -37,8 +37,7 @@ data: {{- with .Values.config }} - logNetStrings: True # log using netstrings encoding: - # http://cr.yp.to/proto/netstrings.txt + logNetStrings: {{ .logNetStrings }} logFormat: {{ .logFormat }} logLevel: {{ .logLevel }} diff --git a/charts/federator/values.yaml b/charts/federator/values.yaml index 9e0439e596..8e8a777e6a 100644 --- a/charts/federator/values.yaml +++ b/charts/federator/values.yaml @@ -24,7 +24,8 @@ resources: cpu: "500m" config: logLevel: Info - logFormat: JSON + logFormat: StructuredJSON + logNetStrings: false optSettings: # Defaults to using system CA store in the federator image for making # connections to remote federators. diff --git a/charts/galley/templates/configmap.yaml b/charts/galley/templates/configmap.yaml index 31070a8ab7..664868ef21 100644 --- a/charts/galley/templates/configmap.yaml +++ b/charts/galley/templates/configmap.yaml @@ -5,10 +5,9 @@ metadata: data: {{- with .Values.config }} galley.yaml: | - logNetStrings: True # log using netstrings encoding: - # http://cr.yp.to/proto/netstrings.txt - logLevel: {{ .logLevel }} logFormat: {{ .logFormat }} + logLevel: {{ .logLevel }} + logNetStrings: {{ .logNetStrings }} galley: host: 0.0.0.0 @@ -48,7 +47,7 @@ data: {{- end }} settings: - httpPoolSize: 128 + httpPoolSize: {{ .settings.httpPoolSize }} intraListing: false maxTeamSize: {{ .settings.maxTeamSize }} maxConvSize: {{ .settings.maxConvSize }} @@ -79,9 +78,9 @@ data: searchVisibilityInbound: {{- toYaml .settings.featureFlags.searchVisibilityInbound | nindent 10 }} {{- end }} - {{- if .settings.featureFlags.validateSAMLemails }} - validateSAMLemails: - {{- toYaml .settings.featureFlags.validateSAMLemails | nindent 10 }} + {{- if .settings.featureFlags.validateSAMLEmails }} + validateSAMLEmails: + {{- toYaml .settings.featureFlags.validateSAMLEmails | nindent 10 }} {{- end }} {{- if .settings.featureFlags.appLock }} appLock: diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index 5332e3ea9d..76406d88aa 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -18,12 +18,14 @@ resources: cpu: "500m" config: logLevel: Info - logFormat: JSON + logFormat: StructuredJSON + logNetStrings: false cassandra: host: aws-cassandra replicaCount: 3 enableFederator: false # keep enableFederator default in sync with brig and cargohold chart's config.enableFederator as well as wire-server chart's tag.federator settings: + httpPoolSize: 128 maxTeamSize: 10000 maxConvSize: 500 # Before making indexedBillingTeamMember true while upgrading, please diff --git a/charts/gundeck/templates/configmap.yaml b/charts/gundeck/templates/configmap.yaml index 44646a6c18..d2b9a18ccc 100644 --- a/charts/gundeck/templates/configmap.yaml +++ b/charts/gundeck/templates/configmap.yaml @@ -5,8 +5,9 @@ metadata: data: {{- with .Values.config }} gundeck.yaml: | - logNetStrings: True # log using netstrings encoding: http://cr.yp.to/proto/netstrings.txt + logFormat: {{ .logFormat }} logLevel: {{ .logLevel }} + logNetStrings: {{ .logNetStrings }} gundeck: host: 0.0.0.0 @@ -43,7 +44,6 @@ data: arnEnv: {{ .arnEnv }} sqsEndpoint: {{ .sqsEndpoint | quote }} snsEndpoint: {{ .snsEndpoint | quote }} - connectionLimit: 256 {{- end }} settings: diff --git a/charts/gundeck/values.yaml b/charts/gundeck/values.yaml index 9e8f022004..d9b10037e2 100644 --- a/charts/gundeck/values.yaml +++ b/charts/gundeck/values.yaml @@ -17,6 +17,8 @@ resources: cpu: "500m" config: logLevel: Info + logFormat: StructuredJSON + logNetStrings: false cassandra: host: aws-cassandra redis: diff --git a/charts/nginz/templates/conf/_nginx.conf.tpl b/charts/nginz/templates/conf/_nginx.conf.tpl index 1a6501429b..098d1f9366 100644 --- a/charts/nginz/templates/conf/_nginx.conf.tpl +++ b/charts/nginz/templates/conf/_nginx.conf.tpl @@ -142,8 +142,8 @@ http { # Rate Limiting # - limit_req_zone $rate_limited_by_zuser zone=reqs_per_user:12m rate=10r/s; - limit_req_zone $rate_limited_by_addr zone=reqs_per_addr:12m rate=5r/m; + limit_req_zone $rate_limited_by_zuser zone=reqs_per_user:12m rate={{ .Values.nginx_conf.rate_limit_reqs_per_user }}; + limit_req_zone $rate_limited_by_addr zone=reqs_per_addr:12m rate={{ .Values.nginx_conf.rate_limit_reqs_per_addr }}; {{- range $limit := .Values.nginx_conf.user_rate_limit_request_zones }} {{ $limit }} @@ -246,7 +246,7 @@ http { {{- if ($location.basic_auth) }} auth_basic "Restricted"; auth_basic_user_file {{ $.Values.nginx_conf.basic_auth_file }}; - {{- end -}} + {{- end }} {{- if ($location.disable_zauth) }} zauth off; @@ -254,17 +254,18 @@ http { # If zauth is off, limit by remote address if not part of limit exemptions {{- if ($location.unlimited_requests_endpoint) }} # Note that this endpoint has no rate limit - {{- else -}} - limit_req zone=reqs_per_addr burst=5 nodelay; + {{- else }} + {{- if not (hasKey $location "specific_user_rate_limit") }} + limit_req zone=reqs_per_addr burst=10 nodelay; limit_conn conns_per_addr 20; - {{- end -}} - {{- else }} - - {{- if hasKey $location "specific_user_rate_limit" }} - limit_req zone={{ $location.specific_user_rate_limit }} nodelay; + {{- end }} {{- end }} {{- end }} + {{- if hasKey $location "specific_user_rate_limit" }} + limit_req zone={{ $location.specific_user_rate_limit }}{{ if hasKey $location "specific_user_rate_limit_burst" }} burst={{ $location.specific_user_rate_limit_burst }}{{ end }} nodelay; + {{- end }} + if ($request_method = 'OPTIONS') { add_header 'Access-Control-Allow-Methods' "GET, POST, PUT, DELETE, OPTIONS"; add_header 'Access-Control-Allow-Headers' "$http_access_control_request_headers, DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type"; diff --git a/charts/nginz/values.yaml b/charts/nginz/values.yaml index 471bb1b14f..83082728e4 100644 --- a/charts/nginz/values.yaml +++ b/charts/nginz/values.yaml @@ -56,11 +56,15 @@ nginx_conf: - /search/top - /search/common + rate_limit_reqs_per_user: "10r/s" + rate_limit_reqs_per_addr: "5r/m" + # This value must be a list of strings. Each string is copied verbatim into # the nginx.conf after the default 'limit_req_zone' directives. This should be # used to create request zones which can then be specified in # 'upstreams...specific_user_rate_limit'. user_rate_limit_request_zones: + - limit_req_zone $rate_limited_by_addr zone=reqs_per_addr_sso:12m rate=50r/s; - limit_req_zone $rate_limited_by_zuser zone=reqs_per_user_signatures:12m rate=10r/m; # The origins from which we allow CORS requests. These are combined with @@ -502,20 +506,28 @@ nginx_conf: - all disable_zauth: true allow_credentials: true + specific_user_rate_limit: reqs_per_addr_sso + specific_user_rate_limit_burst: "10" - path: /sso/finalize-login envs: - all disable_zauth: true allow_credentials: true + specific_user_rate_limit: reqs_per_addr_sso + specific_user_rate_limit_burst: "10" - path: /sso envs: - all disable_zauth: true + specific_user_rate_limit: reqs_per_addr_sso + specific_user_rate_limit_burst: "10" - path: /scim/v2 envs: - all disable_zauth: true allow_credentials: true + specific_user_rate_limit: reqs_per_addr_sso + specific_user_rate_limit_burst: "10" - path: /scim envs: - all diff --git a/charts/proxy/templates/configmap.yaml b/charts/proxy/templates/configmap.yaml index bbce9d594f..5af2ebe10c 100644 --- a/charts/proxy/templates/configmap.yaml +++ b/charts/proxy/templates/configmap.yaml @@ -4,8 +4,9 @@ metadata: name: "proxy" data: proxy.yaml: | - logNetStrings: True # log using netstrings encoding: http://cr.yp.to/proto/netstrings.txt + logFormat: {{ .Values.config.logFormat }} logLevel: {{ .Values.config.logLevel }} + logNetStrings: {{ .Values.config.logNetStrings }} host: 0.0.0.0 port: {{ .Values.service.internalPort }} diff --git a/charts/proxy/values.yaml b/charts/proxy/values.yaml index f7de774ea0..94dbcd70d6 100644 --- a/charts/proxy/values.yaml +++ b/charts/proxy/values.yaml @@ -16,5 +16,7 @@ resources: memory: "512Mi" cpu: "500m" config: - logLevel: Debug + logLevel: Info + logFormat: StructuredJSON + logNetStrings: false proxy: {} diff --git a/charts/spar/templates/configmap.yaml b/charts/spar/templates/configmap.yaml index be553634e2..2a195f7487 100644 --- a/charts/spar/templates/configmap.yaml +++ b/charts/spar/templates/configmap.yaml @@ -5,8 +5,9 @@ metadata: data: {{- with .Values.config }} spar.yaml: | - logNetStrings: True # log using netstrings encoding (see http://cr.yp.to/proto/netstrings.txt) + logFormat: {{ .logFormat }} logLevel: {{ .logLevel }} + logNetStrings: {{ .logNetStrings }} brig: host: brig diff --git a/charts/spar/values.yaml b/charts/spar/values.yaml index 3cdd3b8490..28a9681871 100644 --- a/charts/spar/values.yaml +++ b/charts/spar/values.yaml @@ -21,6 +21,8 @@ config: richInfoLimit: 5000 maxScimTokens: 0 logLevel: Info + logFormat: StructuredJSON + logNetStrings: false maxttlAuthreq: 7200 maxttlAuthresp: 7200 proxy: {} diff --git a/docs/src/how-to/administrate/users.rst b/docs/src/how-to/administrate/users.rst index dbe8906a58..5c21fe4448 100644 --- a/docs/src/how-to/administrate/users.rst +++ b/docs/src/how-to/administrate/users.rst @@ -474,3 +474,113 @@ Close the session and proceed locally to generate the list of all users from tea .. note:: Don't forget to dellete the created csv files after you have downloaded/processed them. + +Create a team using the SCIM API +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you need to create a team manually, maybe because team creation was blocked in the "teams" interface, follow this procedure: + +First download or locate this bash script: `wire-server/deploy/services-demo/create_test_team_scim.sh ` + +Then, run it the following way: + +.. code:: sh + + ./create_test_team_scim.sh -h -s + +Where: + +* In `-h `, replace `` with the base URL for your brig host (for example: `https://brig-host.your-domain.com`, defaults to `http://localhost:8082`) +* In `-s `, replace `` with the base URL for your spar host (for example: `https://spar-host.your-domain.com`, defaults to `http://localhost:8088`) + +You might also need to edit the admin email and admin passwords at lines `48` and `49` of the script. + +To learn more about the different pods and how to identify them, see `this page`. + +You can list your pods with `kubectl get pods --namespace wire`. + +Alternatively, you can run the series of commands manually with `curl`, like this: + +.. code:: sh + + curl -i -s --show-error \ + -XPOST "$BRIG_HOST/i/users" \ + -H'Content-type: application/json' \ + -d'{"email":"$ADMIN_EMAIL","password":"$ADMIN_PASSWORD","name":"$NAME_OF_TEAM","team":{"name":"$NAME_OF_TEAM","icon":"default"}}' + +Where: + +* `$BRIG_HOST` is the base URL for your brig host +* `$ADMIN_EMAIL` is the email for the admin account for the new team +* `$ADMIN_PASSWORD` is the password for the admin account for the new team +* `$NAME_OF_TEAM` is the name of the team newly created + +Out of the result of this command, you will be able to extract an `Admin UUID`, and a `Team UUID`, which you will need later. + +Then run: + +.. code:: sh + + curl -X POST \ + --header 'Content-Type: application/json' \ + --header 'Accept: application/json' \ + -d '{"email":"$ADMIN_EMAIL","password":"$ADMIN_PASSWORD"}' \ + $BRIG_HOST/login'?persist=false' | jq -r .access_token + +Where the values to replace are the same as the command above. + +This command should output an access token, take note of it. + +Then run: + +.. code:: sh + + curl -X POST \ + --header "Authorization: Bearer $ACCESS_TOKEN" \ + --header 'Content-Type: application/json;charset=utf-8' \ + --header 'Z-User: '"$ADMIN_UUID" \ + -d '{ "description": "test '"`date`"'", "password": "'"$ADMIN_PASSWORD"'" }' \ + $SPAR_HOST/scim/auth-tokens + +Where the values to replace are the same as the first command, plus `$ACCESS_TOKEN` is access token you just took note of in the previous command. + +Out of the JSON output of this command, you should be able to extract: + +* A SCIM token (`token` value in the JSON). +* A SCIM token ID (`id` value in the `info` value in the JSON) + +Equiped with those tokens, we move on to the next script, `wire-server/deploy/services-demo/create_team.sh ` + +This script can be run the following way: + +.. code:: sh + + ./create_team.sh -h -o -e -p -v -t -c + +Where: + +* -h : Base URI of brig. default: `http://localhost:8080` +* -o : user display name of the owner of the team to be created. default: "owner name n/a" +* -e : email address of the owner of the team to be created. default: "owner email n/a" +* -p : owner password. default: "owner pass n/a" +* -v : validation code received by email after running the previous script/commands. default: "email code n/a" +* -t : default: "team name n/a" +* -c : default: "USD" + +Alternatively, you can manually run the command: + +.. code:: sh + + curl -i -s --show-error \ + -XPOST "$BRIG_HOST/register" \ + -H'Content-type: application/json' \ + -d'{"name":"$OWNER_NAME","email":"$OWNER_EMAIL","password":"$OWNER_PASSWORD","email_code":"$EMAIL_CODE","team":{"currency":"$TEAM_CURRENCY","icon":"default","name":"$TEAM_NAME"}}' + +Where: + +* `$BRIG_HOST` is the base URL for your brig service +* `$OWNER_NAME` is the name of the of the team to be created +* `$OWNER_PASSWORD` is the password of the owner of the team to be created +* `$EMAIL_CODE` is the validation code received by email after running the previous script/command +* `$TEAM_CURRENCY` is the currency of the team +* `$TEAM_NAME` is the name of the team diff --git a/docs/src/how-to/install/configuration-options.rst b/docs/src/how-to/install/configuration-options.rst index 6948089542..a16df6e641 100644 --- a/docs/src/how-to/install/configuration-options.rst +++ b/docs/src/how-to/install/configuration-options.rst @@ -887,4 +887,54 @@ To change the TeamSearchVisibilityInbound to SearchableByOwnTeam for the team ru curl -XPUT -H 'Content-Type: application/json' -d "{\"status\": \"disabled\"}" http://localhost:9000/i/teams/dcbedf9a-af2a-4f43-9fd5-525953a919e1/features/searchVisibilityInbound - \ No newline at end of file + + +Configuring classified domains +------------------------------ + +As a backend administrator, if you want to control which other backends (identified by their domain) are "classified", + +change the following `galley` configuration in the `value.yaml.gotmpl` file of the wire-server chart: + +.. code:: yaml + + galley: + replicaCount: 1 + config: + ... + featureFlags: + ... + classifiedDomains: + status: enabled + config: + domains: ["domain-that-is-classified.link"] + ... + +This is not only a `backend` configuration, but also a `team` configuration/feature. + +This means that different combinations of configurations will have different results. + +Here is a table to navigate the possible configurations: + ++----------------------------------+---------------------------------------------+-------------------------------+------------------------+---------------------------------+ +| Backend Config enabled/disabled | Backend Config Domains | Team Config enabled/disabled | Team Config Domains | User's view | ++==================================+=============================================+===============================+========================+=================================+ +| Enabled | [domain1.example.com] | Not configured | Not configured | Enabled, [domain1.example.com] | ++----------------------------------+---------------------------------------------+-------------------------------+------------------------+---------------------------------+ +| Enabled | [domain1.example.com][domain1.example.com] | Enabled | Not configured | Enabled, [domain1.example.com] | ++----------------------------------+---------------------------------------------+-------------------------------+------------------------+---------------------------------+ +| Enabled | [domain1.example.com] | Enabled | [domain2.example.com] | Enabled, Undefined | ++----------------------------------+---------------------------------------------+-------------------------------+------------------------+---------------------------------+ +| Enabled | [domain1.example.com] | Disabled | Anything | Undefined | ++----------------------------------+---------------------------------------------+-------------------------------+------------------------+---------------------------------+ +| Disabled | Anything | Not configured | Not configured | Disabled, no domains | ++----------------------------------+---------------------------------------------+-------------------------------+------------------------+---------------------------------+ +| Disabled | Anything | Enabled | [domain2.example.com] | Undefined | ++----------------------------------+---------------------------------------------+-------------------------------+------------------------+---------------------------------+ + +The table assumes the following: + +* When backend level config says that this feature is enabled, it is illegal to not specify domains at the backend level. +* When backend level config says that this feature is disabled, the list of domains is ignored. +* When team level feature is disabled, the accompanying domains are ignored. + diff --git a/hack/bin/integration-test.sh b/hack/bin/integration-test.sh index 94feccde1a..8ffb21d8c7 100755 --- a/hack/bin/integration-test.sh +++ b/hack/bin/integration-test.sh @@ -6,4 +6,4 @@ NAMESPACE=${NAMESPACE:-test-integration} echo "Running integration tests on wire-server" CHART=wire-server -helm test --logs -n "${NAMESPACE}" "${NAMESPACE}-${CHART}" --timeout 600s +helm test --logs -n "${NAMESPACE}" "${NAMESPACE}-${CHART}" --timeout 900s diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 5a49139585..b1c5191414 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -205,6 +205,8 @@ nginz: # NOTE: Web apps are disabled by default allowlisted_origins: [] randomport_allowlisted_origins: [] # default is empty by intention + rate_limit_reqs_per_user: "10r/s" + rate_limit_reqs_per_addr: "100r/s" secrets: basicAuth: "whatever" zAuth: diff --git a/libs/galley-types/test/unit/Test/Galley/Types.hs b/libs/galley-types/test/unit/Test/Galley/Types.hs index e1ac3f2978..3e92614977 100644 --- a/libs/galley-types/test/unit/Test/Galley/Types.hs +++ b/libs/galley-types/test/unit/Test/Galley/Types.hs @@ -100,7 +100,4 @@ instance Arbitrary FeatureFlags where <*> fmap (fmap unlocked) arbitrary where unlocked :: ImplicitLockStatus a -> ImplicitLockStatus a - unlocked = ImplicitLockStatus . setUnlocked . _unImplicitLockStatus - - setUnlocked :: WithStatus a -> WithStatus a - setUnlocked ws = ws {wsLockStatus = Public.LockStatusUnlocked} + unlocked = ImplicitLockStatus . Public.setLockStatus Public.LockStatusUnlocked . _unImplicitLockStatus diff --git a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/ConversationCreated.hs b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/ConversationCreated.hs index f66ede7363..c912a68991 100644 --- a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/ConversationCreated.hs +++ b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/ConversationCreated.hs @@ -28,6 +28,7 @@ import Wire.API.Conversation import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Federation.API.Galley +import Wire.API.MLS.CipherSuite import Wire.API.Provider.Service testObject_ConversationCreated1 :: ConversationCreated ConvId @@ -83,5 +84,5 @@ testObject_ConversationCreated2 = ccNonCreatorMembers = Set.fromList [], ccMessageTimer = Nothing, ccReceiptMode = Nothing, - ccProtocol = ProtocolMLS (ConversationMLSData (GroupId "group") (Epoch 3)) + ccProtocol = ProtocolMLS (ConversationMLSData (GroupId "group") (Epoch 3) MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519) } diff --git a/libs/wire-api-federation/test/golden/testObject_ConversationCreated2.json b/libs/wire-api-federation/test/golden/testObject_ConversationCreated2.json index 29e8fb092a..cca0a6bb7c 100644 --- a/libs/wire-api-federation/test/golden/testObject_ConversationCreated2.json +++ b/libs/wire-api-federation/test/golden/testObject_ConversationCreated2.json @@ -11,6 +11,7 @@ "non_creator_members": [], "orig_user_id": "eed9dea3-5468-45f8-b562-7ad5de2587d0", "protocol": { + "cipher_suite": 1, "epoch": 3, "group_id": "Z3JvdXA=", "protocol": "mls" diff --git a/libs/wire-api/Readme.md b/libs/wire-api/Readme.md new file mode 100644 index 0000000000..ec030f4797 --- /dev/null +++ b/libs/wire-api/Readme.md @@ -0,0 +1,84 @@ +# Multiverb + +We offer the typeclass `AsUnion` to convert a handler return type to a union type +including all possible responses of a `MultiVerb` endpoint. + +Any glue code necessary to convert application types to and from the +canonical `Union` type corresponding to a `MultiVerb` endpoint should be +packaged into an `AsUnion` instance. + +When using flat sum types, you can use Generics to automatically derive this instance, +and for nested types, the following example code should help clarify usage. + +It assumes some understanding of [Data.SOP](https://hackage.haskell.org/package/sop-core-0.5.0.2/docs/Data-SOP.html) + +```haskell +data Success = Success + +data Failure + = InvalidEntry + | AccessDenied + +-- We need a way to map errors to servant and swagger. +instance KnownError (MapError e) => IsSwaggerError (e :: Failure) where + addToSwagger = addStaticErrorToSwagger @(MapError e) + +type instance MapError 'InvalidEntry = 'StaticError 400 "invalid-entry" "Invalid data entered" + +type instance MapError 'AccessDenied = 'StaticError 403 "access-denied" "Access denied" + +data FailureSuccess = SFFailure Failure | SFSuccess Success + +sfToEither :: FailureSuccess -> Either Failure Success +sfToEither (SFFailure b) = Left b +sfToEither (SFSuccess d) = Right d + +sfFromEither :: Either Failure Success -> FailureSuccess +sfFromEither (Left b) = SFFailure b +sfFromEither (Right d) = SFSuccess d + +-- type instance ResponseType Failure = Failure +type instance ResponseType Success = Success + +-- ErrorResponse offers facilities to create errors +type MyErrorResponses = + '[ ErrorResponse 'InvalidEntry, + ErrorResponse 'AccessDenied + ] + +-- Responses is a list of errors and a list of success cases. +type MyResponses = + MyErrorResponses .++ '[Success] + +accessDenied :: DynError +accessDenied = dynError @(MapError 'AccessDenied) + +invalidEntry :: DynError +invalidEntry = dynError @(MapError 'InvalidEntry) + +failToError :: Failure -> NS I (DynError : DynError : xs) +failToError = \case + -- Z . I wraps the first value in the error list. + -- They come from Data.SOP + InvalidEntry -> Z . I $ accessDenied + -- we wrap the value using I (identity), Z (zero) and S (successor) to indicate second item in the response list + AccessDenied -> S . Z . I $ invalidEntry + +instance (res ~ MyResponses) => AsUnion res FailureSuccess where + toUnion = + -- Type application here tells the type engine that we use DynError from ErrorResponse + -- as the return type, and no Failure. + eitherToUnion @'[DynError, DynError] @'[Success] + failToError -- Maps a Failure case to its DynError equivalent + (Z . I) -- [Success] only has one item. + . sfToEither + + fromUnion = + sfFromEither + . eitherFromUnion @'[DynError, DynError] @'[Success] + ( \case + Z _ -> InvalidEntry + S _ -> AccessDenied + ) + (unI . unZ) +``` diff --git a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs index c0bf4d7927..8f5a16a28c 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs @@ -39,8 +39,9 @@ import Data.Schema import Imports import Wire.API.Arbitrary import Wire.API.Conversation.Action.Tag +import Wire.API.MLS.CipherSuite +import Wire.API.MLS.Epoch import Wire.API.MLS.Group -import Wire.API.MLS.Message data ProtocolTag = ProtocolProteusTag | ProtocolMLSTag deriving stock (Eq, Show, Enum, Bounded, Generic) @@ -50,7 +51,9 @@ data ConversationMLSData = ConversationMLSData { -- | The MLS group ID associated to the conversation. cnvmlsGroupId :: GroupId, -- | The current epoch number of the corresponding MLS group. - cnvmlsEpoch :: Epoch + cnvmlsEpoch :: Epoch, + -- | The cipher suite to be used in the MLS group. + cnvmlsCipherSuite :: CipherSuiteTag } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via GenericUniform ConversationMLSData @@ -122,3 +125,8 @@ mlsDataSchema = "epoch" (description ?~ "The epoch number of the corresponding MLS group") schema + <*> cnvmlsCipherSuite + .= fieldWithDocModifier + "cipher_suite" + (description ?~ "The cipher suite of the corresponding MLS group") + schema diff --git a/libs/wire-api/src/Wire/API/Error/Galley.hs b/libs/wire-api/src/Wire/API/Error/Galley.hs index c7b1949ed1..5d1047d58c 100644 --- a/libs/wire-api/src/Wire/API/Error/Galley.hs +++ b/libs/wire-api/src/Wire/API/Error/Galley.hs @@ -77,6 +77,7 @@ data GalleyError | MLSProtocolErrorTag | MLSClientMismatch | MLSStaleMessage + | MLSCommitMissingReferences | -- NoBindingTeamMembers | NoBindingTeam @@ -188,6 +189,8 @@ type instance MapError 'MLSClientMismatch = 'StaticError 409 "mls-client-mismatc type instance MapError 'MLSStaleMessage = 'StaticError 409 "mls-stale-message" "The conversation epoch in a message is too old" +type instance MapError 'MLSCommitMissingReferences = 'StaticError 409 "mls-commit-missing-references" "The commit is not refrencing all pending proposals" + type instance MapError 'NoBindingTeamMembers = 'StaticError 403 "non-binding-team-members" "Both users must be members of the same binding team" type instance MapError 'NoBindingTeam = 'StaticError 403 "no-binding-team" "Operation allowed only on binding teams" diff --git a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs index 60eae0e8fc..b079bffca1 100644 --- a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs +++ b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs @@ -40,6 +40,11 @@ newtype CipherSuite = CipherSuite {cipherSuiteNumber :: Word16} deriving stock (Eq, Show) deriving newtype (ParseMLS, Arbitrary) +instance ToSchema CipherSuite where + schema = + named "CipherSuite" $ + cipherSuiteNumber .= fmap CipherSuite (unnamed schema) + data CipherSuiteTag = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 deriving stock (Bounded, Enum, Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform CipherSuiteTag) diff --git a/libs/wire-api/src/Wire/API/MLS/Context.hs b/libs/wire-api/src/Wire/API/MLS/Context.hs new file mode 100644 index 0000000000..661b7ce632 --- /dev/null +++ b/libs/wire-api/src/Wire/API/MLS/Context.hs @@ -0,0 +1,33 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Wire.API.MLS.Context where + +import Imports + +-- Warning: the "context" string here is different from the one mandated by +-- the spec, but it is the one that happens to be used by openmls. Until +-- openmls is patched and we switch to a fixed version, we will have to use +-- the "wrong" string here as well. +-- +-- This is used when invoking 'csHash'. +context :: ByteString +context = "MLS 1.0 ref" + +proposalContext, keyPackageContext :: ByteString +proposalContext = context +keyPackageContext = context diff --git a/libs/wire-api/src/Wire/API/MLS/Epoch.hs b/libs/wire-api/src/Wire/API/MLS/Epoch.hs new file mode 100644 index 0000000000..79c27bae43 --- /dev/null +++ b/libs/wire-api/src/Wire/API/MLS/Epoch.hs @@ -0,0 +1,32 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Wire.API.MLS.Epoch where + +import Data.Schema +import Imports +import Wire.API.Arbitrary +import Wire.API.MLS.Serialisation + +newtype Epoch = Epoch {epochNumber :: Word64} + deriving stock (Eq, Show) + deriving newtype (Arbitrary, Enum, ToSchema) + +instance ParseMLS Epoch where + parseMLS = Epoch <$> parseMLS diff --git a/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs b/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs index b9360fc654..c5ae118cb9 100644 --- a/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs +++ b/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs @@ -50,6 +50,7 @@ import Imports import Web.HttpApiData import Wire.API.Arbitrary import Wire.API.MLS.CipherSuite +import Wire.API.MLS.Context import Wire.API.MLS.Credential import Wire.API.MLS.Extension import Wire.API.MLS.Serialisation @@ -124,11 +125,7 @@ instance ParseMLS KeyPackageRef where kpRef :: CipherSuiteTag -> KeyPackageData -> KeyPackageRef kpRef cs = KeyPackageRef - -- Warning: the "context" string here is different from the one mandated by - -- the spec, but it is the one that happens to be used by openmls. Until - -- openmls is patched and we switch to a fixed version, we will have to use - -- the "wrong" string here as well. - . csHash cs "MLS 1.0 ref" + . csHash cs keyPackageContext . kpData -- | Compute ref of a key package. Return 'Nothing' if the key package cipher diff --git a/libs/wire-api/src/Wire/API/MLS/Message.hs b/libs/wire-api/src/Wire/API/MLS/Message.hs index ffb2c83427..e1855018a9 100644 --- a/libs/wire-api/src/Wire/API/MLS/Message.hs +++ b/libs/wire-api/src/Wire/API/MLS/Message.hs @@ -20,8 +20,7 @@ -- with this program. If not, see . module Wire.API.MLS.Message - ( Epoch (..), - Message (..), + ( Message (..), WireFormatTag (..), SWireFormatTag (..), SomeMessage (..), @@ -31,28 +30,26 @@ module Wire.API.MLS.Message Sender (..), MLSPlainTextSym0, MLSCipherTextSym0, + MLSMessageSendingStatus (..), ) where +import Control.Lens ((?~)) +import qualified Data.Aeson as A import Data.Binary +import Data.Json.Util import Data.Schema import Data.Singletons.TH import qualified Data.Swagger as S import Imports -import Wire.API.Arbitrary +import Wire.API.Event.Conversation import Wire.API.MLS.Commit +import Wire.API.MLS.Epoch import Wire.API.MLS.Group import Wire.API.MLS.KeyPackage import Wire.API.MLS.Proposal import Wire.API.MLS.Serialisation -newtype Epoch = Epoch {epochNumber :: Word64} - deriving stock (Eq, Show) - deriving newtype (Arbitrary, Enum, ToSchema) - -instance ParseMLS Epoch where - parseMLS = Epoch <$> parseMLS - data WireFormatTag = MLSPlainText | MLSCipherText deriving (Bounded, Enum, Eq, Show) @@ -149,7 +146,7 @@ instance ParseMLS (MessagePayload 'MLSPlainText) where data MessagePayloadTBS = ApplicationMessage ByteString - | ProposalMessage Proposal + | ProposalMessage (RawMLS Proposal) | CommitMessage Commit data ContentType @@ -167,3 +164,24 @@ instance ParseMLS MessagePayloadTBS where ApplicationMessageTag -> ApplicationMessage <$> parseMLSBytes @Word32 ProposalMessageTag -> ProposalMessage <$> parseMLS CommitMessageTag -> CommitMessage <$> parseMLS + +data MLSMessageSendingStatus = MLSMessageSendingStatus + { mmssEvents :: [Event], + mmssTime :: UTCTimeMillis + } + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema MLSMessageSendingStatus + +instance ToSchema MLSMessageSendingStatus where + schema = + object "MLSMessageSendingStatus" $ + MLSMessageSendingStatus + <$> mmssEvents + .= fieldWithDocModifier + "events" + (description ?~ "A list of events caused by sending the message.") + (array schema) + <*> mmssTime + .= fieldWithDocModifier + "time" + (description ?~ "The time of sending the message.") + schema diff --git a/libs/wire-api/src/Wire/API/MLS/Proposal.hs b/libs/wire-api/src/Wire/API/MLS/Proposal.hs index 74d38bafeb..6f6e43a705 100644 --- a/libs/wire-api/src/Wire/API/MLS/Proposal.hs +++ b/libs/wire-api/src/Wire/API/MLS/Proposal.hs @@ -14,14 +14,17 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . +{-# LANGUAGE TemplateHaskell #-} module Wire.API.MLS.Proposal where +import Control.Lens (makePrisms) import Data.Binary import Data.Binary.Get import Imports import Wire.API.Arbitrary import Wire.API.MLS.CipherSuite +import Wire.API.MLS.Context import Wire.API.MLS.Extension import Wire.API.MLS.Group import Wire.API.MLS.KeyPackage @@ -66,6 +69,13 @@ instance ParseMLS Proposal where GroupContextExtensionsProposalTag -> GroupContextExtensionsProposal <$> parseMLSVector @Word32 parseMLS +-- | Compute the proposal ref given a ciphersuite and the raw proposal data. +proposalRef :: CipherSuiteTag -> RawMLS Proposal -> ProposalRef +proposalRef cs = + ProposalRef + . csHash cs proposalContext + . rmRaw + data PreSharedKeyTag = ExternalKeyTag | ResumptionKeyTag deriving (Bounded, Enum, Eq, Show) @@ -142,7 +152,9 @@ instance ParseMLS ProposalOrRef where RefTag -> Ref <$> parseMLS newtype ProposalRef = ProposalRef {unProposalRef :: ByteString} - deriving stock (Eq, Show) + deriving stock (Eq, Show, Ord) instance ParseMLS ProposalRef where parseMLS = ProposalRef <$> getByteString 16 + +makePrisms ''ProposalOrRef 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 a8cabe0110..1f9ae9434c 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -320,6 +320,36 @@ type SelfAPI = :> MultiVerb 'PUT '[JSON] ChangeHandleResponses (Maybe ChangeHandleError) ) +type UserHandleAPI = + Named + "check-user-handles" + ( Summary "Check availability of user handles" + :> ZUser + :> "users" + :> "handles" + :> ReqBody '[JSON] CheckHandles + :> MultiVerb + 'POST + '[JSON] + '[Respond 200 "List of free handles" [Handle]] + [Handle] + ) + :<|> Named + "check-user-handle" + ( Summary "Check whether a user handle can be taken" + :> CanThrow 'InvalidHandle + :> CanThrow 'HandleNotFound + :> ZUser + :> "users" + :> "handles" + :> Capture "handle" Text + :> MultiVerb + 'HEAD + '[JSON] + '[Respond 200 "Handle is taken" ()] + () + ) + type AccountAPI = -- docs/reference/user/registration.md {#RefRegistration} -- @@ -838,6 +868,7 @@ type BrigAPI = :<|> ConnectionAPI :<|> PropertiesAPI :<|> MLSAPI + :<|> UserHandleAPI brigSwagger :: Swagger brigSwagger = toSwagger (Proxy @BrigAPI) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs index 6d47129186..6b88ada0ae 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs @@ -1313,12 +1313,15 @@ type MLSMessagingAPI = :> MultiVerb1 'POST '[JSON] (RespondEmpty 201 "Welcome message sent") ) :<|> Named - "mls-message" + "mls-message-v1" ( Summary "Post an MLS message" + :> Until 'V2 :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvNotFound - :> CanThrow 'MLSKeyPackageRefNotFound + :> CanThrow 'MissingLegalholdConsent :> CanThrow 'MLSClientMismatch + :> CanThrow 'MLSCommitMissingReferences + :> CanThrow 'MLSKeyPackageRefNotFound :> CanThrow 'MLSProtocolErrorTag :> CanThrow 'MLSStaleMessage :> CanThrow MLSProposalFailure @@ -1326,12 +1329,33 @@ type MLSMessagingAPI = :> CanThrow 'MLSUnsupportedMessage :> CanThrow 'MLSUnsupportedProposal :> CanThrow 'LegalHoldNotEnabled - :> CanThrow 'MissingLegalholdConsent :> "messages" :> ZConn :> ReqBody '[MLS] (RawMLS SomeMessage) :> MultiVerb1 'POST '[JSON] (Respond 201 "Message sent" [Event]) ) + :<|> Named + "mls-message" + ( Summary "Post an MLS message" + :> From 'V2 + :> CanThrow 'ConvAccessDenied + :> CanThrow 'ConvNotFound + :> CanThrow 'MissingLegalholdConsent + :> CanThrow 'MLSClientMismatch + :> CanThrow 'MLSCommitMissingReferences + :> CanThrow 'MLSKeyPackageRefNotFound + :> CanThrow 'MLSProtocolErrorTag + :> CanThrow 'MLSStaleMessage + :> CanThrow MLSProposalFailure + :> CanThrow 'MLSProposalNotFound + :> CanThrow 'MLSUnsupportedMessage + :> CanThrow 'MLSUnsupportedProposal + :> CanThrow 'LegalHoldNotEnabled + :> "messages" + :> ZConn + :> ReqBody '[MLS] (RawMLS SomeMessage) + :> MultiVerb1 'POST '[JSON] (Respond 201 "Message sent" MLSMessageSendingStatus) + ) type MLSAPI = LiftNamed (ZLocalUser :> "mls" :> MLSMessagingAPI) diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 6e2dbdbaaf..84a43c56ed 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -25,7 +25,19 @@ module Wire.API.Team.Feature featureName, featureNameBS, LockStatus (..), - WithStatus (..), + WithStatus, + withStatus, + withStatus', + wsStatus, + wsLockStatus, + wsConfig, + setStatus, + setLockStatus, + setConfig, + WithStatusPatch, + wspStatus, + wspLockStatus, + wspConfig, WithStatusNoLock (..), forgetLock, withLockStatus, @@ -157,30 +169,66 @@ featureNameBS :: forall cfg. (IsFeatureConfig cfg, KnownSymbol (FeatureSymbol cf featureNameBS = UTF8.fromString $ symbolVal (Proxy @(FeatureSymbol cfg)) ---------------------------------------------------------------------- --- WithStatus +-- WithStatusBase -data WithStatus (cfg :: *) = WithStatus - { wsStatus :: FeatureStatus, - wsLockStatus :: LockStatus, - wsConfig :: cfg +data WithStatusBase (m :: * -> *) (cfg :: *) = WithStatusBase + { wsbStatus :: m FeatureStatus, + wsbLockStatus :: m LockStatus, + wsbConfig :: m cfg } - deriving stock (Eq, Show, Generic, Typeable, Functor) - deriving (ToJSON, FromJSON, S.ToSchema) via (Schema (WithStatus cfg)) + deriving stock (Generic, Typeable, Functor) + +---------------------------------------------------------------------- +-- WithStatus + +-- FUTUREWORK: use lenses, maybe? +wsStatus :: WithStatus cfg -> FeatureStatus +wsStatus = runIdentity . wsbStatus + +wsLockStatus :: WithStatus cfg -> LockStatus +wsLockStatus = runIdentity . wsbLockStatus + +wsConfig :: WithStatus cfg -> cfg +wsConfig = runIdentity . wsbConfig + +withStatus :: FeatureStatus -> LockStatus -> cfg -> WithStatus cfg +withStatus s ls c = WithStatusBase (Identity s) (Identity ls) (Identity c) + +setStatus :: FeatureStatus -> WithStatus cfg -> WithStatus cfg +setStatus s (WithStatusBase _ ls c) = WithStatusBase (Identity s) ls c + +setLockStatus :: LockStatus -> WithStatus cfg -> WithStatus cfg +setLockStatus ls (WithStatusBase s _ c) = WithStatusBase s (Identity ls) c + +setConfig :: cfg -> WithStatus cfg -> WithStatus cfg +setConfig c (WithStatusBase s ls _) = WithStatusBase s ls (Identity c) + +type WithStatus (cfg :: *) = WithStatusBase Identity cfg -instance Arbitrary cfg => Arbitrary (WithStatus cfg) where - arbitrary = WithStatus <$> arbitrary <*> arbitrary <*> arbitrary +deriving instance (Eq cfg) => Eq (WithStatus cfg) + +deriving instance (Show cfg) => Show (WithStatus cfg) + +deriving via (Schema (WithStatus cfg)) instance (ToSchema (WithStatus cfg)) => ToJSON (WithStatus cfg) + +deriving via (Schema (WithStatus cfg)) instance (ToSchema (WithStatus cfg)) => FromJSON (WithStatus cfg) + +deriving via (Schema (WithStatus cfg)) instance (ToSchema (WithStatus cfg)) => S.ToSchema (WithStatus cfg) instance (ToSchema cfg, IsFeatureConfig cfg) => ToSchema (WithStatus cfg) where schema = object name $ - WithStatus - <$> wsStatus .= field "status" schema - <*> wsLockStatus .= field "lockStatus" schema - <*> wsConfig .= objectSchema @cfg + WithStatusBase + <$> (runIdentity . wsbStatus) .= (Identity <$> field "status" schema) + <*> (runIdentity . wsbLockStatus) .= (Identity <$> field "lockStatus" schema) + <*> (runIdentity . wsbConfig) .= (Identity <$> objectSchema @cfg) where inner = schema @cfg name = fromMaybe "" (getName (schemaDoc inner)) <> ".WithStatus" +instance (Arbitrary cfg, IsFeatureConfig cfg) => Arbitrary (WithStatus cfg) where + arbitrary = WithStatusBase <$> arbitrary <*> arbitrary <*> arbitrary + withStatusModel :: forall cfg. (IsFeatureConfig cfg, KnownSymbol (FeatureSymbol cfg)) => Doc.Model withStatusModel = let name = featureName @cfg @@ -195,6 +243,49 @@ withStatusModel = Doc.property "status" typeFeatureStatus $ Doc.description "status" Doc.property "lockStatus" typeLockStatusValue $ Doc.description "" +---------------------------------------------------------------------- +-- WithStatusPatch + +type WithStatusPatch (cfg :: *) = WithStatusBase Maybe cfg + +deriving instance (Eq cfg) => Eq (WithStatusPatch cfg) + +deriving instance (Show cfg) => Show (WithStatusPatch cfg) + +deriving via (Schema (WithStatusPatch cfg)) instance (ToSchema (WithStatusPatch cfg)) => ToJSON (WithStatusPatch cfg) + +deriving via (Schema (WithStatusPatch cfg)) instance (ToSchema (WithStatusPatch cfg)) => FromJSON (WithStatusPatch cfg) + +deriving via (Schema (WithStatusPatch cfg)) instance (ToSchema (WithStatusPatch cfg)) => S.ToSchema (WithStatusPatch cfg) + +wspStatus :: WithStatusPatch cfg -> Maybe FeatureStatus +wspStatus = wsbStatus + +wspLockStatus :: WithStatusPatch cfg -> Maybe LockStatus +wspLockStatus = wsbLockStatus + +wspConfig :: WithStatusPatch cfg -> Maybe cfg +wspConfig = wsbConfig + +withStatus' :: Maybe FeatureStatus -> Maybe LockStatus -> Maybe cfg -> WithStatusPatch cfg +withStatus' = WithStatusBase + +-- | The ToJSON implementation of `WithStatusPatch` will encode the trivial config as `"config": {}` +-- when the value is a `Just`, if it's `Nothing` it will be omitted, which is the important part. +instance (ToSchema cfg, IsFeatureConfig cfg) => ToSchema (WithStatusPatch cfg) where + schema = + object name $ + WithStatusBase + <$> wsbStatus .= maybe_ (optField "status" schema) + <*> wsbLockStatus .= maybe_ (optField "lockStatus" schema) + <*> wsbConfig .= maybe_ (optField "config" schema) + where + inner = schema @cfg + name = fromMaybe "" (getName (schemaDoc inner)) <> ".WithStatusPatch" + +instance (Arbitrary cfg, IsFeatureConfig cfg) => Arbitrary (WithStatusPatch cfg) where + arbitrary = WithStatusBase <$> arbitrary <*> arbitrary <*> arbitrary + ---------------------------------------------------------------------- -- WithStatusNoLock @@ -209,10 +300,10 @@ instance Arbitrary cfg => Arbitrary (WithStatusNoLock cfg) where arbitrary = WithStatusNoLock <$> arbitrary <*> arbitrary forgetLock :: WithStatus a -> WithStatusNoLock a -forgetLock WithStatus {..} = WithStatusNoLock wsStatus wsConfig +forgetLock ws = WithStatusNoLock (wsStatus ws) (wsConfig ws) withLockStatus :: LockStatus -> WithStatusNoLock a -> WithStatus a -withLockStatus ls (WithStatusNoLock s c) = WithStatus s ls c +withLockStatus ls (WithStatusNoLock s c) = withStatus s ls c withUnlocked :: WithStatusNoLock a -> WithStatus a withUnlocked = withLockStatus LockStatusUnlocked @@ -441,7 +532,8 @@ instance ToSchema GuestLinksConfig where instance IsFeatureConfig GuestLinksConfig where type FeatureSymbol GuestLinksConfig = "conversationGuestLinks" - defFeatureStatus = WithStatus FeatureStatusEnabled LockStatusUnlocked GuestLinksConfig + defFeatureStatus = withStatus FeatureStatusEnabled LockStatusUnlocked GuestLinksConfig + objectSchema = pure GuestLinksConfig instance FeatureTrivialConfig GuestLinksConfig where @@ -456,7 +548,8 @@ data LegalholdConfig = LegalholdConfig instance IsFeatureConfig LegalholdConfig where type FeatureSymbol LegalholdConfig = "legalhold" - defFeatureStatus = WithStatus FeatureStatusDisabled LockStatusUnlocked LegalholdConfig + defFeatureStatus = withStatus FeatureStatusDisabled LockStatusUnlocked LegalholdConfig + objectSchema = pure LegalholdConfig instance ToSchema LegalholdConfig where @@ -474,7 +567,8 @@ data SSOConfig = SSOConfig instance IsFeatureConfig SSOConfig where type FeatureSymbol SSOConfig = "sso" - defFeatureStatus = WithStatus FeatureStatusDisabled LockStatusUnlocked SSOConfig + defFeatureStatus = withStatus FeatureStatusDisabled LockStatusUnlocked SSOConfig + objectSchema = pure SSOConfig instance ToSchema SSOConfig where @@ -494,7 +588,8 @@ data SearchVisibilityAvailableConfig = SearchVisibilityAvailableConfig instance IsFeatureConfig SearchVisibilityAvailableConfig where type FeatureSymbol SearchVisibilityAvailableConfig = "searchVisibility" - defFeatureStatus = WithStatus FeatureStatusDisabled LockStatusUnlocked SearchVisibilityAvailableConfig + defFeatureStatus = withStatus FeatureStatusDisabled LockStatusUnlocked SearchVisibilityAvailableConfig + objectSchema = pure SearchVisibilityAvailableConfig instance ToSchema SearchVisibilityAvailableConfig where @@ -518,7 +613,8 @@ instance ToSchema ValidateSAMLEmailsConfig where instance IsFeatureConfig ValidateSAMLEmailsConfig where type FeatureSymbol ValidateSAMLEmailsConfig = "validateSAMLemails" - defFeatureStatus = WithStatus FeatureStatusEnabled LockStatusUnlocked ValidateSAMLEmailsConfig + defFeatureStatus = withStatus FeatureStatusEnabled LockStatusUnlocked ValidateSAMLEmailsConfig + objectSchema = pure ValidateSAMLEmailsConfig instance HasDeprecatedFeatureName ValidateSAMLEmailsConfig where @@ -536,7 +632,8 @@ data DigitalSignaturesConfig = DigitalSignaturesConfig instance IsFeatureConfig DigitalSignaturesConfig where type FeatureSymbol DigitalSignaturesConfig = "digitalSignatures" - defFeatureStatus = WithStatus FeatureStatusDisabled LockStatusUnlocked DigitalSignaturesConfig + defFeatureStatus = withStatus FeatureStatusDisabled LockStatusUnlocked DigitalSignaturesConfig + objectSchema = pure DigitalSignaturesConfig instance HasDeprecatedFeatureName DigitalSignaturesConfig where @@ -557,7 +654,8 @@ data ConferenceCallingConfig = ConferenceCallingConfig instance IsFeatureConfig ConferenceCallingConfig where type FeatureSymbol ConferenceCallingConfig = "conferenceCalling" - defFeatureStatus = WithStatus FeatureStatusEnabled LockStatusUnlocked ConferenceCallingConfig + defFeatureStatus = withStatus FeatureStatusEnabled LockStatusUnlocked ConferenceCallingConfig + objectSchema = pure ConferenceCallingConfig instance ToSchema ConferenceCallingConfig where @@ -578,7 +676,8 @@ instance ToSchema SndFactorPasswordChallengeConfig where instance IsFeatureConfig SndFactorPasswordChallengeConfig where type FeatureSymbol SndFactorPasswordChallengeConfig = "sndFactorPasswordChallenge" - defFeatureStatus = WithStatus FeatureStatusDisabled LockStatusLocked SndFactorPasswordChallengeConfig + defFeatureStatus = withStatus FeatureStatusDisabled LockStatusLocked SndFactorPasswordChallengeConfig + objectSchema = pure SndFactorPasswordChallengeConfig instance FeatureTrivialConfig SndFactorPasswordChallengeConfig where @@ -593,7 +692,8 @@ data SearchVisibilityInboundConfig = SearchVisibilityInboundConfig instance IsFeatureConfig SearchVisibilityInboundConfig where type FeatureSymbol SearchVisibilityInboundConfig = "searchVisibilityInbound" - defFeatureStatus = WithStatus FeatureStatusDisabled LockStatusUnlocked SearchVisibilityInboundConfig + defFeatureStatus = withStatus FeatureStatusDisabled LockStatusUnlocked SearchVisibilityInboundConfig + objectSchema = pure SearchVisibilityInboundConfig instance ToSchema SearchVisibilityInboundConfig where @@ -621,8 +721,9 @@ instance ToSchema ClassifiedDomainsConfig where instance IsFeatureConfig ClassifiedDomainsConfig where type FeatureSymbol ClassifiedDomainsConfig = "classifiedDomains" + defFeatureStatus = - WithStatus + withStatus FeatureStatusDisabled LockStatusUnlocked (ClassifiedDomainsConfig []) @@ -651,8 +752,9 @@ instance ToSchema AppLockConfig where instance IsFeatureConfig AppLockConfig where type FeatureSymbol AppLockConfig = "appLock" + defFeatureStatus = - WithStatus + withStatus FeatureStatusEnabled LockStatusUnlocked (AppLockConfig (EnforceAppLock False) 60) @@ -679,7 +781,8 @@ data FileSharingConfig = FileSharingConfig instance IsFeatureConfig FileSharingConfig where type FeatureSymbol FileSharingConfig = "fileSharing" - defFeatureStatus = WithStatus FeatureStatusEnabled LockStatusUnlocked FileSharingConfig + defFeatureStatus = withStatus FeatureStatusEnabled LockStatusUnlocked FileSharingConfig + objectSchema = pure FileSharingConfig instance ToSchema FileSharingConfig where @@ -707,10 +810,11 @@ instance ToSchema SelfDeletingMessagesConfig where instance IsFeatureConfig SelfDeletingMessagesConfig where type FeatureSymbol SelfDeletingMessagesConfig = "selfDeletingMessages" defFeatureStatus = - WithStatus + withStatus FeatureStatusEnabled LockStatusUnlocked (SelfDeletingMessagesConfig 0) + configModel = Just $ Doc.defineModel "SelfDeletingMessagesConfig" $ do Doc.property "enforcedTimeoutSeconds" Doc.int32' $ Doc.description "optional; default: `0` (no enforcement)" @@ -741,8 +845,9 @@ instance IsFeatureConfig MLSConfig where type FeatureSymbol MLSConfig = "mls" defFeatureStatus = let config = MLSConfig [] ProtocolProteusTag [MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519] MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 - in WithStatus FeatureStatusDisabled LockStatusUnlocked config + in withStatus FeatureStatusDisabled LockStatusUnlocked config objectSchema = field "config" schema + configModel = Just $ Doc.defineModel "MLSConfig" $ do Doc.property "protocolToggleUsers" (Doc.array Doc.string') $ Doc.description "allowlist of users that may change protocols" diff --git a/libs/wire-api/src/Wire/API/User/Handle.hs b/libs/wire-api/src/Wire/API/User/Handle.hs index 103c4946cc..c62bbd81fb 100644 --- a/libs/wire-api/src/Wire/API/User/Handle.hs +++ b/libs/wire-api/src/Wire/API/User/Handle.hs @@ -74,6 +74,7 @@ data CheckHandles = CheckHandles } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform CheckHandles) + deriving (S.ToSchema) via Schema CheckHandles modelCheckHandles :: Doc.Model modelCheckHandles = Doc.defineModel "CheckHandles" $ do @@ -96,3 +97,10 @@ instance FromJSON CheckHandles where CheckHandles <$> o A..: "handles" <*> o A..:? "return" A..!= unsafeRange 1 + +instance ToSchema CheckHandles where + schema = + object "CheckHandles" $ + CheckHandles + <$> checkHandlesList .= field "handles" (fromRange .= rangedSchema (array schema)) + <*> checkHandlesNum .= field "return" schema diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs index 2f7ee6d5ae..049b631f56 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs @@ -231,6 +231,7 @@ import qualified Test.Wire.API.Golden.Generated.VerifyDeleteUser_user import qualified Test.Wire.API.Golden.Generated.ViewLegalHoldServiceInfo_team import qualified Test.Wire.API.Golden.Generated.ViewLegalHoldService_team import qualified Test.Wire.API.Golden.Generated.WithStatusNoLock_team +import qualified Test.Wire.API.Golden.Generated.WithStatusPatch_team import qualified Test.Wire.API.Golden.Generated.WithStatus_team import qualified Test.Wire.API.Golden.Generated.Wrapped_20_22some_5fint_22_20Int_user import Test.Wire.API.Golden.Runner @@ -1298,5 +1299,81 @@ tests = [ (Test.Wire.API.Golden.Generated.VerificationAction_user.testObject_VerificationAction_user_1, "testObject_VerificationAction_user_1"), (Test.Wire.API.Golden.Generated.VerificationAction_user.testObject_VerificationAction_user_2, "testObject_VerificationAction_user_2"), (Test.Wire.API.Golden.Generated.VerificationAction_user.testObject_VerificationAction_user_3, "testObject_VerificationAction_user_3") - ] + ], + testGroup + "Golden: WithStatusPatch_team 1" + $ testObjects + [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_1, "testObject_WithStatusPatch_team_1.json")], + testGroup + "Golden: WithStatusPatch_team 2" + $ testObjects + [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_2, "testObject_WithStatusPatch_team_2.json")], + testGroup + "Golden: WithStatusPatch_team 3" + $ testObjects + [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_3, "testObject_WithStatusPatch_team_3.json")], + testGroup + "Golden: WithStatusPatch_team 4" + $ testObjects + [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_4, "testObject_WithStatusPatch_team_4.json")], + testGroup + "Golden: WithStatusPatch_team 5" + $ testObjects + [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_5, "testObject_WithStatusPatch_team_5.json")], + testGroup + "Golden: WithStatusPatch_team 6" + $ testObjects + [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_6, "testObject_WithStatusPatch_team_6.json")], + testGroup + "Golden: WithStatusPatch_team 7" + $ testObjects + [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_7, "testObject_WithStatusPatch_team_7.json")], + testGroup + "Golden: WithStatusPatch_team 8" + $ testObjects + [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_8, "testObject_WithStatusPatch_team_8.json")], + testGroup + "Golden: WithStatusPatch_team 9" + $ testObjects + [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_9, "testObject_WithStatusPatch_team_9.json")], + testGroup + "Golden: WithStatusPatch_team 10" + $ testObjects + [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_10, "testObject_WithStatusPatch_team_10.json")], + testGroup + "Golden: WithStatusPatch_team 11" + $ testObjects + [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_11, "testObject_WithStatusPatch_team_11.json")], + testGroup + "Golden: WithStatusPatch_team 12" + $ testObjects + [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_12, "testObject_WithStatusPatch_team_12.json")], + testGroup + "Golden: WithStatusPatch_team 13" + $ testObjects + [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_13, "testObject_WithStatusPatch_team_13.json")], + testGroup + "Golden: WithStatusPatch_team 14" + $ testObjects + [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_14, "testObject_WithStatusPatch_team_14.json")], + testGroup + "Golden: WithStatusPatch_team 15" + $ testObjects + [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_15, "testObject_WithStatusPatch_team_15.json")], + testGroup + "Golden: WithStatusPatch_team 16" + $ testObjects + [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_16, "testObject_WithStatusPatch_team_16.json")], + testGroup + "Golden: WithStatusPatch_team 17" + $ testObjects + [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_17, "testObject_WithStatusPatch_team_17.json")], + testGroup + "Golden: WithStatusPatch_team 18" + $ testObjects + [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_18, "testObject_WithStatusPatch_team_18.json")], + testGroup + "Golden: WithStatusPatch_team 19" + $ testObjects + [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_19, "testObject_WithStatusPatch_team_19.json")] ] diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatusPatch_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatusPatch_team.hs new file mode 100644 index 0000000000..be4dd762a9 --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatusPatch_team.hs @@ -0,0 +1,81 @@ +{-# LANGUAGE OverloadedLists #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Wire.API.Golden.Generated.WithStatusPatch_team where + +import Data.Domain +import Imports +import Wire.API.Team.Feature + +testObject_WithStatusPatch_team_1 :: WithStatusPatch AppLockConfig +testObject_WithStatusPatch_team_1 = withStatus' (Just FeatureStatusEnabled) (Just LockStatusUnlocked) (Just (AppLockConfig (EnforceAppLock False) (-98))) + +testObject_WithStatusPatch_team_2 :: WithStatusPatch AppLockConfig +testObject_WithStatusPatch_team_2 = withStatus' Nothing Nothing (Just (AppLockConfig (EnforceAppLock True) 0)) + +testObject_WithStatusPatch_team_3 :: WithStatusPatch AppLockConfig +testObject_WithStatusPatch_team_3 = withStatus' (Just FeatureStatusEnabled) (Just LockStatusLocked) (Just (AppLockConfig (EnforceAppLock True) 111)) + +testObject_WithStatusPatch_team_4 :: WithStatusPatch SelfDeletingMessagesConfig +testObject_WithStatusPatch_team_4 = withStatus' (Just FeatureStatusEnabled) Nothing (Just (SelfDeletingMessagesConfig (-97))) + +testObject_WithStatusPatch_team_5 :: WithStatusPatch SelfDeletingMessagesConfig +testObject_WithStatusPatch_team_5 = withStatus' (Just FeatureStatusEnabled) (Just LockStatusUnlocked) (Just (SelfDeletingMessagesConfig 0)) + +testObject_WithStatusPatch_team_6 :: WithStatusPatch SelfDeletingMessagesConfig +testObject_WithStatusPatch_team_6 = withStatus' (Just FeatureStatusEnabled) Nothing (Just (SelfDeletingMessagesConfig 77)) + +testObject_WithStatusPatch_team_7 :: WithStatusPatch ClassifiedDomainsConfig +testObject_WithStatusPatch_team_7 = withStatus' (Just FeatureStatusEnabled) (Just LockStatusLocked) (Just (ClassifiedDomainsConfig [])) + +testObject_WithStatusPatch_team_8 :: WithStatusPatch ClassifiedDomainsConfig +testObject_WithStatusPatch_team_8 = withStatus' Nothing (Just LockStatusLocked) (Just (ClassifiedDomainsConfig [Domain "example.com", Domain "test.foobar"])) + +testObject_WithStatusPatch_team_9 :: WithStatusPatch ClassifiedDomainsConfig +testObject_WithStatusPatch_team_9 = withStatus' (Just FeatureStatusEnabled) (Just LockStatusUnlocked) (Just (ClassifiedDomainsConfig [Domain "test.foobar"])) + +testObject_WithStatusPatch_team_10 :: WithStatusPatch SSOConfig +testObject_WithStatusPatch_team_10 = withStatus' (Just FeatureStatusDisabled) (Just LockStatusLocked) (Just SSOConfig) + +testObject_WithStatusPatch_team_11 :: WithStatusPatch SearchVisibilityAvailableConfig +testObject_WithStatusPatch_team_11 = withStatus' (Just FeatureStatusEnabled) (Just LockStatusLocked) (Just SearchVisibilityAvailableConfig) + +testObject_WithStatusPatch_team_12 :: WithStatusPatch ValidateSAMLEmailsConfig +testObject_WithStatusPatch_team_12 = withStatus' (Just FeatureStatusDisabled) Nothing (Just ValidateSAMLEmailsConfig) + +testObject_WithStatusPatch_team_13 :: WithStatusPatch DigitalSignaturesConfig +testObject_WithStatusPatch_team_13 = withStatus' (Just FeatureStatusEnabled) (Just LockStatusLocked) (Just DigitalSignaturesConfig) + +testObject_WithStatusPatch_team_14 :: WithStatusPatch ConferenceCallingConfig +testObject_WithStatusPatch_team_14 = withStatus' Nothing (Just LockStatusUnlocked) (Just ConferenceCallingConfig) + +testObject_WithStatusPatch_team_15 :: WithStatusPatch GuestLinksConfig +testObject_WithStatusPatch_team_15 = withStatus' (Just FeatureStatusEnabled) (Just LockStatusUnlocked) (Just GuestLinksConfig) + +testObject_WithStatusPatch_team_16 :: WithStatusPatch SndFactorPasswordChallengeConfig +testObject_WithStatusPatch_team_16 = withStatus' (Just FeatureStatusDisabled) (Just LockStatusUnlocked) (Just SndFactorPasswordChallengeConfig) + +testObject_WithStatusPatch_team_17 :: WithStatusPatch SearchVisibilityInboundConfig +testObject_WithStatusPatch_team_17 = withStatus' (Just FeatureStatusEnabled) Nothing (Just SearchVisibilityInboundConfig) + +testObject_WithStatusPatch_team_18 :: WithStatusPatch GuestLinksConfig +testObject_WithStatusPatch_team_18 = withStatus' (Just FeatureStatusEnabled) Nothing Nothing + +testObject_WithStatusPatch_team_19 :: WithStatusPatch SelfDeletingMessagesConfig +testObject_WithStatusPatch_team_19 = withStatus' Nothing (Just LockStatusUnlocked) Nothing diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatus_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatus_team.hs index 64bd312547..350789506d 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatus_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatus_team.hs @@ -24,52 +24,52 @@ import Imports import Wire.API.Team.Feature testObject_WithStatus_team_1 :: WithStatus AppLockConfig -testObject_WithStatus_team_1 = WithStatus FeatureStatusEnabled LockStatusUnlocked (AppLockConfig (EnforceAppLock False) (-98)) +testObject_WithStatus_team_1 = withStatus FeatureStatusEnabled LockStatusUnlocked (AppLockConfig (EnforceAppLock False) (-98)) testObject_WithStatus_team_2 :: WithStatus AppLockConfig -testObject_WithStatus_team_2 = WithStatus FeatureStatusEnabled LockStatusUnlocked (AppLockConfig (EnforceAppLock True) 0) +testObject_WithStatus_team_2 = withStatus FeatureStatusEnabled LockStatusUnlocked (AppLockConfig (EnforceAppLock True) 0) testObject_WithStatus_team_3 :: WithStatus AppLockConfig -testObject_WithStatus_team_3 = WithStatus FeatureStatusEnabled LockStatusLocked (AppLockConfig (EnforceAppLock True) 111) +testObject_WithStatus_team_3 = withStatus FeatureStatusEnabled LockStatusLocked (AppLockConfig (EnforceAppLock True) 111) testObject_WithStatus_team_4 :: WithStatus SelfDeletingMessagesConfig -testObject_WithStatus_team_4 = WithStatus FeatureStatusEnabled LockStatusUnlocked (SelfDeletingMessagesConfig (-97)) +testObject_WithStatus_team_4 = withStatus FeatureStatusEnabled LockStatusUnlocked (SelfDeletingMessagesConfig (-97)) testObject_WithStatus_team_5 :: WithStatus SelfDeletingMessagesConfig -testObject_WithStatus_team_5 = WithStatus FeatureStatusEnabled LockStatusUnlocked (SelfDeletingMessagesConfig 0) +testObject_WithStatus_team_5 = withStatus FeatureStatusEnabled LockStatusUnlocked (SelfDeletingMessagesConfig 0) testObject_WithStatus_team_6 :: WithStatus SelfDeletingMessagesConfig -testObject_WithStatus_team_6 = WithStatus FeatureStatusEnabled LockStatusLocked (SelfDeletingMessagesConfig 77) +testObject_WithStatus_team_6 = withStatus FeatureStatusEnabled LockStatusLocked (SelfDeletingMessagesConfig 77) testObject_WithStatus_team_7 :: WithStatus ClassifiedDomainsConfig -testObject_WithStatus_team_7 = WithStatus FeatureStatusEnabled LockStatusLocked (ClassifiedDomainsConfig []) +testObject_WithStatus_team_7 = withStatus FeatureStatusEnabled LockStatusLocked (ClassifiedDomainsConfig []) testObject_WithStatus_team_8 :: WithStatus ClassifiedDomainsConfig -testObject_WithStatus_team_8 = WithStatus FeatureStatusEnabled LockStatusLocked (ClassifiedDomainsConfig [Domain "example.com", Domain "test.foobar"]) +testObject_WithStatus_team_8 = withStatus FeatureStatusEnabled LockStatusLocked (ClassifiedDomainsConfig [Domain "example.com", Domain "test.foobar"]) testObject_WithStatus_team_9 :: WithStatus ClassifiedDomainsConfig -testObject_WithStatus_team_9 = WithStatus FeatureStatusEnabled LockStatusUnlocked (ClassifiedDomainsConfig [Domain "test.foobar"]) +testObject_WithStatus_team_9 = withStatus FeatureStatusEnabled LockStatusUnlocked (ClassifiedDomainsConfig [Domain "test.foobar"]) testObject_WithStatus_team_10 :: WithStatus SSOConfig -testObject_WithStatus_team_10 = WithStatus FeatureStatusDisabled LockStatusLocked SSOConfig +testObject_WithStatus_team_10 = withStatus FeatureStatusDisabled LockStatusLocked SSOConfig testObject_WithStatus_team_11 :: WithStatus SearchVisibilityAvailableConfig -testObject_WithStatus_team_11 = WithStatus FeatureStatusEnabled LockStatusLocked SearchVisibilityAvailableConfig +testObject_WithStatus_team_11 = withStatus FeatureStatusEnabled LockStatusLocked SearchVisibilityAvailableConfig testObject_WithStatus_team_12 :: WithStatus ValidateSAMLEmailsConfig -testObject_WithStatus_team_12 = WithStatus FeatureStatusDisabled LockStatusLocked ValidateSAMLEmailsConfig +testObject_WithStatus_team_12 = withStatus FeatureStatusDisabled LockStatusLocked ValidateSAMLEmailsConfig testObject_WithStatus_team_13 :: WithStatus DigitalSignaturesConfig -testObject_WithStatus_team_13 = WithStatus FeatureStatusEnabled LockStatusLocked DigitalSignaturesConfig +testObject_WithStatus_team_13 = withStatus FeatureStatusEnabled LockStatusLocked DigitalSignaturesConfig testObject_WithStatus_team_14 :: WithStatus ConferenceCallingConfig -testObject_WithStatus_team_14 = WithStatus FeatureStatusDisabled LockStatusUnlocked ConferenceCallingConfig +testObject_WithStatus_team_14 = withStatus FeatureStatusDisabled LockStatusUnlocked ConferenceCallingConfig testObject_WithStatus_team_15 :: WithStatus GuestLinksConfig -testObject_WithStatus_team_15 = WithStatus FeatureStatusEnabled LockStatusUnlocked GuestLinksConfig +testObject_WithStatus_team_15 = withStatus FeatureStatusEnabled LockStatusUnlocked GuestLinksConfig testObject_WithStatus_team_16 :: WithStatus SndFactorPasswordChallengeConfig -testObject_WithStatus_team_16 = WithStatus FeatureStatusDisabled LockStatusUnlocked SndFactorPasswordChallengeConfig +testObject_WithStatus_team_16 = withStatus FeatureStatusDisabled LockStatusUnlocked SndFactorPasswordChallengeConfig testObject_WithStatus_team_17 :: WithStatus SearchVisibilityInboundConfig -testObject_WithStatus_team_17 = WithStatus FeatureStatusEnabled LockStatusUnlocked SearchVisibilityInboundConfig +testObject_WithStatus_team_17 = withStatus FeatureStatusEnabled LockStatusUnlocked SearchVisibilityInboundConfig diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationsResponse.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationsResponse.hs index adb14d93eb..c5d39f191a 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationsResponse.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ConversationsResponse.hs @@ -30,6 +30,7 @@ import Imports import Wire.API.Conversation import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role +import Wire.API.MLS.CipherSuite domain :: Domain domain = Domain "golden.example.com" @@ -127,5 +128,5 @@ conv2 = }, cmOthers = [] }, - cnvProtocol = ProtocolMLS (ConversationMLSData (GroupId ("test_group")) (Epoch 42)) + cnvProtocol = ProtocolMLS (ConversationMLSData (GroupId "test_group") (Epoch 42) MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519) } diff --git a/libs/wire-api/test/golden/testObject_ConversationsResponse_1.json b/libs/wire-api/test/golden/testObject_ConversationsResponse_1.json index e32f3269b5..0bba3b7e15 100644 --- a/libs/wire-api/test/golden/testObject_ConversationsResponse_1.json +++ b/libs/wire-api/test/golden/testObject_ConversationsResponse_1.json @@ -71,6 +71,7 @@ "guest", "service" ], + "cipher_suite": 1, "creator": "00000000-0000-0000-0000-000200000001", "epoch": 42, "group_id": "dGVzdF9ncm91cA==", diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_1.json b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_1.json new file mode 100644 index 0000000000..099dab9f76 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_1.json @@ -0,0 +1,8 @@ +{ + "config": { + "enforceAppLock": false, + "inactivityTimeoutSecs": -98 + }, + "lockStatus": "unlocked", + "status": "enabled" +} diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_10.json b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_10.json new file mode 100644 index 0000000000..708deedc57 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_10.json @@ -0,0 +1,5 @@ +{ + "config": {}, + "lockStatus": "locked", + "status": "disabled" +} diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_11.json b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_11.json new file mode 100644 index 0000000000..c69cbb708e --- /dev/null +++ b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_11.json @@ -0,0 +1,5 @@ +{ + "config": {}, + "lockStatus": "locked", + "status": "enabled" +} diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_12.json b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_12.json new file mode 100644 index 0000000000..1207748c57 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_12.json @@ -0,0 +1,4 @@ +{ + "config": {}, + "status": "disabled" +} diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_13.json b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_13.json new file mode 100644 index 0000000000..c69cbb708e --- /dev/null +++ b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_13.json @@ -0,0 +1,5 @@ +{ + "config": {}, + "lockStatus": "locked", + "status": "enabled" +} diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_14.json b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_14.json new file mode 100644 index 0000000000..77cf61b96b --- /dev/null +++ b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_14.json @@ -0,0 +1,4 @@ +{ + "config": {}, + "lockStatus": "unlocked" +} diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_15.json b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_15.json new file mode 100644 index 0000000000..f17fdcc296 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_15.json @@ -0,0 +1,5 @@ +{ + "config": {}, + "lockStatus": "unlocked", + "status": "enabled" +} diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_16.json b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_16.json new file mode 100644 index 0000000000..31f62a6f3a --- /dev/null +++ b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_16.json @@ -0,0 +1,5 @@ +{ + "config": {}, + "lockStatus": "unlocked", + "status": "disabled" +} diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_17.json b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_17.json new file mode 100644 index 0000000000..1674edc80d --- /dev/null +++ b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_17.json @@ -0,0 +1,4 @@ +{ + "config": {}, + "status": "enabled" +} diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_18.json b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_18.json new file mode 100644 index 0000000000..fb16b19494 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_18.json @@ -0,0 +1,3 @@ +{ + "status": "enabled" +} diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_19.json b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_19.json new file mode 100644 index 0000000000..42f5fd6e5b --- /dev/null +++ b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_19.json @@ -0,0 +1,3 @@ +{ + "lockStatus": "unlocked" +} diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_2.json b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_2.json new file mode 100644 index 0000000000..556db5051b --- /dev/null +++ b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_2.json @@ -0,0 +1,6 @@ +{ + "config": { + "enforceAppLock": true, + "inactivityTimeoutSecs": 0 + } +} diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_3.json b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_3.json new file mode 100644 index 0000000000..67811614ac --- /dev/null +++ b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_3.json @@ -0,0 +1,8 @@ +{ + "config": { + "enforceAppLock": true, + "inactivityTimeoutSecs": 111 + }, + "lockStatus": "locked", + "status": "enabled" +} diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_4.json b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_4.json new file mode 100644 index 0000000000..01de2a36b3 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_4.json @@ -0,0 +1,6 @@ +{ + "config": { + "enforcedTimeoutSeconds": -97 + }, + "status": "enabled" +} diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_5.json b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_5.json new file mode 100644 index 0000000000..0513562a91 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_5.json @@ -0,0 +1,7 @@ +{ + "config": { + "enforcedTimeoutSeconds": 0 + }, + "lockStatus": "unlocked", + "status": "enabled" +} diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_6.json b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_6.json new file mode 100644 index 0000000000..8c21e06566 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_6.json @@ -0,0 +1,6 @@ +{ + "config": { + "enforcedTimeoutSeconds": 77 + }, + "status": "enabled" +} diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_7.json b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_7.json new file mode 100644 index 0000000000..438b212f9f --- /dev/null +++ b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_7.json @@ -0,0 +1,7 @@ +{ + "config": { + "domains": [] + }, + "lockStatus": "locked", + "status": "enabled" +} diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_8.json b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_8.json new file mode 100644 index 0000000000..ab76a8dee9 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_8.json @@ -0,0 +1,9 @@ +{ + "config": { + "domains": [ + "example.com", + "test.foobar" + ] + }, + "lockStatus": "locked" +} diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_9.json b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_9.json new file mode 100644 index 0000000000..1c9c921f51 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_9.json @@ -0,0 +1,9 @@ +{ + "config": { + "domains": [ + "test.foobar" + ] + }, + "lockStatus": "unlocked", + "status": "enabled" +} diff --git a/libs/wire-api/test/unit/Test/Wire/API/MLS.hs b/libs/wire-api/test/unit/Test/Wire/API/MLS.hs index 755172c756..51613740eb 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/MLS.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/MLS.hs @@ -31,6 +31,7 @@ import Test.Tasty.HUnit import Wire.API.MLS.CipherSuite import Wire.API.MLS.Commit import Wire.API.MLS.Credential +import Wire.API.MLS.Epoch import Wire.API.MLS.Extension import Wire.API.MLS.KeyPackage import Wire.API.MLS.Message diff --git a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs index 0ae4315a5a..27cd79e958 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs @@ -189,6 +189,8 @@ tests = testRoundTrip @Team.Conversation.TeamConversation, testRoundTrip @Team.Conversation.TeamConversationList, testRoundTrip @(Team.Feature.WithStatus Team.Feature.LegalholdConfig), + testRoundTrip @(Team.Feature.WithStatusPatch Team.Feature.LegalholdConfig), + testRoundTrip @(Team.Feature.WithStatusPatch Team.Feature.SelfDeletingMessagesConfig), testRoundTrip @(Team.Feature.WithStatusNoLock Team.Feature.LegalholdConfig), testRoundTrip @Team.Feature.AllFeatureConfigs, testRoundTrip @Team.Feature.FeatureStatus, diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 0ad6fb5e2e..5e96d2a1c2 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -46,7 +46,9 @@ library Wire.API.Message.Proto Wire.API.MLS.CipherSuite Wire.API.MLS.Commit + Wire.API.MLS.Context Wire.API.MLS.Credential + Wire.API.MLS.Epoch Wire.API.MLS.Extension Wire.API.MLS.Group Wire.API.MLS.KeyPackage @@ -476,6 +478,7 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Generated.VerifyDeleteUser_user Test.Wire.API.Golden.Generated.ViewLegalHoldService_team Test.Wire.API.Golden.Generated.ViewLegalHoldServiceInfo_team + Test.Wire.API.Golden.Generated.WithStatusPatch_team Test.Wire.API.Golden.Generated.WithStatus_team Test.Wire.API.Golden.Generated.WithStatusNoLock_team Test.Wire.API.Golden.Generated.Wrapped_20_22some_5fint_22_20Int_user diff --git a/nix/pkgs/mls_test_cli/default.nix b/nix/pkgs/mls_test_cli/default.nix index b27ad7d389..ef485c7316 100644 --- a/nix/pkgs/mls_test_cli/default.nix +++ b/nix/pkgs/mls_test_cli/default.nix @@ -15,9 +15,9 @@ rustPlatform.buildRustPackage rec { src = fetchFromGitHub { owner = "wireapp"; repo = "mls-test-cli"; - rev = "4befbebfd5575537167c5952db6e9967f2076001"; - sha256 = "sha256-j1n68VVs91uzQ9vwSuIHIMXxVZV/Xeto+V+69ErmL0Q="; + rev = "05cc435cfc16c0fb68434546ca4578ca35ecf550"; + sha256 = "sha256-Gd9LwWULGKolyaYJpcdK4KpneBf6jEaZqE7LjsRkY9E="; }; doCheck = false; - cargoSha256 = "sha256-6257yC+NyJu4PNLVQUrp+YVOOpvcuUoT9Hd9+uq+73w="; + cargoSha256 = "sha256-IdzcCrYJgaoxKTuJ0e1GPe0a5P1egBWmSKt9/or9nrM="; } diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index f30a84351d..73fa830e2c 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -180,7 +180,7 @@ swaggerDocsAPI (Just V1) = swaggerDocsAPI Nothing = swaggerDocsAPI (Just maxBound) servantSitemap :: ServerT BrigAPI (Handler r) -servantSitemap = userAPI :<|> selfAPI :<|> accountAPI :<|> clientAPI :<|> prekeyAPI :<|> userClientAPI :<|> connectionAPI :<|> propertiesAPI :<|> mlsAPI +servantSitemap = userAPI :<|> selfAPI :<|> accountAPI :<|> clientAPI :<|> prekeyAPI :<|> userClientAPI :<|> connectionAPI :<|> propertiesAPI :<|> mlsAPI :<|> userHandleAPI where userAPI :: ServerT UserAPI (Handler r) userAPI = @@ -266,6 +266,11 @@ servantSitemap = userAPI :<|> selfAPI :<|> accountAPI :<|> clientAPI :<|> prekey :<|> Named @"mls-key-packages-claim" claimKeyPackages :<|> Named @"mls-key-packages-count" countKeyPackages + userHandleAPI :: ServerT UserHandleAPI (Handler r) + userHandleAPI = + Named @"check-user-handles" checkHandles + :<|> Named @"check-user-handle" checkHandle + -- Note [ephemeral user sideeffect] -- If the user is ephemeral and expired, it will be removed upon calling -- CheckUserExists[Un]Qualified, see 'Brig.API.User.userGC'. @@ -277,33 +282,6 @@ sitemap :: Members '[CodeStore, PasswordResetStore] r => Routes Doc.ApiBuilder (Handler r) () sitemap = do - -- User Handle API ---------------------------------------------------- - - post "/users/handles" (continue checkHandlesH) $ - accept "application" "json" - .&. zauthUserId - .&. jsonRequest @Public.CheckHandles - document "POST" "checkUserHandles" $ do - Doc.summary "Check availability of user handles" - Doc.body (Doc.ref Public.modelCheckHandles) $ - Doc.description "JSON body" - Doc.returns (Doc.array Doc.string') - Doc.response 200 "List of free handles" Doc.end - - head "/users/handles/:handle" (continue checkHandleH) $ - zauthUserId - .&. capture "handle" - document "HEAD" "checkUserHandle" $ do - Doc.summary "Check whether a user handle can be taken" - Doc.parameter Doc.Path "handle" Doc.bytes' $ - Doc.description "Handle to check" - Doc.response 200 "Handle is taken" Doc.end - Doc.errorResponse (errorToWai @'E.InvalidHandle) - Doc.errorResponse (errorToWai @'E.HandleNotFound) - - -- some APIs moved to servant - -- end User Handle API - get "/users/:uid/rich-info" (continue getRichInfoH) $ zauthUserId .&. capture "uid" @@ -786,19 +764,19 @@ changeLocale u conn l = lift $ API.changeLocale u conn l -- | (zusr is ignored by this handler, ie. checking handles is allowed as long as you have -- *any* account.) -checkHandleH :: UserId ::: Text -> (Handler r) Response -checkHandleH (_uid ::: hndl) = +checkHandle :: UserId -> Text -> Handler r () +checkHandle _uid hndl = API.checkHandle hndl >>= \case - API.CheckHandleInvalid -> throwE (StdError (errorToWai @'E.InvalidHandle)) - API.CheckHandleFound -> pure $ setStatus status200 empty - API.CheckHandleNotFound -> pure $ setStatus status404 empty + API.CheckHandleInvalid -> throwStd (errorToWai @'E.InvalidHandle) + API.CheckHandleFound -> pure () + API.CheckHandleNotFound -> throwStd (errorToWai @'E.HandleNotFound) -checkHandlesH :: JSON ::: UserId ::: JsonRequest Public.CheckHandles -> (Handler r) Response -checkHandlesH (_ ::: _ ::: req) = do - Public.CheckHandles hs num <- parseJsonBody req +-- | (zusr is ignored by this handler, ie. checking handles is allowed as long as you have +-- *any* account.) +checkHandles :: UserId -> Public.CheckHandles -> Handler r [Handle] +checkHandles _ (Public.CheckHandles hs num) = do let handles = mapMaybe parseHandle (fromRange hs) - free <- lift . wrapClient $ API.checkHandles handles (fromRange num) - pure $ json (free :: [Handle]) + lift $ wrapHttpClient $ API.checkHandles handles (fromRange num) -- | This endpoint returns UserHandleInfo instead of UserProfile for backwards -- compatibility, whereas the corresponding qualified endpoint (implemented by diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index 335cfc91a6..a53eeaab21 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -635,10 +635,7 @@ instance Arbitrary AccountFeatureConfigs where arbitrary = AccountFeatureConfigs <$> fmap unlocked arbitrary <*> fmap unlocked arbitrary where unlocked :: Public.ImplicitLockStatus a -> Public.ImplicitLockStatus a - unlocked = Public.ImplicitLockStatus . setUnlocked . Public._unImplicitLockStatus - - setUnlocked :: Public.WithStatus a -> Public.WithStatus a - setUnlocked ws = ws {Public.wsLockStatus = Public.LockStatusUnlocked} + unlocked = Public.ImplicitLockStatus . Public.setLockStatus Public.LockStatusUnlocked . Public._unImplicitLockStatus instance FromJSON AccountFeatureConfigs where parseJSON = diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 9fe26c3e59..1b0a62d998 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -62,6 +62,7 @@ library Galley.Cassandra.Instances Galley.Cassandra.LegalHold Galley.Cassandra.Paging + Galley.Cassandra.Proposal Galley.Cassandra.Queries Galley.Cassandra.ResultSet Galley.Cassandra.SearchVisibility @@ -91,6 +92,7 @@ library Galley.Effects.ListItems Galley.Effects.MemberStore Galley.Effects.Paging + Galley.Effects.ProposalStore Galley.Effects.Queue Galley.Effects.RemoteConversationListStore Galley.Effects.SearchVisibilityStore @@ -627,6 +629,8 @@ executable galley-schema V66_AddSplashScreen V67_MLSFeature V68_MLSCommitLock + V69_MLSProposal + V70_MLSCipherSuite Paths_galley hs-source-dirs: schema/src diff --git a/services/galley/schema/src/Main.hs b/services/galley/schema/src/Main.hs index 2239be7cef..1d26cc7a89 100644 --- a/services/galley/schema/src/Main.hs +++ b/services/galley/schema/src/Main.hs @@ -71,6 +71,8 @@ import qualified V65_MLSRemoteClients import qualified V66_AddSplashScreen import qualified V67_MLSFeature import qualified V68_MLSCommitLock +import qualified V69_MLSProposal +import qualified V70_MLSCipherSuite main :: IO () main = do @@ -127,7 +129,9 @@ main = do V65_MLSRemoteClients.migration, V66_AddSplashScreen.migration, V67_MLSFeature.migration, - V68_MLSCommitLock.migration + V68_MLSCommitLock.migration, + V69_MLSProposal.migration, + V70_MLSCipherSuite.migration -- When adding migrations here, don't forget to update -- 'schemaVersion' in Galley.Cassandra -- (see also docs/developer/cassandra-interaction.md) diff --git a/services/galley/schema/src/V69_MLSProposal.hs b/services/galley/schema/src/V69_MLSProposal.hs new file mode 100644 index 0000000000..5b0e0c9ab1 --- /dev/null +++ b/services/galley/schema/src/V69_MLSProposal.hs @@ -0,0 +1,38 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module V69_MLSProposal + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + Migration 69 "Introduce an MLS proposals table" $ + schema' + [r| CREATE TABLE mls_proposal_refs ( + group_id blob, + epoch bigint, + ref blob, + proposal blob, + PRIMARY KEY (group_id, epoch, ref) + ) + |] diff --git a/services/galley/schema/src/V70_MLSCipherSuite.hs b/services/galley/schema/src/V70_MLSCipherSuite.hs new file mode 100644 index 0000000000..637b8ea7c4 --- /dev/null +++ b/services/galley/schema/src/V70_MLSCipherSuite.hs @@ -0,0 +1,34 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module V70_MLSCipherSuite + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + Migration 70 "Add the MLS cipher suite column to the conversation table" $ + schema' + [r| ALTER TABLE conversation ADD ( + cipher_suite int + ) + |] diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 2826309aba..2446ad1a91 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -52,6 +52,7 @@ import qualified Galley.Effects.BrigAccess as E import qualified Galley.Effects.ConversationStore as E import qualified Galley.Effects.FireAndForget as E import qualified Galley.Effects.MemberStore as E +import Galley.Effects.ProposalStore (ProposalStore) import Galley.Options import Galley.Types.Conversations.Members import Galley.Types.UserList (UserList (UserList)) @@ -561,7 +562,8 @@ sendMLSMessage :: MemberStore, Resource, TeamStore, - P.TinyLog + P.TinyLog, + ProposalStore ] r ) => diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index ff87ebd586..c289e187b6 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -147,6 +147,7 @@ type IFeatureAPI = :<|> IFeatureStatusGet FileSharingConfig :<|> IFeatureStatusPut '() FileSharingConfig :<|> IFeatureStatusLockStatusPut FileSharingConfig + :<|> IFeatureStatusPatch '() FileSharingConfig -- ConferenceCallingConfig :<|> IFeatureStatusGet ConferenceCallingConfig :<|> IFeatureStatusPut '() ConferenceCallingConfig @@ -154,14 +155,17 @@ type IFeatureAPI = :<|> IFeatureStatusGet SelfDeletingMessagesConfig :<|> IFeatureStatusPut '() SelfDeletingMessagesConfig :<|> IFeatureStatusLockStatusPut SelfDeletingMessagesConfig + :<|> IFeatureStatusPatch '() SelfDeletingMessagesConfig -- GuestLinksConfig :<|> IFeatureStatusGet GuestLinksConfig :<|> IFeatureStatusPut '() GuestLinksConfig :<|> IFeatureStatusLockStatusPut GuestLinksConfig + :<|> IFeatureStatusPatch '() GuestLinksConfig -- SndFactorPasswordChallengeConfig :<|> IFeatureStatusGet SndFactorPasswordChallengeConfig :<|> IFeatureStatusPut '() SndFactorPasswordChallengeConfig :<|> IFeatureStatusLockStatusPut SndFactorPasswordChallengeConfig + :<|> IFeatureStatusPatch '() SndFactorPasswordChallengeConfig -- SearchVisibilityInboundConfig :<|> IFeatureStatusGet SearchVisibilityInboundConfig :<|> IFeatureStatusPut '() SearchVisibilityInboundConfig @@ -352,8 +356,29 @@ type IFeatureStatusGet f = Named '("iget", f) (FeatureStatusBaseGet f) type IFeatureStatusPut errs f = Named '("iput", f) (FeatureStatusBasePutInternal errs f) +type IFeatureStatusPatch errs f = Named '("ipatch", f) (FeatureStatusBasePatchInternal errs f) + type FeatureStatusBasePutInternal errs featureConfig = - Summary (AppendSymbol "Put config for " (FeatureSymbol featureConfig)) + FeatureStatusBaseInternal + (AppendSymbol "Put config for " (FeatureSymbol featureConfig)) + errs + featureConfig + ( ReqBody '[Servant.JSON] (WithStatusNoLock featureConfig) + :> QueryParam "ttl" FeatureTTL + :> Put '[Servant.JSON] (WithStatus featureConfig) + ) + +type FeatureStatusBasePatchInternal errs featureConfig = + FeatureStatusBaseInternal + (AppendSymbol "Patch config for " (FeatureSymbol featureConfig)) + errs + featureConfig + ( ReqBody '[Servant.JSON] (WithStatusPatch featureConfig) + :> Patch '[Servant.JSON] (WithStatus featureConfig) + ) + +type FeatureStatusBaseInternal desc errs featureConfig a = + Summary desc :> CanThrow OperationDenied :> CanThrow 'NotATeamMember :> CanThrow 'TeamNotFound @@ -363,13 +388,11 @@ type FeatureStatusBasePutInternal errs featureConfig = :> Capture "tid" TeamId :> "features" :> FeatureSymbol featureConfig - :> ReqBody '[Servant.JSON] (WithStatusNoLock featureConfig) - :> QueryParam "ttl" FeatureTTL - :> Put '[Servant.JSON] (WithStatus featureConfig) + :> a type IFeatureStatusLockStatusPut featureName = Named - '("lock", featureName) + '("ilock", featureName) ( Summary (AppendSymbol "(Un-)lock " (FeatureSymbol featureName)) :> CanThrow 'NotATeamMember :> CanThrow 'TeamNotFound @@ -445,41 +468,45 @@ iTeamsAPI = mkAPI $ \tid -> hoistAPIHandler id (base tid) featureAPI :: API IFeatureAPI GalleyEffects featureAPI = - mkNamedAPI (getFeatureStatus @Cassandra DontDoAuth) - <@> mkNamedAPI (\tid ws ttl -> setFeatureStatus @Cassandra ttl DontDoAuth tid ws) - <@> mkNamedAPI (getFeatureStatus @Cassandra DontDoAuth) - <@> mkNamedAPI (\tid ws ttl -> setFeatureStatus @Cassandra ttl DontDoAuth tid ws) - <@> mkNamedAPI (getFeatureStatus @Cassandra DontDoAuth) - <@> mkNamedAPI (\tid ws ttl -> setFeatureStatus @Cassandra ttl DontDoAuth tid ws) - <@> mkNamedAPI (getFeatureStatus @Cassandra DontDoAuth) - <@> mkNamedAPI (\tid ws ttl -> setFeatureStatus @Cassandra ttl DontDoAuth tid ws) - <@> mkNamedAPI (getFeatureStatus @Cassandra DontDoAuth) - <@> mkNamedAPI (\tid ws ttl -> setFeatureStatus @Cassandra ttl DontDoAuth tid ws) - <@> mkNamedAPI (getFeatureStatus @Cassandra DontDoAuth) - <@> mkNamedAPI (\tid ws ttl -> setFeatureStatus @Cassandra ttl DontDoAuth tid ws) - <@> mkNamedAPI (getFeatureStatus @Cassandra DontDoAuth) - <@> mkNamedAPI (\tid ws ttl -> setFeatureStatus @Cassandra ttl DontDoAuth tid ws) - <@> mkNamedAPI (setLockStatus @Cassandra @FileSharingConfig) - <@> mkNamedAPI (getFeatureStatus @Cassandra DontDoAuth) - <@> mkNamedAPI (\tid ws ttl -> setFeatureStatus @Cassandra ttl DontDoAuth tid ws) - <@> mkNamedAPI (getFeatureStatus @Cassandra DontDoAuth) - <@> mkNamedAPI (\tid ws ttl -> setFeatureStatus @Cassandra ttl DontDoAuth tid ws) - <@> mkNamedAPI (setLockStatus @Cassandra @SelfDeletingMessagesConfig) - <@> mkNamedAPI (getFeatureStatus @Cassandra DontDoAuth) - <@> mkNamedAPI (\tid ws ttl -> setFeatureStatus @Cassandra ttl DontDoAuth tid ws) - <@> mkNamedAPI (setLockStatus @Cassandra @GuestLinksConfig) - <@> mkNamedAPI (getFeatureStatus @Cassandra DontDoAuth) - <@> mkNamedAPI (\tid ws ttl -> setFeatureStatus @Cassandra ttl DontDoAuth tid ws) - <@> mkNamedAPI (setLockStatus @Cassandra @SndFactorPasswordChallengeConfig) - <@> mkNamedAPI (getFeatureStatus @Cassandra DontDoAuth) - <@> mkNamedAPI (\tid ws ttl -> setFeatureStatus @Cassandra ttl DontDoAuth tid ws) - <@> mkNamedAPI (getFeatureStatusMulti @Cassandra @SearchVisibilityInboundConfig) - <@> mkNamedAPI (getFeatureStatus @Cassandra DontDoAuth) - <@> mkNamedAPI (getFeatureStatus @Cassandra DontDoAuth) - <@> mkNamedAPI (\tid ws ttl -> setFeatureStatus @Cassandra ttl DontDoAuth tid ws) - <@> mkNamedAPI (getFeatureStatus @Cassandra DontDoAuth) - <@> mkNamedAPI (\tid ws ttl -> setFeatureStatus @Cassandra ttl DontDoAuth tid ws) - <@> mkNamedAPI (maybe (getAllFeatureConfigsForServer @Cassandra) (getAllFeatureConfigsForUser @Cassandra)) + mkNamedAPI @'("iget", SSOConfig) (getFeatureStatus @Cassandra DontDoAuth) + <@> mkNamedAPI @'("iput", SSOConfig) (setFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @'("iget", LegalholdConfig) (getFeatureStatus @Cassandra DontDoAuth) + <@> mkNamedAPI @'("iput", LegalholdConfig) (setFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @'("iget", SearchVisibilityAvailableConfig) (getFeatureStatus @Cassandra DontDoAuth) + <@> mkNamedAPI @'("iput", SearchVisibilityAvailableConfig) (setFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @'("iget", ValidateSAMLEmailsConfig) (getFeatureStatus @Cassandra DontDoAuth) + <@> mkNamedAPI @'("iput", ValidateSAMLEmailsConfig) (setFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @'("iget", DigitalSignaturesConfig) (getFeatureStatus @Cassandra DontDoAuth) + <@> mkNamedAPI @'("iput", DigitalSignaturesConfig) (setFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @'("iget", AppLockConfig) (getFeatureStatus @Cassandra DontDoAuth) + <@> mkNamedAPI @'("iput", AppLockConfig) (setFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @'("iget", FileSharingConfig) (getFeatureStatus @Cassandra DontDoAuth) + <@> mkNamedAPI @'("iput", FileSharingConfig) (setFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @'("ilock", FileSharingConfig) (updateLockStatus @Cassandra @FileSharingConfig) + <@> mkNamedAPI @'("ipatch", FileSharingConfig) (patchFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @'("iget", ConferenceCallingConfig) (getFeatureStatus @Cassandra DontDoAuth) + <@> mkNamedAPI @'("iput", ConferenceCallingConfig) (setFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @'("iget", SelfDeletingMessagesConfig) (getFeatureStatus @Cassandra DontDoAuth) + <@> mkNamedAPI @'("iput", SelfDeletingMessagesConfig) (setFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @'("ilock", SelfDeletingMessagesConfig) (updateLockStatus @Cassandra @SelfDeletingMessagesConfig) + <@> mkNamedAPI @'("ipatch", SelfDeletingMessagesConfig) (patchFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @'("iget", GuestLinksConfig) (getFeatureStatus @Cassandra DontDoAuth) + <@> mkNamedAPI @'("iput", GuestLinksConfig) (setFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @'("ilock", GuestLinksConfig) (updateLockStatus @Cassandra @GuestLinksConfig) + <@> mkNamedAPI @'("ipatch", GuestLinksConfig) (patchFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @'("iget", SndFactorPasswordChallengeConfig) (getFeatureStatus @Cassandra DontDoAuth) + <@> mkNamedAPI @'("iput", SndFactorPasswordChallengeConfig) (setFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @'("ilock", SndFactorPasswordChallengeConfig) (updateLockStatus @Cassandra @SndFactorPasswordChallengeConfig) + <@> mkNamedAPI @'("ipatch", SndFactorPasswordChallengeConfig) (patchFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @'("iget", SearchVisibilityInboundConfig) (getFeatureStatus @Cassandra DontDoAuth) + <@> mkNamedAPI @'("iput", SearchVisibilityInboundConfig) (setFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @'("igetmulti", SearchVisibilityInboundConfig) (getFeatureStatusMulti @Cassandra) + <@> mkNamedAPI @'("iget", ClassifiedDomainsConfig) (getFeatureStatus @Cassandra DontDoAuth) + <@> mkNamedAPI @'("iget", MLSConfig) (getFeatureStatus @Cassandra DontDoAuth) + <@> mkNamedAPI @'("iput", MLSConfig) (setFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @'("iget", SearchVisibilityInboundConfig) (getFeatureStatus @Cassandra DontDoAuth) + <@> mkNamedAPI @'("iput", SearchVisibilityInboundConfig) (setFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @"feature-configs-internal" (maybe (getAllFeatureConfigsForServer @Cassandra) (getAllFeatureConfigsForUser @Cassandra)) internalSitemap :: Routes a (Sem GalleyEffects) () internalSitemap = do diff --git a/services/galley/src/Galley/API/MLS.hs b/services/galley/src/Galley/API/MLS.hs index c2886cdc9b..1e89f9bd59 100644 --- a/services/galley/src/Galley/API/MLS.hs +++ b/services/galley/src/Galley/API/MLS.hs @@ -19,6 +19,7 @@ module Galley.API.MLS ( postMLSWelcome, postMLSMessage, postMLSMessageFromLocalUser, + postMLSMessageFromLocalUserV1, ) where diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index fab451cf17..f6679837be 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -18,6 +18,7 @@ module Galley.API.MLS.Message ( postMLSMessageFromLocalUser, + postMLSMessageFromLocalUserV1, postMLSMessage, MLSMessageStaticErrors, ) @@ -33,6 +34,7 @@ import Data.List.NonEmpty (NonEmpty, nonEmpty) import qualified Data.Map as Map import Data.Qualified import qualified Data.Set as Set +import qualified Data.Text as T import Data.Time import Galley.API.Action import Galley.API.Error @@ -48,6 +50,7 @@ import Galley.Effects.BrigAccess import Galley.Effects.ConversationStore import Galley.Effects.FederatorAccess import Galley.Effects.MemberStore +import Galley.Effects.ProposalStore import Galley.Options import Galley.Types.Conversations.Members import Imports @@ -59,6 +62,7 @@ import Polysemy.Internal import Polysemy.Resource (Resource, bracket) import Polysemy.TinyLog import qualified System.Logger.Class as Logger +import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Error @@ -71,10 +75,10 @@ import Wire.API.Federation.Error import Wire.API.MLS.CipherSuite import Wire.API.MLS.Commit import Wire.API.MLS.Credential -import Wire.API.MLS.Group import Wire.API.MLS.KeyPackage import Wire.API.MLS.Message import Wire.API.MLS.Proposal +import qualified Wire.API.MLS.Proposal as Proposal import Wire.API.MLS.Serialisation import Wire.API.Message @@ -87,10 +91,11 @@ type MLSMessageStaticErrors = ErrorS 'MissingLegalholdConsent, ErrorS 'MLSKeyPackageRefNotFound, ErrorS 'MLSClientMismatch, - ErrorS 'MLSUnsupportedProposal + ErrorS 'MLSUnsupportedProposal, + ErrorS 'MLSCommitMissingReferences ] -postMLSMessageFromLocalUser :: +postMLSMessageFromLocalUserV1 :: ( HasProposalEffects r, Members '[ Resource, @@ -98,10 +103,13 @@ postMLSMessageFromLocalUser :: ErrorS 'ConvAccessDenied, ErrorS 'ConvNotFound, Error InternalError, + ErrorS 'MLSCommitMissingReferences, + ErrorS 'MLSProposalNotFound, ErrorS 'MLSUnsupportedMessage, ErrorS 'MLSStaleMessage, - ErrorS 'MLSProposalNotFound, ErrorS 'MissingLegalholdConsent, + Input (Local ()), + ProposalStore, TinyLog ] r @@ -110,10 +118,40 @@ postMLSMessageFromLocalUser :: ConnId -> RawMLS SomeMessage -> Sem r [Event] -postMLSMessageFromLocalUser lusr conn msg = +postMLSMessageFromLocalUserV1 lusr conn msg = map lcuEvent <$> postMLSMessage lusr (qUntagged lusr) (Just conn) msg +postMLSMessageFromLocalUser :: + ( HasProposalEffects r, + Members + '[ Resource, + Error FederationError, + ErrorS 'ConvAccessDenied, + ErrorS 'ConvNotFound, + Error InternalError, + ErrorS 'MLSCommitMissingReferences, + ErrorS 'MLSUnsupportedMessage, + ErrorS 'MLSStaleMessage, + ErrorS 'MLSProposalNotFound, + ErrorS 'MissingLegalholdConsent, + Input (Local ()), + ProposalStore, + TinyLog + ] + r + ) => + Local UserId -> + ConnId -> + RawMLS SomeMessage -> + Sem r MLSMessageSendingStatus +postMLSMessageFromLocalUser lusr conn msg = do + -- FUTUREWORK: Inline the body of 'postMLSMessageFromLocalUserV1' once version + -- V1 is dropped + events <- postMLSMessageFromLocalUserV1 lusr conn msg + t <- toUTCTimeMillis <$> input + pure $ MLSMessageSendingStatus events t + postMLSMessage :: ( HasProposalEffects r, Members @@ -125,8 +163,11 @@ postMLSMessage :: ErrorS 'MLSStaleMessage, ErrorS 'MLSProposalNotFound, ErrorS 'MissingLegalholdConsent, + ErrorS 'MLSCommitMissingReferences, Resource, - TinyLog + TinyLog, + ProposalStore, + Input (Local ()) ] r ) => @@ -155,8 +196,11 @@ postMLSMessageToLocalConv :: ErrorS 'MLSStaleMessage, ErrorS 'MLSProposalNotFound, ErrorS 'MissingLegalholdConsent, + ErrorS 'MLSCommitMissingReferences, Resource, - TinyLog + TinyLog, + ProposalStore, + Input (Local ()) ] r ) => @@ -169,13 +213,19 @@ postMLSMessageToLocalConv qusr con smsg lcnv = case rmValue smsg of SomeMessage tag msg -> do conv <- getConversation (tUnqualified lcnv) >>= noteS @'ConvNotFound + -- check that sender is part of conversation + loc <- qualifyLocal () + isMember' <- foldQualified loc (fmap isJust . getLocalMember (convId conv) . tUnqualified) (fmap isJust . getRemoteMember (convId conv)) qusr + unless isMember' $ throwS @'ConvNotFound + -- validate message events <- case tag of SMLSPlainText -> case msgTBS (msgPayload msg) of CommitMessage c -> processCommit qusr con (qualifyAs lcnv conv) (msgEpoch msg) (msgSender msg) c ApplicationMessage _ -> throwS @'MLSUnsupportedMessage - ProposalMessage _ -> pure mempty -- FUTUREWORK: handle proposals + ProposalMessage prop -> + processProposal qusr conv msg prop $> mempty SMLSCipherText -> case toMLSEnum' (msgContentType (msgPayload msg)) of Right CommitMessageTag -> throwS @'MLSUnsupportedMessage Right ProposalMessageTag -> throwS @'MLSUnsupportedMessage @@ -262,7 +312,9 @@ processCommit :: Member (Error FederationError) r, Member (Error InternalError) r, Member (ErrorS 'MissingLegalholdConsent) r, - Member Resource r + Member (ErrorS 'MLSCommitMissingReferences) r, + Member Resource r, + Member ProposalStore r ) => Qualified UserId -> Maybe ConnId -> @@ -286,6 +338,7 @@ processCommit qusr con lconv epoch sender commit = do let ttlSeconds :: Int = 600 -- 10 minutes withCommitLock groupId epoch (fromIntegral ttlSeconds) $ do + checkEpoch epoch (tUnqualified lconv) when (epoch == Epoch 0) $ do -- this is a newly created conversation, and it should contain exactly one -- client (the creator) @@ -305,8 +358,14 @@ processCommit qusr con lconv epoch sender commit = do -- the sender of the first commit must be a member _ -> throw (mlsProtocolError "Unexpected sender") + -- check all pending proposals are referenced in the commit + allPendingProposals <- getAllPendingProposals groupId epoch + let referencedProposals = Set.fromList $ mapMaybe (\x -> preview Proposal._Ref x) (cProposals commit) + unless (all (`Set.member` referencedProposals) allPendingProposals) $ + throwS @'MLSCommitMissingReferences + -- process and execute proposals - action <- foldMap applyProposalRef (cProposals commit) + action <- foldMap (applyProposalRef (tUnqualified lconv) groupId epoch) (cProposals commit) updates <- executeProposalAction qusr con lconv action -- increment epoch number @@ -316,12 +375,30 @@ processCommit qusr con lconv epoch sender commit = do applyProposalRef :: ( HasProposalEffects r, - Member (ErrorS 'MLSProposalNotFound) r + Members + '[ ErrorS 'ConvNotFound, + ErrorS 'MLSProposalNotFound, + ErrorS 'MLSStaleMessage, + ProposalStore + ] + r ) => + Data.Conversation -> + GroupId -> + Epoch -> ProposalOrRef -> Sem r ProposalAction -applyProposalRef (Ref _) = throwS @'MLSProposalNotFound -applyProposalRef (Inline p) = applyProposal p +applyProposalRef conv groupId epoch (Ref ref) = do + p <- getProposal groupId epoch ref >>= noteS @'MLSProposalNotFound + checkEpoch epoch conv + checkGroup groupId conv + applyProposal (rmValue p) +applyProposalRef conv _groupId _epoch (Inline p) = do + suite <- + preview (to convProtocol . _ProtocolMLS . to cnvmlsCipherSuite) conv + & noteS @'ConvNotFound + checkProposal suite p + applyProposal p applyProposal :: HasProposalEffects r => Proposal -> Sem r ProposalAction applyProposal (AddProposal kp) = do @@ -332,6 +409,62 @@ applyProposal (AddProposal kp) = do pure (paClient qclient) applyProposal _ = throwS @'MLSUnsupportedProposal +checkProposal :: + Members + '[ Error MLSProtocolError, + ProposalStore + ] + r => + CipherSuiteTag -> + Proposal -> + Sem r () +checkProposal suite (AddProposal kpRaw) = do + let kp = rmValue kpRaw + unless (kpCipherSuite kp == tagCipherSuite suite) + . throw + . mlsProtocolError + . T.pack + $ "The group's cipher suite " + <> show (cipherSuiteNumber (tagCipherSuite suite)) + <> " and the cipher suite of the proposal's key package " + <> show (cipherSuiteNumber (kpCipherSuite kp)) + <> " do not match." +checkProposal _suite _prop = pure () + +processProposal :: + HasProposalEffects r => + Members + '[ Error MLSProtocolError, + ErrorS 'ConvNotFound, + ErrorS 'MLSStaleMessage, + ProposalStore, + Input (Local ()) + ] + r => + Qualified UserId -> + Data.Conversation -> + Message 'MLSPlainText -> + RawMLS Proposal -> + Sem r () +processProposal qusr conv msg prop = do + checkEpoch (msgEpoch msg) conv + checkGroup (msgGroupId msg) conv + suiteTag <- + preview (to convProtocol . _ProtocolMLS . to cnvmlsCipherSuite) conv + & noteS @'ConvNotFound + + -- validate the proposal + -- + -- is the user a member of the conversation? + loc <- qualifyLocal () + isMember' <- foldQualified loc (fmap isJust . getLocalMember (convId conv) . tUnqualified) (fmap isJust . getRemoteMember (convId conv)) qusr + unless isMember' $ throwS @'ConvNotFound + + -- FUTUREWORK: validate the member's conversation role + let propRef = proposalRef suiteTag prop + checkProposal suiteTag (rmValue prop) + storeProposal (msgGroupId msg) (msgEpoch msg) propRef prop + executeProposalAction :: forall r. ( Member BrigAccess r, @@ -339,6 +472,7 @@ executeProposalAction :: Member (ErrorS 'ConvNotFound) r, Member (Error FederationError) r, Member (ErrorS 'MLSClientMismatch) r, + Member (Error MLSProtocolError) r, Member (Error MLSProposalFailure) r, Member (ErrorS 'MissingLegalholdConsent) r, Member (ErrorS 'MLSUnsupportedProposal) r, @@ -357,10 +491,8 @@ executeProposalAction :: ProposalAction -> Sem r [LocalConversationUpdate] executeProposalAction qusr con lconv action = do - -- For the moment, assume a fixed ciphersuite. - -- FUTUREWORK: store ciphersuite with the conversation - let cs = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 - ss = csSignatureScheme cs + cs <- preview (to convProtocol . _ProtocolMLS . to cnvmlsCipherSuite) (tUnqualified lconv) & noteS @'ConvNotFound + let ss = csSignatureScheme cs cm = convClientMap lconv newUserClients = Map.assocs (paAdd action) -- check that all clients of each user are added to the conversation, and @@ -496,6 +628,34 @@ getRemoteMLSClients rusr ss = do mcrSignatureScheme = ss } +-- | Check if the epoch number matches that of a conversation +checkEpoch :: + Members + '[ ErrorS 'ConvNotFound, + ErrorS 'MLSStaleMessage + ] + r => + Epoch -> + Data.Conversation -> + Sem r () +checkEpoch epoch conv = do + curEpoch <- + preview (to convProtocol . _ProtocolMLS . to cnvmlsEpoch) conv + & noteS @'ConvNotFound + unless (epoch == curEpoch) $ throwS @'MLSStaleMessage + +-- | Check if the group ID matches that of a conversation +checkGroup :: + Member (ErrorS 'ConvNotFound) r => + GroupId -> + Data.Conversation -> + Sem r () +checkGroup gId conv = do + groupId <- + preview (to convProtocol . _ProtocolMLS . to cnvmlsGroupId) conv + & noteS @'ConvNotFound + unless (gId == groupId) $ throwS @'ConvNotFound + -------------------------------------------------------------------------------- -- Error handling of proposal execution diff --git a/services/galley/src/Galley/API/Public/Servant.hs b/services/galley/src/Galley/API/Public/Servant.hs index 0d78ea19bd..6d64082700 100644 --- a/services/galley/src/Galley/API/Public/Servant.hs +++ b/services/galley/src/Galley/API/Public/Servant.hs @@ -159,6 +159,7 @@ servantSitemap = mls :: API MLSAPI GalleyEffects mls = mkNamedAPI @"mls-welcome-message" postMLSWelcome + <@> mkNamedAPI @"mls-message-v1" postMLSMessageFromLocalUserV1 <@> mkNamedAPI @"mls-message" postMLSMessageFromLocalUser customBackend :: API CustomBackendAPI GalleyEffects diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index b016d2508b..1e55dbe5d7 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -104,7 +104,7 @@ import Wire.API.Federation.API.Galley import Wire.API.Federation.Error import qualified Wire.API.Provider.Bot as Public import qualified Wire.API.Routes.MultiTablePaging as Public -import Wire.API.Team.Feature as Public +import Wire.API.Team.Feature as Public hiding (setStatus) getBotConversationH :: Members '[ConversationStore, ErrorS 'ConvNotFound, Input (Local ())] r => diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index f220f103a0..6f443c8e93 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -19,11 +19,13 @@ module Galley.API.Teams.Features ( getFeatureStatus, getFeatureStatusMulti, setFeatureStatus, + setFeatureStatusInternal, + patchFeatureStatusInternal, getFeatureStatusForUser, getAllFeatureConfigsForServer, getAllFeatureConfigsForTeam, getAllFeatureConfigsForUser, - setLockStatus, + updateLockStatus, -- Don't export methods of this typeclass GetFeatureConfig, -- Don't export methods of this typeclass @@ -223,6 +225,40 @@ getFeatureStatusMulti (Multi.TeamFeatureNoConfigMultiRequest tids) = do toTeamStatus :: TeamId -> WithStatusNoLock cfg -> Multi.TeamStatus cfg toTeamStatus tid ws = Multi.TeamStatus tid (wssStatus ws) +patchFeatureStatusInternal :: + forall db cfg r. + ( SetFeatureConfig db cfg, + GetConfigForTeamConstraints db cfg r, + SetConfigForTeamConstraints db cfg r, + FeaturePersistentConstraint db cfg, + Members + '[ ErrorS 'NotATeamMember, + ErrorS OperationDenied, + ErrorS 'TeamNotFound, + Error TeamFeatureError, + TeamStore, + TeamFeatureStore db, + P.Logger (Log.Msg -> Log.Msg), + GundeckAccess + ] + r + ) => + TeamId -> + WithStatusPatch cfg -> + Sem r (WithStatus cfg) +patchFeatureStatusInternal tid patch = do + currentFeatureStatus <- getFeatureStatus @db @cfg DontDoAuth tid + let newFeatureStatus = applyPatch currentFeatureStatus + when (isJust $ wspLockStatus patch) $ void $ updateLockStatus @db @cfg tid (wsLockStatus newFeatureStatus) + setConfigForTeam @db @cfg tid (forgetLock newFeatureStatus) Nothing + where + applyPatch :: WithStatus cfg -> WithStatus cfg + applyPatch current = + current + & setStatus (fromMaybe (wsStatus current) (wspStatus patch)) + & setLockStatus (fromMaybe (wsLockStatus current) (wspLockStatus patch)) + & setConfig (fromMaybe (wsConfig current) (wspConfig patch)) + setFeatureStatus :: forall db cfg r. ( SetFeatureConfig db cfg, @@ -256,7 +292,31 @@ setFeatureStatus mTtl doauth tid wsnl = do guardLockStatus . wsLockStatus =<< getConfigForTeam @db @cfg tid setConfigForTeam @db @cfg tid wsnl mTtl -setLockStatus :: +setFeatureStatusInternal :: + forall db cfg r. + ( SetFeatureConfig db cfg, + GetConfigForTeamConstraints db cfg r, + SetConfigForTeamConstraints db cfg r, + FeaturePersistentConstraint db cfg, + Members + '[ ErrorS 'NotATeamMember, + ErrorS OperationDenied, + ErrorS 'TeamNotFound, + Error TeamFeatureError, + TeamStore, + TeamFeatureStore db, + P.Logger (Log.Msg -> Log.Msg), + GundeckAccess + ] + r + ) => + TeamId -> + WithStatusNoLock cfg -> + Maybe FeatureTTL -> + Sem r (WithStatus cfg) +setFeatureStatusInternal tid wsnl mTtl = setFeatureStatus @db @cfg mTtl DontDoAuth tid wsnl + +updateLockStatus :: forall db cfg r. ( FeaturePersistentConstraint db cfg, Member (TeamFeatureStore db) r, @@ -266,7 +326,7 @@ setLockStatus :: TeamId -> LockStatus -> Sem r LockStatusResponse -setLockStatus tid lockStatus = do +updateLockStatus tid lockStatus = do assertTeamExists tid TeamFeatures.setFeatureLockStatus @db (Proxy @cfg) tid lockStatus pure $ LockStatusResponse lockStatus @@ -568,7 +628,7 @@ instance GetFeatureConfig db SSOConfig where inputs (view (optSettings . setFeatureFlags . flagSSO)) <&> \case FeatureSSOEnabledByDefault -> FeatureStatusEnabled FeatureSSODisabledByDefault -> FeatureStatusDisabled - pure $ defFeatureStatus {wsStatus = status} + pure $ setStatus status defFeatureStatus getConfigForUser = genericGetConfigForUser @db @@ -587,7 +647,7 @@ instance GetFeatureConfig db SearchVisibilityAvailableConfig where inputs (view (optSettings . setFeatureFlags . flagTeamSearchVisibility)) <&> \case FeatureTeamSearchVisibilityAvailableByDefault -> FeatureStatusEnabled FeatureTeamSearchVisibilityUnavailableByDefault -> FeatureStatusDisabled - pure $ defFeatureStatus {wsStatus = status} + pure $ setStatus status defFeatureStatus instance SetFeatureConfig db SearchVisibilityAvailableConfig where type SetConfigForTeamConstraints db SearchVisibilityAvailableConfig (r :: EffectRow) = (Members '[SearchVisibilityStore] r) @@ -641,7 +701,7 @@ instance GetFeatureConfig db LegalholdConfig where isLegalHoldEnabledForTeam @db tid <&> \case True -> FeatureStatusEnabled False -> FeatureStatusDisabled - pure $ defFeatureStatus {wsStatus = status} + pure $ setStatus status defFeatureStatus instance SetFeatureConfig db LegalholdConfig where type diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index dc18e0eaef..4257c7b863 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -129,7 +129,7 @@ import Wire.API.Provider.Service (ServiceRef) import Wire.API.Routes.Public.Galley import Wire.API.Routes.Public.Util (UpdateResult (..)) import Wire.API.ServantProto (RawProto (..)) -import Wire.API.Team.Feature +import Wire.API.Team.Feature hiding (setStatus) import Wire.API.Team.Member import Wire.API.User.Client diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index 43452af1ae..7ea627cc3b 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -67,6 +67,7 @@ import Galley.Cassandra.Conversation.Members import Galley.Cassandra.ConversationList import Galley.Cassandra.CustomBackend import Galley.Cassandra.LegalHold +import Galley.Cassandra.Proposal import Galley.Cassandra.SearchVisibility import Galley.Cassandra.Services import Galley.Cassandra.Team @@ -224,7 +225,7 @@ evalGalley :: Env -> Sem GalleyEffects a -> ExceptT Wai.Error IO a evalGalley e = ExceptT . runFinal @IO - . runResource + . resourceToIOFinal . runError . embedToFinal @IO . mapError toWai @@ -254,6 +255,7 @@ evalGalley e = . interpretLegalHoldStoreToCassandra lh . interpretCustomBackendStoreToCassandra . interpretConversationStoreToCassandra + . interpretProposalStoreToCassandra . interpretCodeStoreToCassandra . interpretClientStoreToCassandra . interpretFireAndForget diff --git a/services/galley/src/Galley/Cassandra.hs b/services/galley/src/Galley/Cassandra.hs index e1250c5b4a..4d70622b88 100644 --- a/services/galley/src/Galley/Cassandra.hs +++ b/services/galley/src/Galley/Cassandra.hs @@ -20,4 +20,4 @@ module Galley.Cassandra (schemaVersion) where import Imports schemaVersion :: Int32 -schemaVersion = 68 +schemaVersion = 70 diff --git a/services/galley/src/Galley/Cassandra/Conversation.hs b/services/galley/src/Galley/Cassandra/Conversation.hs index 95e94c427e..9956105bb6 100644 --- a/services/galley/src/Galley/Cassandra/Conversation.hs +++ b/services/galley/src/Galley/Cassandra/Conversation.hs @@ -52,22 +52,32 @@ import qualified System.Logger as Log import qualified UnliftIO import Wire.API.Conversation hiding (Conversation, Member) import Wire.API.Conversation.Protocol +import Wire.API.MLS.CipherSuite import Wire.API.MLS.Group createConversation :: Local ConvId -> NewConversation -> Client Conversation createConversation lcnv nc = do let meta = ncMetadata nc - (proto, mgid, mep) = case ncProtocol nc of - ProtocolProteusTag -> (ProtocolProteus, Nothing, Nothing) + (proto, mgid, mep, mcs) = case ncProtocol nc of + ProtocolProteusTag -> (ProtocolProteus, Nothing, Nothing, Nothing) ProtocolMLSTag -> let gid = convToGroupId lcnv + ep = Epoch 0 + cs = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 in ( ProtocolMLS ConversationMLSData { cnvmlsGroupId = gid, - cnvmlsEpoch = Epoch 0 + cnvmlsEpoch = ep, + cnvmlsCipherSuite = cs }, Just gid, - Just (Epoch 0) + Just ep, + -- FUTUREWORK: Make the cipher suite be a record field in + -- 'NewConversation' instead of hard-coding it here. + -- + -- 'CipherSuite 1' corresponds to + -- 'MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519'. + Just cs ) retry x5 . batch $ do setType BatchLogged @@ -85,7 +95,8 @@ createConversation lcnv nc = do cnvmReceiptMode meta, ncProtocol nc, mgid, - mep + mep, + mcs ) for_ (cnvmTeam meta) $ \tid -> addPrepQuery Cql.insertTeamConv (tid, tUnqualified lcnv) for_ mgid $ \gid -> addPrepQuery Cql.insertGroupId (gid, tUnqualified lcnv, tDomain lcnv) @@ -117,7 +128,7 @@ conversationMeta conv = (toConvMeta =<<) <$> retry x1 (query1 Cql.selectConv (params LocalQuorum (Identity conv))) where - toConvMeta (t, c, a, r, r', n, i, _, mt, rm, _, _, _) = do + toConvMeta (t, c, a, r, r', n, i, _, mt, rm, _, _, _, _) = do let mbAccessRolesV2 = Set.fromList . Cql.fromSet <$> r' accessRoles = maybeRole t $ parseAccessRoles r mbAccessRolesV2 pure $ ConversationMetadata t c (defAccess t a) accessRoles n i mt rm @@ -230,23 +241,28 @@ remoteConversationStatusOnDomain uid rconvs = toMemberStatus (omus, omur, oar, oarr, hid, hidr) ) -toProtocol :: Maybe ProtocolTag -> Maybe GroupId -> Maybe Epoch -> Maybe Protocol -toProtocol Nothing _ _ = Just ProtocolProteus -toProtocol (Just ProtocolProteusTag) _ _ = Just ProtocolProteus -toProtocol (Just ProtocolMLSTag) mgid mepoch = - ProtocolMLS <$> (ConversationMLSData <$> mgid <*> mepoch) +toProtocol :: + Maybe ProtocolTag -> + Maybe GroupId -> + Maybe Epoch -> + Maybe CipherSuiteTag -> + Maybe Protocol +toProtocol Nothing _ _ _ = Just ProtocolProteus +toProtocol (Just ProtocolProteusTag) _ _ _ = Just ProtocolProteus +toProtocol (Just ProtocolMLSTag) mgid mepoch mcs = + ProtocolMLS <$> (ConversationMLSData <$> mgid <*> mepoch <*> mcs) toConv :: ConvId -> [LocalMember] -> [RemoteMember] -> - Maybe (ConvType, UserId, Maybe (Cql.Set Access), Maybe AccessRoleLegacy, Maybe (Cql.Set AccessRoleV2), Maybe Text, Maybe TeamId, Maybe Bool, Maybe Milliseconds, Maybe ReceiptMode, Maybe ProtocolTag, Maybe GroupId, Maybe Epoch) -> + Maybe (ConvType, UserId, Maybe (Cql.Set Access), Maybe AccessRoleLegacy, Maybe (Cql.Set AccessRoleV2), Maybe Text, Maybe TeamId, Maybe Bool, Maybe Milliseconds, Maybe ReceiptMode, Maybe ProtocolTag, Maybe GroupId, Maybe Epoch, Maybe CipherSuiteTag) -> Maybe Conversation toConv cid ms remoteMems mconv = do - (cty, uid, acc, role, roleV2, nme, ti, del, timer, rm, ptag, mgid, mep) <- mconv + (cty, uid, acc, role, roleV2, nme, ti, del, timer, rm, ptag, mgid, mep, mcs) <- mconv let mbAccessRolesV2 = Set.fromList . Cql.fromSet <$> roleV2 accessRoles = maybeRole cty $ parseAccessRoles role mbAccessRolesV2 - proto <- toProtocol ptag mgid mep + proto <- toProtocol ptag mgid mep mcs pure Conversation { convId = cid, diff --git a/services/galley/src/Galley/Cassandra/Conversation/MLS.hs b/services/galley/src/Galley/Cassandra/Conversation/MLS.hs index 607e386609..7fda951968 100644 --- a/services/galley/src/Galley/Cassandra/Conversation/MLS.hs +++ b/services/galley/src/Galley/Cassandra/Conversation/MLS.hs @@ -23,8 +23,8 @@ import Data.Time import qualified Galley.Cassandra.Queries as Cql import Galley.Data.Types import Imports +import Wire.API.MLS.Epoch import Wire.API.MLS.Group -import Wire.API.MLS.Message acquireCommitLock :: GroupId -> Epoch -> NominalDiffTime -> Client LockAcquired acquireCommitLock groupId epoch ttl = do diff --git a/services/galley/src/Galley/Cassandra/Conversation/Members.hs b/services/galley/src/Galley/Cassandra/Conversation/Members.hs index b00e34a0a8..0edf47b65c 100644 --- a/services/galley/src/Galley/Cassandra/Conversation/Members.hs +++ b/services/galley/src/Galley/Cassandra/Conversation/Members.hs @@ -26,6 +26,7 @@ module Galley.Cassandra.Conversation.Members where import Cassandra +import Data.Domain import Data.Id import qualified Data.List.Extra as List import Data.Monoid @@ -178,6 +179,17 @@ newRemoteMemberWithRole ur@(qUntagged -> (Qualified (u, r) _)) = rmMLSClients = mempty } +lookupRemoteMember :: ConvId -> Domain -> UserId -> Client (Maybe RemoteMember) +lookupRemoteMember conv domain usr = do + mkMem <$$> retry x1 (query1 Cql.selectRemoteMember (params LocalQuorum (conv, domain, usr))) + where + mkMem (role, clients) = + RemoteMember + { rmId = toRemoteUnsafe domain usr, + rmConvRoleName = role, + rmMLSClients = Set.fromList (fromSet clients) + } + lookupRemoteMembers :: ConvId -> Client [RemoteMember] lookupRemoteMembers conv = do fmap (map mkMem) . retry x1 $ query Cql.selectRemoteMembers (params LocalQuorum (Identity conv)) @@ -360,6 +372,7 @@ interpretMemberStoreToCassandra = interpret $ \case CreateBotMember sr bid cid -> embedClient $ addBotMember sr bid cid GetLocalMember cid uid -> embedClient $ member cid uid GetLocalMembers cid -> embedClient $ members cid + GetRemoteMember cid uid -> embedClient $ lookupRemoteMember cid (tDomain uid) (tUnqualified uid) GetRemoteMembers rcid -> embedClient $ lookupRemoteMembers rcid SelectRemoteMembers uids rcnv -> embedClient $ filterRemoteConvMembers uids rcnv SetSelfMember qcid luid upd -> embedClient $ updateSelfMember qcid luid upd diff --git a/services/galley/src/Galley/Cassandra/Instances.hs b/services/galley/src/Galley/Cassandra/Instances.hs index ce40c92f1e..f25ffc2a27 100644 --- a/services/galley/src/Galley/Cassandra/Instances.hs +++ b/services/galley/src/Galley/Cassandra/Instances.hs @@ -28,6 +28,8 @@ import Control.Error (note) import Data.ByteString.Conversion import qualified Data.ByteString.Lazy as LBS import Data.Domain (Domain, domainText, mkDomain) +import Data.Either.Combinators hiding (fromRight) +import qualified Data.Text as T import qualified Data.Text.Encoding as T import Galley.Types.Bot () import Galley.Types.Teams.Intra @@ -35,7 +37,9 @@ import Imports import Wire.API.Asset (AssetKey, assetKeyToText) import Wire.API.Conversation import Wire.API.Conversation.Protocol -import Wire.API.MLS.CipherSuite (CipherSuite (CipherSuite, cipherSuiteNumber), CipherSuiteTag, cipherSuiteTag, tagCipherSuite) +import Wire.API.MLS.CipherSuite +import Wire.API.MLS.Proposal +import Wire.API.MLS.Serialisation import Wire.API.Team import qualified Wire.API.Team.Feature as Public import Wire.API.Team.SearchVisibility @@ -222,3 +226,24 @@ instance Cql CipherSuiteTag where Just tag -> Right tag Nothing -> Left "CipherSuiteTag: unexpected index" fromCql _ = Left "CipherSuiteTag: int expected" + +instance Cql ProposalRef where + ctype = Tagged BlobColumn + toCql = CqlBlob . LBS.fromStrict . unProposalRef + fromCql (CqlBlob b) = Right . ProposalRef . LBS.toStrict $ b + fromCql _ = Left "ProposalRef: blob expected" + +instance Cql (RawMLS Proposal) where + ctype = Tagged BlobColumn + toCql = CqlBlob . LBS.fromStrict . rmRaw + fromCql (CqlBlob b) = mapLeft T.unpack $ decodeMLS b + fromCql _ = Left "Proposal: blob expected" + +instance Cql CipherSuite where + ctype = Tagged IntColumn + toCql = CqlInt . fromIntegral . cipherSuiteNumber + fromCql (CqlInt i) = + if i < 2 ^ (16 :: Integer) + then Right . CipherSuite . fromIntegral $ i + else Left "CipherSuite: an out of bounds value for Word16" + fromCql _ = Left "CipherSuite: int expected" diff --git a/services/galley/src/Galley/Cassandra/Proposal.hs b/services/galley/src/Galley/Cassandra/Proposal.hs new file mode 100644 index 0000000000..9b45e5da8f --- /dev/null +++ b/services/galley/src/Galley/Cassandra/Proposal.hs @@ -0,0 +1,66 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Galley.Cassandra.Proposal (interpretProposalStoreToCassandra) where + +import Cassandra +import Galley.Cassandra.Instances () +import Galley.Cassandra.Store +import Galley.Effects.ProposalStore +import Imports +import Polysemy +import Polysemy.Input +import Wire.API.MLS.Epoch +import Wire.API.MLS.Group +import Wire.API.MLS.Proposal +import Wire.API.MLS.Serialisation + +type TTL = Integer + +-- | Proposals in the database expire after this timeout in seconds +defaultTTL :: TTL +defaultTTL = 28 * 24 * 60 * 60 + +interpretProposalStoreToCassandra :: + Members '[Embed IO, Input ClientState] r => + Sem (ProposalStore ': r) a -> + Sem r a +interpretProposalStoreToCassandra = + interpret $ + embedClient . \case + StoreProposal groupId epoch ref raw -> + retry x5 $ + write (storeQuery defaultTTL) (params LocalQuorum (groupId, epoch, ref, raw)) + GetProposal groupId epoch ref -> + runIdentity <$$> retry x1 (query1 getQuery (params LocalQuorum (groupId, epoch, ref))) + GetAllPendingProposals groupId epoch -> + runIdentity <$$> retry x1 (query getAllPending (params LocalQuorum (groupId, epoch))) + +storeQuery :: TTL -> PrepQuery W (GroupId, Epoch, ProposalRef, RawMLS Proposal) () +storeQuery ttl = + fromString $ + "insert into mls_proposal_refs (group_id, epoch, ref, proposal)\ + \ values (?, ?, ?, ?) using ttl " + <> show ttl + +getQuery :: PrepQuery R (GroupId, Epoch, ProposalRef) (Identity (RawMLS Proposal)) +getQuery = "select proposal from mls_proposal_refs where group_id = ? and epoch = ? and ref = ?" + +getAllPending :: PrepQuery R (GroupId, Epoch) (Identity ProposalRef) +getAllPending = "select ref from mls_proposal_refs where group_id = ? and epoch = ?" diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index b955d30573..bf16449999 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -34,6 +34,7 @@ import Wire.API.Conversation import Wire.API.Conversation.Code import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role +import Wire.API.MLS.CipherSuite import Wire.API.Provider import Wire.API.Provider.Service import Wire.API.Team @@ -195,8 +196,8 @@ updateTeamSplashScreen = "update team set splash_screen = ? where team = ?" -- Conversations ------------------------------------------------------------ -selectConv :: PrepQuery R (Identity ConvId) (ConvType, UserId, Maybe (C.Set Access), Maybe AccessRoleLegacy, Maybe (C.Set AccessRoleV2), Maybe Text, Maybe TeamId, Maybe Bool, Maybe Milliseconds, Maybe ReceiptMode, Maybe ProtocolTag, Maybe GroupId, Maybe Epoch) -selectConv = "select type, creator, access, access_role, access_roles_v2, name, team, deleted, message_timer, receipt_mode, protocol, group_id, epoch from conversation where conv = ?" +selectConv :: PrepQuery R (Identity ConvId) (ConvType, UserId, Maybe (C.Set Access), Maybe AccessRoleLegacy, Maybe (C.Set AccessRoleV2), Maybe Text, Maybe TeamId, Maybe Bool, Maybe Milliseconds, Maybe ReceiptMode, Maybe ProtocolTag, Maybe GroupId, Maybe Epoch, Maybe CipherSuiteTag) +selectConv = "select type, creator, access, access_role, access_roles_v2, name, team, deleted, message_timer, receipt_mode, protocol, group_id, epoch, cipher_suite from conversation where conv = ?" selectReceiptMode :: PrepQuery R (Identity ConvId) (Identity (Maybe ReceiptMode)) selectReceiptMode = "select receipt_mode from conversation where conv = ?" @@ -204,8 +205,8 @@ selectReceiptMode = "select receipt_mode from conversation where conv = ?" isConvDeleted :: PrepQuery R (Identity ConvId) (Identity (Maybe Bool)) isConvDeleted = "select deleted from conversation where conv = ?" -insertConv :: PrepQuery W (ConvId, ConvType, UserId, C.Set Access, C.Set AccessRoleV2, Maybe Text, Maybe TeamId, Maybe Milliseconds, Maybe ReceiptMode, ProtocolTag, Maybe GroupId, Maybe Epoch) () -insertConv = "insert into conversation (conv, type, creator, access, access_roles_v2, name, team, message_timer, receipt_mode, protocol, group_id, epoch) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" +insertConv :: PrepQuery W (ConvId, ConvType, UserId, C.Set Access, C.Set AccessRoleV2, Maybe Text, Maybe TeamId, Maybe Milliseconds, Maybe ReceiptMode, ProtocolTag, Maybe GroupId, Maybe Epoch, Maybe CipherSuiteTag) () +insertConv = "insert into conversation (conv, type, creator, access, access_roles_v2, name, team, message_timer, receipt_mode, protocol, group_id, epoch, cipher_suite) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" updateConvAccess :: PrepQuery W (C.Set Access, C.Set AccessRoleV2, ConvId) () updateConvAccess = "update conversation set access = ?, access_roles_v2 = ? where conv = ?" @@ -307,6 +308,9 @@ insertRemoteMember = "insert into member_remote_user (conv, user_remote_domain, removeRemoteMember :: PrepQuery W (ConvId, Domain, UserId) () removeRemoteMember = "delete from member_remote_user where conv = ? and user_remote_domain = ? and user_remote_id = ?" +selectRemoteMember :: PrepQuery R (ConvId, Domain, UserId) (RoleName, C.Set ClientId) +selectRemoteMember = "select conversation_role, mls_clients from member_remote_user where conv = ? and user_remote_domain = ? and user_remote_id = ?" + selectRemoteMembers :: PrepQuery R (Identity ConvId) (Domain, UserId, RoleName, C.Set ClientId) selectRemoteMembers = "select user_remote_domain, user_remote_id, conversation_role, mls_clients from member_remote_user where conv = ?" diff --git a/services/galley/src/Galley/Cassandra/TeamFeatures.hs b/services/galley/src/Galley/Cassandra/TeamFeatures.hs index 96697b9bda..f085cb6cb2 100644 --- a/services/galley/src/Galley/Cassandra/TeamFeatures.hs +++ b/services/galley/src/Galley/Cassandra/TeamFeatures.hs @@ -75,7 +75,7 @@ getTrivialConfigC statusCol tid = do mFeatureStatus <- (>>= runIdentity) <$> retry x1 q pure $ case mFeatureStatus of Nothing -> Nothing - Just status -> Just . forgetLock $ defFeatureStatus {wsStatus = status} + Just status -> Just . forgetLock $ setStatus status defFeatureStatus where select :: PrepQuery R (Identity TeamId) (Identity (Maybe FeatureStatus)) select = diff --git a/services/galley/src/Galley/Effects.hs b/services/galley/src/Galley/Effects.hs index be8a4dd882..ca74c9d98d 100644 --- a/services/galley/src/Galley/Effects.hs +++ b/services/galley/src/Galley/Effects.hs @@ -77,6 +77,7 @@ import Galley.Effects.GundeckAccess import Galley.Effects.LegalHoldStore import Galley.Effects.ListItems import Galley.Effects.MemberStore +import Galley.Effects.ProposalStore import Galley.Effects.Queue import Galley.Effects.SearchVisibilityStore import Galley.Effects.ServiceStore @@ -105,6 +106,7 @@ type GalleyEffects1 = FireAndForget, ClientStore, CodeStore, + ProposalStore, ConversationStore, CustomBackendStore, LegalHoldStore, diff --git a/services/galley/src/Galley/Effects/ConversationStore.hs b/services/galley/src/Galley/Effects/ConversationStore.hs index 0765a3f71f..442d2cfe8c 100644 --- a/services/galley/src/Galley/Effects/ConversationStore.hs +++ b/services/galley/src/Galley/Effects/ConversationStore.hs @@ -64,7 +64,7 @@ import Galley.Types.Conversations.Members import Imports import Polysemy import Wire.API.Conversation hiding (Conversation, Member) -import Wire.API.MLS.Message +import Wire.API.MLS.Epoch data ConversationStore m a where CreateConversationId :: ConversationStore m ConvId diff --git a/services/galley/src/Galley/Effects/MemberStore.hs b/services/galley/src/Galley/Effects/MemberStore.hs index 2def279536..51606436f2 100644 --- a/services/galley/src/Galley/Effects/MemberStore.hs +++ b/services/galley/src/Galley/Effects/MemberStore.hs @@ -30,6 +30,7 @@ module Galley.Effects.MemberStore -- * Read members getLocalMember, getLocalMembers, + getRemoteMember, getRemoteMembers, selectRemoteMembers, @@ -61,6 +62,7 @@ data MemberStore m a where CreateBotMember :: ServiceRef -> BotId -> ConvId -> MemberStore m BotMember GetLocalMember :: ConvId -> UserId -> MemberStore m (Maybe LocalMember) GetLocalMembers :: ConvId -> MemberStore m [LocalMember] + GetRemoteMember :: ConvId -> Remote UserId -> MemberStore m (Maybe RemoteMember) GetRemoteMembers :: ConvId -> MemberStore m [RemoteMember] SelectRemoteMembers :: [UserId] -> Remote ConvId -> MemberStore m ([UserId], Bool) SetSelfMember :: Qualified ConvId -> Local UserId -> MemberUpdate -> MemberStore m () diff --git a/services/galley/src/Galley/Effects/ProposalStore.hs b/services/galley/src/Galley/Effects/ProposalStore.hs new file mode 100644 index 0000000000..4bbd86c871 --- /dev/null +++ b/services/galley/src/Galley/Effects/ProposalStore.hs @@ -0,0 +1,46 @@ +{-# LANGUAGE TemplateHaskell #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Galley.Effects.ProposalStore where + +import Imports +import Polysemy +import Wire.API.MLS.Epoch +import Wire.API.MLS.Group +import Wire.API.MLS.Proposal +import Wire.API.MLS.Serialisation + +data ProposalStore m a where + StoreProposal :: + GroupId -> + Epoch -> + ProposalRef -> + RawMLS Proposal -> + ProposalStore m () + GetProposal :: + GroupId -> + Epoch -> + ProposalRef -> + ProposalStore m (Maybe (RawMLS Proposal)) + GetAllPendingProposals :: + GroupId -> + Epoch -> + ProposalStore m [ProposalRef] + +makeSem ''ProposalStore diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 644df340f6..47a6d88bcd 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -1266,7 +1266,7 @@ testJoinTeamConvGuestLinksDisabled = do let checkFeatureStatus fstatus = Util.getTeamFeatureFlagWithGalley @Public.GuestLinksConfig galley owner teamId !!! do const 200 === statusCode - const (Right (Public.WithStatus fstatus Public.LockStatusUnlocked Public.GuestLinksConfig)) === responseJsonEither + const (Right (Public.withStatus fstatus Public.LockStatusUnlocked Public.GuestLinksConfig)) === responseJsonEither -- guest can join if guest link feature is enabled checkFeatureStatus Public.FeatureStatusEnabled diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 3d8f25675e..ea2554f3a2 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -1,4 +1,6 @@ {-# LANGUAGE RecordWildCards #-} +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -15,7 +17,6 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} module API.MLS (tests) where @@ -24,8 +25,10 @@ import API.Util import Bilge hiding (head) import Bilge.Assert import Cassandra +import Control.Arrow import Control.Lens (view) import qualified Data.Aeson as Aeson +import qualified Data.ByteString as BS import Data.Default import Data.Domain import Data.Id @@ -54,10 +57,13 @@ import Wire.API.Conversation import Wire.API.Conversation.Action import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role +import Wire.API.Error.Galley import Wire.API.Event.Conversation import Wire.API.Federation.API.Common import Wire.API.Federation.API.Galley +import Wire.API.MLS.CipherSuite import Wire.API.MLS.Group (convToGroupId) +import Wire.API.MLS.Message import Wire.API.Message tests :: IO TestSetup -> TestTree @@ -65,6 +71,9 @@ tests s = testGroup "MLS" [ testGroup + "Message" + [test s "sender must be part of conversation" testSenderNotInConversation], + testGroup "Welcome" [ test s "local welcome" testLocalWelcome, test s "local welcome (client with no public key)" testWelcomeNoKey, @@ -84,7 +93,11 @@ tests s = test s "add new client of an already-present user to a conversation" testAddNewClient, test s "send a stale commit" testStaleCommit, test s "add remote user to a conversation" testAddRemoteUser, - test s "return error when commit is locked" testCommitLock + test s "return error when commit is locked" testCommitLock, + test s "add remote user to a conversation" testAddRemoteUser, + test s "add user to a conversation with proposal + commit" testAddUserBareProposalCommit, + test s "post commit that references a unknown proposal" testUnknownProposalRefCommit, + test s "post commit that is not referencing all proposals" testCommitNotReferencingAllProposals ], testGroup "Application Message" @@ -100,13 +113,20 @@ tests s = ], testGroup "Remote Sender/Local Conversation" - [ test s "POST /federation/send-mls-message" testRemoteToLocal + [ test s "POST /federation/send-mls-message" testRemoteToLocal, + test s "POST /federation/send-mls-message, remote user is not a conversation member" testRemoteNonMemberToLocal ], testGroup "Remote Sender/Remote Conversation" [ test s "POST /federation/on-mls-message-sent" testRemoteToRemote ] -- all is mocked ], + testGroup + "Proposal" + [ test s "add a new client to a non-existing conversation" propNonExistingConv, + test s "add a new client to an existing conversation" propExistingConv, + test s "add a new client in an invalid epoch" propInvalidEpoch + ], testGroup "Protocol mismatch" [ test s "send a commit to a proteus conversation" testAddUsersToProteus, @@ -147,6 +167,41 @@ postMLSConvOk = do cid <- assertConv rsp RegularConv alice qalice [] (Just nameMaxSize) Nothing checkConvCreateEvent cid wsA +testSenderNotInConversation :: TestM () +testSenderNotInConversation = do + withSystemTempDirectory "mls" $ \tmp -> do + (alice, [bob]) <- withLastPrekeys $ setupParticipants tmp def [(1, LocalUser)] + _ <- setupGroup tmp CreateConv alice "group" + + (_commit, _welcome) <- + liftIO $ + setupCommit tmp alice "group" "group" $ + toList (pClients bob) + + void . liftIO $ + spawn + ( cli + (pClientQid bob) + tmp + [ "group", + "from-welcome", + "--group-out", + tmp "group", + tmp "welcome" + ] + ) + Nothing + + message <- liftIO $ createMessage tmp bob "group" "some text" + + -- send the message as bob, who is not in the conversation + err <- + responseJsonError + =<< postMessage (qUnqualified (pUserId bob)) message + do (groupId, epoch) ) +testAddUserBareProposalCommit :: TestM () +testAddUserBareProposalCommit = withSystemTempDirectory "mls" $ \tmp -> do + (alice, [bob]) <- withLastPrekeys $ setupParticipants tmp def [(1, LocalUser)] + + (groupId, conversation) <- setupGroup tmp CreateConv alice "group" + + prop <- liftIO $ bareAddProposal tmp alice bob "group" "group" + postMessage (qUnqualified (pUserId alice)) prop + !!! const 201 === statusCode + + (commit, mbWelcome) <- + liftIO $ + pendingProposalsCommit tmp alice "group" + + welcome <- assertJust mbWelcome + + testSuccessfulCommit MessagingSetup {creator = alice, users = [bob], ..} + + -- check that bob can now see the conversation + convs <- + responseJsonError =<< getConvs (qUnqualified (pUserId bob)) Nothing Nothing + do + (alice, [bob]) <- withLastPrekeys $ setupParticipants tmp def [(1, LocalUser)] + + (groupId, conversation) <- setupGroup tmp CreateConv alice "group" + + -- create proposal, but don't send it to group + void $ liftIO $ bareAddProposal tmp alice bob "group" "group" + + (commit, mbWelcome) <- + liftIO $ + pendingProposalsCommit tmp alice "group" + + welcome <- assertJust mbWelcome + + err <- testFailedCommit (MessagingSetup {creator = alice, users = [bob], ..}) 404 + liftIO $ Wai.label err @?= "mls-proposal-not-found" + +testCommitNotReferencingAllProposals :: TestM () +testCommitNotReferencingAllProposals = withSystemTempDirectory "mls" $ \tmp -> do + (alice, [bob, dee]) <- withLastPrekeys $ setupParticipants tmp def [(1, LocalUser), (1, LocalUser)] + + (groupId, conversation) <- setupGroup tmp CreateConv alice "group" + + propBob <- liftIO $ bareAddProposal tmp alice bob "group" "group" + postMessage (qUnqualified (pUserId alice)) propBob + !!! const 201 === statusCode + + propDee <- liftIO $ bareAddProposal tmp alice dee "group" "group2" + postMessage (qUnqualified (pUserId alice)) propDee + !!! const 201 === statusCode + + (commit, mbWelcome) <- + liftIO $ + pendingProposalsCommit tmp alice "group" + + welcome <- assertJust mbWelcome + + err <- testFailedCommit (MessagingSetup {creator = alice, users = [bob, dee], ..}) 409 + liftIO $ Wai.label err @?= "mls-commit-missing-references" + testRemoteAppMessage :: TestM () testRemoteAppMessage = withSystemTempDirectory "mls" $ \tmp -> do let opts = @@ -578,12 +701,12 @@ testRemoteAppMessage = withSystemTempDirectory "mls" $ \tmp -> do . pClients $ bob ms -> assertFailure ("unmocked endpoint called: " <> cs ms) - (events :: [Event], reqs) <- withTempMockFederator' mock $ do + (events :: [Event], reqs) <- fmap (first mmssEvents) . withTempMockFederator' mock $ do galley <- viewGalley void $ postCommit MessagingSetup {creator = alice, users = [bob], ..} responseJsonError =<< post - ( galley . paths ["mls", "messages"] + ( galley . paths ["v2", "mls", "messages"] . zUser (qUnqualified (pUserId alice)) . zConn "conn" . content "message/mls" @@ -678,7 +801,7 @@ testLocalToRemote = withSystemTempDirectory "mls" $ \tmp -> do qcnv <- randomQualifiedId (qDomain (pUserId alice)) let nrc = NewRemoteConversation (qUnqualified qcnv) $ - ProtocolMLS (ConversationMLSData groupId (Epoch 1)) + ProtocolMLS (ConversationMLSData groupId (Epoch 1) MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519) void $ runFedClient @"on-new-remote-conversation" @@ -785,7 +908,7 @@ testAppMessage2 = do createMessage tmp bob "group" "some text" pure (setup, message) - let [bob, charlie] = users + let (bob, charlie) = assertTwo users galley <- viewGalley cannon <- view tsCannon @@ -890,7 +1013,24 @@ testRemoteToLocal = do def { createConv = CreateConv } - let bob = head (users setup) + bob <- assertOne (users setup) + let mockedResponse fedReq = + case frRPC fedReq of + "mls-welcome" -> pure (Aeson.encode EmptyResponse) + "on-new-remote-conversation" -> pure (Aeson.encode EmptyResponse) + "on-conversation-updated" -> pure (Aeson.encode ()) + "get-mls-clients" -> + pure + . Aeson.encode + . Set.fromList + . map snd + . toList + . pClients + $ bob + ms -> assertFailure ("unmocked endpoint called: " <> cs ms) + + void . withTempMockFederator' mockedResponse $ + postCommit setup void . liftIO $ spawn ( cli @@ -936,3 +1076,151 @@ testRemoteToLocal = do resp @?= MLSMessageResponseUpdates [] WS.assertMatch_ (5 # Second) ws $ wsAssertMLSMessage conversation (pUserId bob) message + +testRemoteNonMemberToLocal :: TestM () +testRemoteNonMemberToLocal = do + -- alice is local, bob is remote + -- alice creates a local conversation and invites bob + -- bob then sends a message to the conversation + + let bobDomain = Domain "faraway.example.com" + + -- Simulate the whole MLS setup for both clients first. In reality, + -- backend calls would need to happen in order for bob to get ahold of a + -- welcome message, but that should not affect the correctness of the test. + + (MessagingSetup {..}, message) <- withSystemTempDirectory "mls" $ \tmp -> do + setup <- + aliceInvitesBobWithTmp + tmp + (1, RemoteUser bobDomain) + def + { createConv = CreateConv + } + bob <- assertOne (users setup) + void . liftIO $ + spawn + ( cli + (pClientQid bob) + tmp + [ "group", + "from-welcome", + "--group-out", + tmp "groupB.json", + tmp "welcome" + ] + ) + Nothing + message <- + liftIO $ + spawn + ( cli + (pClientQid bob) + tmp + ["message", "--group", tmp "groupB.json", "hello from another backend"] + ) + Nothing + pure (setup, message) + + let bob = head users + fedGalleyClient <- view tsFedGalleyClient + + -- actual test + + let msr = + MessageSendRequest + { msrConvId = qUnqualified conversation, + msrSender = qUnqualified (pUserId bob), + msrRawMessage = Base64ByteString message + } + + resp <- runFedClient @"send-mls-message" fedGalleyClient bobDomain msr + liftIO $ do + resp @?= MLSMessageResponseError ConvNotFound + +-- | The group exists in mls-test-cli's store, but not in wire-server's database. +propNonExistingConv :: TestM () +propNonExistingConv = withSystemTempDirectory "mls" $ \tmp -> do + (creator, [bob]) <- withLastPrekeys $ setupParticipants tmp def [(1, LocalUser)] + + let groupId = toBase64Text "test_group" + groupJSON <- + liftIO $ + spawn (cli (pClientQid creator) tmp ["group", "create", T.unpack groupId]) Nothing + liftIO $ BS.writeFile (tmp cs groupId) groupJSON + + prop <- + liftIO $ + spawn + ( cli + (pClientQid creator) + tmp + [ "proposal", + "--group-in", + tmp cs groupId, + "--in-place", + "add", + tmp pClientQid bob + ] + ) + Nothing + postMessage (qUnqualified (pUserId creator)) prop !!! do + const 404 === statusCode + const (Just "no-conversation") === fmap Wai.label . responseJsonError + +propExistingConv :: TestM () +propExistingConv = withSystemTempDirectory "mls" $ \tmp -> do + (creator, [bob]) <- withLastPrekeys $ setupParticipants tmp def [(1, LocalUser)] + -- setupGroup :: HasCallStack => FilePath -> CreateConv -> Participant -> String -> TestM (Qualified ConvId) + void $ setupGroup tmp CreateConv creator "group.json" + + prop <- liftIO $ bareAddProposal tmp creator bob "group.json" "group.json" + + events <- + fmap mmssEvents . responseJsonError + =<< postMessage (qUnqualified (pUserId creator)) prop + do + (creator, users) <- withLastPrekeys $ setupParticipants tmp def [(1, LocalUser), (1, LocalUser), (1, LocalUser)] + (groupId, conversation) <- setupGroup tmp CreateConv creator "group.0.json" + + let (bob, charlie, dee) = assertThree users + + -- Add bob -> epoch 1 + do + (commit, welcome) <- + liftIO $ + setupCommit tmp creator "group.0.json" "group.1.json" $ + toList (pClients bob) + testSuccessfulCommit MessagingSetup {users = [bob], ..} + + -- try to request a proposal that with too old epoch (0) + do + prop <- liftIO $ bareAddProposal tmp creator charlie "group.0.json" "group.0.json" + err <- + responseJsonError + =<< postMessage (qUnqualified (pUserId creator)) prop + BS.readFile (tmp "welcome") +bareAddProposal :: + HasCallStack => + String -> + Participant -> + Participant -> + String -> + String -> + IO ByteString +bareAddProposal tmp creator participantToAdd groupIn groupOut = + spawn + ( cli + (pClientQid creator) + tmp + $ [ "proposal", + "--group-in", + tmp groupIn, + "--group-out", + tmp groupOut, + "add", + tmp pClientQid participantToAdd + ] + ) + Nothing + +pendingProposalsCommit :: + HasCallStack => + String -> + Participant -> + String -> + IO (ByteString, Maybe ByteString) +pendingProposalsCommit tmp creator groupName = do + let welcomeFile = tmp "welcome" + commit <- + spawn + ( cli + (pClientQid creator) + tmp + $ [ "commit", + "--group", + tmp groupName, + "--welcome-out", + welcomeFile + ] + ) + Nothing + welcome <- + doesFileExist welcomeFile >>= \case + False -> pure Nothing + True -> Just <$> BS.readFile welcomeFile + pure (commit, welcome) + createMessage :: + HasCallStack => String -> Participant -> String -> @@ -391,17 +444,31 @@ claimKeyPackage brig claimant target = ) postCommit :: HasCallStack => MessagingSetup -> TestM [Event] -postCommit MessagingSetup {..} = do +postCommit MessagingSetup {..} = + fmap mmssEvents . responseJsonError + =<< postMessage (qUnqualified (pUserId creator)) commit + + UserId -> + ByteString -> + m ResponseLBS +postMessage sender msg = do galley <- viewGalley - responseJsonError - =<< post - ( galley . paths ["mls", "messages"] - . zUser (qUnqualified (pUserId creator)) - . zConn "conn" - . content "message/mls" - . bytes commit - ) - UserId -> ByteString -> m ResponseLBS postWelcome uid welcome = do diff --git a/services/galley/test/integration/API/Teams/Feature.hs b/services/galley/test/integration/API/Teams/Feature.hs index 709a3cc720..792fe382e0 100644 --- a/services/galley/test/integration/API/Teams/Feature.hs +++ b/services/galley/test/integration/API/Teams/Feature.hs @@ -20,9 +20,11 @@ module API.Teams.Feature (tests) where import API.Util (HasGalley, getFeatureStatusMulti, withSettingsOverrides) import qualified API.Util as Util +import API.Util.TeamFeature (patchFeatureStatusInternal) import qualified API.Util.TeamFeature as Util import Bilge import Bilge.Assert +import Brig.Types.Test.Arbitrary (Arbitrary (arbitrary)) import Cassandra as Cql import Control.Lens (over, to, view) import Control.Monad.Catch (MonadCatch) @@ -30,6 +32,7 @@ import Data.Aeson (FromJSON, ToJSON) import qualified Data.Aeson as Aeson import qualified Data.Aeson.Key as AesonKey import qualified Data.Aeson.KeyMap as KeyMap +import Data.ByteString.Char8 (unpack) import Data.Domain (Domain (..)) import Data.Id import Data.List1 (list1) @@ -43,6 +46,7 @@ import Galley.Types.Teams import Imports import Network.Wai.Utilities (label) import Test.Hspec (expectationFailure) +import Test.QuickCheck (generate) import Test.Tasty import qualified Test.Tasty.Cannon as WS import Test.Tasty.HUnit (assertFailure, (@?=)) @@ -93,9 +97,51 @@ tests s = test s "Unlimited to unlimited" $ testSimpleFlagTTLOverride @Public.ConferenceCallingConfig Public.FeatureStatusEnabled FeatureTTLUnlimited FeatureTTLUnlimited ], test s "MLS feature config" testMLS, - test s "SearchVisibilityInbound" $ testSimpleFlag @Public.SearchVisibilityInboundConfig Public.FeatureStatusDisabled + test s "SearchVisibilityInbound" $ testSimpleFlag @Public.SearchVisibilityInboundConfig Public.FeatureStatusDisabled, + testGroup + "Patch" + [ test s (unpack $ Public.featureNameBS @Public.FileSharingConfig) $ + testPatch @Public.FileSharingConfig Public.FeatureStatusEnabled Public.FileSharingConfig, + test s (unpack $ Public.featureNameBS @Public.GuestLinksConfig) $ + testPatch @Public.GuestLinksConfig Public.FeatureStatusEnabled Public.GuestLinksConfig, + test s (unpack $ Public.featureNameBS @Public.SndFactorPasswordChallengeConfig) $ + testPatch @Public.SndFactorPasswordChallengeConfig Public.FeatureStatusDisabled Public.SndFactorPasswordChallengeConfig, + test s (unpack $ Public.featureNameBS @Public.SelfDeletingMessagesConfig) $ + testPatch @Public.SelfDeletingMessagesConfig Public.FeatureStatusEnabled (Public.SelfDeletingMessagesConfig 0) + ] ] +testPatch :: + forall cfg. + ( HasCallStack, + Public.IsFeatureConfig cfg, + Typeable cfg, + ToSchema cfg, + Eq cfg, + Show cfg, + KnownSymbol (Public.FeatureSymbol cfg), + Arbitrary (Public.WithStatus cfg), + Arbitrary (Public.WithStatusPatch cfg) + ) => + Public.FeatureStatus -> + cfg -> + TestM () +testPatch defStatus defConfig = do + (_, tid) <- Util.createBindingTeam + Just original <- responseJsonMaybe <$> Util.getFeatureStatusInternal @cfg tid + rndFeatureConfig :: Public.WithStatusPatch cfg <- liftIO (generate arbitrary) + patchFeatureStatusInternal tid rndFeatureConfig !!! statusCode === const 200 + Just actual <- responseJsonMaybe <$> Util.getFeatureStatusInternal @cfg tid + liftIO $ + if Public.wsLockStatus actual == Public.LockStatusLocked + then do + Public.wsStatus actual @?= defStatus + Public.wsConfig actual @?= defConfig + else do + Public.wsStatus actual @?= fromMaybe (Public.wsStatus original) (Public.wspStatus rndFeatureConfig) + Public.wsLockStatus actual @?= fromMaybe (Public.wsLockStatus original) (Public.wspLockStatus rndFeatureConfig) + Public.wsConfig actual @?= fromMaybe (Public.wsConfig original) (Public.wspConfig rndFeatureConfig) + testSSO :: TestM () testSSO = do owner <- Util.randomUser @@ -319,7 +365,7 @@ testClassifiedDomainsDisabled = do opts & over (optSettings . setFeatureFlags . flagClassifiedDomains) - (\(ImplicitLockStatus s) -> ImplicitLockStatus $ s {Public.wsStatus = Public.FeatureStatusDisabled, Public.wsConfig = Public.ClassifiedDomainsConfig []}) + (\(ImplicitLockStatus s) -> ImplicitLockStatus (s & Public.setStatus Public.FeatureStatusDisabled & Public.setConfig (Public.ClassifiedDomainsConfig []))) withSettingsOverrides classifiedDomainsDisabled $ do getClassifiedDomains member tid expected getClassifiedDomainsInternal tid expected @@ -653,7 +699,7 @@ testSelfDeletingMessages = do (Public.SelfDeletingMessagesConfig tout) settingWithLockStatus :: FeatureStatus -> Int32 -> Public.LockStatus -> Public.WithStatus Public.SelfDeletingMessagesConfig settingWithLockStatus stat tout lockStatus = - Public.WithStatus + Public.withStatus stat lockStatus (Public.SelfDeletingMessagesConfig tout) @@ -756,7 +802,7 @@ testGuestLinks getStatus putStatus setLockStatusInternal = do checkGet status lock = getStatus owner tid !!! do statusCode === const 200 - responseJsonEither === const (Right (Public.WithStatus status lock Public.GuestLinksConfig)) + responseJsonEither === const (Right (Public.withStatus status lock Public.GuestLinksConfig)) checkSet :: HasCallStack => Public.FeatureStatus -> Int -> TestM () checkSet status expectedStatusCode = @@ -823,20 +869,20 @@ testAllFeatures = do where expected confCalling lockStateSelfDeleting = Public.AllFeatureConfigs - { Public.afcLegalholdStatus = Public.WithStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.LegalholdConfig, - Public.afcSSOStatus = Public.WithStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.SSOConfig, - Public.afcTeamSearchVisibilityAvailable = Public.WithStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.SearchVisibilityAvailableConfig, - Public.afcValidateSAMLEmails = Public.WithStatus FeatureStatusEnabled Public.LockStatusUnlocked Public.ValidateSAMLEmailsConfig, - Public.afcDigitalSignatures = Public.WithStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.DigitalSignaturesConfig, - Public.afcAppLock = Public.WithStatus FeatureStatusEnabled Public.LockStatusUnlocked (Public.AppLockConfig (Public.EnforceAppLock False) (60 :: Int32)), - Public.afcFileSharing = Public.WithStatus FeatureStatusEnabled Public.LockStatusUnlocked Public.FileSharingConfig, - Public.afcClassifiedDomains = Public.WithStatus FeatureStatusEnabled Public.LockStatusUnlocked (Public.ClassifiedDomainsConfig [Domain "example.com"]), - Public.afcConferenceCalling = Public.WithStatus confCalling Public.LockStatusUnlocked Public.ConferenceCallingConfig, - Public.afcSelfDeletingMessages = Public.WithStatus FeatureStatusEnabled lockStateSelfDeleting (Public.SelfDeletingMessagesConfig 0), - Public.afcGuestLink = Public.WithStatus FeatureStatusEnabled Public.LockStatusUnlocked Public.GuestLinksConfig, - Public.afcSndFactorPasswordChallenge = Public.WithStatus FeatureStatusDisabled Public.LockStatusLocked Public.SndFactorPasswordChallengeConfig, - Public.afcMLS = Public.WithStatus FeatureStatusDisabled Public.LockStatusUnlocked (Public.MLSConfig [] ProtocolProteusTag [MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519] MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519), - Public.afcSearchVisibilityInboundConfig = Public.WithStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.SearchVisibilityInboundConfig + { Public.afcLegalholdStatus = Public.withStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.LegalholdConfig, + Public.afcSSOStatus = Public.withStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.SSOConfig, + Public.afcTeamSearchVisibilityAvailable = Public.withStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.SearchVisibilityAvailableConfig, + Public.afcValidateSAMLEmails = Public.withStatus FeatureStatusEnabled Public.LockStatusUnlocked Public.ValidateSAMLEmailsConfig, + Public.afcDigitalSignatures = Public.withStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.DigitalSignaturesConfig, + Public.afcAppLock = Public.withStatus FeatureStatusEnabled Public.LockStatusUnlocked (Public.AppLockConfig (Public.EnforceAppLock False) (60 :: Int32)), + Public.afcFileSharing = Public.withStatus FeatureStatusEnabled Public.LockStatusUnlocked Public.FileSharingConfig, + Public.afcClassifiedDomains = Public.withStatus FeatureStatusEnabled Public.LockStatusUnlocked (Public.ClassifiedDomainsConfig [Domain "example.com"]), + Public.afcConferenceCalling = Public.withStatus confCalling Public.LockStatusUnlocked Public.ConferenceCallingConfig, + Public.afcSelfDeletingMessages = Public.withStatus FeatureStatusEnabled lockStateSelfDeleting (Public.SelfDeletingMessagesConfig 0), + Public.afcGuestLink = Public.withStatus FeatureStatusEnabled Public.LockStatusUnlocked Public.GuestLinksConfig, + Public.afcSndFactorPasswordChallenge = Public.withStatus FeatureStatusDisabled Public.LockStatusLocked Public.SndFactorPasswordChallengeConfig, + Public.afcMLS = Public.withStatus FeatureStatusDisabled Public.LockStatusUnlocked (Public.MLSConfig [] ProtocolProteusTag [MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519] MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519), + Public.afcSearchVisibilityInboundConfig = Public.withStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.SearchVisibilityInboundConfig } testFeatureConfigConsistency :: TestM () @@ -1019,7 +1065,7 @@ assertFlagNoConfigWithLockStatus res expectedStatus expectedLockStatus = do res !!! do statusCode === const 200 responseJsonEither @(Public.WithStatus cfg) - === const (Right (Public.WithStatus expectedStatus expectedLockStatus (Public.trivialConfig @cfg))) + === const (Right (Public.withStatus expectedStatus expectedLockStatus (Public.trivialConfig @cfg))) assertFlagWithConfig :: forall cfg m. @@ -1058,7 +1104,7 @@ wsAssertFeatureTrivialConfigUpdate status notification = do let e :: FeatureConfig.Event = List1.head (WS.unpackPayload notification) FeatureConfig._eventType e @?= FeatureConfig.Update FeatureConfig._eventFeatureName e @?= Public.featureName @cfg - FeatureConfig._eventData e @?= Aeson.toJSON (Public.WithStatus status (Public.wsLockStatus (Public.defFeatureStatus @cfg)) (Public.trivialConfig @cfg)) + FeatureConfig._eventData e @?= Aeson.toJSON (Public.withStatus status (Public.wsLockStatus (Public.defFeatureStatus @cfg)) (Public.trivialConfig @cfg)) wsAssertFeatureConfigWithLockStatusUpdate :: forall cfg. @@ -1076,7 +1122,7 @@ wsAssertFeatureConfigWithLockStatusUpdate status lockStatus notification = do let e :: FeatureConfig.Event = List1.head (WS.unpackPayload notification) FeatureConfig._eventType e @?= FeatureConfig.Update FeatureConfig._eventFeatureName e @?= (Public.featureName @cfg) - FeatureConfig._eventData e @?= Aeson.toJSON (Public.WithStatus status lockStatus (Public.trivialConfig @cfg)) + FeatureConfig._eventData e @?= Aeson.toJSON (Public.withStatus status lockStatus (Public.trivialConfig @cfg)) wsAssertFeatureConfigUpdate :: forall cfg. diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index de13c45a14..b2014a05fe 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -2705,15 +2705,23 @@ parseFedRequest fr = eitherDecode (frBody fr) assertOne :: (HasCallStack, MonadIO m, Show a) => [a] -> m a assertOne [a] = pure a -assertOne xs = liftIO . assertFailure $ "Expected exactly one element, found " <> show xs +assertOne xs = liftIO . error $ "Expected exactly one element, found " <> show xs + +assertTwo :: (HasCallStack, Show a) => [a] -> (a, a) +assertTwo [a, b] = (a, b) +assertTwo xs = error $ "Expected two elements, found " <> show xs + +assertThree :: (HasCallStack, Show a) => [a] -> (a, a, a) +assertThree [a1, a2, a3] = (a1, a2, a3) +assertThree xs = error $ "Expected three elements, found " <> show xs assertNone :: (HasCallStack, MonadIO m, Show a) => [a] -> m () assertNone [] = pure () -assertNone xs = liftIO . assertFailure $ "Expected exactly no elements, found " <> show xs +assertNone xs = liftIO . error $ "Expected exactly no elements, found " <> show xs assertJust :: (HasCallStack, MonadIO m) => Maybe a -> m a assertJust (Just a) = pure a -assertJust Nothing = liftIO $ assertFailure "Expected Just, got Nothing" +assertJust Nothing = liftIO $ error "Expected Just, got Nothing" iUpsertOne2OneConversation :: UpsertOne2OneConversationRequest -> TestM ResponseLBS iUpsertOne2OneConversation req = do diff --git a/services/galley/test/integration/API/Util/TeamFeature.hs b/services/galley/test/integration/API/Util/TeamFeature.hs index db5cd46b2b..2020fa9171 100644 --- a/services/galley/test/integration/API/Util/TeamFeature.hs +++ b/services/galley/test/integration/API/Util/TeamFeature.hs @@ -245,6 +245,39 @@ setLockStatusInternal reqmod tid lockStatus = do . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg, toByteString' lockStatus] . reqmod +getFeatureStatusInternal :: + forall cfg. + ( HasCallStack, + Public.IsFeatureConfig cfg, + KnownSymbol (Public.FeatureSymbol cfg), + ToJSON Public.LockStatus + ) => + TeamId -> + TestM ResponseLBS +getFeatureStatusInternal tid = do + galley <- view tsGalley + get $ + galley + . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg] + +patchFeatureStatusInternal :: + forall cfg. + ( HasCallStack, + Public.IsFeatureConfig cfg, + KnownSymbol (Public.FeatureSymbol cfg), + ToJSON Public.LockStatus, + ToSchema cfg + ) => + TeamId -> + Public.WithStatusPatch cfg -> + TestM ResponseLBS +patchFeatureStatusInternal tid reqBody = do + galley <- view tsGalley + patch $ + galley + . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg] + . json reqBody + getGuestLinkStatus :: HasCallStack => (Request -> Request) -> diff --git a/services/nginz/third_party/nginx-module-vts b/services/nginz/third_party/nginx-module-vts index b606b13006..3c6cf41315 160000 --- a/services/nginz/third_party/nginx-module-vts +++ b/services/nginz/third_party/nginx-module-vts @@ -1 +1 @@ -Subproject commit b606b13006ffc3c694e8e6326a85f629c1288568 +Subproject commit 3c6cf41315bfcb48c35a3a0be81ddba6d0d01dac diff --git a/services/spar/test-integration/Test/LoggingSpec.hs b/services/spar/test-integration/Test/LoggingSpec.hs index 4b3a8d0875..b61e77d017 100644 --- a/services/spar/test-integration/Test/LoggingSpec.hs +++ b/services/spar/test-integration/Test/LoggingSpec.hs @@ -41,7 +41,7 @@ spec = describe "logging" $ do (out, _) <- capture $ do Log.fatal logger $ Log.msg ("hrgh\n\nwoaa" :: Text) Log.flush logger - out `shouldContain` "hrgh woaa" + out `shouldContain` "hrgh" out `shouldNotContain` "hrgh\n\nwoaa" context "loglevel == debug" $ do it "400 on finalize-login causes log of entire request" $ do diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index c851f6a598..e78c6cefdd 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -65,7 +65,7 @@ import Stern.Types import System.Logger.Class hiding (Error, name, trace, (.=)) import Util.Options import Wire.API.Connection -import Wire.API.Team.Feature +import Wire.API.Team.Feature hiding (setStatus) import qualified Wire.API.Team.Feature as Public import Wire.API.Team.SearchVisibility import qualified Wire.API.Team.SearchVisibility as Public