Skip to content

Commit e538df4

Browse files
committed
Staging megamerge
4 parents f17b8d5 + afde807 + 4c98227 + c3a8cf2 commit e538df4

86 files changed

Lines changed: 2561 additions & 835 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ jobs:
249249
# Start share and it's dependencies in the background
250250
docker compose -f docker/docker-compose.yml up --wait
251251
252+
252253
# Run the transcript tests
253254
zsh ./transcripts/run-transcripts.zsh
254255

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ docker_staging_push: $(docker_server_release)
5353

5454
serve: $(installed_share)
5555
trap 'docker compose -f docker/docker-compose.yml down' EXIT INT TERM
56-
docker compose -f docker/docker-compose.yml up postgres redis &
57-
while ! ( pg_isready --host localhost -U postgres -p 5432 && redis-cli -p 6379 ping) do \
56+
docker compose -f docker/docker-compose.yml up postgres redis vault &
57+
while ! ( pg_isready --host localhost -U postgres -p 5432 && redis-cli -p 6379 ping && VAULT_ADDR=http://localhost:8200 vault status) do \
5858
echo "Waiting for postgres and redis..."; \
5959
sleep 1; \
6060
done;

app/Env.hs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ import Share.Utils.Servant.Cookies qualified as Cookies
2424
import Share.Web.Authentication (cookieSessionTTL)
2525
import Hasql.Pool qualified as Pool
2626
import Hasql.Pool.Config qualified as Pool
27-
import Network.URI (parseURI)
27+
import Network.URI (parseURI, uriScheme)
2828
import Servant.API qualified as Servant
29+
import Servant.Client qualified as ServantClient
2930
import System.Environment (lookupEnv)
3031
import System.Exit
3132
import System.Log.FastLogger qualified as FL
@@ -34,6 +35,10 @@ import System.Log.Raven.Transport.HttpConduit qualified as Sentry
3435
import System.Log.Raven.Types qualified as Sentry
3536
import Unison.Runtime.Interface as RT
3637
import Data.Time.Clock qualified as Time
38+
import Network.HTTP.Client.TLS qualified as TLS
39+
import Network.HTTP.Client qualified as HTTPClient
40+
import Vault qualified
41+
import Data.ByteString.Char8 qualified as BSC
3742

3843
withEnv :: (Env () -> IO a) -> IO a
3944
withEnv action = do
@@ -114,6 +119,35 @@ withEnv action = do
114119
pgConnectionPool <- Pool.acquire pgSettings
115120
timeCache <- FL.newTimeCache FL.simpleTimeFormat -- E.g. 05/Sep/2023:13:23:56 -0700
116121
sandboxedRuntime <- RT.startRuntime True RT.Persistent "share"
122+
123+
-- Vault setup
124+
unproxiedHttpClient <- TLS.newTlsManager
125+
vaultHost <- fromEnv "VAULT_HOST" parseBaseUrl
126+
userSecretsVaultMount <- fromEnv "USER_SECRETS_VAULT_MOUNT" ((fmap . fmap) Vault.SecretMount . nonEmptyTextParser "USER_SECRETS_VAULT_MOUNT")
127+
shareVaultToken <- fromEnv "VAULT_TOKEN" ((fmap . fmap) Vault.VaultToken . nonEmptyTextParser "VAULT_TOKEN")
128+
let vaultClientEnv = ServantClient.mkClientEnv unproxiedHttpClient vaultHost
129+
130+
131+
132+
proxiedHttpClient <- do
133+
if Deployment.onLocal
134+
then TLS.newTlsManager
135+
else do
136+
httpProxyHost <- fromEnv "SHARE_PROXY_HOST" (\proxyHost -> case parseURI proxyHost of
137+
Nothing -> pure $ Left "Invalid SHARE_PROXY_ADDRESS"
138+
Just uri -> if uriScheme uri == "http:" || uriScheme uri == "https:"
139+
then pure $ Right (BSC.pack proxyHost)
140+
else pure $ Left "SHARE_PROXY_ADDRESS must be http or https")
141+
httpProxyPort <- fromEnv "SHARE_PROXY_PORT" (pure . maybeToEither "Invalid SHARE_PROXY_PORT" . readMaybe)
142+
143+
-- http proxy setup
144+
let proxyOverride = HTTPClient.useProxy (HTTPClient.Proxy{HTTPClient.proxyHost = httpProxyHost, HTTPClient.proxyPort = httpProxyPort})
145+
let proxiedManagerSettings =
146+
TLS.tlsManagerSettings
147+
& HTTPClient.managerSetProxy proxyOverride
148+
TLS.newTlsManagerWith proxiedManagerSettings
149+
150+
-- Logging setup
117151
let ctx = ()
118152
-- We use a zero-width-space to separate log-lines on ingestion, this allows us to use newlines for
119153
-- formatting, but without affecting log-grouping.
@@ -122,6 +156,15 @@ withEnv action = do
122156
action $ Env {logger = (logger . (\msg -> zeroWidthSpace <> msg <> "\n")), ..}
123157
where
124158
readPort p = pure $ maybeToRight "SHARE_PORT was not a number" (readMaybe p)
159+
nonEmptyTextParser :: Text -> String -> IO (Either String Text)
160+
nonEmptyTextParser varName = \case
161+
"" -> pure . Left . Text.unpack $ "Expected a value for env var " <> varName <> ", but got an empty string"
162+
str -> pure . Right $ Text.pack str
163+
164+
parseBaseUrl :: String -> IO (Either String ServantClient.BaseUrl)
165+
parseBaseUrl str = do
166+
u <- ServantClient.parseBaseUrl str
167+
pure $ Right u
125168

126169
fromEnv :: String -> (String -> IO (Either String a)) -> IO a
127170
fromEnv var from = do

docker/docker-compose.yml

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,37 @@ services:
3232
ports:
3333
- "6379:6379"
3434

35+
vault:
36+
image: 'hashicorp/vault:1.19'
37+
container_name: vault
38+
healthcheck:
39+
test: ["CMD", "vault", "status"]
40+
interval: 3s
41+
timeout: 10s
42+
retries: 3
43+
ports:
44+
- "8200:8200"
45+
environment:
46+
VAULT_DEV_ROOT_TOKEN_ID: "sekrit"
47+
VAULT_KV_V1_MOUNT_PATH: "secret"
48+
VAULT_ADDR: "http://127.0.0.1:8200"
49+
cap_add:
50+
- IPC_LOCK
51+
# # Use kv version 1
52+
# command: server -dev
53+
3554
share:
3655
image: share-api
3756
container_name: share-api
3857
depends_on:
39-
- redis
40-
- postgres
58+
redis:
59+
condition: service_healthy
60+
postgres:
61+
condition: service_healthy
62+
vault:
63+
condition: service_healthy
64+
http-echo:
65+
condition: service_started
4166
healthcheck:
4267
test: ["CMD", "curl", "-f", "http://localhost:5424/health"]
4368
interval: 3s
@@ -53,10 +78,10 @@ services:
5378
- SHARE_SERVER_PORT=5424
5479
- SHARE_REDIS=redis://redis:6379
5580
- SHARE_POSTGRES=postgresql://postgres:sekrit@postgres:5432
56-
- SHARE_HMAC_KEY=hmac-key-test-key-test-key-test-
57-
- SHARE_EDDSA_KEY=eddsa-key-test-key-test-key-test
5881
- SHARE_POSTGRES_CONN_TTL=30
5982
- SHARE_POSTGRES_CONN_MAX=10
83+
- SHARE_HMAC_KEY=hmac-key-test-key-test-key-test-
84+
- SHARE_EDDSA_KEY=eddsa-key-test-key-test-key-test
6085
- SHARE_SHARE_UI_ORIGIN=http://localhost:1234
6186
- SHARE_CLOUD_UI_ORIGIN=http://localhost:5678
6287
- SHARE_HOMEPAGE_ORIGIN=http://localhost:1111
@@ -65,6 +90,9 @@ services:
6590
- SHARE_COMMIT=dev
6691
- SHARE_MAX_PARALLELISM_PER_DOWNLOAD_REQUEST=1
6792
- SHARE_MAX_PARALLELISM_PER_UPLOAD_REQUEST=5
93+
- VAULT_HOST=http://vault:8200/v1
94+
- VAULT_TOKEN=sekrit
95+
- USER_SECRETS_VAULT_MOUNT=secret # A default mount in dev vault
6896
- SHARE_ZENDESK_API_USER=invaliduser@example.com
6997
- SHARE_ZENDESK_API_TOKEN=bad-password
7098
- SHARE_GITHUB_CLIENTID=invalid
@@ -73,6 +101,17 @@ services:
73101
links:
74102
- redis
75103
- postgres
104+
- vault
105+
- http-echo
106+
107+
http-echo:
108+
image: 'mendhak/http-https-echo:36'
109+
container_name: http-echo
110+
environment:
111+
HTTP_PORT: 9999
112+
ECHO_BACK_TO_CLIENT: "false"
113+
ports:
114+
- "9999:9999"
76115

77116
# volumes:
78117
# postgresVolume:

local.env

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ export SHARE_LOG_LEVEL="DEBUG"
1919
export SHARE_COMMIT="dev"
2020
export SHARE_MAX_PARALLELISM_PER_DOWNLOAD_REQUEST="1"
2121
export SHARE_MAX_PARALLELISM_PER_UPLOAD_REQUEST="5"
22+
export VAULT_HOST="http://localhost:8200/v1"
23+
export VAULT_TOKEN="sekrit"
24+
export USER_SECRETS_VAULT_MOUNT="secret" # A default mount in dev vault
25+
# Proxies aren't used locally, but are required in staging and production
26+
# export SHARE_PROXY_HOST="http://localhost"
27+
# export SHARE_PROXY_PORT="9999"
2228

2329
# Placeholders, these features don't work on localhost.
2430
export SHARE_ZENDESK_API_USER="invaliduser@example.com"

share-api.cabal

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ library
3636
Share.BackgroundJobs.Search.DefinitionSync.Types
3737
Share.BackgroundJobs.SerializedEntitiesMigration.Queries
3838
Share.BackgroundJobs.SerializedEntitiesMigration.Worker
39+
Share.BackgroundJobs.Webhooks.Queries
40+
Share.BackgroundJobs.Webhooks.Types
41+
Share.BackgroundJobs.Webhooks.Worker
3942
Share.BackgroundJobs.Workers
4043
Share.Branch
4144
Share.Codebase
@@ -50,8 +53,10 @@ library
5053
Share.NamespaceDiffs
5154
Share.Notifications.API
5255
Share.Notifications.Impl
56+
Share.Notifications.Ops
5357
Share.Notifications.Queries
5458
Share.Notifications.Types
59+
Share.Notifications.Webhooks.Secrets
5560
Share.Postgres
5661
Share.Postgres.Admin
5762
Share.Postgres.Authorization.Queries

share-auth/src/Share/JWT/Types.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ instance (Applicative m) => JWT.VerificationKeyStore m (JWT.JWSHeader ()) payloa
233233

234234
-- | A newtype for JWTs which provides the appropriate encoding/decoding instances.
235235
newtype JWTParam = JWTParam JWT.SignedJWT
236+
deriving newtype (Eq)
236237
deriving (Show) via (Censored JWTParam)
237238

238239
instance ToHttpApiData JWTParam where

share-utils/package.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,15 @@ dependencies:
6969
- cryptonite
7070
- http-api-data
7171
- http-types
72+
- http-media
7273
- jose
7374
- memory
7475
- network-uri
7576
- pretty-simple
7677
- random
7778
- servant-auth
7879
- servant-server
80+
- servant-client
7981
- text
8082
- time
8183
- uuid

share-utils/share-utils.cabal

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ library
3333
Share.Utils.Servant.Cookies
3434
Share.Utils.Show
3535
Share.Utils.URI
36+
Vault
3637
other-modules:
3738
Paths_share_utils
3839
hs-source-dirs:
@@ -81,6 +82,7 @@ library
8182
, hasql
8283
, hasql-interpolate
8384
, http-api-data
85+
, http-media
8486
, http-types
8587
, jose
8688
, lens
@@ -89,6 +91,7 @@ library
8991
, pretty-simple
9092
, random
9193
, servant-auth
94+
, servant-client
9295
, servant-server
9396
, text
9497
, time

share-utils/src/Vault.hs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
{-# LANGUAGE DataKinds #-}
2+
{-# LANGUAGE DuplicateRecordFields #-}
3+
{-# LANGUAGE TypeOperators #-}
4+
5+
module Vault
6+
( deleteSecret,
7+
patchSecret,
8+
putSecret,
9+
fetchSecret,
10+
SecretMount (..),
11+
VaultToken (..),
12+
SecretPath (..),
13+
SecretRequest (..),
14+
SecretResponse (..),
15+
SecretData (..),
16+
SecretPatch (..),
17+
Options (..),
18+
)
19+
where
20+
21+
import Data.Aeson qualified as Aeson
22+
import Data.Map (Map)
23+
import Data.Text (Text)
24+
import GHC.Generics (Generic)
25+
import Network.HTTP.Media.MediaType qualified as MT
26+
import Servant
27+
import Servant.Client qualified as SC
28+
import Share.Utils.Show (Censored (..))
29+
30+
type RequiredHeader = Header' '[Required, Strict]
31+
32+
-- API definition
33+
type VaultAPI =
34+
(RequiredHeader "X-Vault-Token" VaultToken :> Capture "mount" SecretMount :> "data" :> Capture "path" SecretPath :> DeleteNoContent)
35+
:<|> (RequiredHeader "X-Vault-Token" VaultToken :> Capture "mount" SecretMount :> "data" :> Capture "path" SecretPath :> ReqBody '[JsonMergePatch] SecretPatch :> PatchNoContent)
36+
:<|> (RequiredHeader "X-Vault-Token" VaultToken :> Capture "mount" SecretMount :> "data" :> Capture "path" SecretPath :> ReqBody '[JSON] SecretRequest :> PutNoContent)
37+
:<|> (RequiredHeader "X-Vault-Token" VaultToken :> Capture "mount" SecretMount :> "data" :> Capture "path" SecretPath :> Get '[JSON] SecretResponse)
38+
39+
vaultApi :: Proxy VaultAPI
40+
vaultApi = Proxy
41+
42+
data JsonMergePatch
43+
44+
instance Accept JsonMergePatch where
45+
contentType _ = "application" MT.// "merge-patch+json"
46+
47+
newtype SecretMount = SecretMount Text
48+
deriving newtype (Show, Eq, Ord, FromHttpApiData, ToHttpApiData)
49+
50+
newtype SecretPath = SecretPath Text
51+
deriving newtype (Show, Eq, Ord, FromHttpApiData, ToHttpApiData)
52+
53+
newtype VaultToken = VaultToken Text
54+
deriving newtype (Eq, Ord, FromHttpApiData, ToHttpApiData)
55+
deriving (Show) via Censored VaultToken
56+
57+
deleteSecret :: VaultToken -> SecretMount -> SecretPath -> SC.ClientM NoContent
58+
patchSecret :: VaultToken -> SecretMount -> SecretPath -> SecretPatch -> SC.ClientM NoContent
59+
putSecret :: VaultToken -> SecretMount -> SecretPath -> SecretRequest -> SC.ClientM NoContent
60+
fetchSecret :: VaultToken -> SecretMount -> SecretPath -> SC.ClientM SecretResponse
61+
deleteSecret :<|> patchSecret :<|> putSecret :<|> fetchSecret = SC.client vaultApi
62+
63+
-- Data types
64+
65+
data SecretRequest = SecretRequest
66+
{ options :: Maybe Options,
67+
data_ :: Aeson.Value
68+
}
69+
deriving (Generic)
70+
71+
instance Aeson.ToJSON SecretRequest where
72+
toJSON (SecretRequest options data_) = Aeson.object ["options" Aeson..= options, "data" Aeson..= data_]
73+
74+
newtype Options = Options
75+
{ cas :: Int
76+
}
77+
deriving (Generic)
78+
79+
instance Aeson.ToJSON Options
80+
81+
newtype SecretResponse = SecretResponse
82+
{ -- yes, the data is nested under two separate "data" keys
83+
data_ :: SecretData
84+
}
85+
86+
newtype SecretData = SecretData {data_ :: Aeson.Value}
87+
88+
data SecretPatch = SecretPatch !(Maybe Options) !(Map Text (Maybe Text))
89+
90+
instance MimeRender JsonMergePatch SecretPatch where
91+
mimeRender _ (SecretPatch options data_) = Aeson.encode $ Aeson.object ["options" Aeson..= options, "data" Aeson..= data_]
92+
93+
instance Aeson.FromJSON SecretResponse where
94+
parseJSON = Aeson.withObject "SecretResponse" $ \o -> do
95+
data_ <- o Aeson..: "data"
96+
return $ SecretResponse {data_}
97+
98+
instance Aeson.FromJSON SecretData where
99+
parseJSON = Aeson.withObject "SecretData" $ \o -> do
100+
data_ <- o Aeson..: "data"
101+
return $ SecretData {data_}

0 commit comments

Comments
 (0)