diff --git a/Makefile b/Makefile index 17e11e3aec0..f774012f3e6 100644 --- a/Makefile +++ b/Makefile @@ -294,9 +294,9 @@ db-reset: c ./dist/gundeck-schema --keyspace gundeck_test2 --replication-factor 1 --reset ./dist/spar-schema --keyspace spar_test2 --replication-factor 1 --reset ./integration/scripts/integration-dynamic-backends-db-schemas.sh --replication-factor 1 --reset - ./dist/brig-index reset --elasticsearch-index-prefix directory --elasticsearch-server http://localhost:9200 > /dev/null - ./dist/brig-index reset --elasticsearch-index-prefix directory2 --elasticsearch-server http://localhost:9200 > /dev/null - ./integration/scripts/integration-dynamic-backends-brig-index.sh --elasticsearch-server http://localhost:9200 > /dev/null + ./dist/brig-index reset --elasticsearch-index-prefix directory --elasticsearch-server http://localhost:9200 --elasticsearch-credentials ./services/brig/test/resources/elasticsearch-credentials.yaml > /dev/null + ./dist/brig-index reset --elasticsearch-index-prefix directory2 --elasticsearch-server http://localhost:9200 --elasticsearch-credentials ./services/brig/test/resources/elasticsearch-credentials.yaml > /dev/null + ./integration/scripts/integration-dynamic-backends-brig-index.sh --elasticsearch-server http://localhost:9200 --elasticsearch-credentials ./services/brig/test/resources/elasticsearch-credentials.yaml > /dev/null @@ -312,9 +312,9 @@ db-migrate: c ./dist/gundeck-schema --keyspace gundeck_test2 --replication-factor 1 > /dev/null ./dist/spar-schema --keyspace spar_test2 --replication-factor 1 > /dev/null ./integration/scripts/integration-dynamic-backends-db-schemas.sh --replication-factor 1 > /dev/null - ./dist/brig-index reset --elasticsearch-index-prefix directory --elasticsearch-server http://localhost:9200 > /dev/null - ./dist/brig-index reset --elasticsearch-index-prefix directory2 --elasticsearch-server http://localhost:9200 > /dev/null - ./integration/scripts/integration-dynamic-backends-brig-index.sh --elasticsearch-server http://localhost:9200 > /dev/null + ./dist/brig-index reset --elasticsearch-index-prefix directory --elasticsearch-server http://localhost:9200 --elasticsearch-credentials ./services/brig/test/resources/elasticsearch-credentials.yaml > /dev/null + ./dist/brig-index reset --elasticsearch-index-prefix directory2 --elasticsearch-server http://localhost:9200 --elasticsearch-credentials ./services/brig/test/resources/elasticsearch-credentials.yaml > /dev/null + ./integration/scripts/integration-dynamic-backends-brig-index.sh --elasticsearch-server http://localhost:9200 --elasticsearch-credentials ./services/brig/test/resources/elasticsearch-credentials.yaml > /dev/null ################################# ## dependencies diff --git a/changelog.d/2-features/WPB-6717 b/changelog.d/2-features/WPB-6717 new file mode 100644 index 00000000000..6720334b245 --- /dev/null +++ b/changelog.d/2-features/WPB-6717 @@ -0,0 +1 @@ +Support for Elasticsearch password authentication diff --git a/charts/brig/templates/configmap.yaml b/charts/brig/templates/configmap.yaml index e128169dbe4..7065407f57c 100644 --- a/charts/brig/templates/configmap.yaml +++ b/charts/brig/templates/configmap.yaml @@ -38,6 +38,12 @@ data: {{- if .elasticsearch.additionalWriteIndex }} additionalWriteIndex: {{ .elasticsearch.additionalWriteIndex }} {{- end }} + {{- if $.Values.secrets.elasticsearch }} + credentials: /etc/wire/brig/secrets/elasticsearch-credentials.yaml + {{- end }} + {{- if $.Values.secrets.elasticsearchAdditional }} + additionalCredentials: /etc/wire/brig/secrets/elasticsearch-additional-credentials.yaml + {{- end }} cargohold: host: cargohold diff --git a/charts/brig/templates/secret.yaml b/charts/brig/templates/secret.yaml index a4e51228b60..c2359979f57 100644 --- a/charts/brig/templates/secret.yaml +++ b/charts/brig/templates/secret.yaml @@ -35,4 +35,10 @@ data: rabbitmqUsername: {{ .rabbitmq.username | b64enc | quote }} rabbitmqPassword: {{ .rabbitmq.password | b64enc | quote }} {{- end }} + {{- if .elasticsearch }} + elasticsearch-credentials.yaml: {{ .elasticsearch | toYaml | b64enc }} + {{- end }} + {{- if .elasticsearchAdditional }} + elasticsearch-additional-credentials.yaml: {{ .elasticsearchAdditional | toYaml | b64enc }} + {{- end }} {{- end }} diff --git a/charts/elasticsearch-ephemeral/templates/es.yaml b/charts/elasticsearch-ephemeral/templates/es.yaml index 79526560ad1..4a82cbd28bf 100644 --- a/charts/elasticsearch-ephemeral/templates/es.yaml +++ b/charts/elasticsearch-ephemeral/templates/es.yaml @@ -32,6 +32,11 @@ spec: value: "single-node" - name: "action.auto_create_index" value: ".watches,.triggered_watches,.watcher-history-*,pod-*,node-*" + - name: "xpack.security.enabled" + value: "true" + # setting the password here is ok, as this chart is only used for integration tests on CI + - name: "ELASTIC_PASSWORD" + value: "changeme" ports: - containerPort: 9200 name: http diff --git a/charts/elasticsearch-index/templates/create-index.yaml b/charts/elasticsearch-index/templates/create-index.yaml index 9e2c5fda798..19ddd6854e0 100644 --- a/charts/elasticsearch-index/templates/create-index.yaml +++ b/charts/elasticsearch-index/templates/create-index.yaml @@ -21,20 +21,34 @@ spec: chart: "{{.Chart.Name}}-{{.Chart.Version}}" spec: restartPolicy: OnFailure + {{- if hasKey .Values.secrets "elasticsearch" }} + volumes: + - name: elasticsearch-index-secrets + secret: + secretName: elasticsearch-index + {{- end }} initContainers: # Creates index in elasticsearch only when it doesn't exist. # Does nothing if the index exists. - name: brig-index-create image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - imagePullPolicy: {{ default "" .Values.imagePullPolicy | quote }} - {{- if eq (include "includeSecurityContext" .) "true" }} + {{- if hasKey .Values.secrets "elasticsearch" }} + volumeMounts: + - name: "elasticsearch-index-secrets" + mountPath: "/etc/wire/elasticsearch-index/secrets" + {{- end }} + {{- if eq (include "includeSecurityContext" .) "true" }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 12 }} - {{- end }} + {{- end }} args: - create - --elasticsearch-server - "http://{{ required "missing elasticsearch-index.elasticsearch.host!" .Values.elasticsearch.host }}:{{ .Values.elasticsearch.port }}" + {{- if hasKey .Values.secrets "elasticsearch" }} + - --elasticsearch-credentials + - "/etc/wire/elasticsearch-index/secrets/elasticsearch-credentials.yaml" + {{- end }} - --elasticsearch-index - "{{ or (.Values.elasticsearch.additionalWriteIndex) (.Values.elasticsearch.index) }}" - --elasticsearch-shards=5 @@ -48,13 +62,22 @@ spec: - name: brig-index-update-mapping image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" imagePullPolicy: {{ default "" .Values.imagePullPolicy | quote }} - {{- if eq (include "includeSecurityContext" .) "true" }} + {{- if hasKey .Values.secrets "elasticsearch" }} + volumeMounts: + - name: "elasticsearch-index-secrets" + mountPath: "/etc/wire/elasticsearch-index/secrets" + {{- end }} + {{- if eq (include "includeSecurityContext" .) "true" }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 12 }} - {{- end }} + {{- end }} args: - update-mapping - --elasticsearch-server - "http://{{ required "missing elasticsearch-index.elasticsearch.host!" .Values.elasticsearch.host }}:{{ .Values.elasticsearch.port }}" + {{- if hasKey .Values.secrets "elasticsearch" }} + - --elasticsearch-credentials + - "/etc/wire/elasticsearch-index/secrets/elasticsearch-credentials.yaml" + {{- end }} - --elasticsearch-index - "{{ or (.Values.elasticsearch.additionalWriteIndex) (.Values.elasticsearch.index) }}" diff --git a/charts/elasticsearch-index/templates/migrate-data.yaml b/charts/elasticsearch-index/templates/migrate-data.yaml index 3d54e1f51b8..b3cb21dae3a 100644 --- a/charts/elasticsearch-index/templates/migrate-data.yaml +++ b/charts/elasticsearch-index/templates/migrate-data.yaml @@ -31,6 +31,10 @@ spec: - migrate-data - --elasticsearch-server - "http://{{ required "missing elasticsearch-index.elasticsearch.host!" .Values.elasticsearch.host }}:{{ .Values.elasticsearch.port }}" + {{- if hasKey .Values.secrets "elasticsearch" }} + - --elasticsearch-credentials + - "/etc/wire/elasticsearch-index/secrets/elasticsearch-credentials.yaml" + {{- end }} - --elasticsearch-index - "{{ or (.Values.elasticsearch.additionalWriteIndex) (.Values.elasticsearch.index) }}" - --cassandra-host @@ -47,14 +51,23 @@ spec: - --tls-ca-certificate-file - /certs/{{- (include "tlsSecretRef" .Values | fromYaml).key }} {{- end }} - {{- if eq (include "useCassandraTLS" .Values) "true" }} volumeMounts: + {{- if hasKey .Values.secrets "elasticsearch" }} + - name: "elasticsearch-index-secrets" + mountPath: "/etc/wire/elasticsearch-index/secrets" + {{- end }} + {{- if eq (include "useCassandraTLS" .Values) "true" }} - name: elasticsearch-index-migrate-cassandra-client-ca mountPath: "/certs" {{- end }} - {{- if eq (include "useCassandraTLS" .Values) "true" }} volumes: + {{- if hasKey .Values.secrets "elasticsearch" }} + - name: elasticsearch-index-secrets + secret: + secretName: elasticsearch-index + {{- end }} + {{- if eq (include "useCassandraTLS" .Values) "true" }} - name: elasticsearch-index-migrate-cassandra-client-ca secret: secretName: {{ (include "tlsSecretRef" .Values | fromYaml).name }} - {{- end}} + {{- end}} diff --git a/charts/elasticsearch-index/templates/secret.yaml b/charts/elasticsearch-index/templates/secret.yaml new file mode 100644 index 00000000000..cda93a046bc --- /dev/null +++ b/charts/elasticsearch-index/templates/secret.yaml @@ -0,0 +1,20 @@ +{{- if hasKey .Values.secrets "elasticsearch" }} +apiVersion: v1 +kind: Secret +metadata: + name: elasticsearch-index + labels: + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-delete-policy": "before-hook-creation" +type: Opaque +data: + {{- with .Values.secrets }} + {{- if .elasticsearch }} + elasticsearch-credentials.yaml: {{ .elasticsearch | toYaml | b64enc }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/elasticsearch-index/values.yaml b/charts/elasticsearch-index/values.yaml index 93e8a97ef6f..876edd92e4a 100644 --- a/charts/elasticsearch-index/values.yaml +++ b/charts/elasticsearch-index/values.yaml @@ -30,3 +30,5 @@ podSecurityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault + +secrets: {} diff --git a/charts/integration/templates/integration-integration.yaml b/charts/integration/templates/integration-integration.yaml index 5199c03def4..5f6a98f7595 100644 --- a/charts/integration/templates/integration-integration.yaml +++ b/charts/integration/templates/integration-integration.yaml @@ -127,7 +127,7 @@ spec: --tls-ca-certificate-file /certs/{{- (include "tlsSecretRef" .Values.config | fromYaml).key }} {{ end }} - integration-dynamic-backends-brig-index.sh --elasticsearch-server http://{{ .Values.config.elasticsearch.host }}:9200 + integration-dynamic-backends-brig-index.sh --elasticsearch-server http://elastic:changeme@{{ .Values.config.elasticsearch.host }}:9200 integration-dynamic-backends-ses.sh {{ .Values.config.sesEndpointUrl }} integration-dynamic-backends-s3.sh {{ .Values.config.s3EndpointUrl }} {{- range $name, $dynamicBackend := .Values.config.dynamicBackends }} diff --git a/deploy/dockerephemeral/docker-compose.yaml b/deploy/dockerephemeral/docker-compose.yaml index 2ac1a1843e8..2ad299a6d19 100644 --- a/deploy/dockerephemeral/docker-compose.yaml +++ b/deploy/dockerephemeral/docker-compose.yaml @@ -159,10 +159,13 @@ services: elasticsearch: container_name: demo_wire_elasticsearch - #image: elasticsearch:5.6 - image: julialongtin/elasticsearch:0.0.9-amd64 - # https://hub.docker.com/_/elastic is deprecated, but 6.2.4 did not work without further changes. - # image: docker.elastic.co/elasticsearch/elasticsearch:6.2.4 + build: + context: . + dockerfile_inline: | + FROM julialongtin/elasticsearch:0.0.9-amd64 + RUN /usr/share/elasticsearch/bin/elasticsearch-plugin install x-pack -b + # this seems to be necessary to run X-Pack on Alpine (https://discuss.elastic.co/t/elasticsearch-failing-to-start-due-to-x-pack/85125/7) + RUN rm -rf /usr/share/elasticsearch/plugins/x-pack/platform/linux-x86_64 ulimits: nofile: soft: 65536 @@ -171,10 +174,9 @@ services: - "127.0.0.1:9200:9200" - "127.0.0.1:9300:9300" environment: + - "xpack.ml.enabled=false" + - "xpack.security.enabled=true" - "bootstrap.system_call_filter=false" -# ES_JVM_OPTIONS is reserved, so... -# what's present in the jvm.options file by default. -# - "JVM_OPTIONS_ES=-Xmx2g -Xms2g" - "JVM_OPTIONS_ES=-Xmx512m -Xms512m" - "discovery.type=single-node" networks: diff --git a/deploy/dockerephemeral/federation-v0/brig.yaml b/deploy/dockerephemeral/federation-v0/brig.yaml index 06dfefe80e3..6c2216b3c1a 100644 --- a/deploy/dockerephemeral/federation-v0/brig.yaml +++ b/deploy/dockerephemeral/federation-v0/brig.yaml @@ -10,7 +10,8 @@ cassandra: # filterNodesByDatacentre: datacenter1 elasticsearch: - url: http://demo_wire_elasticsearch:9200 + # FUTUREWORK: use separate ES v0 instance + url: http://elastic:changeme@demo_wire_elasticsearch:9200 index: directory_test rabbitmq: diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index 9b376c49e8c..857dee00806 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -853,3 +853,31 @@ accessible to services (and not the private key.) The corresponding Cassandra options are described in Cassandra's documentation: [client_encryption_options](https://cassandra.apache.org/doc/stable/cassandra/configuration/cass_yaml_file.html#client_encryption_options) + +## Configure Elasticsearch basic authentication + +When the Wire backend is configured to work against a custom Elasticsearch instance, it may be desired to enable basic authentication for the internal communication between the Wire backend and the ES instance. To do so the Elasticsearch credentials can be set in wire-server's secrets for `brig` and `elasticsearch-index` as follows: + +```yaml +brig: + secrets: + elasticsearch: + username: elastic + password: changeme + +elasticsearch-index: + secrets: + elasticsearch: + username: elastic + password: changeme +``` + +In some cases an additional Elasticsearch instance is needed (e.g. for index migrations). To configure credentials for the additional ES instance add the secret as follows: + +```yaml +brig: + secrets: + elasticsearchAdditional: + username: elastic + password: changeme +``` diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 437159b7263..d215f8efd6a 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -38,6 +38,10 @@ elasticsearch-index: name: "cassandra-jks-keystore" key: "ca.crt" {{- end }} + secrets: + elasticsearch: + username: "elastic" + password: "changeme" brig: replicaCount: 1 @@ -151,6 +155,12 @@ brig: rabbitmq: username: {{ .Values.rabbitmqUsername }} password: {{ .Values.rabbitmqPassword }} + elasticsearch: + username: "elastic" + password: "changeme" + elasticsearchAdditional: + username: "elastic" + password: "changeme" tests: enableFederationTests: true {{- if .Values.uploadXml }} diff --git a/libs/types-common/src/Data/Credentials.hs b/libs/types-common/src/Data/Credentials.hs new file mode 100644 index 00000000000..5423b574e7a --- /dev/null +++ b/libs/types-common/src/Data/Credentials.hs @@ -0,0 +1,31 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2024 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Data.Credentials where + +import Data.Aeson (FromJSON) +import Data.Text +import Imports + +-- | Generic credentials for authenticating a user. Usually used for deserializing from a secret yaml file. +data Credentials = Credentials + { username :: Text, + password :: Text + } + deriving stock (Generic) + +instance FromJSON Credentials diff --git a/libs/types-common/src/Util/Options.hs b/libs/types-common/src/Util/Options.hs index 74437b78a27..f9beac14583 100644 --- a/libs/types-common/src/Util/Options.hs +++ b/libs/types-common/src/Util/Options.hs @@ -79,14 +79,19 @@ urlPort u = do makeLenses ''AWSEndpoint newtype FilePathSecrets = FilePathSecrets FilePath - deriving (Eq, Show, FromJSON) + deriving (Eq, Show, FromJSON, IsString) -loadSecret :: FromJSON a => FilePathSecrets -> IO (Either String a) +initCredentials :: (MonadIO m, FromJSON a) => FilePathSecrets -> m a +initCredentials secretFile = do + dat <- loadSecret secretFile + pure $ either (\e -> error $ "Could not load secrets from " ++ show secretFile ++ ": " ++ e) id dat + +loadSecret :: (MonadIO m, FromJSON a) => FilePathSecrets -> m (Either String a) loadSecret (FilePathSecrets p) = do path <- canonicalizePath p exists <- doesFileExist path if exists - then over _Left show . decodeEither' <$> BS.readFile path + then liftIO $ over _Left show . decodeEither' <$> BS.readFile path else pure (Left "File doesn't exist") -- | Get configuration options from the command line or configuration file. diff --git a/libs/types-common/types-common.cabal b/libs/types-common/types-common.cabal index 0c13025aabb..dc15cfbc2e2 100644 --- a/libs/types-common/types-common.cabal +++ b/libs/types-common/types-common.cabal @@ -15,6 +15,7 @@ library exposed-modules: Data.Code Data.CommaSeparatedList + Data.Credentials Data.Domain Data.ETag Data.Handle diff --git a/services/brig/brig.integration.yaml b/services/brig/brig.integration.yaml index 71e83cf664c..a536c77626d 100644 --- a/services/brig/brig.integration.yaml +++ b/services/brig/brig.integration.yaml @@ -12,6 +12,8 @@ cassandra: elasticsearch: url: http://127.0.0.1:9200 index: directory_test + credentials: test/resources/elasticsearch-credentials.yaml + additionalCredentials: test/resources/elasticsearch-credentials.yaml rabbitmq: host: 127.0.0.1 diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 4467ad7c34f..b9f5a099cfc 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -96,7 +96,7 @@ import Bilge.IO import Bilge.RPC (HasRequestId (..)) import Brig.AWS qualified as AWS import Brig.Calling qualified as Calling -import Brig.Options (Opts, Settings) +import Brig.Options (Opts, Settings (..)) import Brig.Options qualified as Opt import Brig.Provider.Template import Brig.Queue.Stomp qualified as Stomp @@ -118,6 +118,7 @@ import Control.Lens hiding (index, (.=)) import Control.Monad.Catch import Control.Monad.Trans.Resource import Data.ByteString.Conversion +import Data.Credentials (Credentials (..)) import Data.Domain import Data.Metrics (Metrics) import Data.Metrics.Middleware qualified as Metrics @@ -128,7 +129,6 @@ import Data.Text.Encoding (encodeUtf8) import Data.Text.Encoding qualified as Text import Data.Text.IO qualified as Text import Data.Time.Clock -import Data.Yaml (FromJSON) import Database.Bloodhound qualified as ES import HTTP2.Client.Manager (Http2Manager, http2ManagerWithSSLCtx) import Imports @@ -259,6 +259,8 @@ newEnv o = do kpLock <- newMVar () rabbitChan <- traverse (Q.mkRabbitMqChannelMVar lgr) o.rabbitmq let allDisabledVersions = foldMap expandVersionExp (Opt.setDisabledAPIVersions sett) + mEsCreds <- for (Opt.credentials (Opt.elasticsearch o)) initCredentials + mEsAddCreds <- for (Opt.additionalCredentials (Opt.elasticsearch o)) initCredentials pure $! Env @@ -294,7 +296,7 @@ newEnv o = do _zauthEnv = zau, _digestMD5 = md5, _digestSHA256 = sha256, - _indexEnv = mkIndexEnv o lgr mgr mtr (Opt.galley o), + _indexEnv = mkIndexEnv o lgr mgr mtr mEsCreds mEsAddCreds (Opt.galley o), _randomPrekeyLocalLock = prekeyLocalLock, _keyPackageLocalLock = kpLock, _rabbitmqChannel = rabbitChan, @@ -313,14 +315,16 @@ newEnv o = do pure (Nothing, Just smtp) mkEndpoint service = RPC.host (encodeUtf8 (service ^. host)) . RPC.port (service ^. port) $ RPC.empty -mkIndexEnv :: Opts -> Logger -> Manager -> Metrics -> Endpoint -> IndexEnv -mkIndexEnv o lgr mgr mtr galleyEp = - let bhe = ES.mkBHEnv (ES.Server (Opt.url (Opt.elasticsearch o))) mgr +mkIndexEnv :: Opts -> Logger -> Manager -> Metrics -> Maybe Credentials -> Maybe Credentials -> Endpoint -> IndexEnv +mkIndexEnv o lgr mgr mtr mCreds mAddCreds galleyEp = + let mkBhe url mcs = + let bhe = ES.mkBHEnv (ES.Server url) mgr + in maybe bhe (\creds -> bhe {ES.bhRequestHook = ES.basicAuthHook (ES.EsUsername creds.username) (ES.EsPassword creds.password)}) mcs lgr' = Log.clone (Just "index.brig") lgr mainIndex = ES.IndexName $ Opt.index (Opt.elasticsearch o) additionalIndex = ES.IndexName <$> Opt.additionalWriteIndex (Opt.elasticsearch o) - additionalBhe = flip ES.mkBHEnv mgr . ES.Server <$> Opt.additionalWriteIndexUrl (Opt.elasticsearch o) - in IndexEnv mtr lgr' bhe Nothing mainIndex additionalIndex additionalBhe galleyEp mgr + additionalBhe = flip mkBhe mAddCreds <$> Opt.additionalWriteIndexUrl (Opt.elasticsearch o) + in IndexEnv mtr lgr' (mkBhe (Opt.url (Opt.elasticsearch o)) mCreds) Nothing mainIndex additionalIndex additionalBhe galleyEp mgr initZAuth :: Opts -> IO ZAuth.Env initZAuth o = do @@ -409,11 +413,6 @@ initCassandra o g = (Just schemaVersion) g -initCredentials :: (FromJSON a) => FilePathSecrets -> IO a -initCredentials secretFile = do - dat <- loadSecret secretFile - pure $ either (\e -> error $ "Could not load secrets from " ++ show secretFile ++ ": " ++ e) id dat - userTemplates :: (MonadReader Env m) => Maybe Locale -> m (Locale, UserTemplates) userTemplates l = forLocale l <$> view usrTemplates diff --git a/services/brig/src/Brig/Index/Eval.hs b/services/brig/src/Brig/Index/Eval.hs index ed412d8d0d2..8cae3079b05 100644 --- a/services/brig/src/Brig/Index/Eval.hs +++ b/services/brig/src/Brig/Index/Eval.hs @@ -32,37 +32,46 @@ import Control.Monad.Catch import Control.Retry import Data.Aeson (FromJSON) import Data.Aeson qualified as Aeson +import Data.Credentials (Credentials (..)) import Data.Metrics qualified as Metrics import Database.Bloodhound qualified as ES import Imports import Network.HTTP.Client as HTTP import System.Logger qualified as Log import System.Logger.Class (Logger, MonadLogger (..)) +import Util.Options (initCredentials) runCommand :: Logger -> Command -> IO () runCommand l = \case Create es galley -> do - e <- initIndex es galley + mCreds <- for (es ^. esCredentials) initCredentials + e <- initIndex es mCreds galley runIndexIO e $ createIndexIfNotPresent (mkCreateIndexSettings es) Reset es galley -> do - e <- initIndex es galley + mCreds <- for (es ^. esCredentials) initCredentials + e <- initIndex es mCreds galley runIndexIO e $ resetIndex (mkCreateIndexSettings es) Reindex es cas galley -> do - e <- initIndex es galley + mCreds <- for (es ^. esCredentials) initCredentials + e <- initIndex es mCreds galley c <- initDb cas runReindexIO e c reindexAll ReindexSameOrNewer es cas galley -> do - e <- initIndex es galley + mCreds <- for (es ^. esCredentials) initCredentials + e <- initIndex es mCreds galley c <- initDb cas runReindexIO e c reindexAllIfSameOrNewer - UpdateMapping esURI indexName galley -> do - e <- initIndex' esURI indexName galley + UpdateMapping esURI indexName mSecretPath galley -> do + mCreds <- for mSecretPath initCredentials + e <- initIndex' esURI indexName mCreds galley runIndexIO e updateMapping Migrate es cas galley -> do - migrate l es cas galley + mCreds <- for (es ^. esCredentials) initCredentials + migrate l mCreds es cas galley ReindexFromAnotherIndex reindexSettings -> do mgr <- newManager defaultManagerSettings - let bhEnv = initES (view reindexEsServer reindexSettings) mgr + mCreds <- for (view reindexCredentials reindexSettings) initCredentials + let bhEnv = initES (view reindexEsServer reindexSettings) mgr mCreds ES.runBH bhEnv $ do let src = view reindexSrcIndex reindexSettings dest = view reindexDestIndex reindexSettings @@ -85,22 +94,26 @@ runCommand l = \case waitForTaskToComplete @ES.ReindexResponse timeoutSeconds taskNodeId Log.info l $ Log.msg ("Finished reindexing" :: ByteString) where - initIndex es gly = - initIndex' (es ^. esServer) (es ^. esIndex) gly - initIndex' esURI indexName galleyEndpoint = do + initIndex es mCreds gly = + initIndex' (es ^. esServer) (es ^. esIndex) mCreds gly + + initIndex' esURI indexName mCreds galleyEndpoint = do mgr <- newManager defaultManagerSettings IndexEnv <$> Metrics.metrics <*> pure l - <*> pure (initES esURI mgr) + <*> pure (initES esURI mgr mCreds) <*> pure Nothing <*> pure indexName <*> pure Nothing <*> pure Nothing <*> pure galleyEndpoint <*> pure mgr - initES esURI mgr = - ES.mkBHEnv (toESServer esURI) mgr + + initES esURI mgr mCreds = + let env = ES.mkBHEnv (toESServer esURI) mgr + in maybe env (\(creds :: Credentials) -> env {ES.bhRequestHook = ES.basicAuthHook (ES.EsUsername creds.username) (ES.EsPassword creds.password)}) mCreds + initDb cas = defInitCassandra (toCassandraOpts cas) l waitForTaskToComplete :: forall a m. (ES.MonadBH m, MonadThrow m, FromJSON a) => Int -> ES.TaskNodeId -> m () diff --git a/services/brig/src/Brig/Index/Migrations.hs b/services/brig/src/Brig/Index/Migrations.hs index da7e78cc1a0..c0320fb7d5b 100644 --- a/services/brig/src/Brig/Index/Migrations.hs +++ b/services/brig/src/Brig/Index/Migrations.hs @@ -27,6 +27,7 @@ import Cassandra.Util (defInitCassandra) import Control.Lens (view, (^.)) import Control.Monad.Catch (MonadThrow, catchAll, finally, throwM) import Data.Aeson (Value, object, (.=)) +import Data.Credentials (Credentials (..)) import Data.Metrics qualified as Metrics import Data.Text qualified as Text import Database.Bloodhound qualified as ES @@ -37,9 +38,9 @@ import System.Logger.Class qualified as Log import System.Logger.Extended (runWithLogger) import Util.Options qualified as Options -migrate :: Logger -> Opts.ElasticSettings -> Opts.CassandraSettings -> Options.Endpoint -> IO () -migrate l es cas galleyEndpoint = do - env <- mkEnv l es cas galleyEndpoint +migrate :: Logger -> Maybe Credentials -> Opts.ElasticSettings -> Opts.CassandraSettings -> Options.Endpoint -> IO () +migrate l mCreds es cas galleyEndpoint = do + env <- mkEnv l mCreds es cas galleyEndpoint finally (go env `catchAll` logAndThrowAgain) (cleanup env) where go :: Env -> IO () @@ -74,10 +75,12 @@ indexMapping = ["migration_version" .= object ["index" .= True, "type" .= ("integer" :: Text)]] ] -mkEnv :: Logger -> Opts.ElasticSettings -> Opts.CassandraSettings -> Options.Endpoint -> IO Env -mkEnv l es cas galleyEndpoint = do +mkEnv :: Logger -> Maybe Credentials -> Opts.ElasticSettings -> Opts.CassandraSettings -> Options.Endpoint -> IO Env +mkEnv l mCreds es cas galleyEndpoint = do mgr <- HTTP.newManager HTTP.defaultManagerSettings - Env (ES.mkBHEnv (Opts.toESServer (es ^. Opts.esServer)) mgr) + let env = ES.mkBHEnv (Opts.toESServer (es ^. Opts.esServer)) mgr + let envWithAuth = maybe env (\(creds :: Credentials) -> env {ES.bhRequestHook = ES.basicAuthHook (ES.EsUsername creds.username) (ES.EsPassword creds.password)}) mCreds + Env envWithAuth <$> initCassandra <*> initLogger <*> Metrics.metrics diff --git a/services/brig/src/Brig/Index/Options.hs b/services/brig/src/Brig/Index/Options.hs index 89da5997cb4..c40dbb571a5 100644 --- a/services/brig/src/Brig/Index/Options.hs +++ b/services/brig/src/Brig/Index/Options.hs @@ -28,6 +28,7 @@ module Brig.Index.Options esIndexReplicas, esIndexRefreshInterval, esDeleteTemplate, + esCredentials, CassandraSettings, toCassandraOpts, cHost, @@ -44,6 +45,7 @@ module Brig.Index.Options reindexSrcIndex, reindexEsServer, reindexTimeoutSeconds, + reindexCredentials, ) where @@ -59,7 +61,7 @@ import Imports import Options.Applicative import URI.ByteString import URI.ByteString.QQ -import Util.Options (CassandraOpts (..), Endpoint (..)) +import Util.Options (CassandraOpts (..), Endpoint (..), FilePathSecrets) data Command = Create ElasticSettings Endpoint @@ -67,7 +69,7 @@ data Command | Reindex ElasticSettings CassandraSettings Endpoint | ReindexSameOrNewer ElasticSettings CassandraSettings Endpoint | -- | 'ElasticSettings' has shards and other settings that are not needed here. - UpdateMapping (URIRef Absolute) ES.IndexName Endpoint + UpdateMapping (URIRef Absolute) ES.IndexName (Maybe FilePathSecrets) Endpoint | Migrate ElasticSettings CassandraSettings Endpoint | ReindexFromAnotherIndex ReindexFromAnotherIndexSettings deriving (Show) @@ -78,7 +80,8 @@ data ElasticSettings = ElasticSettings _esIndexShardCount :: Int, _esIndexReplicas :: ES.ReplicaCount, _esIndexRefreshInterval :: NominalDiffTime, - _esDeleteTemplate :: Maybe ES.TemplateName + _esDeleteTemplate :: Maybe ES.TemplateName, + _esCredentials :: Maybe FilePathSecrets } deriving (Show) @@ -94,7 +97,8 @@ data ReindexFromAnotherIndexSettings = ReindexFromAnotherIndexSettings { _reindexEsServer :: URIRef Absolute, _reindexSrcIndex :: ES.IndexName, _reindexDestIndex :: ES.IndexName, - _reindexTimeoutSeconds :: Int + _reindexTimeoutSeconds :: Int, + _reindexCredentials :: Maybe FilePathSecrets } deriving (Show) @@ -130,7 +134,8 @@ localElasticSettings = _esIndexShardCount = 1, _esIndexReplicas = ES.ReplicaCount 1, _esIndexRefreshInterval = 1, - _esDeleteTemplate = Nothing + _esDeleteTemplate = Nothing, + _esCredentials = Nothing } localCassandraSettings :: CassandraSettings @@ -168,10 +173,12 @@ restrictedElasticSettingsParser = do <> value "directory" <> showDefault ) + mCreds <- credentialsPathParser pure $ localElasticSettings & esServer .~ server & esIndex .~ ES.IndexName (prefix <> "_test") + & esCredentials .~ mCreds indexNameParser :: Parser ES.IndexName indexNameParser = @@ -193,6 +200,7 @@ elasticSettingsParser = <*> indexReplicaCountParser <*> indexRefreshIntervalParser <*> templateParser + <*> credentialsPathParser where indexShardCountParser = option @@ -234,6 +242,16 @@ elasticSettingsParser = ) ) +credentialsPathParser :: Parser (Maybe FilePathSecrets) +credentialsPathParser = + optional + ( strOption + ( long "elasticsearch-credentials" + <> metavar "FILE" + <> help "Location of a file containing the Elasticsearch credentials" + ) + ) + cassandraSettingsParser :: Parser CassandraSettings cassandraSettingsParser = CassandraSettings @@ -293,6 +311,7 @@ reindexToAnotherIndexSettingsParser = <> value 600 <> showDefault ) + <*> credentialsPathParser galleyEndpointParser :: Parser Endpoint galleyEndpointParser = @@ -325,7 +344,7 @@ commandParser = <> command "update-mapping" ( info - (UpdateMapping <$> elasticServerParser <*> indexNameParser <*> galleyEndpointParser) + (UpdateMapping <$> elasticServerParser <*> indexNameParser <*> credentialsPathParser <*> galleyEndpointParser) (progDesc "Update mapping of the user index.") ) <> command diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index 22a553228d8..3d21ee2533d 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -90,7 +90,11 @@ data ElasticSearchOpts = ElasticSearchOpts -- new instace of ES. It is necessary to provide 'additionalWriteIndex' for -- this to be used. If this is 'Nothing' and 'additionalWriteIndex' is -- configured, the 'url' field will be used. - additionalWriteIndexUrl :: !(Maybe Text) + additionalWriteIndexUrl :: !(Maybe Text), + -- | Elasticsearch credentials + credentials :: !(Maybe FilePathSecrets), + -- | Credentials for additional ES index (maily used for migrations) + additionalCredentials :: !(Maybe FilePathSecrets) } deriving (Show, Generic) diff --git a/services/brig/test/integration/API/Search.hs b/services/brig/test/integration/API/Search.hs index ef5a530ba61..5c1b341bec1 100644 --- a/services/brig/test/integration/API/Search.hs +++ b/services/brig/test/integration/API/Search.hs @@ -766,10 +766,12 @@ deleteIndex opts name = do let indexName = ES.IndexName name void $ runBH opts $ ES.deleteIndex indexName -runBH :: MonadIO m => Opt.Opts -> ES.BH IO a -> m a -runBH opts = +runBH :: MonadIO m => Opt.Opts -> ES.BH m a -> m a +runBH opts action = do let esURL = opts ^. Opt.elasticsearchL . Opt.urlL - in liftIO . ES.withBH HTTP.defaultManagerSettings (ES.Server esURL) + mgr <- liftIO $ HTTP.newManager HTTP.defaultManagerSettings + let bEnv = mkBHEnv esURL mgr + ES.runBH bEnv action -- | This was copied from at Brig.User.Search.Index at commit 3242aa26 analysisSettings :: ES.Analysis diff --git a/services/brig/test/integration/API/Search/Util.hs b/services/brig/test/integration/API/Search/Util.hs index 8be0145a09c..10e738a0eab 100644 --- a/services/brig/test/integration/API/Search/Util.hs +++ b/services/brig/test/integration/API/Search/Util.hs @@ -27,7 +27,9 @@ import Data.Id import Data.Qualified (Qualified (..)) import Data.Range (Range) import Data.Text.Encoding (encodeUtf8) +import Database.Bloodhound qualified as ES import Imports +import Network.HTTP.Client qualified as HTTP import Test.Tasty.HUnit import Util import Wire.API.User @@ -147,3 +149,7 @@ executeTeamUserSearchWithMaybeState brig teamid self mbSearchText mRoleFilter mS HTTP.Manager -> ES.BHEnv +mkBHEnv url mgr = do + (ES.mkBHEnv (ES.Server url) mgr) {ES.bhRequestHook = ES.basicAuthHook (ES.EsUsername "elastic") (ES.EsPassword "changeme")} diff --git a/services/brig/test/integration/Index/Create.hs b/services/brig/test/integration/Index/Create.hs index b31dd74725e..398e2126806 100644 --- a/services/brig/test/integration/Index/Create.hs +++ b/services/brig/test/integration/Index/Create.hs @@ -17,6 +17,7 @@ module Index.Create where +import API.Search.Util (mkBHEnv) import Brig.Index.Eval qualified as IndexEval import Brig.Index.Options qualified as IndexOpts import Brig.Options (Opts (galley)) @@ -48,6 +49,7 @@ spec brigOpts = testCreateIndexWhenNotPresent :: BrigOpts.Opts -> Assertion testCreateIndexWhenNotPresent brigOpts = do let esURL = brigOpts ^. BrigOpts.elasticsearchL . BrigOpts.urlL + let mCreds = BrigOpts.credentials . BrigOpts.elasticsearch $ brigOpts case parseURI strictURIParserOptions (Text.encodeUtf8 esURL) of Left e -> fail $ "Invalid ES URL: " <> show esURL <> "\nerror: " <> show e Right esURI -> do @@ -62,9 +64,12 @@ testCreateIndexWhenNotPresent brigOpts = do & IndexOpts.esIndexReplicas .~ ES.ReplicaCount replicas & IndexOpts.esIndexShardCount .~ shards & IndexOpts.esIndexRefreshInterval .~ refreshInterval + & IndexOpts.esCredentials .~ mCreds devNullLogger <- Log.create (Log.Path "/dev/null") IndexEval.runCommand devNullLogger (IndexOpts.Create esSettings (galley brigOpts)) - ES.withBH HTTP.defaultManagerSettings (ES.Server esURL) $ do + mgr <- liftIO $ HTTP.newManager HTTP.defaultManagerSettings + let bEnv = (mkBHEnv esURL mgr) {ES.bhRequestHook = ES.basicAuthHook (ES.EsUsername "elastic") (ES.EsPassword "changeme")} + ES.runBH bEnv $ do indexExists <- ES.indexExists indexName lift $ assertBool "Index should exist" indexExists @@ -75,16 +80,19 @@ testCreateIndexWhenNotPresent brigOpts = do Right indexSettings -> do assertEqual "Shard count should be set" (ES.ShardCount replicas) (ES.indexShards . ES.sSummaryFixedSettings $ indexSettings) assertEqual "Replica count should be set" (ES.ReplicaCount replicas) (ES.indexReplicas . ES.sSummaryFixedSettings $ indexSettings) - assertEqual "Refresh internval should be set" [ES.RefreshInterval refreshInterval] (ES.sSummaryUpdateable indexSettings) + assertEqual "Refresh interval should be set" [ES.RefreshInterval refreshInterval] (ES.sSummaryUpdateable indexSettings) testCreateIndexWhenPresent :: BrigOpts.Opts -> Assertion testCreateIndexWhenPresent brigOpts = do let esURL = brigOpts ^. BrigOpts.elasticsearchL . BrigOpts.urlL + let mCreds = BrigOpts.credentials . BrigOpts.elasticsearch $ brigOpts case parseURI strictURIParserOptions (Text.encodeUtf8 esURL) of Left e -> fail $ "Invalid ES URL: " <> show esURL <> "\nerror: " <> show e Right esURI -> do indexName <- ES.IndexName . Text.pack <$> replicateM 20 (Random.randomRIO ('a', 'z')) - ES.withBH HTTP.defaultManagerSettings (ES.Server esURL) $ do + mgr <- liftIO $ HTTP.newManager HTTP.defaultManagerSettings + let bEnv = (mkBHEnv esURL mgr) {ES.bhRequestHook = ES.basicAuthHook (ES.EsUsername "elastic") (ES.EsPassword "changeme")} + ES.runBH bEnv $ do _ <- ES.createIndex (ES.IndexSettings (ES.ShardCount 1) (ES.ReplicaCount 1)) indexName indexExists <- ES.indexExists indexName lift $ @@ -99,9 +107,10 @@ testCreateIndexWhenPresent brigOpts = do & IndexOpts.esIndexReplicas .~ ES.ReplicaCount replicas & IndexOpts.esIndexShardCount .~ shards & IndexOpts.esIndexRefreshInterval .~ refreshInterval + & IndexOpts.esCredentials .~ mCreds devNullLogger <- Log.create (Log.Path "/dev/null") IndexEval.runCommand devNullLogger (IndexOpts.Create esSettings (galley brigOpts)) - ES.withBH HTTP.defaultManagerSettings (ES.Server esURL) $ do + ES.runBH bEnv $ do indexExists <- ES.indexExists indexName lift $ assertBool "Index should still exist" indexExists @@ -112,4 +121,4 @@ testCreateIndexWhenPresent brigOpts = do Right indexSettings -> do assertEqual "Shard count should not be updated" (ES.ShardCount 1) (ES.indexShards . ES.sSummaryFixedSettings $ indexSettings) assertEqual "Replica count should not be updated" (ES.ReplicaCount 1) (ES.indexReplicas . ES.sSummaryFixedSettings $ indexSettings) - assertEqual "Refresh internval should not be updated" [] (ES.sSummaryUpdateable indexSettings) + assertEqual "Refresh interval should not be updated" [] (ES.sSummaryUpdateable indexSettings) diff --git a/services/brig/test/resources/elasticsearch-credentials.yaml b/services/brig/test/resources/elasticsearch-credentials.yaml new file mode 100644 index 00000000000..c1865766f56 --- /dev/null +++ b/services/brig/test/resources/elasticsearch-credentials.yaml @@ -0,0 +1,2 @@ +username: "elastic" +password: "changeme"