diff --git a/.dockerignore b/.dockerignore index 3f55d4808ea..07d6e94f52c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,4 +2,5 @@ **/dist **/target **/*.aci +**/*.tgz services/nginz/src/objs diff --git a/CHANGELOG.md b/CHANGELOG.md index 275d98241de..c13e45b639d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,35 @@ --> +# [2021-01-06] + +## Release Notes + +This release contains bugfixes and internal changes. + +## Bug fixes and other updates + +* [SCIM] Bug fix: handle is lost after registration (#1303) +* [SCIM] Better error message (#1306) + +## Documentation + +* [SCIM] Document `validateSAMLemails` feature in docs/reference/spar-braindump.md (#1299) + +## Internal changes + +* [federation] Servantify get users by unqualified ids or handles (#1291) +* [federation] Add endpoint to get users by qualified ids or handles (#1291) +* Allow overriding NAMESPACE for kube-integration target (#1305) +* Add script create_test_team_scim.sh for development (#1302) +* Update brig helm chart: Add `setExpiredUserCleanupTimeout` (#1304) +* Nit-picks (#1300) +* nginz_disco: docker building consistency (#1311) +* Add tools/db/repair-handles (#1310) +* small speedup for 'make upload-charts' by inlining loop (#1308) +* Cleanup stack.yaml. (#1312) (#1316) + + # [2020-12-21] ## Release Notes diff --git a/Makefile b/Makefile index 682587c3d2d..df627f20dab 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,8 @@ SHELL := /usr/bin/env bash LANG := en_US.UTF-8 DOCKER_USER ?= quay.io/wire +# kubernetes namespace for running integration tests +NAMESPACE ?= test-$(USER) # default docker image tag is your system username, you can override it via environment variable. DOCKER_TAG ?= $(USER) # default helm chart version must be 0.0.42 for local development (because 42 is the answer to the universe and everything) @@ -8,7 +10,10 @@ HELM_SEMVER ?= 0.0.42 # The list of helm charts needed for integration tests on kubernetes CHARTS_INTEGRATION := wire-server databases-ephemeral fake-aws # The list of helm charts to publish on S3 -# FUTUREWORK: after we "inline local subcharts", i.e. move charts/brig to charts/wire-server/brig this list could be generated from the folder names under ./charts/ +# FUTUREWORK: after we "inline local subcharts", +# (e.g. move charts/brig to charts/wire-server/brig) +# this list could be generated from the folder names under ./charts/ like so: +# CHARTS_RELEASE := $(shell find charts/ -maxdepth 1 -type d | xargs -n 1 basename | grep -v charts) CHARTS_RELEASE := wire-server databases-ephemeral fake-aws aws-ingress backoffice calling-test demo-smtp elasticsearch-curator elasticsearch-external fluent-bit minio-external cassandra-external nginx-ingress-controller nginx-ingress-services reaper wire-server-metrics sftd default: fast @@ -237,14 +242,15 @@ hie.yaml: # - kubectl # - a valid kubectl context configured (i.e. access to a kubernetes cluster) .PHONY: kube-integration -kube-integration: charts +kube-integration: charts-integration # by default "test- is used as namespace - export NAMESPACE=test-$(USER); ./hack/bin/integration-setup.sh - export NAMESPACE=test-$(USER); ./hack/bin/integration-test.sh + # you can override the default by setting the NAMESPACE environment variable + export NAMESPACE=$(NAMESPACE); ./hack/bin/integration-setup.sh + export NAMESPACE=$(NAMESPACE); ./hack/bin/integration-test.sh .PHONY: kube-integration-teardown kube-integration-teardown: - export NAMESPACE=test-$(USER); ./hack/bin/integration-teardown.sh + export NAMESPACE=$(NAMESPACE); ./hack/bin/integration-teardown.sh .PHONY: latest-brig-tag latest-brig-tag: @@ -307,4 +313,9 @@ upload-chart-%: release-chart-% # To uplaod all helm charts in the CHARTS_RELEASE list (see top of the time) # (assummption: CI sets DOCKER_TAG and HELM_SEMVER) .PHONY: upload-charts -upload-charts: $(foreach chartName,$(CHARTS_RELEASE),upload-chart-$(chartName)) +upload-charts: charts-release + ./hack/bin/upload-helm-charts-s3.sh + +.PHONY: echo-release-charts +echo-release-charts: + @echo ${CHARTS_RELEASE} diff --git a/charts/brig/templates/configmap.yaml b/charts/brig/templates/configmap.yaml index 2f11716a3c7..f11b177b27f 100644 --- a/charts/brig/templates/configmap.yaml +++ b/charts/brig/templates/configmap.yaml @@ -167,6 +167,9 @@ data: optSettings: setActivationTimeout: {{ .setActivationTimeout }} setTeamInvitationTimeout: {{ .setTeamInvitationTimeout }} + {{- if .setExpiredUserCleanupTimeout }} + setExpiredUserCleanupTimeout: {{ .setExpiredUserCleanupTimeout }} + {{- end }} setTwilio: /etc/wire/brig/secrets/twilio-credentials.yaml setNexmo: /etc/wire/brig/secrets/nexmo-credentials.yaml setUserMaxConnections: {{ .setUserMaxConnections }} diff --git a/deploy/services-demo/create_test_team_scim.sh b/deploy/services-demo/create_test_team_scim.sh new file mode 100755 index 00000000000..1df41191a35 --- /dev/null +++ b/deploy/services-demo/create_test_team_scim.sh @@ -0,0 +1,216 @@ +#!/usr/bin/env bash + +set -e + +BRIG_HOST="http://localhost:8082" +SPAR_HOST="http://localhost:8088" + +USAGE=" +This bash script craates +1) team +2) team admin +3) scim token +4) a regular user via team invitation +5) a scim-managed user (without IDP) + +Note that this uses internal brig and spar endpoints. It is not exposed over +nginz and can only be used if you have direct access to brig and spar simultaneously. + +USAGE: $0 + -h : Base URI of brig. default: ${BRIG_HOST} + -s : Base URI of spar. default: ${SPAR_HOST} +" + +# Option parsing: +# https://sookocheff.com/post/bash/parsing-bash-script-arguments-with-shopts/ +while getopts ":n:h:c" opt; do + case ${opt} in + h ) BRIG_HOST="$OPTARG" + ;; + s ) SPAR_HOST="$OPTARG" + ;; + : ) echo "-$OPTARG" requires an argument 1>&2 + exit 1 + ;; + \? ) echo "$USAGE" 1>&2 + exit 1 + ;; + esac +done +shift $((OPTIND -1)) + +if [ "$#" -ne 0 ]; then + echo "$USAGE" 1>&2 + exit 1 +fi + + +ADMIN_EMAIL=$(cat /dev/urandom | env LC_CTYPE=C tr -dc a-zA-Z0-9 | head -c 8)"@example.com" +ADMIN_PASSWORD=$(cat /dev/urandom | env LC_CTYPE=C tr -dc a-zA-Z0-9 | head -c 8) + +CURL_OUT=$(curl -i -s --show-error \ + -XPOST "$BRIG_HOST/i/users" \ + -H'Content-type: application/json' \ + -d'{"email":"'"$ADMIN_EMAIL"'","password":"'"$ADMIN_PASSWORD"'","name":"demo","team":{"name":"Wire team","icon":"default"}}') + +ADMIN_UUID=$(echo "$CURL_OUT" | tail -1 | sed 's/.*\"id\":\"\([a-z0-9-]*\)\".*/\1/') +TEAM_UUID=$(echo "$CURL_OUT" | tail -1 | sed 's/.*\"team\":\"\([a-z0-9-]*\)\".*/\1/') + + +BEARER=$(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) + +SCIM_TOKEN_FULL=$(curl -X POST \ + --header "Authorization: Bearer $BEARER" \ + --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) + +SCIM_TOKEN=$(echo $SCIM_TOKEN_FULL | jq -r .token) +SCIM_TOKEN_ID=$(echo $SCIM_TOKEN_FULL | jq -r .info.id) + + +# Create regular user via team invitation + +REGULAR_USER_EMAIL=$(cat /dev/urandom | env LC_CTYPE=C tr -dc a-zA-Z0-9 | head -c 8)"@example.com" +REGULAR_USER_PASSWORD=$(cat /dev/urandom | env LC_CTYPE=C tr -dc a-zA-Z0-9 | head -c 8) +CURL_OUT_INVITATION=$(curl -i -s --show-error \ + -XPOST "$BRIG_HOST/teams/$TEAM_UUID/invitations" \ + -H'Content-type: application/json' \ + -H'Z-User: '"$ADMIN_UUID"'' \ + -d'{"email":"'"$REGULAR_USER_EMAIL"'","name":"Replace with name","inviter_name":"Team admin"}') + +INVITATION_ID=$(echo "$CURL_OUT_INVITATION" | tail -1 | sed 's/.*\"id\":\"\([a-z0-9-]*\)\".*/\1/') + +sleep 1 + +if ( ( echo "$INVITATION_ID" | grep -q '"code"' ) && + ( echo "$INVITATION_ID" | grep -q '"label"' ) ) ; then + echo "Got an error while creating $REGULAR_USER_EMAIL, aborting: $INVITATION_ID" + exit 1 +fi + +sleep 1 + +if ( ( echo "$INVITATION_ID" | grep -q '"code"' ) && + ( echo "$INVITATION_ID" | grep -q '"label"' ) ) ; then + echo "Got an error while creating $REGULAR_USER_EMAIL, aborting: $INVITATION_ID" + exit 1 +fi + +# Get the code +CURL_OUT_INVITATION_CODE=$(curl -i -s --show-error \ + -XGET "$BRIG_HOST/i/teams/invitation-code?team=$TEAM_UUID&invitation_id=$INVITATION_ID") + +INVITATION_CODE=$(echo "$CURL_OUT_INVITATION_CODE" | tail -1 | sed -n -e '/"code":/ s/^.*"\(.*\)".*/\1/p') + +sleep 1 + +# Create the user using that code +CURL_OUT=$(curl -i -s --show-error \ + -XPOST "$BRIG_HOST/i/users" \ + -H'Content-type: application/json' \ + -d'{"email":"'"$REGULAR_USER_EMAIL"'","password":"'"$REGULAR_USER_PASSWORD"'","name":"demo","team_code":"'"$INVITATION_CODE"'"}') + +REGULAR_TEAM_MEMBER_UUID=$(echo "$CURL_OUT" | tail -1 | sed 's/.*\"id\":\"\([a-z0-9-]*\)\".*/\1/') + + +# Create user via SCIM invitation + + +scimUserName=$(cat /dev/urandom | env LC_CTYPE=C tr -dc a-zA-Z0-9 | head -c 8) +scimUserDisplayName="Display of $scimUserName" +scimUserEmail="$scimUserName@example.com" +scimUserExternalId="$scimUserEmail" + +SCIM_USER=$(cat < Not reindexing by default. Pass the --reindex flag in case the index.yaml is incomplete. See all wire charts using \n helm search $REPO_NAME/ -l\n\n" + printf "\n--> Not reindexing by default. Pass the --reindex flag in case the index.yaml is incomplete. See all wire charts using \n helm search repo $REPO_NAME/ -l\n\n" fi diff --git a/libs/types-common/src/Data/CommaSeparatedList.hs b/libs/types-common/src/Data/CommaSeparatedList.hs new file mode 100644 index 00000000000..bd936c76090 --- /dev/null +++ b/libs/types-common/src/Data/CommaSeparatedList.hs @@ -0,0 +1,30 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} + +module Data.CommaSeparatedList where + +import Control.Lens ((?~)) +import qualified Data.Bifunctor as Bifunctor +import Data.ByteString.Conversion (FromByteString, List, fromList, parser, runParser) +import Data.Proxy (Proxy (..)) +import Data.Range (Bounds, Range) +import Data.Swagger (CollectionFormat (CollectionCSV), SwaggerItems (SwaggerItemsPrimitive), SwaggerType (SwaggerString), ToParamSchema (..), items, type_) +import qualified Data.Text as Text +import Data.Text.Encoding (encodeUtf8) +import Imports +import Servant (FromHttpApiData (..)) + +newtype CommaSeparatedList a = CommaSeparatedList {fromCommaSeparatedList :: [a]} + deriving stock (Show, Eq) + deriving newtype (Bounds) + +instance FromByteString (List a) => FromHttpApiData (CommaSeparatedList a) where + parseUrlPiece t = + CommaSeparatedList . fromList <$> Bifunctor.first Text.pack (runParser parser $ encodeUtf8 t) + +instance ToParamSchema (CommaSeparatedList a) where + toParamSchema _ = mempty & type_ ?~ SwaggerString + +instance (ToParamSchema a, ToParamSchema (Range n m [a])) => ToParamSchema (Range n m (CommaSeparatedList a)) where + toParamSchema _ = + toParamSchema (Proxy @(Range n m [a])) + & items ?~ SwaggerItemsPrimitive (Just CollectionCSV) (toParamSchema (Proxy @a)) diff --git a/libs/types-common/src/Data/Qualified.hs b/libs/types-common/src/Data/Qualified.hs index 779e725d7b6..9f4a5585187 100644 --- a/libs/types-common/src/Data/Qualified.hs +++ b/libs/types-common/src/Data/Qualified.hs @@ -28,9 +28,6 @@ module Data.Qualified -- * Qualified Qualified (..), renderQualifiedId, - mkQualifiedId, - renderQualifiedHandle, - mkQualifiedHandle, partitionRemoteOrLocalIds, deprecatedUnqualifiedSchemaRef, ) @@ -38,10 +35,9 @@ where import Control.Applicative (optional) import Control.Lens (view, (.~), (?~)) -import Data.Aeson (FromJSON, ToJSON, withObject, withText, (.:), (.=)) +import Data.Aeson (FromJSON, ToJSON, withObject, (.:), (.=)) import qualified Data.Aeson as Aeson import qualified Data.Attoparsec.ByteString.Char8 as Atto -import Data.Bifunctor (first) import Data.ByteString.Conversion (FromByteString (parser)) import Data.Domain (Domain, domainText) import Data.Handle (Handle (..)) @@ -49,11 +45,9 @@ import Data.Id (Id (toUUID)) import Data.Proxy (Proxy (..)) import Data.String.Conversions (cs) import Data.Swagger -import Data.Swagger.Declare (Declare) -import qualified Data.Text.Encoding as Text.E +import Data.Swagger.Declare (Declare, DeclareT) import qualified Data.UUID as UUID import Imports hiding (local) -import Servant.API (FromHttpApiData (parseUrlPiece)) import Test.QuickCheck (Arbitrary (arbitrary)) ---------------------------------------------------------------------- @@ -101,26 +95,12 @@ data Qualified a = Qualified } deriving stock (Eq, Ord, Show, Generic) +-- | FUTUREWORK: Maybe delete this, it is only used in printing federation not +-- implemented errors renderQualified :: (a -> Text) -> Qualified a -> Text renderQualified renderLocal (Qualified localPart domain) = renderLocal localPart <> "@" <> domainText domain --- FUTUREWORK: do we want a different way to serialize these than with an '@' ? A '/' was talked about also. --- --- renderQualified :: (a -> Text) -> Qualified a -> Text --- renderQualified renderLocal (Qualified localPart domain) = --- domainText domain <> "/" <> renderLocal localPart --- --- qualifiedParser :: Atto.Parser a -> Atto.Parser (Qualified a) --- domain <- parser @Domain --- _ <- Atto.char '/' --- local <- localParser --- pure $ Qualified local domain - -qualifiedParser :: Atto.Parser a -> Atto.Parser (Qualified a) -qualifiedParser localParser = do - Qualified <$> localParser <*> (Atto.char '@' *> parser @Domain) - partitionRemoteOrLocalIds :: Foldable f => Domain -> f (Qualified a) -> ([Qualified a], [a]) partitionRemoteOrLocalIds localDomain = foldMap $ \qualifiedId -> if qDomain qualifiedId == localDomain @@ -132,9 +112,6 @@ partitionRemoteOrLocalIds localDomain = foldMap $ \qualifiedId -> renderQualifiedId :: Qualified (Id a) -> Text renderQualifiedId = renderQualified (cs . UUID.toString . toUUID) -mkQualifiedId :: Text -> Either String (Qualified (Id a)) -mkQualifiedId = Atto.parseOnly (parser <* Atto.endOfInput) . Text.E.encodeUtf8 - deprecatedUnqualifiedSchemaRef :: ToSchema a => Proxy a -> Text -> Declare (Definitions Schema) (Referenced Schema) deprecatedUnqualifiedSchemaRef p newField = Inline @@ -143,17 +120,8 @@ deprecatedUnqualifiedSchemaRef p newField = <$> declareNamedSchema p instance ToSchema (Qualified (Id a)) where - declareNamedSchema _ = do - idSchema <- declareSchemaRef (Proxy @(Id a)) - domainSchema <- declareSchemaRef (Proxy @Domain) - return $ - NamedSchema (Just "QualifiedId") $ - mempty - & type_ ?~ SwaggerObject - & properties - .~ [ ("id", idSchema), - ("domain", domainSchema) - ] + declareNamedSchema _ = + declareQualifiedSchema "Qualified Id" "id" =<< declareSchemaRef (Proxy @(Id a)) instance ToJSON (Qualified (Id a)) where toJSON qu = @@ -166,31 +134,34 @@ instance FromJSON (Qualified (Id a)) where parseJSON = withObject "QualifiedUserId" $ \o -> Qualified <$> o .: "id" <*> o .: "domain" -instance FromHttpApiData (Qualified (Id a)) where - parseUrlPiece = first cs . mkQualifiedId - -instance FromByteString (Qualified (Id a)) where - parser = qualifiedParser parser +declareQualifiedSchema :: Text -> Text -> Referenced Schema -> DeclareT (Definitions Schema) Identity NamedSchema +declareQualifiedSchema qualifiedSchemaName unqualifiedFieldName unqualifiedSchemaRef = do + domainSchema <- declareSchemaRef (Proxy @Domain) + return $ + NamedSchema (Just qualifiedSchemaName) $ + mempty + & type_ ?~ SwaggerObject + & properties + .~ [ (unqualifiedFieldName, unqualifiedSchemaRef), + ("domain", domainSchema) + ] ---------------------------------------------------------------------- -renderQualifiedHandle :: Qualified Handle -> Text -renderQualifiedHandle = renderQualified fromHandle - -mkQualifiedHandle :: Text -> Either String (Qualified Handle) -mkQualifiedHandle = Atto.parseOnly (parser <* Atto.endOfInput) . Text.E.encodeUtf8 +instance ToSchema (Qualified Handle) where + declareNamedSchema _ = + declareQualifiedSchema "Qualified Handle" "handle" =<< declareSchemaRef (Proxy @Handle) instance ToJSON (Qualified Handle) where - toJSON = Aeson.String . renderQualifiedHandle + toJSON qh = + Aeson.object + [ "handle" .= qUnqualified qh, + "domain" .= qDomain qh + ] instance FromJSON (Qualified Handle) where - parseJSON = withText "QualifiedHandle" $ either fail pure . mkQualifiedHandle - -instance FromHttpApiData (Qualified Handle) where - parseUrlPiece = first cs . mkQualifiedHandle - -instance FromByteString (Qualified Handle) where - parser = qualifiedParser parser + parseJSON = withObject "Qualified Handle" $ \o -> + Qualified <$> o .: "handle" <*> o .: "domain" ---------------------------------------------------------------------- -- ARBITRARY diff --git a/libs/types-common/src/Data/Range.hs b/libs/types-common/src/Data/Range.hs index f372dbe0c0e..47b91acd926 100644 --- a/libs/types-common/src/Data/Range.hs +++ b/libs/types-common/src/Data/Range.hs @@ -51,12 +51,18 @@ module Data.Range ) where -import Cassandra hiding (Set) -import Data.Aeson -import Data.Aeson.Types as Aeson +import Cassandra (ColumnType, Cql (..), Tagged, retag) +import Control.Lens ((?~)) +import Data.Aeson (FromJSON (parseJSON), ToJSON (toJSON)) +import Data.Aeson.Types as Aeson (Parser) import qualified Data.Attoparsec.ByteString as Atto +import qualified Data.Bifunctor as Bifunctor import qualified Data.ByteString as B import Data.ByteString.Conversion + ( FromByteString (..), + List (fromList), + ToByteString (..), + ) import qualified Data.ByteString.Lazy as BL import qualified Data.HashMap.Strict as HashMap import qualified Data.HashSet as HashSet @@ -72,12 +78,14 @@ import Data.Singletons import Data.Singletons.Prelude.Num import Data.Singletons.Prelude.Ord import Data.Singletons.TypeLits +import Data.Swagger (ParamSchema, ToParamSchema (..), ToSchema (..), maxItems, maxLength, maximum_, minItems, minLength, minimum_) import qualified Data.Text as T import Data.Text.Ascii (AsciiChar, AsciiChars, AsciiText, fromAsciiChars) import qualified Data.Text.Ascii as Ascii import qualified Data.Text.Lazy as TL import Imports -import Numeric.Natural +import Numeric.Natural (Natural) +import Servant (FromHttpApiData (..)) import System.Random (Random) import Test.QuickCheck (Arbitrary (arbitrary, shrink), Gen) import qualified Test.QuickCheck as QC @@ -90,8 +98,8 @@ newtype Range (n :: Nat) (m :: Nat) a = Range deriving (Eq, Ord, Show) instance (Show a, Num a, Within a n m, KnownNat n, KnownNat m) => Bounded (Range n m a) where - minBound = unsafeRange $ (fromKnownNat (Proxy @n) :: a) - maxBound = unsafeRange $ (fromKnownNat (Proxy @m) :: a) + minBound = unsafeRange (fromKnownNat (Proxy @n) :: a) + maxBound = unsafeRange (fromKnownNat (Proxy @m) :: a) instance NFData (Range n m a) where rnf (Range a) = seq a () @@ -112,6 +120,63 @@ instance (Within a n m, Cql a) => Cql (Range n m a) where msg :: Bounds a => SNat n -> SNat m -> Either String (Range n m a) msg sn sm = Left (errorMsg (fromSing sn) (fromSing sm) "") +instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m Integer) where toParamSchema = rangedNumToParamSchema + +instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m Int) where toParamSchema = rangedNumToParamSchema + +instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m Int8) where toParamSchema = rangedNumToParamSchema + +instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m Int16) where toParamSchema = rangedNumToParamSchema + +instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m Int32) where toParamSchema = rangedNumToParamSchema + +instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m Int64) where toParamSchema = rangedNumToParamSchema + +instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m Natural) where toParamSchema = rangedNumToParamSchema + +instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m Word) where toParamSchema = rangedNumToParamSchema + +instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m Word8) where toParamSchema = rangedNumToParamSchema + +instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m Word16) where toParamSchema = rangedNumToParamSchema + +instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m Word32) where toParamSchema = rangedNumToParamSchema + +instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m Word64) where toParamSchema = rangedNumToParamSchema + +instance (ToParamSchema a, KnownNat n, KnownNat m) => ToParamSchema (Range n m [a]) where + toParamSchema _ = + toParamSchema (Proxy @[a]) + & minItems ?~ fromKnownNat (Proxy @n) + & maxItems ?~ fromKnownNat (Proxy @m) + +instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m String) where + toParamSchema _ = + toParamSchema (Proxy @String) + & maxLength ?~ fromKnownNat (Proxy @n) + & minLength ?~ fromKnownNat (Proxy @m) + +instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m T.Text) where + toParamSchema _ = + toParamSchema (Proxy @T.Text) + & maxLength ?~ fromKnownNat (Proxy @n) + & minLength ?~ fromKnownNat (Proxy @m) + +instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m TL.Text) where + toParamSchema _ = + toParamSchema (Proxy @TL.Text) + & maxLength ?~ fromKnownNat (Proxy @n) + & minLength ?~ fromKnownNat (Proxy @m) + +instance ToSchema a => ToSchema (Range n m a) where + declareNamedSchema _ = + declareNamedSchema (Proxy @a) + +instance (Within a n m, FromHttpApiData a) => FromHttpApiData (Range n m a) where + parseUrlPiece t = do + unchecked <- parseUrlPiece t + Bifunctor.first T.pack $ checkedEither @_ @n @m unchecked + type LTE (n :: Nat) (m :: Nat) = (SingI n, SingI m, (n <= m) ~ 'True) type Within a (n :: Nat) (m :: Nat) = (Bounds a, LTE n m) @@ -184,6 +249,12 @@ rappend (Range a) (Range b) = Range (a <> b) rsingleton :: a -> Range 1 1 [a] rsingleton = Range . pure +rangedNumToParamSchema :: forall a n m t. (ToParamSchema a, Num a, KnownNat n, KnownNat m) => Proxy (Range n m a) -> ParamSchema t +rangedNumToParamSchema _ = + toParamSchema (Proxy @a) + & minimum_ ?~ fromKnownNat (Proxy @n) + & maximum_ ?~ fromKnownNat (Proxy @m) + ----------------------------------------------------------------------------- class Bounds a where diff --git a/libs/types-common/test/Test/Qualified.hs b/libs/types-common/test/Test/Qualified.hs index 6f270eb8b63..8e11f791037 100644 --- a/libs/types-common/test/Test/Qualified.hs +++ b/libs/types-common/test/Test/Qualified.hs @@ -22,13 +22,10 @@ where import Data.Aeson (FromJSON (parseJSON), ToJSON (toJSON)) import qualified Data.Aeson.Types as Aeson -import qualified Data.ByteString.Conversion as BS.C -import Data.Domain (Domain (Domain)) -import Data.Handle (Handle (Handle, fromHandle)) -import Data.Id (Id (Id, toUUID), UserId) -import Data.Qualified (OptionallyQualified, Qualified (Qualified), eitherQualifiedOrNot, mkQualifiedHandle, mkQualifiedId, renderQualifiedHandle, renderQualifiedId) -import Data.String.Conversions (cs) -import qualified Data.Text.Encoding as Text.E +import Data.Domain (Domain (..)) +import Data.Handle (Handle) +import Data.Id (Id (..), UserId) +import Data.Qualified (Qualified (..), renderQualifiedId) import qualified Data.UUID as UUID import Imports import Test.Tasty @@ -45,31 +42,12 @@ tests = testQualifiedSerialization :: [TestTree] testQualifiedSerialization = - [ testCase "render foo@bar.com" $ do - assertEqual "" "foo@bar.com" $ - (renderQualifiedHandle (Qualified (Handle "foo") (Domain "bar.com"))), - testCase "render 61a73a52-e526-4892-82a9-3d638d77629f@example.com" $ do + [ testCase "render 61a73a52-e526-4892-82a9-3d638d77629f@example.com" $ do uuid <- maybe (assertFailure "invalid UUID") pure $ UUID.fromString "61a73a52-e526-4892-82a9-3d638d77629f" assertEqual "" "61a73a52-e526-4892-82a9-3d638d77629f@example.com" $ (renderQualifiedId (Qualified (Id uuid) (Domain "example.com"))), - testProperty "roundtrip for Qualified Handle" $ - \(x :: Qualified Handle) -> - mkQualifiedHandle (renderQualifiedHandle x) === Right x, - testProperty "roundtrip for Qualified UserId" $ - \(x :: Qualified UserId) -> - mkQualifiedId (renderQualifiedId x) === Right x, - testProperty "roundtrip for OptionallyQualified Handle" $ - \(x :: OptionallyQualified Handle) -> do - let render = Text.E.encodeUtf8 . either fromHandle renderQualifiedHandle . eitherQualifiedOrNot - let parse = BS.C.runParser BS.C.parser - parse (render x) === Right x, - testProperty "roundtrip for OptionallyQualified UserId" $ - \(x :: OptionallyQualified UserId) -> do - let render = Text.E.encodeUtf8 . either (cs . UUID.toString . toUUID) renderQualifiedId . eitherQualifiedOrNot - let parse = BS.C.runParser BS.C.parser - parse (render x) === Right x, jsonRoundtrip @(Qualified Handle), jsonRoundtrip @(Qualified UserId) ] diff --git a/libs/types-common/types-common.cabal b/libs/types-common/types-common.cabal index 80df4052a9e..2a2a5f9b0a4 100644 --- a/libs/types-common/types-common.cabal +++ b/libs/types-common/types-common.cabal @@ -4,7 +4,7 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: 2f5a186135570c9214149d64bf331f1512c069ce0b4d100166f0a7fa6174f464 +-- hash: ee2619b6133e11f5de7ace09995fa308d2c5c6ecc46e81b8e10283594a7aef26 name: types-common version: 0.16.0 @@ -21,6 +21,7 @@ build-type: Simple library exposed-modules: Data.Code + Data.CommaSeparatedList Data.Domain Data.ETag Data.Handle diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 7f8efb3cc9b..2b306a3d29c 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -68,6 +68,9 @@ module Wire.API.User mkVerifyDeleteUser, DeletionCodeTimeout (..), + -- * List Users + ListUsersQuery (..), + -- * helpers parseIdentity, @@ -91,7 +94,7 @@ module Wire.API.User where import Control.Error.Safe (rightMay) -import Control.Lens (over, view) +import Control.Lens (over, view, (.~), (?~)) import Data.Aeson ( FromJSON (parseJSON), KeyValue ((.=)), @@ -108,6 +111,7 @@ import qualified Data.Aeson.Types as Aeson import Data.ByteString.Conversion import qualified Data.Code as Code import qualified Data.Currency as Currency +import Data.Domain (Domain (Domain)) import Data.Handle (Handle) import qualified Data.HashMap.Strict as HashMap import qualified Data.HashMap.Strict.InsOrd as InsOrdHashMap @@ -118,10 +122,11 @@ import Data.Misc (PlainTextPassword (..)) import Data.Proxy (Proxy (..)) import Data.Qualified import Data.Range -import Data.Swagger (ToSchema (..), genericDeclareNamedSchema, properties, required, schema) +import Data.Swagger (HasExample (example), NamedSchema (..), SwaggerType (..), ToSchema (..), declareSchemaRef, description, genericDeclareNamedSchema, properties, required, schema, type_) import qualified Data.Swagger.Build.Api as Doc import Data.Text.Ascii import Data.UUID (UUID, nil) +import qualified Data.UUID as UUID import Deriving.Swagger import Imports import qualified Test.QuickCheck as QC @@ -1025,3 +1030,37 @@ instance ToJSON DeletionCodeTimeout where instance FromJSON DeletionCodeTimeout where parseJSON = withObject "DeletionCodeTimeout" $ \o -> DeletionCodeTimeout <$> o .: "expires_in" + +data ListUsersQuery + = ListUsersByIds [Qualified UserId] + | ListUsersByHandles (Range 1 4 [Qualified Handle]) + deriving (Show, Eq) + +instance FromJSON ListUsersQuery where + parseJSON = + withObject "ListUsersQuery" $ \o -> do + mUids <- ListUsersByIds <$$> o .: "qualified_ids" + mHandles <- ListUsersByHandles <$$> o .: "qualified_handles" + case (mUids, mHandles) of + (Just uids, Nothing) -> pure uids + (Nothing, Just handles) -> pure handles + (_, _) -> fail "exactly one of qualified_ids or qualified_handles must be provided." + +instance ToJSON ListUsersQuery where + toJSON (ListUsersByIds uids) = object ["qualified_ids" .= uids] + toJSON (ListUsersByHandles handles) = object ["qualified_handles" .= handles] + +-- NB: It is not possible to specific mutually exclusive fields in swagger2, so +-- here we write it in description and modify the example to have the correct +-- JSON. +instance ToSchema ListUsersQuery where + declareNamedSchema _ = do + uids <- declareSchemaRef (Proxy @[Qualified UserId]) + handles <- declareSchemaRef (Proxy @(Range 1 4 [Qualified Handle])) + return $ + NamedSchema (Just "ListUsersQuery") $ + mempty + & type_ ?~ SwaggerObject + & description ?~ "exactly one of qualifie_ids or qualified_handles must be provided." + & properties .~ InsOrdHashMap.fromList [("qualified_ids", uids), ("qualified_handles", handles)] + & example ?~ toJSON (ListUsersByIds [Qualified (Id UUID.nil) (Domain "example.com")]) diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 130c87f2900..e5afe1c95a5 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -1,5 +1,6 @@ {-# LANGUAGE DerivingVia #-} {-# LANGUAGE RecordWildCards #-} +{-# OPTIONS_GHC -Wno-orphans #-} -- This file is part of the Wire Server implementation. -- @@ -58,14 +59,14 @@ import Control.Monad.Catch (throwM) import Data.Aeson hiding (json) import Data.ByteString.Conversion import qualified Data.ByteString.Lazy as Lazy +import Data.CommaSeparatedList (CommaSeparatedList (fromCommaSeparatedList)) import Data.Domain import Data.Handle (Handle, parseHandle) import Data.Id as Id import Data.IdMapping (MappedOrLocalId (Local)) import qualified Data.Map.Strict as Map import Data.Misc (IpAddr (..)) -import Data.Proxy (Proxy (..)) -import Data.Qualified (Qualified (..)) +import Data.Qualified (Qualified (..), partitionRemoteOrLocalIds) import Data.Range import Data.Swagger (HasInfo (info), HasTitle (title), Swagger, ToSchema (..), description) import qualified Data.Swagger.Build.Api as Doc @@ -84,7 +85,7 @@ import Network.Wai.Utilities as Utilities import Network.Wai.Utilities.Swagger (document, mkSwaggerApi) import qualified Network.Wai.Utilities.Swagger as Doc import Network.Wai.Utilities.ZAuth (zauthConnId, zauthUserId) -import Servant (Capture, Capture', DefaultErrorFormatters, Description, ErrorFormatters, Get, HasContextEntry, HasServer, HasStatus, Header', ServerT, StdMethod (HEAD), Summary, UVerb, Union, WithStatus, (:<|>) (..), (:>), type (.++)) +import Servant hiding (Handler, JSON, addHeader, respond) import qualified Servant import Servant.Swagger (HasSwagger (toSwagger)) import Servant.Swagger.Internal.Orphans () @@ -251,6 +252,25 @@ type GetHandleInfoQualified = :> Capture' '[Description "The user handle"] "handle" Handle :> Get '[Servant.JSON] Public.UserHandleInfo +-- See Note [ephemeral user sideeffect] +type ListUsersByUnqualifiedIdsOrHandles = + Summary "List users (deprecated)" + :> Description "The 'ids' and 'handles' parameters are mutually exclusive." + :> ZAuthServant + :> "users" + :> QueryParam' [Optional, Strict, Description "User IDs of users to fetch"] "ids" (CommaSeparatedList UserId) + :> QueryParam' [Optional, Strict, Description "Handles of users to fetch, min 1 and max 4 (the check for handles is rather expensive)"] "handles" (Range 1 4 (CommaSeparatedList Handle)) + :> Get '[Servant.JSON] [Public.UserProfile] + +-- See Note [ephemeral user sideeffect] +type ListUsersByIdsOrHandles = + Summary "List users" + :> Description "The 'ids' and 'handles' parameters are mutually exclusive." + :> ZAuthServant + :> "list-users" + :> Servant.ReqBody '[Servant.JSON] Public.ListUsersQuery + :> Post '[Servant.JSON] [Public.UserProfile] + type OutsideWorldAPI = CheckUserExistsUnqualified :<|> CheckUserExistsQualified @@ -259,6 +279,8 @@ type OutsideWorldAPI = :<|> GetSelf :<|> GetHandleInfoUnqualified :<|> GetHandleInfoQualified + :<|> ListUsersByUnqualifiedIdsOrHandles + :<|> ListUsersByIdsOrHandles type SwaggerDocsAPI = "api" :> SwaggerSchemaUI "swagger-ui" "swagger.json" @@ -284,6 +306,8 @@ servantSitemap = :<|> getSelf :<|> getHandleInfoUnqualifiedH :<|> getHandleInfoH + :<|> listUsersByUnqualifiedIdsOrHandles + :<|> listUsersByIdsOrHandles -- Note [ephemeral user sideeffect] -- If the user is ephemeral and expired, it will be removed upon calling @@ -321,26 +345,6 @@ sitemap o = do -- some APIs moved to servant -- end User Handle API - -- If the user is ephemeral and expired, it will be removed, see 'Brig.API.User.userGC'. - -- This leads to the following events being sent: - -- - UserDeleted event to contacts of the user - -- - MemberLeave event to members for all conversations the user was in (via galley) - get "/users" (continue listUsersH) $ - accept "application" "json" - .&. zauthUserId - .&. (param "ids" ||| param "handles") - document "GET" "users" $ do - Doc.summary "List users" - Doc.notes "The 'ids' and 'handles' parameters are mutually exclusive." - Doc.parameter Doc.Query "ids" Doc.string' $ do - Doc.description "User IDs of users to fetch" - Doc.optional - Doc.parameter Doc.Query "handles" Doc.string' $ do - Doc.description "Handles of users to fetch, min 1 and max 4 (the check for handles is rather expensive)" - Doc.optional - Doc.returns (Doc.array (Doc.ref Public.modelUser)) - Doc.response 200 "List of users" Doc.end - -- User Prekey API ---------------------------------------------------- post "/users/prekeys" (continue getMultiPrekeyBundlesH) $ @@ -1220,26 +1224,36 @@ getUserDisplayNameH (_ ::: self) = do Just n -> json $ object ["name" .= n] Nothing -> setStatus status404 empty -listUsersH :: JSON ::: UserId ::: Either (List UserId) (Range 1 4 (List Handle)) -> Handler Response -listUsersH (_ ::: self ::: qry) = - toResponse <$> listUsers self qry - where - toResponse = \case - [] -> setStatus status404 empty - ps -> json ps - --- | 'listUsers' only handles listing local users by ID or handle. We decided to --- create a new federation aware endpoint which accepts federation aware Ids or --- handles in the request body using a 'POST' request to avoid specifying --- complex objects in the query parameters. -listUsers :: UserId -> Either (List UserId) (Range 1 4 (List Handle)) -> Handler [Public.UserProfile] -listUsers self = \case - Left us -> do - domain <- viewFederationDomain - byIds $ map (`Qualified` domain) (fromList us) - Right hs -> do - us <- getIds (fromList $ fromRange hs) - filterHandleResults self =<< byIds us +-- FUTUREWORK: Make servant understand that at least one of these is required +listUsersByUnqualifiedIdsOrHandles :: UserId -> Maybe (CommaSeparatedList UserId) -> Maybe (Range 1 4 (CommaSeparatedList Handle)) -> Handler [Public.UserProfile] +listUsersByUnqualifiedIdsOrHandles self mUids mHandles = do + domain <- viewFederationDomain + case (mUids, mHandles) of + (Just uids, _) -> listUsersByIdsOrHandles self (Public.ListUsersByIds ((`Qualified` domain) <$> fromCommaSeparatedList uids)) + (_, Just handles) -> + let normalRangedList = fromCommaSeparatedList $ fromRange handles + qualifiedList = (`Qualified` domain) <$> normalRangedList + -- Use of unsafeRange here is ok only because we know that 'handles' + -- is valid for 'Range 1 4'. However, we must not forget to keep this + -- annotation here otherwise a change in 'Public.ListUsersByHandles' + -- could cause this code to break. + qualifiedRangedList :: Range 1 4 [Qualified Handle] = unsafeRange qualifiedList + in listUsersByIdsOrHandles self (Public.ListUsersByHandles qualifiedRangedList) + (Nothing, Nothing) -> throwStd $ badRequest "at least one ids or handles must be provided" + +listUsersByIdsOrHandles :: UserId -> Public.ListUsersQuery -> Handler [Public.UserProfile] +listUsersByIdsOrHandles self q = do + foundUsers <- case q of + Public.ListUsersByIds us -> + byIds us + Public.ListUsersByHandles hs -> do + domain <- viewFederationDomain + let (_remoteHandles, localHandles) = partitionRemoteOrLocalIds domain (fromRange hs) + us <- getIds localHandles + filterHandleResults self =<< byIds us + case foundUsers of + [] -> throwStd $ notFound "None of the specified ids or handles match any users" + _ -> pure foundUsers where getIds :: [Handle] -> Handler [Qualified UserId] getIds localHandles = do @@ -1301,7 +1315,7 @@ changeLocaleH (u ::: conn ::: req) = do lift $ API.changeLocale u conn l return empty --- | (zusr are is ignored by this handler, ie. checking handles is allowed as long as you have +-- | (zusr is ignored by this handler, ie. checking handles is allowed as long as you have -- *any* account.) checkHandleH :: UserId ::: Text -> Handler Response checkHandleH (_uid ::: hndl) = do diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 7eb74a5bb03..e8875dc65b1 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -187,16 +187,16 @@ createUser new@NewUser {..} = do -- Create account (account, pw) <- lift $ do - new' <- - case Team.inInvitation . fst <$> teamInvitation of - Just (Id uuid) -> do - mAcc <- Data.lookupAccount (Id uuid) - case mAcc of - Just existingAccount -> - pure (new {newUserManagedBy = Just . userManagedBy . accountUser $ existingAccount}) - Nothing -> pure new - Nothing -> pure new - newAccount new' {newUserIdentity = ident} (Team.inInvitation . fst <$> teamInvitation) tid + let mbInv = Team.inInvitation . fst <$> teamInvitation + mbExistingAccount <- join <$> for mbInv (\(Id uuid) -> Data.lookupAccount (Id uuid)) + let new' = + new + { newUserManagedBy = case mbExistingAccount of + Nothing -> newUserManagedBy + Just acc -> Just . userManagedBy . accountUser $ acc, + newUserIdentity = ident + } + newAccount new' mbInv tid (userHandle . accountUser =<< mbExistingAccount) let uid = userId (accountUser account) Log.debug $ field "user" (toByteString uid) . field "action" (Log.val "User.createUser") diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 26613a0348e..9e45d8ef25f 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -104,8 +104,16 @@ data ReAuthError = ReAuthError !AuthError | ReAuthMissingPassword -newAccount :: NewUser -> Maybe InvitationId -> Maybe TeamId -> AppIO (UserAccount, Maybe Password) -newAccount u inv tid = do +-- | Preconditions: +-- +-- 1. @newUserUUID u == Just inv || isNothing (newUserUUID u)@. +-- 2. If @isJust@, @mbHandle@ must be claimed by user with id @inv@. +-- +-- Condition (2.) is essential for maintaining handle uniqueness. It is guaranteed by the +-- fact that we're setting getting @mbHandle@ from table @"user"@, and when/if it was added +-- there, it was claimed properly. +newAccount :: NewUser -> Maybe InvitationId -> Maybe TeamId -> Maybe Handle -> AppIO (UserAccount, Maybe Password) +newAccount u inv tid mbHandle = do defLoc <- setDefaultLocale <$> view settings domain <- viewFederationDomain uid <- @@ -138,7 +146,7 @@ newAccount u inv tid = do colour = fromMaybe defaultAccentId (newUserAccentId u) locale defLoc = fromMaybe defLoc (newUserLocale u) managedBy = fromMaybe defaultManagedBy (newUserManagedBy u) - user uid domain l e = User uid (Qualified uid domain) ident name pict assets colour False l Nothing Nothing e tid managedBy + user uid domain l e = User uid (Qualified uid domain) ident name pict assets colour False l Nothing mbHandle e tid managedBy newAccountInviteViaScim :: UserId -> TeamId -> Maybe Locale -> Name -> Email -> AppIO UserAccount newAccountInviteViaScim uid tid locale name email = do diff --git a/services/brig/src/Brig/User/Handle.hs b/services/brig/src/Brig/User/Handle.hs index b04e959bc15..6dea85029c6 100644 --- a/services/brig/src/Brig/User/Handle.hs +++ b/services/brig/src/Brig/User/Handle.hs @@ -35,29 +35,25 @@ import Imports -- | Claim a new handle for an existing 'User'. claimHandle :: UserId -> Maybe Handle -> Handle -> AppIO Bool -claimHandle uid oldHandle newHandle = isJust <$> claimHandleWith (User.updateHandle) uid oldHandle newHandle - --- | Claim a handle for an invitation or a user. Invitations can be referenced by the coerced --- 'UserId'. -claimHandleWith :: (UserId -> Handle -> AppIO a) -> UserId -> Maybe Handle -> Handle -> AppIO (Maybe a) -claimHandleWith updOperation uid oldHandle h = do - owner <- lookupHandle h - case owner of - Just uid' | uid /= uid' -> return Nothing - _ -> do - env <- ask - let key = "@" <> fromHandle h - withClaim uid key (30 # Minute) $ - runAppT env $ - do - -- Record ownership - retry x5 $ write handleInsert (params Quorum (h, uid)) - -- Update profile - result <- updOperation uid h - -- Free old handle (if it changed) - for_ (mfilter (/= h) oldHandle) $ - freeHandle uid - return result +claimHandle uid oldHandle newHandle = + isJust <$> do + owner <- lookupHandle newHandle + case owner of + Just uid' | uid /= uid' -> return Nothing + _ -> do + env <- ask + let key = "@" <> fromHandle newHandle + withClaim uid key (30 # Minute) $ + runAppT env $ + do + -- Record ownership + retry x5 $ write handleInsert (params Quorum (newHandle, uid)) + -- Update profile + result <- User.updateHandle uid newHandle + -- Free old handle (if it changed) + for_ (mfilter (/= newHandle) oldHandle) $ + freeHandle uid + return result -- | Free a 'Handle', making it available to be claimed again. freeHandle :: UserId -> Handle -> AppIO () diff --git a/services/brig/test/integration/API/UserPendingActivation.hs b/services/brig/test/integration/API/UserPendingActivation.hs index a63cbff86fd..9c839021e24 100644 --- a/services/brig/test/integration/API/UserPendingActivation.hs +++ b/services/brig/test/integration/API/UserPendingActivation.hs @@ -3,6 +3,23 @@ {-# LANGUAGE RecordWildCards #-} {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2020 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + module API.UserPendingActivation where import API.Team.Util (getTeams) diff --git a/services/federator/src/Federator/API.hs b/services/federator/src/Federator/API.hs index ba5026aee23..7348e871fd6 100644 --- a/services/federator/src/Federator/API.hs +++ b/services/federator/src/Federator/API.hs @@ -23,8 +23,8 @@ module Federator.API ) where +import Data.Domain (Domain) import Data.Id (ConvId, UserId) -import Data.Qualified (Qualified) import Imports import Servant.API import Servant.API.Generic @@ -37,7 +37,8 @@ data Api route = Api route :- "i" :> "users" - :> Capture "id" (Qualified UserId) + :> Capture "domain" Domain + :> Capture "id" UserId :> "prekeys" -- FUTUREWORK(federation): -- this should return a version of PrekeyBundle with qualified UserId, @@ -47,7 +48,8 @@ data Api route = Api route :- "i" :> "conversations" - :> Capture "cnv" (Qualified ConvId) + :> Capture "domain" Domain + :> Capture "cnv" ConvId :> "join" :> ReqBody '[JSON] Fed.JoinConversationByIdRequest :> Post '[JSON] (Fed.ConversationUpdateResult Fed.MemberJoin) diff --git a/services/spar/src/Spar/Scim/User.hs b/services/spar/src/Spar/Scim/User.hs index 5136428d998..d2c05f399d9 100644 --- a/services/spar/src/Spar/Scim/User.hs +++ b/services/spar/src/Spar/Scim/User.hs @@ -566,7 +566,7 @@ assertExternalIdNotUsedElsewhere :: ST.ValidExternalId -> UserId -> Scim.ScimHan assertExternalIdNotUsedElsewhere veid wireUserId = do mExistingUserId <- lift $ getUser veid unless (mExistingUserId `elem` [Nothing, Just wireUserId]) $ do - throwError Scim.conflict {Scim.detail = Just "externalId does not match UserId"} + throwError Scim.conflict {Scim.detail = Just "externalId already in use by another Wire user"} assertHandleUnused :: Handle -> Scim.ScimHandler Spar () assertHandleUnused = assertHandleUnused' "userName is already taken" @@ -581,7 +581,7 @@ assertHandleNotUsedElsewhere :: UserId -> Handle -> Scim.ScimHandler Spar () assertHandleNotUsedElsewhere uid hndl = do musr <- lift $ Brig.getBrigUser Brig.WithPendingInvitations uid unless ((userHandle =<< musr) == Just hndl) $ - assertHandleUnused' "userName does not match UserId" hndl + assertHandleUnused' "userName already in use by another wire user" hndl -- | Helper function that translates a given brig user into a 'Scim.StoredUser', with some -- effects like updating the 'ManagedBy' field in brig and storing creation and update time diff --git a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs index f404280e7d7..66d5020068f 100644 --- a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs @@ -283,7 +283,7 @@ testCreateUserNoIdP = do >>= maybe (error "could not find user in brig") pure liftIO $ accountStatus brigUser `shouldBe` Active liftIO $ userManagedBy (accountUser brigUser) `shouldBe` ManagedByScim - + liftIO $ userHandle (accountUser brigUser) `shouldBe` Just handle susr <- getUser tok userid let usr = Scim.value . Scim.thing $ susr liftIO $ Scim.User.active usr `shouldNotBe` Just (Scim.ScimBool False) diff --git a/stack.yaml b/stack.yaml index c862559aef2..6a2df74e1f1 100644 --- a/stack.yaml +++ b/stack.yaml @@ -43,6 +43,7 @@ packages: - tools/db/billing-team-member-backfill - tools/db/find-undead - tools/db/move-team +- tools/db/repair-handles - tools/makedeb - tools/rex - tools/stern @@ -157,6 +158,8 @@ extra-deps: - stompl-0.5.0 - pattern-trie-0.1.0 - markov-chain-usage-model-0.0.0 +- wai-predicates-1.0.0 +- redis-io-1.1.0 # Not latest as latst one breaks wai-routing - wai-route-0.4.0 @@ -189,12 +192,6 @@ extra-deps: - git: https://github.com/dpwright/HaskellNet-SSL commit: ca84ef29a93eaef7673fa58056cdd8dae1568d2d # master (Sep 14, 2020) -# Forks with pending PRs -- git: https://gitlab.com/axeman/wai-predicates.git - commit: 999d195b27104b9b39174f5ce18f5214b018a177 # ghc-8.8 (Sep 14, 2020, PR: https://gitlab.com/twittner/wai-predicates/-/merge_requests/1) -- git: https://gitlab.com/axeman/redis-io.git - commit: a0f39b1c517df21ad284ff91ecb062cbe41a4ad1 # ghc-8.8 (Sep 21 , 2020, https://gitlab.com/twittner/redis-io/-/merge_requests/5) - ############################################################ # Development tools ############################################################ diff --git a/stack.yaml.lock b/stack.yaml.lock index 9bb03414ae4..734a8c8aa84 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -456,6 +456,20 @@ packages: sha256: 6871bd9281acf589296d0998a3d62892b036040ab10e74e8a0f356f68c194f4f original: hackage: markov-chain-usage-model-0.0.0 +- completed: + hackage: wai-predicates-1.0.0@sha256:46ec4b4afcecac28810e6a13613fedc0c995419cb7c5e18d3dd62eed258b434f,2848 + pantry-tree: + size: 1378 + sha256: 58c340a179ad44850ace6f5d7d14df2f2e8cc7fd95f8a83b1c0c19e8b777760b + original: + hackage: wai-predicates-1.0.0 +- completed: + hackage: redis-io-1.1.0@sha256:16ff8142557658df2c8ad569a1108042bab4cc4aec73fbc9dfd910477222c2e8,2732 + pantry-tree: + size: 809 + sha256: db61f70aa7387090c26ccca0545ffdeea0adfcf93b76d5eaf6a954c0e5a34064 + original: + hackage: redis-io-1.1.0 - completed: hackage: wai-route-0.4.0@sha256:ee52f13d2945e4a56147e91e515e184f840654f2e3d9071c73bec3d8aa1f4444,2119 pantry-tree: @@ -583,28 +597,6 @@ packages: original: git: https://github.com/dpwright/HaskellNet-SSL commit: ca84ef29a93eaef7673fa58056cdd8dae1568d2d -- completed: - name: wai-predicates - version: 0.10.0 - git: https://gitlab.com/axeman/wai-predicates.git - pantry-tree: - size: 1585 - sha256: 8675a538bbbfb171b9d565831f333e443118ea5a70b1be8bffa635cb847d04fa - commit: 999d195b27104b9b39174f5ce18f5214b018a177 - original: - git: https://gitlab.com/axeman/wai-predicates.git - commit: 999d195b27104b9b39174f5ce18f5214b018a177 -- completed: - name: redis-io - version: 1.0.0 - git: https://gitlab.com/axeman/redis-io.git - pantry-tree: - size: 912 - sha256: 3e8093b581c621df7ecbf2f6f79686afdea8bfeb56f0e546fff1e9d86de3bf80 - commit: a0f39b1c517df21ad284ff91ecb062cbe41a4ad1 - original: - git: https://gitlab.com/axeman/redis-io.git - commit: a0f39b1c517df21ad284ff91ecb062cbe41a4ad1 - completed: hackage: ormolu-0.1.2.0@sha256:24e6512750576978b6f045c1e53a7aad28ab61960f738a3c74fb0bc2beaf4030,6237 pantry-tree: diff --git a/tools/db/repair-handles/README.md b/tools/db/repair-handles/README.md new file mode 100644 index 00000000000..2432c64c742 --- /dev/null +++ b/tools/db/repair-handles/README.md @@ -0,0 +1,8 @@ +`repair-handles` is CLI that fixes inconsistencies between the tables `brig.user` and `brig.user_handle`. + +This must be run to repair cassandra_brig after a bug fixed in https://github.com/wireapp/wire-server/pull/1303. It fixes two kinds of inconsistencies: + +1. A user for which `brig.user.handle` is `null` and there is exactly one row `` in `brig.user_handle` matching the user. + The tool fixes this by setting `brig.user.handle` to `.handle`. +2. A user for which `brig.user.handle` is set to a value `` and there are exactly 2 rows ``, `` in `brig.user_handle` matching the user, and `.handle` equals ``. + The tool fixes this by setting `brig.user.handle` to `.handle` and deleting `.handle` from `brig.user_handle`. diff --git a/tools/db/repair-handles/package.yaml b/tools/db/repair-handles/package.yaml new file mode 100644 index 00000000000..a5dd1e8b997 --- /dev/null +++ b/tools/db/repair-handles/package.yaml @@ -0,0 +1,43 @@ +defaults: + local: ../../../package-defaults.yaml +name: repair-handles +version: '1.0.0' +synopsis: Repair inconsistencies between tables user and user_handle +category: Network +author: Wire Swiss GmbH +maintainer: Wire Swiss GmbH +copyright: (c) 2018 Wire Swiss GmbH +license: AGPL-3 + +dependencies: +- base +- aeson +- brig +- brig-types +- bytestring +- bytestring-conversion +- cassandra-util +- conduit +- containers +- cql +- extended +- imports +- lens +- mtl +- optparse-applicative +- string-conversions +- text +- time +- tinylog +- types-common +- unliftio +- uuid +- vector +- wire-api + +executables: + repair-handles: + main: Main.hs + source-dirs: + - repair-handles + - src diff --git a/tools/db/repair-handles/repair-handles.cabal b/tools/db/repair-handles/repair-handles.cabal new file mode 100644 index 00000000000..c5527197d2d --- /dev/null +++ b/tools/db/repair-handles/repair-handles.cabal @@ -0,0 +1,56 @@ +cabal-version: 1.12 + +-- This file has been generated from package.yaml by hpack version 0.33.0. +-- +-- see: https://github.com/sol/hpack +-- +-- hash: 25507d1dbb4afa0928786e4a83417111283b6090077f749d0c9e8e5a99da729e + +name: repair-handles +version: 1.0.0 +synopsis: Repair inconsistencies between tables user and user_handle +category: Network +author: Wire Swiss GmbH +maintainer: Wire Swiss GmbH +copyright: (c) 2018 Wire Swiss GmbH +license: AGPL-3 +build-type: Simple + +executable repair-handles + main-is: Main.hs + other-modules: + Options + Types + Work + Paths_repair_handles + hs-source-dirs: + repair-handles + src + default-extensions: AllowAmbiguousTypes BangPatterns ConstraintKinds DataKinds DefaultSignatures DerivingStrategies DeriveFunctor DeriveGeneric DeriveLift DeriveTraversable EmptyCase FlexibleContexts FlexibleInstances FunctionalDependencies GADTs InstanceSigs KindSignatures LambdaCase MultiParamTypeClasses MultiWayIf NamedFieldPuns NoImplicitPrelude OverloadedStrings PackageImports PatternSynonyms PolyKinds QuasiQuotes RankNTypes ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeFamilyDependencies TypeOperators UndecidableInstances ViewPatterns + ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + build-depends: + aeson + , base + , brig + , brig-types + , bytestring + , bytestring-conversion + , cassandra-util + , conduit + , containers + , cql + , extended + , imports + , lens + , mtl + , optparse-applicative + , string-conversions + , text + , time + , tinylog + , types-common + , unliftio + , uuid + , vector + , wire-api + default-language: Haskell2010 diff --git a/tools/db/repair-handles/repair-handles/Main.hs b/tools/db/repair-handles/repair-handles/Main.hs new file mode 100644 index 00000000000..d94393f0709 --- /dev/null +++ b/tools/db/repair-handles/repair-handles/Main.hs @@ -0,0 +1,5 @@ +import qualified Work +import Prelude + +main :: IO () +main = Work.main diff --git a/tools/db/repair-handles/src/Options.hs b/tools/db/repair-handles/src/Options.hs new file mode 100644 index 00000000000..50f19ed7f29 --- /dev/null +++ b/tools/db/repair-handles/src/Options.hs @@ -0,0 +1,68 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2020 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Options where + +import Brig.Data.Instances () +import Cassandra hiding (Set) +import Data.Id +import Data.String.Conversions (cs) +import Data.UUID +import Imports +import Options.Applicative hiding (action) +import Types + +settingsParser :: Parser Settings +settingsParser = + Settings + <$> cassandraSettingsParser "brig" + <*> cassandraSettingsParser "galley" + <*> switch (short 'n' <> long "dry-run") + <*> switch (short 'd' <> long "debug") + <*> option auto (short 's' <> long "page-size" <> value 1000) + <*> (Id . parseUUID <$> strArgument (metavar "TEAM-UUID")) + +parseUUID :: HasCallStack => String -> UUID +parseUUID = fromJust . Data.UUID.fromString + +cassandraSettingsParser :: String -> Parser CassandraSettings +cassandraSettingsParser ks = + CassandraSettings + <$> strOption + ( long ("cassandra-host-" ++ ks) + <> metavar "HOST" + <> help ("Cassandra Host for: " ++ ks) + <> value "localhost" + <> showDefault + ) + <*> option + auto + ( long ("cassandra-port-" ++ ks) + <> metavar "PORT" + <> help ("Cassandra Port for: " ++ ks) + <> value 9042 + <> showDefault + ) + <*> ( Keyspace . cs + <$> strOption + ( long ("cassandra-keyspace-" ++ ks) + <> metavar "STRING" + <> help ("Cassandra Keyspace for: " ++ ks) + <> value (ks ++ "_test") + <> showDefault + ) + ) diff --git a/tools/db/repair-handles/src/Types.hs b/tools/db/repair-handles/src/Types.hs new file mode 100644 index 00000000000..9ca8a76d716 --- /dev/null +++ b/tools/db/repair-handles/src/Types.hs @@ -0,0 +1,55 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2020 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Types where + +import Brig.Data.Instances () +import Cassandra hiding (Set) +import Control.Lens +import Data.Id +import Imports +import qualified System.Logger as Log + +data Env = Env + { envBrig :: ClientState, + envGalley :: ClientState, + envPageSize :: Int32, + envTeam :: TeamId, + envSettings :: Settings, + envLogger :: Log.Logger + } + +data Settings = Settings + { _setCasBrig :: !CassandraSettings, + _setCasGalley :: !CassandraSettings, + _setDryRun :: Bool, + _setDebug :: Bool, + _setPageSize :: Int32, + _setTeamId :: TeamId + } + deriving (Show) + +data CassandraSettings = CassandraSettings + { _cHosts :: !String, + _cPort :: !Word16, + _cKeyspace :: !Keyspace + } + deriving (Show) + +makeLenses ''Settings + +makeLenses ''CassandraSettings diff --git a/tools/db/repair-handles/src/Work.hs b/tools/db/repair-handles/src/Work.hs new file mode 100644 index 00000000000..869e257841b --- /dev/null +++ b/tools/db/repair-handles/src/Work.hs @@ -0,0 +1,238 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2020 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . +{-# LANGUAGE RecordWildCards #-} + +module Work where + +import Brig.Data.Instances () +import Cassandra hiding (Set) +import qualified Cassandra as Cas +import qualified Cassandra.Settings as Cas +import Conduit +import Control.Lens +import Control.Monad.Except +import qualified Data.Conduit.Combinators as C +import Data.Handle (Handle) +import Data.Id +import qualified Data.Map.Strict as Map +import Data.String.Conversions (cs) +import qualified Data.Text as T +import Imports +import Options +import Options.Applicative hiding (action) +import qualified System.Logger as Log +import Types + +-- | The table user_handle grouped by user +type HandleMap = Map UserId [Handle] + +readHandleMap :: Env -> IO HandleMap +readHandleMap Env {..} = + runConduit $ + (transPipe (runClient envBrig) $ paginateC selectUserHandle (paramsP Quorum () envPageSize) x1) + .| (C.foldM insertAndLog (Map.empty, 0) <&> fst) + where + selectUserHandle :: PrepQuery R () (Maybe UserId, Maybe Handle) + selectUserHandle = "SELECT user, handle FROM user_handle" + + insertAndLog :: (HandleMap, Int) -> [(Maybe UserId, Maybe Handle)] -> IO (HandleMap, Int) + insertAndLog (hmap, nTotal) pairs = do + let n = length pairs + Log.info envLogger $ Log.msg @Text $ "handles loaded: " <> (cs . show $ nTotal + n) + pure (foldl' insert hmap pairs, nTotal + n) + + insert :: HandleMap -> (Maybe UserId, Maybe Handle) -> HandleMap + insert hmap (Just uid, maybeToList -> handles) = + Map.insertWith (++) uid handles hmap + insert hmap (Nothing, _) = hmap + +data Action + = -- | Set the handle for user (column "handle" in table user). + -- Precondition: + -- The handle is the sole handle owned by the user (according to table user_handle) + SetHandle + UserId + Handle + | -- | Change the handle for user (column "handle" in table user) + -- and delete the other handle. + -- Precondition: + -- Both handles are already owned by the user (according to table user_handle) + -- and the user doesnt own any more handles. + -- syntax: ResetHandle uid handle handleToBeRemoved + ResetHandle + UserId + Handle + Handle + | NoActionRequired UserId + deriving (Show) + +data ActionError = ActionError UserId Text [Handle] + deriving (Show) + +type ActionResult = Either ActionError Action + +decideAction :: + UserId -> + -- | "handle" column in table "brig.user" + Maybe Handle -> + -- | All handles owned by user in table "brig.user_handle" + [Handle] -> + ActionResult +decideAction uid Nothing [handle] = pure $ SetHandle uid handle +decideAction uid Nothing [] = pure $ NoActionRequired uid +decideAction uid Nothing handles = throwError $ ActionError uid "No handle set, but multiple handles owned" handles +decideAction uid (Just currentHandle) handles = + case filter (/= currentHandle) handles of + [] -> pure $ NoActionRequired uid + [otherHandle] -> pure $ ResetHandle uid otherHandle currentHandle + handles' -> throwError $ ActionError uid "Handle is set, but multiple handles owned" handles' + +sourceActions :: Env -> HandleMap -> ConduitM () ActionResult IO () +sourceActions Env {..} hmap = + ( transPipe (runClient envGalley) $ + paginateC selectTeam (paramsP Quorum (pure envTeam) envPageSize) x5 + .| C.map (fmap runIdentity) + ) + .| C.mapM readUsersPage + .| C.concat + .| C.map + ( \(uid, mbHandle) -> + decideAction uid mbHandle (fromMaybe [] $ Map.lookup uid hmap) + ) + where + selectTeam :: PrepQuery R (Identity TeamId) (Identity UserId) + selectTeam = "SELECT user FROM team_member WHERE team = ?" + + readUsersPage :: [UserId] -> IO [(UserId, Maybe Handle)] + readUsersPage uids = + runClient envBrig $ + query selectUsers (params Quorum (pure uids)) + + selectUsers :: PrepQuery R (Identity [UserId]) (UserId, Maybe Handle) + selectUsers = "SELECT id, handle FROM user WHERE id in ?" + +executeAction :: Env -> Action -> IO () +executeAction env = \case + (NoActionRequired _) -> pure () + (SetHandle uid handle) -> + setUserHandle env uid handle + (ResetHandle uid handle handleRemove) -> do + setUserHandle env uid handle + removeHandle env handleRemove + where + setUserHandle :: Env -> UserId -> Handle -> IO () + setUserHandle Env {..} uid handle = + runClient envBrig $ + Cas.write updateHandle $ params Quorum (handle, uid) + where + updateHandle :: PrepQuery W (Handle, UserId) () + updateHandle = "UPDATE user SET handle = ? WHERE id = ?" + + removeHandle :: Env -> Handle -> IO () + removeHandle Env {..} handle = + runClient envBrig $ + Cas.write deleteHandle $ params Quorum (pure handle) + where + deleteHandle :: PrepQuery W (Identity Handle) () + deleteHandle = "DELETE FROM user_handle WHERE handle = ?" + +runCommand :: Env -> IO () +runCommand env@Env {..} = do + Log.info envLogger (Log.msg @Text "Loading all handles...") + hmap <- readHandleMap env + Log.info envLogger $ Log.msg @Text ("Processing team" <> (if envSettings ^. setDryRun then " (dry-run) " else " ") <> "...") + runConduit $ + sourceActions env hmap + .| C.iterM (handleAction env) + .| chunkify 100 + .| C.mapM_ (logSummary env) + Log.info envLogger $ Log.msg @Text "Done." + where + handleAction :: Env -> ActionResult -> IO () + handleAction _env (Left err) = Log.err envLogger (Log.msg . show $ err) + handleAction _env (Right action) = do + Log.debug envLogger $ Log.msg @Text (cs . show $ action) + unless (envSettings ^. setDryRun) $ + executeAction env action + + logSummary :: Env -> [ActionResult] -> IO () + logSummary _env results = do + Log.info envLogger $ + Log.msg $ + ("Action summary for batch" <> (if envSettings ^. setDryRun then " (dry-run) " else " ") <> ":\n") + <> T.intercalate + "\n" + ( let (nErrs, nReset, nSet, nNoOp) = foldl' tally (0, 0, 0, 0) results + in catMaybes + [ mark " error(!)" nErrs, + mark " reset original handle" nReset, + mark " set missing handle" nSet, + mark " do nothing" nNoOp + ] + ) + where + mark :: Text -> Int -> Maybe Text + mark msg n + | n > 0 = Just $ msg <> ": " <> cs (show n) + | otherwise = Nothing + + tally :: (Int, Int, Int, Int) -> ActionResult -> (Int, Int, Int, Int) + tally (nErrs, nReset, nSet, nNoOp) (Right ResetHandle {}) = (nErrs, nReset + 1, nSet, nNoOp) + tally (nErrs, nReset, nSet, nNoOp) (Right SetHandle {}) = (nErrs, nReset, nSet + 1, nNoOp) + tally (nErrs, nReset, nSet, nNoOp) (Right NoActionRequired {}) = (nErrs, nReset, nSet, nNoOp + 1) + tally (nErrs, nReset, nSet, nNoOp) (Left _) = (nErrs + 1, nReset, nSet, nNoOp) + + chunkify :: Monad m => Int -> ConduitT i [i] m () + chunkify n = void (C.map (: [])) .| C.chunksOfE n + +main :: IO () +main = do + s <- execParser (info (helper <*> settingsParser) desc) + lgr <- initLogger (if s ^. setDebug then Log.Debug else Log.Info) + + brig <- initCas (s ^. setCasBrig) lgr + galley <- initCas (s ^. setCasGalley) lgr + + let env = + Env + { envBrig = brig, + envGalley = galley, + envPageSize = s ^. setPageSize, + envTeam = s ^. setTeamId, + envSettings = s, + envLogger = lgr + } + runCommand env + where + desc = + header "repair-handles" + <> progDesc "Fix dangling and missing handles" + <> fullDesc + initLogger level = + Log.new + . Log.setOutput Log.StdOut + . Log.setFormat Nothing + . Log.setBufSize 0 + $ Log.setLogLevel level Log.defSettings + initCas cas l = + Cas.init + . Cas.setLogger (Cas.mkLogger l) + . Cas.setContacts (cas ^. cHosts) [] + . Cas.setPortNumber (fromIntegral $ cas ^. cPort) + . Cas.setKeyspace (cas ^. cKeyspace) + . Cas.setProtocolVersion Cas.V4 + $ Cas.defSettings diff --git a/tools/nginz_disco/Dockerfile b/tools/nginz_disco/Dockerfile index 03c93e35769..9d15426f594 100644 --- a/tools/nginz_disco/Dockerfile +++ b/tools/nginz_disco/Dockerfile @@ -1,7 +1,7 @@ -FROM alpine:3.10 +FROM alpine:3.12.3 RUN apk add --no-cache curl bash openssl bind-tools -COPY nginz_disco.sh /usr/bin/nginz_disco.sh +COPY tools/nginz_disco/nginz_disco.sh /usr/bin/nginz_disco.sh ENTRYPOINT ["/usr/bin/nginz_disco.sh"] diff --git a/tools/nginz_disco/Makefile b/tools/nginz_disco/Makefile index 318e9cdd257..eb25fb19feb 100644 --- a/tools/nginz_disco/Makefile +++ b/tools/nginz_disco/Makefile @@ -1,4 +1,4 @@ .PHONY: docker docker: - docker build -t quay.io/wire/nginz_disco . + docker build -t quay.io/wire/nginz_disco -f Dockerfile ../..