Group configuration by component ownership
This commit is contained in:
parent
48a5b8a433
commit
67cc0cb802
27 changed files with 486 additions and 258 deletions
34
app/Main.hs
34
app/Main.hs
|
|
@ -24,34 +24,40 @@ main :: IO ()
|
||||||
main = do
|
main = do
|
||||||
hSetBuffering stdout LineBuffering
|
hSetBuffering stdout LineBuffering
|
||||||
cfg <- loadConfig
|
cfg <- loadConfig
|
||||||
|
let runtimeCfg = cfgRuntime cfg
|
||||||
|
httpCfg = cfgHttp cfg
|
||||||
|
postgresCfg = cfgPostgres cfg
|
||||||
|
redisCfg = cfgRedis cfg
|
||||||
|
s3Cfg = cfgS3 cfg
|
||||||
|
productCfg = cfgProduct cfg
|
||||||
retryStartup
|
retryStartup
|
||||||
(cfgStartupAttempts cfg)
|
(cfgStartupAttempts runtimeCfg)
|
||||||
(cfgStartupDelayMs cfg)
|
(cfgStartupDelayMs runtimeCfg)
|
||||||
"postgresql"
|
"postgresql"
|
||||||
(withDb cfg $ \db -> do
|
(withDb postgresCfg $ \db -> do
|
||||||
_ <- query_ db "SELECT 1" :: IO [Only Int]
|
_ <- query_ db "SELECT 1" :: IO [Only Int]
|
||||||
pure ())
|
pure ())
|
||||||
withDb cfg $ \db -> do
|
withDb postgresCfg $ \db -> do
|
||||||
schemaPath <- findSchema
|
schemaPath <- findSchema
|
||||||
retryStartup
|
retryStartup
|
||||||
(cfgStartupAttempts cfg)
|
(cfgStartupAttempts runtimeCfg)
|
||||||
(cfgStartupDelayMs cfg)
|
(cfgStartupDelayMs runtimeCfg)
|
||||||
"migration"
|
"migration"
|
||||||
(migrate db schemaPath cfg)
|
(migrate db schemaPath productCfg)
|
||||||
withRedis cfg $ \redis -> do
|
withRedis redisCfg $ \redis -> do
|
||||||
s3 <- newS3 cfg
|
s3 <- newS3 s3Cfg
|
||||||
let env = newEnv cfg db redis s3
|
let env = newEnv cfg db redis s3
|
||||||
withLinkedWorker (runRecovery env) $ do
|
withLinkedWorker (runRecovery env) $ do
|
||||||
let settings =
|
let settings =
|
||||||
setPort (cfgListenPort cfg) $
|
setPort (cfgListenPort httpCfg) $
|
||||||
setHost (readHost (cfgListenHost cfg)) defaultSettings
|
setHost (readHost (cfgListenHost httpCfg)) defaultSettings
|
||||||
putStrLn $
|
putStrLn $
|
||||||
"nuthatch listening on "
|
"nuthatch listening on "
|
||||||
<> cfgListenHost cfg
|
<> cfgListenHost httpCfg
|
||||||
<> ":"
|
<> ":"
|
||||||
<> show (cfgListenPort cfg)
|
<> show (cfgListenPort httpCfg)
|
||||||
runTLS
|
runTLS
|
||||||
(tlsSettings (cfgTlsCertPath cfg) (cfgTlsKeyPath cfg))
|
(tlsSettings (cfgTlsCertPath httpCfg) (cfgTlsKeyPath httpCfg))
|
||||||
settings
|
settings
|
||||||
(application env)
|
(application env)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,7 @@ library
|
||||||
Nuthatch.Mutation.Service
|
Nuthatch.Mutation.Service
|
||||||
Nuthatch.Oidc
|
Nuthatch.Oidc
|
||||||
Nuthatch.Paths
|
Nuthatch.Paths
|
||||||
|
Nuthatch.Product.Limits
|
||||||
Nuthatch.Protocol
|
Nuthatch.Protocol
|
||||||
Nuthatch.Redis
|
Nuthatch.Redis
|
||||||
Nuthatch.Recovery
|
Nuthatch.Recovery
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ module Nuthatch.App
|
||||||
|
|
||||||
import Nuthatch.Api (api)
|
import Nuthatch.Api (api)
|
||||||
import Nuthatch.App.Env
|
import Nuthatch.App.Env
|
||||||
|
import Nuthatch.Config (Config (..))
|
||||||
import Nuthatch.Http.Auth (authenticationHandler)
|
import Nuthatch.Http.Auth (authenticationHandler)
|
||||||
import Nuthatch.Http.Server (server, utf8Boundary)
|
import Nuthatch.Http.Server (server, utf8Boundary)
|
||||||
import Servant
|
import Servant
|
||||||
|
|
@ -15,5 +16,5 @@ application env =
|
||||||
utf8Boundary $
|
utf8Boundary $
|
||||||
serveWithContext
|
serveWithContext
|
||||||
api
|
api
|
||||||
(authenticationHandler (envConfig env) :. EmptyContext)
|
(authenticationHandler (cfgHttp (envConfig env)) :. EmptyContext)
|
||||||
(server env)
|
(server env)
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,10 @@ handleBrowseRequest env session selectedRoot selectedPrefix search continuation
|
||||||
let user = sessionUser session
|
let user = sessionUser session
|
||||||
root = maybe Personal id selectedRoot
|
root = maybe Personal id selectedRoot
|
||||||
prefix = maybe "" browsePrefixText selectedPrefix
|
prefix = maybe "" browsePrefixText selectedPrefix
|
||||||
liftIO (ensurePersonalPool (envDb env) (envConfig env) (suUserId user))
|
liftIO (ensurePersonalPool
|
||||||
|
(envDb env)
|
||||||
|
(cfgProduct (envConfig env))
|
||||||
|
(suUserId user))
|
||||||
result <- liftIO $ case search of
|
result <- liftIO $ case search of
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
listed <- listEntries env user root prefix
|
listed <- listEntries env user root prefix
|
||||||
|
|
@ -83,7 +86,7 @@ handleDeletePlanRequest env session root path continuation = do
|
||||||
objectPrefix
|
objectPrefix
|
||||||
Nothing
|
Nothing
|
||||||
(unPaginationToken <$> continuation)
|
(unPaginationToken <$> continuation)
|
||||||
(cfgDeletePageSize (envConfig env))
|
(cfgDeletePageSize (cfgProduct (envConfig env)))
|
||||||
page <- either
|
page <- either
|
||||||
(throwOperationError status502Error
|
(throwOperationError status502Error
|
||||||
. DeletePrefixMutationRejected
|
. DeletePrefixMutationRejected
|
||||||
|
|
@ -121,7 +124,7 @@ listEntries env user root prefix continuation = do
|
||||||
<> ":"
|
<> ":"
|
||||||
<> prefix
|
<> prefix
|
||||||
<> ":"
|
<> ":"
|
||||||
<> T.pack (show (cfgBrowsePageSize (envConfig env)))
|
<> T.pack (show (cfgBrowsePageSize (cfgProduct (envConfig env))))
|
||||||
<> ":"
|
<> ":"
|
||||||
<> maybe "unavailable" TE.decodeUtf8 generation
|
<> maybe "unavailable" TE.decodeUtf8 generation
|
||||||
s3Prefix =
|
s3Prefix =
|
||||||
|
|
@ -143,7 +146,7 @@ listEntries env user root prefix continuation = do
|
||||||
s3Prefix
|
s3Prefix
|
||||||
(Just "/")
|
(Just "/")
|
||||||
continuation
|
continuation
|
||||||
(cfgBrowsePageSize (envConfig env))
|
(cfgBrowsePageSize (cfgProduct (envConfig env)))
|
||||||
case page of
|
case page of
|
||||||
Left err -> pure (Left (renderS3Error err))
|
Left err -> pure (Left (renderS3Error err))
|
||||||
Right listed -> do
|
Right listed -> do
|
||||||
|
|
@ -181,7 +184,7 @@ listEntries env user root prefix continuation = do
|
||||||
cacheSet
|
cacheSet
|
||||||
(envRedis env)
|
(envRedis env)
|
||||||
cacheKey
|
cacheKey
|
||||||
(cfgListCacheTtlSec (envConfig env))
|
(cfgListCacheTtlSec (cfgProduct (envConfig env)))
|
||||||
(BL.toStrict (A.encode result))
|
(BL.toStrict (A.encode result))
|
||||||
pure (Right result)
|
pure (Right result)
|
||||||
|
|
||||||
|
|
@ -199,11 +202,11 @@ searchEntries env user root query = do
|
||||||
needle = T.toCaseFold query
|
needle = T.toCaseFold query
|
||||||
started <- getMonotonicTimeNSec
|
started <- getMonotonicTimeNSec
|
||||||
let deadline =
|
let deadline =
|
||||||
started + fromInteger (cfgSearchTimeoutMs (envConfig env) * 1000000)
|
started + fromInteger (cfgSearchTimeoutMs (cfgProduct (envConfig env)) * 1000000)
|
||||||
keysResult <- collectKeysBounded
|
keysResult <- collectKeysBounded
|
||||||
env
|
env
|
||||||
s3Prefix
|
s3Prefix
|
||||||
(cfgSearchMaxKeys (envConfig env))
|
(cfgSearchMaxKeys (cfgProduct (envConfig env)))
|
||||||
deadline
|
deadline
|
||||||
pure $ case keysResult of
|
pure $ case keysResult of
|
||||||
Left err -> Left err
|
Left err -> Left err
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,13 @@
|
||||||
module Nuthatch.Config
|
module Nuthatch.Config
|
||||||
( Config (..)
|
( Config (..)
|
||||||
|
, RuntimeConfig (..)
|
||||||
|
, HttpConfig (..)
|
||||||
|
, OidcConfig (..)
|
||||||
|
, PostgresConfig (..)
|
||||||
|
, RedisConfig (..)
|
||||||
|
, S3Config (..)
|
||||||
|
, ProductConfig (..)
|
||||||
|
, RecoveryConfig (..)
|
||||||
, Secret
|
, Secret
|
||||||
, secret
|
, secret
|
||||||
, revealSecret
|
, revealSecret
|
||||||
|
|
@ -17,7 +25,7 @@ import Data.Text (Text)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Data.Text.Encoding as TE
|
import qualified Data.Text.Encoding as TE
|
||||||
import qualified Env
|
import qualified Env
|
||||||
import Nuthatch.Streaming (maximumMultipartBytes)
|
import Nuthatch.Product.Limits (maximumObjectBytes)
|
||||||
import qualified Text.URI as URI
|
import qualified Text.URI as URI
|
||||||
|
|
||||||
newtype Secret value = Secret value
|
newtype Secret value = Secret value
|
||||||
|
|
@ -29,6 +37,22 @@ revealSecret :: Secret value -> value
|
||||||
revealSecret (Secret value) = value
|
revealSecret (Secret value) = value
|
||||||
|
|
||||||
data Config = Config
|
data Config = Config
|
||||||
|
{ cfgRuntime :: RuntimeConfig
|
||||||
|
, cfgHttp :: HttpConfig
|
||||||
|
, cfgOidc :: OidcConfig
|
||||||
|
, cfgPostgres :: PostgresConfig
|
||||||
|
, cfgRedis :: RedisConfig
|
||||||
|
, cfgS3 :: S3Config
|
||||||
|
, cfgProduct :: ProductConfig
|
||||||
|
, cfgRecovery :: RecoveryConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
data RuntimeConfig = RuntimeConfig
|
||||||
|
{ cfgStartupAttempts :: Int
|
||||||
|
, cfgStartupDelayMs :: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
data HttpConfig = HttpConfig
|
||||||
{ cfgListenHost :: String
|
{ cfgListenHost :: String
|
||||||
, cfgListenPort :: Int
|
, cfgListenPort :: Int
|
||||||
, cfgTlsCertPath :: FilePath
|
, cfgTlsCertPath :: FilePath
|
||||||
|
|
@ -39,41 +63,59 @@ data Config = Config
|
||||||
, cfgCookieName :: Text
|
, cfgCookieName :: Text
|
||||||
, cfgCsrfCookieName :: Text
|
, cfgCsrfCookieName :: Text
|
||||||
, cfgOidcTransactionCookieName :: Text
|
, cfgOidcTransactionCookieName :: Text
|
||||||
, cfgOidcIssuer :: Text
|
}
|
||||||
|
|
||||||
|
data OidcConfig = OidcConfig
|
||||||
|
{ cfgOidcIssuer :: Text
|
||||||
, cfgOidcClientId :: Text
|
, cfgOidcClientId :: Text
|
||||||
, cfgOidcAdditionalAudiences :: [Text]
|
, cfgOidcAdditionalAudiences :: [Text]
|
||||||
, cfgOidcClientSecret :: Secret Text
|
, cfgOidcClientSecret :: Secret Text
|
||||||
, cfgOidcRedirectUri :: Text
|
, cfgOidcRedirectUri :: Text
|
||||||
, cfgOidcTransactionMinutes :: Integer
|
, cfgOidcTransactionMinutes :: Integer
|
||||||
, cfgOidcMaxIdTokenSeconds :: Integer
|
, cfgOidcMaxIdTokenSeconds :: Integer
|
||||||
, cfgPgConn :: Secret String
|
, cfgOidcTransactionSecret :: Secret Text
|
||||||
, cfgRedisHost :: String
|
}
|
||||||
|
|
||||||
|
data PostgresConfig = PostgresConfig
|
||||||
|
{ cfgPgConn :: Secret String
|
||||||
|
}
|
||||||
|
|
||||||
|
data RedisConfig = RedisConfig
|
||||||
|
{ cfgRedisHost :: String
|
||||||
, cfgRedisPort :: Int
|
, cfgRedisPort :: Int
|
||||||
, cfgRedisDb :: Integer
|
, cfgRedisDb :: Integer
|
||||||
, cfgS3Endpoint :: Text
|
, cfgThrottleBytesPerSec :: Integer
|
||||||
|
, cfgThrottleBurstBytes :: Integer
|
||||||
|
}
|
||||||
|
|
||||||
|
data S3Config = S3Config
|
||||||
|
{ cfgS3Endpoint :: Text
|
||||||
, cfgS3Region :: Text
|
, cfgS3Region :: Text
|
||||||
, cfgS3Bucket :: Text
|
, cfgS3Bucket :: Text
|
||||||
, cfgS3AccessKey :: Secret Text
|
, cfgS3AccessKey :: Secret Text
|
||||||
, cfgS3SecretKey :: Secret Text
|
, cfgS3SecretKey :: Secret Text
|
||||||
, cfgS3PathStyle :: Bool
|
, cfgS3PathStyle :: Bool
|
||||||
, cfgPersonalQuotaBytes :: Integer
|
, cfgS3RequestTimeoutSec :: Integer
|
||||||
|
}
|
||||||
|
|
||||||
|
data ProductConfig = ProductConfig
|
||||||
|
{ cfgPersonalQuotaBytes :: Integer
|
||||||
, cfgSharedQuotaBytes :: Integer
|
, cfgSharedQuotaBytes :: Integer
|
||||||
, cfgMaxObjectBytes :: Integer
|
, cfgMaxObjectBytes :: Integer
|
||||||
, cfgThrottleBytesPerSec :: Integer
|
|
||||||
, cfgThrottleBurstBytes :: Integer
|
|
||||||
, cfgListCacheTtlSec :: Integer
|
, cfgListCacheTtlSec :: Integer
|
||||||
, cfgBrowsePageSize :: Int
|
, cfgBrowsePageSize :: Int
|
||||||
, cfgSearchMaxKeys :: Int
|
, cfgSearchMaxKeys :: Int
|
||||||
, cfgSearchTimeoutMs :: Integer
|
, cfgSearchTimeoutMs :: Integer
|
||||||
, cfgDeletePageSize :: Int
|
, cfgDeletePageSize :: Int
|
||||||
, cfgRecoveryGraceSec :: Integer
|
|
||||||
, cfgIdempotencyWindowSec :: Integer
|
, cfgIdempotencyWindowSec :: Integer
|
||||||
, cfgObjectRequestTimeoutSec :: Integer
|
|
||||||
, cfgStartupAttempts :: Int
|
|
||||||
, cfgStartupDelayMs :: Int
|
|
||||||
, cfgAuditObjectKeys :: Bool
|
, cfgAuditObjectKeys :: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data RecoveryConfig = RecoveryConfig
|
||||||
|
{ cfgRecoveryGraceSec :: Integer
|
||||||
|
, cfgObjectRequestTimeoutSec :: Integer
|
||||||
|
}
|
||||||
|
|
||||||
loadConfig :: IO Config
|
loadConfig :: IO Config
|
||||||
loadConfig = do
|
loadConfig = do
|
||||||
listen <- envString "NUTHATCH_LISTEN" "127.0.0.1:8080"
|
listen <- envString "NUTHATCH_LISTEN" "127.0.0.1:8080"
|
||||||
|
|
@ -131,7 +173,7 @@ loadConfig = do
|
||||||
maxObject <- envInteger "NUTHATCH_MAX_OBJECT_BYTES" (100 * 1024 * 1024)
|
maxObject <- envInteger "NUTHATCH_MAX_OBJECT_BYTES" (100 * 1024 * 1024)
|
||||||
throttleRate <- envInteger "NUTHATCH_THROTTLE_BYTES_PER_SEC" (10 * 1024 * 1024)
|
throttleRate <- envInteger "NUTHATCH_THROTTLE_BYTES_PER_SEC" (10 * 1024 * 1024)
|
||||||
throttleBurst <- envInteger "NUTHATCH_THROTTLE_BURST_BYTES" (20 * 1024 * 1024)
|
throttleBurst <- envInteger "NUTHATCH_THROTTLE_BURST_BYTES" (20 * 1024 * 1024)
|
||||||
when (maxObject <= 0 || maxObject > maximumMultipartBytes) $
|
when (maxObject <= 0 || maxObject > maximumObjectBytes) $
|
||||||
fail "NUTHATCH_MAX_OBJECT_BYTES exceeds the supported streaming range"
|
fail "NUTHATCH_MAX_OBJECT_BYTES exceeds the supported streaming range"
|
||||||
when (throttleRate <= 0 || throttleRate > luaExactIntegerMax) $
|
when (throttleRate <= 0 || throttleRate > luaExactIntegerMax) $
|
||||||
fail "NUTHATCH_THROTTLE_BYTES_PER_SEC exceeds the exact throttle range"
|
fail "NUTHATCH_THROTTLE_BYTES_PER_SEC exceeds the exact throttle range"
|
||||||
|
|
@ -167,6 +209,11 @@ loadConfig = do
|
||||||
when (searchTimeoutMs <= 0 || searchTimeoutMs > 60000) $
|
when (searchTimeoutMs <= 0 || searchTimeoutMs > 60000) $
|
||||||
fail "NUTHATCH_SEARCH_TIMEOUT_MS must be between 1 and 60000"
|
fail "NUTHATCH_SEARCH_TIMEOUT_MS must be between 1 and 60000"
|
||||||
pure Config
|
pure Config
|
||||||
|
{ cfgRuntime = RuntimeConfig
|
||||||
|
{ cfgStartupAttempts = startupAttempts
|
||||||
|
, cfgStartupDelayMs = startupDelayMs
|
||||||
|
}
|
||||||
|
, cfgHttp = HttpConfig
|
||||||
{ cfgListenHost = host
|
{ cfgListenHost = host
|
||||||
, cfgListenPort = port
|
, cfgListenPort = port
|
||||||
, cfgTlsCertPath = tlsCertPath
|
, cfgTlsCertPath = tlsCertPath
|
||||||
|
|
@ -177,40 +224,53 @@ loadConfig = do
|
||||||
, cfgCookieName = cookieName
|
, cfgCookieName = cookieName
|
||||||
, cfgCsrfCookieName = csrfCookie
|
, cfgCsrfCookieName = csrfCookie
|
||||||
, cfgOidcTransactionCookieName = transactionCookie
|
, cfgOidcTransactionCookieName = transactionCookie
|
||||||
, cfgOidcIssuer = oidcIssuer
|
}
|
||||||
|
, cfgOidc = OidcConfig
|
||||||
|
{ cfgOidcIssuer = oidcIssuer
|
||||||
, cfgOidcClientId = oidcClientId
|
, cfgOidcClientId = oidcClientId
|
||||||
, cfgOidcAdditionalAudiences = oidcAdditionalAudiences
|
, cfgOidcAdditionalAudiences = oidcAdditionalAudiences
|
||||||
, cfgOidcClientSecret = oidcClientSecret
|
, cfgOidcClientSecret = oidcClientSecret
|
||||||
, cfgOidcRedirectUri = oidcRedirectUri
|
, cfgOidcRedirectUri = oidcRedirectUri
|
||||||
, cfgOidcTransactionMinutes = oidcTransactionMinutes
|
, cfgOidcTransactionMinutes = oidcTransactionMinutes
|
||||||
, cfgOidcMaxIdTokenSeconds = oidcMaxIdTokenSeconds
|
, cfgOidcMaxIdTokenSeconds = oidcMaxIdTokenSeconds
|
||||||
, cfgPgConn = pgConn
|
, cfgOidcTransactionSecret = jwtSecret
|
||||||
, cfgRedisHost = redisHost
|
}
|
||||||
|
, cfgPostgres = PostgresConfig
|
||||||
|
{ cfgPgConn = pgConn
|
||||||
|
}
|
||||||
|
, cfgRedis = RedisConfig
|
||||||
|
{ cfgRedisHost = redisHost
|
||||||
, cfgRedisPort = redisPort
|
, cfgRedisPort = redisPort
|
||||||
, cfgRedisDb = redisDb
|
, cfgRedisDb = redisDb
|
||||||
, cfgS3Endpoint = s3Endpoint
|
, cfgThrottleBytesPerSec = throttleRate
|
||||||
|
, cfgThrottleBurstBytes = throttleBurst
|
||||||
|
}
|
||||||
|
, cfgS3 = S3Config
|
||||||
|
{ cfgS3Endpoint = s3Endpoint
|
||||||
, cfgS3Region = s3Region
|
, cfgS3Region = s3Region
|
||||||
, cfgS3Bucket = s3Bucket
|
, cfgS3Bucket = s3Bucket
|
||||||
, cfgS3AccessKey = s3Access
|
, cfgS3AccessKey = s3Access
|
||||||
, cfgS3SecretKey = s3Secret
|
, cfgS3SecretKey = s3Secret
|
||||||
, cfgS3PathStyle = pathStyle
|
, cfgS3PathStyle = pathStyle
|
||||||
, cfgPersonalQuotaBytes = personalQuota
|
, cfgS3RequestTimeoutSec = objectRequestTimeout
|
||||||
|
}
|
||||||
|
, cfgProduct = ProductConfig
|
||||||
|
{ cfgPersonalQuotaBytes = personalQuota
|
||||||
, cfgSharedQuotaBytes = sharedQuota
|
, cfgSharedQuotaBytes = sharedQuota
|
||||||
, cfgMaxObjectBytes = maxObject
|
, cfgMaxObjectBytes = maxObject
|
||||||
, cfgThrottleBytesPerSec = throttleRate
|
|
||||||
, cfgThrottleBurstBytes = throttleBurst
|
|
||||||
, cfgListCacheTtlSec = listTtl
|
, cfgListCacheTtlSec = listTtl
|
||||||
, cfgBrowsePageSize = browsePageSize
|
, cfgBrowsePageSize = browsePageSize
|
||||||
, cfgSearchMaxKeys = searchMax
|
, cfgSearchMaxKeys = searchMax
|
||||||
, cfgSearchTimeoutMs = searchTimeoutMs
|
, cfgSearchTimeoutMs = searchTimeoutMs
|
||||||
, cfgDeletePageSize = deletePageSize
|
, cfgDeletePageSize = deletePageSize
|
||||||
, cfgRecoveryGraceSec = recoveryGrace
|
|
||||||
, cfgIdempotencyWindowSec = idempotencyWindow
|
, cfgIdempotencyWindowSec = idempotencyWindow
|
||||||
, cfgObjectRequestTimeoutSec = objectRequestTimeout
|
|
||||||
, cfgStartupAttempts = startupAttempts
|
|
||||||
, cfgStartupDelayMs = startupDelayMs
|
|
||||||
, cfgAuditObjectKeys = auditObjectKeys
|
, cfgAuditObjectKeys = auditObjectKeys
|
||||||
}
|
}
|
||||||
|
, cfgRecovery = RecoveryConfig
|
||||||
|
{ cfgRecoveryGraceSec = recoveryGrace
|
||||||
|
, cfgObjectRequestTimeoutSec = objectRequestTimeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
luaExactIntegerMax :: Integer
|
luaExactIntegerMax :: Integer
|
||||||
luaExactIntegerMax = 9007199254740991
|
luaExactIntegerMax = 9007199254740991
|
||||||
|
|
|
||||||
|
|
@ -37,12 +37,16 @@ import Database.PostgreSQL.Simple.Migration
|
||||||
, runMigration
|
, runMigration
|
||||||
)
|
)
|
||||||
import qualified Database.PostgreSQL.Simple.Transaction as PGT
|
import qualified Database.PostgreSQL.Simple.Transaction as PGT
|
||||||
import Nuthatch.Config (Config (..), revealSecret)
|
import Nuthatch.Config
|
||||||
|
( PostgresConfig (..)
|
||||||
|
, ProductConfig (..)
|
||||||
|
, revealSecret
|
||||||
|
)
|
||||||
import Nuthatch.Types
|
import Nuthatch.Types
|
||||||
|
|
||||||
type Db = Pool Connection
|
type Db = Pool Connection
|
||||||
|
|
||||||
withDb :: Config -> (Db -> IO a) -> IO a
|
withDb :: PostgresConfig -> (Db -> IO a) -> IO a
|
||||||
withDb cfg action = do
|
withDb cfg action = do
|
||||||
let mk = connectPostgreSQL (BS8.pack (revealSecret (cfgPgConn cfg)))
|
let mk = connectPostgreSQL (BS8.pack (revealSecret (cfgPgConn cfg)))
|
||||||
poolCfg =
|
poolCfg =
|
||||||
|
|
@ -50,7 +54,7 @@ withDb cfg action = do
|
||||||
defaultPoolConfig mk close 30 10
|
defaultPoolConfig mk close 30 10
|
||||||
bracket (newPool poolCfg) destroyAllResources action
|
bracket (newPool poolCfg) destroyAllResources action
|
||||||
|
|
||||||
migrate :: Db -> FilePath -> Config -> IO ()
|
migrate :: Db -> FilePath -> ProductConfig -> IO ()
|
||||||
migrate db migrationDirectory cfg = withResource db $ \conn -> do
|
migrate db migrationDirectory cfg = withResource db $ \conn -> do
|
||||||
void
|
void
|
||||||
(PG.query_ conn
|
(PG.query_ conn
|
||||||
|
|
@ -72,7 +76,7 @@ migrate db migrationDirectory cfg = withResource db $ \conn -> do
|
||||||
configurePoolLimitConnection conn sharedPool (cfgSharedQuotaBytes cfg)
|
configurePoolLimitConnection conn sharedPool (cfgSharedQuotaBytes cfg)
|
||||||
MigrationError name -> throwIO (userError ("migration failed: " <> name))
|
MigrationError name -> throwIO (userError ("migration failed: " <> name))
|
||||||
|
|
||||||
ensurePersonalPool :: Db -> Config -> UserId -> IO ()
|
ensurePersonalPool :: Db -> ProductConfig -> UserId -> IO ()
|
||||||
ensurePersonalPool db cfg uid =
|
ensurePersonalPool db cfg uid =
|
||||||
configurePoolLimit db (personalPool uid) (cfgPersonalQuotaBytes cfg)
|
configurePoolLimit db (personalPool uid) (cfgPersonalQuotaBytes cfg)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ import Web.Cookie
|
||||||
, setCookieValue
|
, setCookieValue
|
||||||
)
|
)
|
||||||
|
|
||||||
authenticationHandler :: Config -> AuthHandler Request SessionContext
|
authenticationHandler :: HttpConfig -> AuthHandler Request SessionContext
|
||||||
authenticationHandler cfg = mkAuthHandler $ \request -> do
|
authenticationHandler cfg = mkAuthHandler $ \request -> do
|
||||||
let cookies = requestCookies request
|
let cookies = requestCookies request
|
||||||
sessionName = TE.encodeUtf8 (cfgCookieName cfg)
|
sessionName = TE.encodeUtf8 (cfgCookieName cfg)
|
||||||
|
|
@ -90,8 +90,10 @@ authenticationHandler cfg = mkAuthHandler $ \request -> do
|
||||||
|
|
||||||
handleLoginRequest :: Env -> Handler LoginStart
|
handleLoginRequest :: Env -> Handler LoginStart
|
||||||
handleLoginRequest env = do
|
handleLoginRequest env = do
|
||||||
let cfg = envConfig env
|
let config = envConfig env
|
||||||
authorizationResult <- liftIO (beginAuthorization cfg)
|
httpCfg = cfgHttp config
|
||||||
|
oidcCfg = cfgOidc config
|
||||||
|
authorizationResult <- liftIO (beginAuthorization oidcCfg)
|
||||||
authorization <- case authorizationResult of
|
authorization <- case authorizationResult of
|
||||||
Left _ -> do
|
Left _ -> do
|
||||||
liftIO $ audit "login_start" "rejected" Nothing
|
liftIO $ audit "login_start" "rejected" Nothing
|
||||||
|
|
@ -102,7 +104,7 @@ handleLoginRequest env = do
|
||||||
pure $
|
pure $
|
||||||
addHeader
|
addHeader
|
||||||
(cookieValue
|
(cookieValue
|
||||||
(cfgOidcTransactionCookieName cfg)
|
(cfgOidcTransactionCookieName httpCfg)
|
||||||
(authorizationTransaction authorization)
|
(authorizationTransaction authorization)
|
||||||
"/oidc/callback"
|
"/oidc/callback"
|
||||||
True) $
|
True) $
|
||||||
|
|
@ -115,26 +117,30 @@ handleOidcCallback
|
||||||
-> Text
|
-> Text
|
||||||
-> Handler LoginSuccess
|
-> Handler LoginSuccess
|
||||||
handleOidcCallback env code state cookieHeader = do
|
handleOidcCallback env code state cookieHeader = do
|
||||||
let cfg = envConfig env
|
let config = envConfig env
|
||||||
|
httpCfg = cfgHttp config
|
||||||
|
oidcCfg = cfgOidc config
|
||||||
|
productCfg = cfgProduct config
|
||||||
cookies = parseCookies (TE.encodeUtf8 cookieHeader)
|
cookies = parseCookies (TE.encodeUtf8 cookieHeader)
|
||||||
transactionName = TE.encodeUtf8 (cfgOidcTransactionCookieName cfg)
|
transactionName = TE.encodeUtf8 (cfgOidcTransactionCookieName httpCfg)
|
||||||
transactionBytes <- case lookup transactionName cookies of
|
transactionBytes <- case lookup transactionName cookies of
|
||||||
Nothing -> rejectCallback "missing_transaction"
|
Nothing -> rejectCallback "missing_transaction"
|
||||||
Just value -> pure value
|
Just value -> pure value
|
||||||
transaction <- case TE.decodeUtf8' transactionBytes of
|
transaction <- case TE.decodeUtf8' transactionBytes of
|
||||||
Right value | not (T.null value) && T.length value <= 8192 -> pure value
|
Right value | not (T.null value) && T.length value <= 8192 -> pure value
|
||||||
_ -> rejectCallback "malformed_transaction"
|
_ -> rejectCallback "malformed_transaction"
|
||||||
identityResult <- liftIO (redeemAuthorizationCode cfg transaction state code)
|
identityResult <- liftIO
|
||||||
|
(redeemAuthorizationCode oidcCfg transaction state code)
|
||||||
identity <- case identityResult of
|
identity <- case identityResult of
|
||||||
Left reason -> do
|
Left reason -> do
|
||||||
liftIO $ audit "login" "rejected" Nothing [("reason", A.String reason)]
|
liftIO $ audit "login" "rejected" Nothing [("reason", A.String reason)]
|
||||||
throwOperationError err400 OidcCallbackRejected
|
throwOperationError err400 OidcCallbackRejected
|
||||||
Right accepted -> pure accepted
|
Right accepted -> pure accepted
|
||||||
let uid = encodeUserId (identityIssuer identity) (identitySubject identity)
|
let uid = encodeUserId (identityIssuer identity) (identitySubject identity)
|
||||||
liftIO (ensurePersonalPool (envDb env) cfg uid)
|
liftIO (ensurePersonalPool (envDb env) productCfg uid)
|
||||||
tokenResult <- liftIO $
|
tokenResult <- liftIO $
|
||||||
mintSession
|
mintSession
|
||||||
cfg
|
httpCfg
|
||||||
(identityIssuer identity)
|
(identityIssuer identity)
|
||||||
(identitySubject identity)
|
(identitySubject identity)
|
||||||
(identityDisplay identity)
|
(identityDisplay identity)
|
||||||
|
|
@ -148,14 +154,14 @@ handleOidcCallback env code state cookieHeader = do
|
||||||
tokenResult
|
tokenResult
|
||||||
csrf <- liftIO newCsrfToken
|
csrf <- liftIO newCsrfToken
|
||||||
liftIO $ audit "login" "ok" (Just (unUserId uid)) []
|
liftIO $ audit "login" "ok" (Just (unUserId uid)) []
|
||||||
pure (loginResponse cfg token csrf)
|
pure (loginResponse httpCfg token csrf)
|
||||||
where
|
where
|
||||||
rejectCallback reason = do
|
rejectCallback reason = do
|
||||||
liftIO $ audit "login" "rejected" Nothing
|
liftIO $ audit "login" "rejected" Nothing
|
||||||
[("reason", A.String reason)]
|
[("reason", A.String reason)]
|
||||||
throwOperationError err400 OidcCallbackRejected
|
throwOperationError err400 OidcCallbackRejected
|
||||||
|
|
||||||
loginResponse :: Config -> SessionToken -> CsrfToken -> LoginSuccess
|
loginResponse :: HttpConfig -> SessionToken -> CsrfToken -> LoginSuccess
|
||||||
loginResponse cfg token csrf =
|
loginResponse cfg token csrf =
|
||||||
addHeader (cookieValue (cfgCookieName cfg) (unSessionToken token) "/" True) $
|
addHeader (cookieValue (cfgCookieName cfg) (unSessionToken token) "/" True) $
|
||||||
addHeader (cookieValue (cfgCsrfCookieName cfg) (unCsrfToken csrf) "/" False) $
|
addHeader (cookieValue (cfgCsrfCookieName cfg) (unCsrfToken csrf) "/" False) $
|
||||||
|
|
@ -172,7 +178,7 @@ handleLogoutRequest
|
||||||
-> Handler LogoutSuccess
|
-> Handler LogoutSuccess
|
||||||
handleLogoutRequest env session providedCsrf = do
|
handleLogoutRequest env session providedCsrf = do
|
||||||
requireCsrf "logout" LogoutCsrfRejected session providedCsrf
|
requireCsrf "logout" LogoutCsrfRejected session providedCsrf
|
||||||
let cfg = envConfig env
|
let cfg = cfgHttp (envConfig env)
|
||||||
liftIO $ audit "logout" "ok"
|
liftIO $ audit "logout" "ok"
|
||||||
(Just (unUserId (suUserId (sessionUser session)))) []
|
(Just (unUserId (suUserId (sessionUser session)))) []
|
||||||
pure $
|
pure $
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,7 @@ handleDeleteRequest env session request = do
|
||||||
([ ("root", A.toJSON (rootLabel root))
|
([ ("root", A.toJSON (rootLabel root))
|
||||||
, ("operation_id", A.toJSON (unOperationId operation))
|
, ("operation_id", A.toJSON (unOperationId operation))
|
||||||
, ("reason", A.String "database_unavailable")
|
, ("reason", A.String "database_unavailable")
|
||||||
] <> objectKeyAudit (envConfig env) (deletePath request))
|
] <> objectKeyAudit (cfgProduct (envConfig env)) (deletePath request))
|
||||||
throwOperationError err503
|
throwOperationError err503
|
||||||
(DeleteMutationRejected "database unavailable")
|
(DeleteMutationRejected "database unavailable")
|
||||||
Right value -> pure value
|
Right value -> pure value
|
||||||
|
|
@ -179,7 +179,7 @@ handleDeleteRequest env session request = do
|
||||||
([ ("root", A.toJSON (rootLabel root))
|
([ ("root", A.toJSON (rootLabel root))
|
||||||
, ("operation_id", A.toJSON (unOperationId operation))
|
, ("operation_id", A.toJSON (unOperationId operation))
|
||||||
, ("reason", A.String "object_store_unavailable")
|
, ("reason", A.String "object_store_unavailable")
|
||||||
] <> objectKeyAudit (envConfig env) (deletePath request))
|
] <> objectKeyAudit (cfgProduct (envConfig env)) (deletePath request))
|
||||||
throwOperationError err409 (DeleteMutationRejected reason)
|
throwOperationError err409 (DeleteMutationRejected reason)
|
||||||
Right value -> pure value
|
Right value -> pure value
|
||||||
liftIO $ audit "delete" (mutationAuditResult result)
|
liftIO $ audit "delete" (mutationAuditResult result)
|
||||||
|
|
@ -187,12 +187,12 @@ handleDeleteRequest env session request = do
|
||||||
([ ("root", A.toJSON (rootLabel root))
|
([ ("root", A.toJSON (rootLabel root))
|
||||||
, ("operation_id", A.toJSON (unOperationId operation))
|
, ("operation_id", A.toJSON (unOperationId operation))
|
||||||
] <> mutationReasonFields result
|
] <> mutationReasonFields result
|
||||||
<> objectKeyAudit (envConfig env) (deletePath request))
|
<> objectKeyAudit (cfgProduct (envConfig env)) (deletePath request))
|
||||||
pure DeleteResponse
|
pure DeleteResponse
|
||||||
{ deleteResponseOperationId = operation
|
{ deleteResponseOperationId = operation
|
||||||
, deleteResponseResult = result
|
, deleteResponseResult = result
|
||||||
, deleteResponseIdempotencyWindowSeconds =
|
, deleteResponseIdempotencyWindowSeconds =
|
||||||
cfgIdempotencyWindowSec (envConfig env)
|
cfgIdempotencyWindowSec (cfgProduct (envConfig env))
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteBatchRequest
|
handleDeleteBatchRequest
|
||||||
|
|
@ -207,7 +207,7 @@ handleDeleteBatchRequest env session request = do
|
||||||
session
|
session
|
||||||
(deleteBatchCsrf request)
|
(deleteBatchCsrf request)
|
||||||
let items = deleteBatchItems request
|
let items = deleteBatchItems request
|
||||||
when (null items || length items > cfgDeletePageSize (envConfig env)) $ do
|
when (null items || length items > cfgDeletePageSize (cfgProduct (envConfig env))) $ do
|
||||||
liftIO $ audit "delete_batch" "rejected"
|
liftIO $ audit "delete_batch" "rejected"
|
||||||
(Just (unUserId (suUserId (sessionUser session))))
|
(Just (unUserId (suUserId (sessionUser session))))
|
||||||
[ ("root", A.toJSON (rootLabel (deleteBatchRoot request)))
|
[ ("root", A.toJSON (rootLabel (deleteBatchRoot request)))
|
||||||
|
|
@ -239,7 +239,7 @@ handleDeleteBatchRequest env session request = do
|
||||||
(const [("reason", A.String failureReason)])
|
(const [("reason", A.String failureReason)])
|
||||||
mutationReasonFields
|
mutationReasonFields
|
||||||
outcome
|
outcome
|
||||||
<> objectKeyAudit (envConfig env) (deleteBatchPath item))
|
<> objectKeyAudit (cfgProduct (envConfig env)) (deleteBatchPath item))
|
||||||
pure $ case outcome of
|
pure $ case outcome of
|
||||||
Left message -> DeleteItemResponse
|
Left message -> DeleteItemResponse
|
||||||
{ deleteItemPath = deleteBatchPath item
|
{ deleteItemPath = deleteBatchPath item
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ mutationReasonFields MutationRejected =
|
||||||
mutationReasonFields MutationConflict =
|
mutationReasonFields MutationConflict =
|
||||||
[("reason", A.String "operation_conflict")]
|
[("reason", A.String "operation_conflict")]
|
||||||
|
|
||||||
objectKeyAudit :: Config -> RelativePath -> [(Text, A.Value)]
|
objectKeyAudit :: ProductConfig -> RelativePath -> [(Text, A.Value)]
|
||||||
objectKeyAudit cfg =
|
objectKeyAudit cfg =
|
||||||
objectKeyFields (cfgAuditObjectKeys cfg) . unRelativePath
|
objectKeyFields (cfgAuditObjectKeys cfg) . unRelativePath
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ runRecovery env = forever $ do
|
||||||
|
|
||||||
recoverOnce :: Env -> IO ()
|
recoverOnce :: Env -> IO ()
|
||||||
recoverOnce env = do
|
recoverOnce env = do
|
||||||
let grace = cfgRecoveryGraceSec (envConfig env)
|
let grace = cfgRecoveryGraceSec (cfgRecovery (envConfig env))
|
||||||
batch <- withTransaction (envDb env) $ \conn -> claimActiveBatch conn 20 grace
|
batch <- withTransaction (envDb env) $ \conn -> claimActiveBatch conn 20 grace
|
||||||
mapConcurrently_ (recoverOne env) batch
|
mapConcurrently_ (recoverOne env) batch
|
||||||
|
|
||||||
|
|
@ -69,7 +69,7 @@ finalizeRecovered env record = do
|
||||||
, ("operation_id", A.toJSON (unOperationId (mrOperationId record)))
|
, ("operation_id", A.toJSON (unOperationId (mrOperationId record)))
|
||||||
] <> mutationReasonFields mutationResult
|
] <> mutationReasonFields mutationResult
|
||||||
<> objectKeyAudit
|
<> objectKeyAudit
|
||||||
(envConfig env)
|
(cfgProduct (envConfig env))
|
||||||
(mrRelativePath record))
|
(mrRelativePath record))
|
||||||
when (result == Just MutationSucceeded) $
|
when (result == Just MutationSucceeded) $
|
||||||
invalidateMutationCaches
|
invalidateMutationCaches
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,10 @@ runPut
|
||||||
-> (ObjectKey -> IO (Either PutFailure ()))
|
-> (ObjectKey -> IO (Either PutFailure ()))
|
||||||
-> IO (Either PutFailure MutationResult)
|
-> IO (Either PutFailure MutationResult)
|
||||||
runPut env user root rel op size sha dispatchPut = do
|
runPut env user root rel op size sha dispatchPut = do
|
||||||
ensurePersonalPool (envDb env) (envConfig env) (suUserId user)
|
ensurePersonalPool
|
||||||
|
(envDb env)
|
||||||
|
(cfgProduct (envConfig env))
|
||||||
|
(suUserId user)
|
||||||
let uid = suUserId user
|
let uid = suUserId user
|
||||||
key = buildObjectKey root uid rel
|
key = buildObjectKey root uid rel
|
||||||
pool = case root of
|
pool = case root of
|
||||||
|
|
@ -53,7 +56,7 @@ runPut env user root rel op size sha dispatchPut = do
|
||||||
Shared -> sharedPool
|
Shared -> sharedPool
|
||||||
admitted <- withTransaction (envDb env) $ \conn -> do
|
admitted <- withTransaction (envDb env) $ \conn -> do
|
||||||
existing <- mutationEpochExists
|
existing <- mutationEpochExists
|
||||||
conn uid Put op (cfgIdempotencyWindowSec (envConfig env))
|
conn uid Put op (cfgIdempotencyWindowSec (cfgProduct (envConfig env)))
|
||||||
available <-
|
available <-
|
||||||
if existing then pure True else objectStoreAvailable (envS3 env)
|
if existing then pure True else objectStoreAvailable (envS3 env)
|
||||||
if available
|
if available
|
||||||
|
|
@ -67,9 +70,9 @@ runPut env user root rel op size sha dispatchPut = do
|
||||||
MutationFingerprint Put key size (Just sha)
|
MutationFingerprint Put key size (Just sha)
|
||||||
}
|
}
|
||||||
AdmissionLimits
|
AdmissionLimits
|
||||||
{ admissionMaxObjectBytes = cfgMaxObjectBytes (envConfig env)
|
{ admissionMaxObjectBytes = cfgMaxObjectBytes (cfgProduct (envConfig env))
|
||||||
, admissionIdempotencyWindowSec =
|
, admissionIdempotencyWindowSec =
|
||||||
cfgIdempotencyWindowSec (envConfig env)
|
cfgIdempotencyWindowSec (cfgProduct (envConfig env))
|
||||||
}
|
}
|
||||||
else pure Nothing
|
else pure Nothing
|
||||||
case admitted of
|
case admitted of
|
||||||
|
|
@ -102,7 +105,10 @@ runDelete
|
||||||
-> OperationId
|
-> OperationId
|
||||||
-> IO (Either Text MutationResult)
|
-> IO (Either Text MutationResult)
|
||||||
runDelete env user root rel op = do
|
runDelete env user root rel op = do
|
||||||
ensurePersonalPool (envDb env) (envConfig env) (suUserId user)
|
ensurePersonalPool
|
||||||
|
(envDb env)
|
||||||
|
(cfgProduct (envConfig env))
|
||||||
|
(suUserId user)
|
||||||
let uid = suUserId user
|
let uid = suUserId user
|
||||||
key = buildObjectKey root uid rel
|
key = buildObjectKey root uid rel
|
||||||
pool = case root of
|
pool = case root of
|
||||||
|
|
@ -110,7 +116,7 @@ runDelete env user root rel op = do
|
||||||
Shared -> sharedPool
|
Shared -> sharedPool
|
||||||
admitted <- withTransaction (envDb env) $ \conn -> do
|
admitted <- withTransaction (envDb env) $ \conn -> do
|
||||||
existing <- mutationEpochExists
|
existing <- mutationEpochExists
|
||||||
conn uid Delete op (cfgIdempotencyWindowSec (envConfig env))
|
conn uid Delete op (cfgIdempotencyWindowSec (cfgProduct (envConfig env)))
|
||||||
available <-
|
available <-
|
||||||
if existing then pure True else objectStoreAvailable (envS3 env)
|
if existing then pure True else objectStoreAvailable (envS3 env)
|
||||||
if available
|
if available
|
||||||
|
|
@ -124,9 +130,9 @@ runDelete env user root rel op = do
|
||||||
MutationFingerprint Delete key 0 Nothing
|
MutationFingerprint Delete key 0 Nothing
|
||||||
}
|
}
|
||||||
AdmissionLimits
|
AdmissionLimits
|
||||||
{ admissionMaxObjectBytes = cfgMaxObjectBytes (envConfig env)
|
{ admissionMaxObjectBytes = cfgMaxObjectBytes (cfgProduct (envConfig env))
|
||||||
, admissionIdempotencyWindowSec =
|
, admissionIdempotencyWindowSec =
|
||||||
cfgIdempotencyWindowSec (envConfig env)
|
cfgIdempotencyWindowSec (cfgProduct (envConfig env))
|
||||||
}
|
}
|
||||||
else pure Nothing
|
else pure Nothing
|
||||||
case admitted of
|
case admitted of
|
||||||
|
|
@ -156,7 +162,7 @@ dispatchAndFinalize
|
||||||
-> IO (Either failure ())
|
-> IO (Either failure ())
|
||||||
-> IO (Either failure MutationResult)
|
-> IO (Either failure MutationResult)
|
||||||
dispatchAndFinalize env user root record timeoutFailure dispatch = do
|
dispatchAndFinalize env user root record timeoutFailure dispatch = do
|
||||||
let cfg = envConfig env
|
let cfg = cfgRecovery (envConfig env)
|
||||||
requestTimeoutSec = cfgObjectRequestTimeoutSec cfg
|
requestTimeoutSec = cfgObjectRequestTimeoutSec cfg
|
||||||
prepared <- withTransaction (envDb env) $ \conn -> do
|
prepared <- withTransaction (envDb env) $ \conn -> do
|
||||||
moved <- markApplying conn record
|
moved <- markApplying conn record
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ import Network.HTTP.Simple
|
||||||
, setRequestMethod
|
, setRequestMethod
|
||||||
)
|
)
|
||||||
import Network.HTTP.Types.URI (renderQuery)
|
import Network.HTTP.Types.URI (renderQuery)
|
||||||
import Nuthatch.Config (Config (..), revealSecret)
|
import Nuthatch.Config (OidcConfig (..), revealSecret)
|
||||||
import System.Entropy (getEntropy)
|
import System.Entropy (getEntropy)
|
||||||
|
|
||||||
data AuthorizationRequest = AuthorizationRequest
|
data AuthorizationRequest = AuthorizationRequest
|
||||||
|
|
@ -186,7 +186,7 @@ instance A.ToJSON IdTokenClaims where
|
||||||
, ("preferred_username", maybe A.Null A.String (idTokenPreferred claims))
|
, ("preferred_username", maybe A.Null A.String (idTokenPreferred claims))
|
||||||
]
|
]
|
||||||
|
|
||||||
beginAuthorization :: Config -> IO (Either Text AuthorizationRequest)
|
beginAuthorization :: OidcConfig -> IO (Either Text AuthorizationRequest)
|
||||||
beginAuthorization cfg = do
|
beginAuthorization cfg = do
|
||||||
discoveryResult <- fetchDiscovery cfg
|
discoveryResult <- fetchDiscovery cfg
|
||||||
case discoveryResult of
|
case discoveryResult of
|
||||||
|
|
@ -220,7 +220,7 @@ beginAuthorization cfg = do
|
||||||
}
|
}
|
||||||
|
|
||||||
redeemAuthorizationCode
|
redeemAuthorizationCode
|
||||||
:: Config
|
:: OidcConfig
|
||||||
-> Text
|
-> Text
|
||||||
-> Text
|
-> Text
|
||||||
-> Text
|
-> Text
|
||||||
|
|
@ -255,7 +255,7 @@ redeemAuthorizationCode cfg transactionToken returnedState code = do
|
||||||
(transactionNonce transaction)
|
(transactionNonce transaction)
|
||||||
idToken
|
idToken
|
||||||
|
|
||||||
fetchDiscovery :: Config -> IO (Either Text Discovery)
|
fetchDiscovery :: OidcConfig -> IO (Either Text Discovery)
|
||||||
fetchDiscovery cfg = catchOidcHttp "OIDC discovery request failed" $ do
|
fetchDiscovery cfg = catchOidcHttp "OIDC discovery request failed" $ do
|
||||||
request <- parseRequest $
|
request <- parseRequest $
|
||||||
T.unpack (cfgOidcIssuer cfg <> "/.well-known/openid-configuration")
|
T.unpack (cfgOidcIssuer cfg <> "/.well-known/openid-configuration")
|
||||||
|
|
@ -270,7 +270,7 @@ fetchDiscovery cfg = catchOidcHttp "OIDC discovery request failed" $ do
|
||||||
| otherwise -> Left "OIDC discovery issuer mismatch"
|
| otherwise -> Left "OIDC discovery issuer mismatch"
|
||||||
|
|
||||||
exchangeCode
|
exchangeCode
|
||||||
:: Config
|
:: OidcConfig
|
||||||
-> Discovery
|
-> Discovery
|
||||||
-> TransactionClaims
|
-> TransactionClaims
|
||||||
-> Text
|
-> Text
|
||||||
|
|
@ -299,7 +299,7 @@ exchangeCode cfg discovery transaction code = do
|
||||||
Right tokenResponse -> Right (tokenResponseIdToken tokenResponse)
|
Right tokenResponse -> Right (tokenResponseIdToken tokenResponse)
|
||||||
|
|
||||||
fetchAndVerifyIdToken
|
fetchAndVerifyIdToken
|
||||||
:: Config
|
:: OidcConfig
|
||||||
-> Discovery
|
-> Discovery
|
||||||
-> Text
|
-> Text
|
||||||
-> Text
|
-> Text
|
||||||
|
|
@ -317,7 +317,7 @@ fetchAndVerifyIdToken cfg discovery expectedNonce compact = do
|
||||||
verifyIdToken cfg jwks now expectedNonce compact
|
verifyIdToken cfg jwks now expectedNonce compact
|
||||||
|
|
||||||
verifyIdToken
|
verifyIdToken
|
||||||
:: Config
|
:: OidcConfig
|
||||||
-> JWKSet
|
-> JWKSet
|
||||||
-> UTCTime
|
-> UTCTime
|
||||||
-> Text
|
-> Text
|
||||||
|
|
@ -384,7 +384,7 @@ verifyIdToken cfg jwks now expectedNonce compact = do
|
||||||
}
|
}
|
||||||
|
|
||||||
mintTransaction
|
mintTransaction
|
||||||
:: Config
|
:: OidcConfig
|
||||||
-> Text
|
-> Text
|
||||||
-> Text
|
-> Text
|
||||||
-> Text
|
-> Text
|
||||||
|
|
@ -410,16 +410,16 @@ mintTransaction cfg state nonce verifier = do
|
||||||
, transactionRedirectUri = cfgOidcRedirectUri cfg
|
, transactionRedirectUri = cfgOidcRedirectUri cfg
|
||||||
, transactionReturnTarget = "/"
|
, transactionReturnTarget = "/"
|
||||||
}
|
}
|
||||||
key = fromOctets (TE.encodeUtf8 (revealSecret (cfgJwtSecret cfg)))
|
key = fromOctets (TE.encodeUtf8 (revealSecret (cfgOidcTransactionSecret cfg)))
|
||||||
result <- runJOSE $
|
result <- runJOSE $
|
||||||
encodeCompact <$> signJWT key (newJWSHeaderProtected HS256) claims
|
encodeCompact <$> signJWT key (newJWSHeaderProtected HS256) claims
|
||||||
pure $ case result of
|
pure $ case result of
|
||||||
Left (_ :: JWTError) -> Left "OIDC transaction creation failed"
|
Left (_ :: JWTError) -> Left "OIDC transaction creation failed"
|
||||||
Right token -> Right (TE.decodeUtf8 (BL.toStrict token))
|
Right token -> Right (TE.decodeUtf8 (BL.toStrict token))
|
||||||
|
|
||||||
verifyTransaction :: Config -> Text -> IO (Either Text TransactionClaims)
|
verifyTransaction :: OidcConfig -> Text -> IO (Either Text TransactionClaims)
|
||||||
verifyTransaction cfg compact = do
|
verifyTransaction cfg compact = do
|
||||||
let key = fromOctets (TE.encodeUtf8 (revealSecret (cfgJwtSecret cfg)))
|
let key = fromOctets (TE.encodeUtf8 (revealSecret (cfgOidcTransactionSecret cfg)))
|
||||||
settings =
|
settings =
|
||||||
defaultJWTValidationSettings (== "nuthatch-oidc-callback")
|
defaultJWTValidationSettings (== "nuthatch-oidc-callback")
|
||||||
& issuerPredicate .~ (== "nuthatch")
|
& issuerPredicate .~ (== "nuthatch")
|
||||||
|
|
|
||||||
10
src/Nuthatch/Product/Limits.hs
Normal file
10
src/Nuthatch/Product/Limits.hs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
module Nuthatch.Product.Limits
|
||||||
|
( multipartPartBytes
|
||||||
|
, maximumObjectBytes
|
||||||
|
) where
|
||||||
|
|
||||||
|
multipartPartBytes :: Int
|
||||||
|
multipartPartBytes = 5 * 1024 * 1024
|
||||||
|
|
||||||
|
maximumObjectBytes :: Integer
|
||||||
|
maximumObjectBytes = toInteger multipartPartBytes * 10000
|
||||||
|
|
@ -28,13 +28,13 @@ import Database.Redis
|
||||||
, setex
|
, setex
|
||||||
)
|
)
|
||||||
import qualified Database.Redis as R
|
import qualified Database.Redis as R
|
||||||
import Nuthatch.Config (Config (..))
|
import Nuthatch.Config (RedisConfig (..))
|
||||||
import Nuthatch.Recovery (trySynchronous)
|
import Nuthatch.Recovery (trySynchronous)
|
||||||
import Nuthatch.Types
|
import Nuthatch.Types
|
||||||
|
|
||||||
type Redis = Connection
|
type Redis = Connection
|
||||||
|
|
||||||
withRedis :: Config -> (Redis -> IO a) -> IO a
|
withRedis :: RedisConfig -> (Redis -> IO a) -> IO a
|
||||||
withRedis cfg action = do
|
withRedis cfg action = do
|
||||||
conn <- connect defaultConnectInfo
|
conn <- connect defaultConnectInfo
|
||||||
{ connectHost = cfgRedisHost cfg
|
{ connectHost = cfgRedisHost cfg
|
||||||
|
|
@ -93,16 +93,16 @@ cacheGenerationKey uid root =
|
||||||
Personal -> unUserId uid <> ":personal"
|
Personal -> unUserId uid <> ":personal"
|
||||||
Shared -> "shared"
|
Shared -> "shared"
|
||||||
|
|
||||||
throttleCheck :: Redis -> Config -> UserId -> Integer -> IO (Either Text Bool)
|
throttleCheck :: Redis -> RedisConfig -> UserId -> Integer -> IO (Either Text Bool)
|
||||||
throttleCheck conn cfg uid bytes =
|
throttleCheck conn cfg uid bytes =
|
||||||
throttleRequest conn cfg uid bytes False
|
throttleRequest conn cfg uid bytes False
|
||||||
|
|
||||||
throttleTake :: Redis -> Config -> UserId -> Integer -> IO (Either Text Bool)
|
throttleTake :: Redis -> RedisConfig -> UserId -> Integer -> IO (Either Text Bool)
|
||||||
throttleTake conn cfg uid bytes = throttleRequest conn cfg uid bytes True
|
throttleTake conn cfg uid bytes = throttleRequest conn cfg uid bytes True
|
||||||
|
|
||||||
throttleRequest
|
throttleRequest
|
||||||
:: Redis
|
:: Redis
|
||||||
-> Config
|
-> RedisConfig
|
||||||
-> UserId
|
-> UserId
|
||||||
-> Integer
|
-> Integer
|
||||||
-> Bool
|
-> Bool
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ import qualified Network.HTTP.Client as HTTP
|
||||||
import Network.HTTP.Client.TLS (tlsManagerSettings)
|
import Network.HTTP.Client.TLS (tlsManagerSettings)
|
||||||
import Network.HTTP.Types.Status (statusCode)
|
import Network.HTTP.Types.Status (statusCode)
|
||||||
import Network.URI (URI (..), URIAuth (..), parseURI)
|
import Network.URI (URI (..), URIAuth (..), parseURI)
|
||||||
import Nuthatch.Config (Config (..), revealSecret)
|
import Nuthatch.Config (S3Config (..), revealSecret)
|
||||||
import Nuthatch.Streaming
|
import Nuthatch.Streaming
|
||||||
( BandwidthError (..)
|
( BandwidthError (..)
|
||||||
, UploadStreamError (..)
|
, UploadStreamError (..)
|
||||||
|
|
@ -112,7 +112,7 @@ data ListPage = ListPage
|
||||||
}
|
}
|
||||||
deriving stock (Eq, Show)
|
deriving stock (Eq, Show)
|
||||||
|
|
||||||
newS3 :: Config -> IO S3
|
newS3 :: S3Config -> IO S3
|
||||||
newS3 cfg = do
|
newS3 cfg = do
|
||||||
(protocol, endpointHost, endpointPort) <- parseEndpoint (cfgS3Endpoint cfg)
|
(protocol, endpointHost, endpointPort) <- parseEndpoint (cfgS3Endpoint cfg)
|
||||||
credentials <- Aws.makeCredentials
|
credentials <- Aws.makeCredentials
|
||||||
|
|
@ -122,7 +122,7 @@ newS3 cfg = do
|
||||||
AwsCore.HTTP -> 80
|
AwsCore.HTTP -> 80
|
||||||
AwsCore.HTTPS -> 443
|
AwsCore.HTTPS -> 443
|
||||||
requestTimeoutMicros =
|
requestTimeoutMicros =
|
||||||
fromInteger (cfgObjectRequestTimeoutSec cfg * 1000000)
|
fromInteger (cfgS3RequestTimeoutSec cfg * 1000000)
|
||||||
signedHost =
|
signedHost =
|
||||||
if endpointPort == defaultPort
|
if endpointPort == defaultPort
|
||||||
then endpointHost
|
then endpointHost
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ import Data.Text (Text)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Data.Text.Encoding as TE
|
import qualified Data.Text.Encoding as TE
|
||||||
import Data.Time.Clock (addUTCTime, getCurrentTime)
|
import Data.Time.Clock (addUTCTime, getCurrentTime)
|
||||||
import Nuthatch.Config (Config (..), revealSecret)
|
import Nuthatch.Config (HttpConfig (..), revealSecret)
|
||||||
import Nuthatch.Paths (encodeUserId)
|
import Nuthatch.Paths (encodeUserId)
|
||||||
import Nuthatch.Types
|
import Nuthatch.Types
|
||||||
import System.Entropy (getEntropy)
|
import System.Entropy (getEntropy)
|
||||||
|
|
@ -87,7 +87,7 @@ instance A.ToJSON SessionClaims where
|
||||||
(displayName claims)
|
(displayName claims)
|
||||||
value -> value
|
value -> value
|
||||||
|
|
||||||
mintSession :: Config -> Text -> Text -> Maybe Text -> IO (Either Text SessionToken)
|
mintSession :: HttpConfig -> Text -> Text -> Maybe Text -> IO (Either Text SessionToken)
|
||||||
mintSession cfg issuer subject display = do
|
mintSession cfg issuer subject display = do
|
||||||
now <- getCurrentTime
|
now <- getCurrentTime
|
||||||
let expires = addUTCTime (fromInteger (cfgSessionHours cfg * 3600)) now
|
let expires = addUTCTime (fromInteger (cfgSessionHours cfg * 3600)) now
|
||||||
|
|
@ -116,7 +116,7 @@ mintSession cfg issuer subject display = do
|
||||||
Right compact ->
|
Right compact ->
|
||||||
validateSessionToken (TE.decodeUtf8 (BL.toStrict compact))
|
validateSessionToken (TE.decodeUtf8 (BL.toStrict compact))
|
||||||
|
|
||||||
verifySession :: Config -> SessionToken -> IO (Either Text SessionUser)
|
verifySession :: HttpConfig -> SessionToken -> IO (Either Text SessionUser)
|
||||||
verifySession cfg (SessionToken token) = do
|
verifySession cfg (SessionToken token) = do
|
||||||
let applicationIssuer = "nuthatch"
|
let applicationIssuer = "nuthatch"
|
||||||
applicationAudience = "nuthatch"
|
applicationAudience = "nuthatch"
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,10 @@ import qualified Data.ByteString as BS
|
||||||
import qualified Data.ByteString.Base16 as B16
|
import qualified Data.ByteString.Base16 as B16
|
||||||
import Data.IORef
|
import Data.IORef
|
||||||
import qualified Data.Text.Encoding as TE
|
import qualified Data.Text.Encoding as TE
|
||||||
|
import Nuthatch.Product.Limits
|
||||||
|
( maximumObjectBytes
|
||||||
|
, multipartPartBytes
|
||||||
|
)
|
||||||
import Nuthatch.Types
|
import Nuthatch.Types
|
||||||
import Servant.Types.SourceT
|
import Servant.Types.SourceT
|
||||||
( SourceT
|
( SourceT
|
||||||
|
|
@ -34,11 +38,8 @@ data BandwidthError
|
||||||
|
|
||||||
instance Exception BandwidthError
|
instance Exception BandwidthError
|
||||||
|
|
||||||
multipartPartBytes :: Int
|
|
||||||
multipartPartBytes = 5 * 1024 * 1024
|
|
||||||
|
|
||||||
maximumMultipartBytes :: Integer
|
maximumMultipartBytes :: Integer
|
||||||
maximumMultipartBytes = toInteger multipartPartBytes * 10000
|
maximumMultipartBytes = maximumObjectBytes
|
||||||
|
|
||||||
data UploadState = UploadState
|
data UploadState = UploadState
|
||||||
{ usPending :: [BS.ByteString]
|
{ usPending :: [BS.ByteString]
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ handleDownloadRequest env session root path requestedRange = do
|
||||||
liftIO $ audit "download" "ok" (Just (unUserId (suUserId user)))
|
liftIO $ audit "download" "ok" (Just (unUserId (suUserId user)))
|
||||||
([ ("root", A.toJSON (rootLabel root))
|
([ ("root", A.toJSON (rootLabel root))
|
||||||
, ("bytes", A.toJSON (oiSize info))
|
, ("bytes", A.toJSON (oiSize info))
|
||||||
] <> objectKeyAudit (envConfig env) path)
|
] <> objectKeyAudit (cfgProduct (envConfig env)) path)
|
||||||
pure $
|
pure $
|
||||||
addHeader (contentDisposition (basename path)) $
|
addHeader (contentDisposition (basename path)) $
|
||||||
addHeader "nosniff" $
|
addHeader "nosniff" $
|
||||||
|
|
@ -97,14 +97,14 @@ handleUploadRequest
|
||||||
requireCsrf "put" UploadCsrfRejected session providedCsrf
|
requireCsrf "put" UploadCsrfRejected session providedCsrf
|
||||||
let user = sessionUser session
|
let user = sessionUser session
|
||||||
size = unDeclaredSize declaredSize
|
size = unDeclaredSize declaredSize
|
||||||
configuredMaximum = cfgMaxObjectBytes (envConfig env)
|
configuredMaximum = cfgMaxObjectBytes (cfgProduct (envConfig env))
|
||||||
when (size > configuredMaximum) $ do
|
when (size > configuredMaximum) $ do
|
||||||
liftIO $ audit "put" "rejected" (Just (unUserId (suUserId user)))
|
liftIO $ audit "put" "rejected" (Just (unUserId (suUserId user)))
|
||||||
([ ("root", A.toJSON (rootLabel root))
|
([ ("root", A.toJSON (rootLabel root))
|
||||||
, ("bytes", A.toJSON size)
|
, ("bytes", A.toJSON size)
|
||||||
, ("operation_id", A.toJSON (unOperationId operation))
|
, ("operation_id", A.toJSON (unOperationId operation))
|
||||||
, ("reason", A.String "object_too_large")
|
, ("reason", A.String "object_too_large")
|
||||||
] <> objectKeyAudit (envConfig env) path)
|
] <> objectKeyAudit (cfgProduct (envConfig env)) path)
|
||||||
throwOperationError err413 UploadTooLarge
|
throwOperationError err413 UploadTooLarge
|
||||||
requireUploadBandwidth env user root path operation size
|
requireUploadBandwidth env user root path operation size
|
||||||
putAttempt <- liftIO $
|
putAttempt <- liftIO $
|
||||||
|
|
@ -134,7 +134,7 @@ handleUploadRequest
|
||||||
, ("bytes", A.toJSON size)
|
, ("bytes", A.toJSON size)
|
||||||
, ("operation_id", A.toJSON (unOperationId operation))
|
, ("operation_id", A.toJSON (unOperationId operation))
|
||||||
, ("reason", A.String "database_unavailable")
|
, ("reason", A.String "database_unavailable")
|
||||||
] <> objectKeyAudit (envConfig env) path)
|
] <> objectKeyAudit (cfgProduct (envConfig env)) path)
|
||||||
throwOperationError err503
|
throwOperationError err503
|
||||||
(UploadMutationRejected "database unavailable")
|
(UploadMutationRejected "database unavailable")
|
||||||
Right value -> pure value
|
Right value -> pure value
|
||||||
|
|
@ -145,7 +145,7 @@ handleUploadRequest
|
||||||
, ("bytes", A.toJSON size)
|
, ("bytes", A.toJSON size)
|
||||||
, ("operation_id", A.toJSON (unOperationId operation))
|
, ("operation_id", A.toJSON (unOperationId operation))
|
||||||
, ("reason", A.String (putFailureReason failure))
|
, ("reason", A.String (putFailureReason failure))
|
||||||
] <> objectKeyAudit (envConfig env) path)
|
] <> objectKeyAudit (cfgProduct (envConfig env)) path)
|
||||||
throwPutFailure failure
|
throwPutFailure failure
|
||||||
Right value -> pure value
|
Right value -> pure value
|
||||||
liftIO $ audit "put" (mutationAuditResult result)
|
liftIO $ audit "put" (mutationAuditResult result)
|
||||||
|
|
@ -154,12 +154,12 @@ handleUploadRequest
|
||||||
, ("bytes", A.toJSON size)
|
, ("bytes", A.toJSON size)
|
||||||
, ("operation_id", A.toJSON (unOperationId operation))
|
, ("operation_id", A.toJSON (unOperationId operation))
|
||||||
] <> mutationReasonFields result
|
] <> mutationReasonFields result
|
||||||
<> objectKeyAudit (envConfig env) path)
|
<> objectKeyAudit (cfgProduct (envConfig env)) path)
|
||||||
pure UploadResponse
|
pure UploadResponse
|
||||||
{ uploadResponseOperationId = operation
|
{ uploadResponseOperationId = operation
|
||||||
, uploadResponseResult = result
|
, uploadResponseResult = result
|
||||||
, uploadResponseIdempotencyWindowSeconds =
|
, uploadResponseIdempotencyWindowSeconds =
|
||||||
cfgIdempotencyWindowSec (envConfig env)
|
cfgIdempotencyWindowSec (cfgProduct (envConfig env))
|
||||||
}
|
}
|
||||||
|
|
||||||
requireDownloadBandwidth :: Env -> SessionUser -> Integer -> Handler ()
|
requireDownloadBandwidth :: Env -> SessionUser -> Integer -> Handler ()
|
||||||
|
|
@ -167,7 +167,7 @@ requireDownloadBandwidth env user size = do
|
||||||
available <- liftIO $
|
available <- liftIO $
|
||||||
throttleCheck
|
throttleCheck
|
||||||
(envRedis env)
|
(envRedis env)
|
||||||
(envConfig env)
|
(cfgRedis (envConfig env))
|
||||||
(suUserId user)
|
(suUserId user)
|
||||||
(min size (toInteger (transferPieceSize env)))
|
(min size (toInteger (transferPieceSize env)))
|
||||||
case available of
|
case available of
|
||||||
|
|
@ -187,7 +187,7 @@ requireUploadBandwidth env user root path operation size = do
|
||||||
available <- liftIO $
|
available <- liftIO $
|
||||||
throttleCheck
|
throttleCheck
|
||||||
(envRedis env)
|
(envRedis env)
|
||||||
(envConfig env)
|
(cfgRedis (envConfig env))
|
||||||
(suUserId user)
|
(suUserId user)
|
||||||
(min size (toInteger (transferPieceSize env)))
|
(min size (toInteger (transferPieceSize env)))
|
||||||
case available of
|
case available of
|
||||||
|
|
@ -202,14 +202,14 @@ requireUploadBandwidth env user root path operation size = do
|
||||||
, ("bytes", A.toJSON size)
|
, ("bytes", A.toJSON size)
|
||||||
, ("operation_id", A.toJSON (unOperationId operation))
|
, ("operation_id", A.toJSON (unOperationId operation))
|
||||||
, ("reason", A.String reason)
|
, ("reason", A.String reason)
|
||||||
] <> objectKeyAudit (envConfig env) path)
|
] <> objectKeyAudit (cfgProduct (envConfig env)) path)
|
||||||
throwOperationError status response
|
throwOperationError status response
|
||||||
|
|
||||||
inspectBandwidth :: Env -> SessionUser -> BS.ByteString -> IO ()
|
inspectBandwidth :: Env -> SessionUser -> BS.ByteString -> IO ()
|
||||||
inspectBandwidth env user chunk = do
|
inspectBandwidth env user chunk = do
|
||||||
result <- throttleTake
|
result <- throttleTake
|
||||||
(envRedis env)
|
(envRedis env)
|
||||||
(envConfig env)
|
(cfgRedis (envConfig env))
|
||||||
(suUserId user)
|
(suUserId user)
|
||||||
(toInteger (BS.length chunk))
|
(toInteger (BS.length chunk))
|
||||||
case result of
|
case result of
|
||||||
|
|
@ -219,7 +219,7 @@ inspectBandwidth env user chunk = do
|
||||||
|
|
||||||
transferPieceSize :: Env -> Int
|
transferPieceSize :: Env -> Int
|
||||||
transferPieceSize env =
|
transferPieceSize env =
|
||||||
fromInteger (max 1 (min 65536 (cfgThrottleBurstBytes (envConfig env))))
|
fromInteger (max 1 (min 65536 (cfgThrottleBurstBytes (cfgRedis (envConfig env)))))
|
||||||
|
|
||||||
throwPutFailure :: PutFailure -> Handler a
|
throwPutFailure :: PutFailure -> Handler a
|
||||||
throwPutFailure failure =
|
throwPutFailure failure =
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import qualified Data.ByteString.Base16 as B16
|
||||||
import qualified Data.Text as Text
|
import qualified Data.Text as Text
|
||||||
import qualified Data.Text.Encoding as TextEncoding
|
import qualified Data.Text.Encoding as TextEncoding
|
||||||
import Database.PostgreSQL.Simple (Only (..))
|
import Database.PostgreSQL.Simple (Only (..))
|
||||||
import Nuthatch.Config (loadConfig)
|
import Nuthatch.Config (Config (..), loadConfig)
|
||||||
import qualified Nuthatch.Db as Db
|
import qualified Nuthatch.Db as Db
|
||||||
import Nuthatch.Probe.Assert
|
import Nuthatch.Probe.Assert
|
||||||
import Nuthatch.Probe.Curl
|
import Nuthatch.Probe.Curl
|
||||||
|
|
@ -27,14 +27,14 @@ main = do
|
||||||
setEnv "NUTHATCH_OIDC_CLIENT_SECRET" "synthetic-fixture-client-secret"
|
setEnv "NUTHATCH_OIDC_CLIENT_SECRET" "synthetic-fixture-client-secret"
|
||||||
config <- loadConfig
|
config <- loadConfig
|
||||||
verifyProfileFile profile lane pgConnection
|
verifyProfileFile profile lane pgConnection
|
||||||
Db.withDb config $ \database -> do
|
Db.withDb (cfgPostgres config) $ \database -> do
|
||||||
_ <- Db.execute_ database
|
_ <- Db.execute_ database
|
||||||
"CREATE TEMP TABLE lane_canary (value text NOT NULL)"
|
"CREATE TEMP TABLE lane_canary (value text NOT NULL)"
|
||||||
_ <- Db.execute database
|
_ <- Db.execute database
|
||||||
"INSERT INTO lane_canary VALUES (?)"
|
"INSERT INTO lane_canary VALUES (?)"
|
||||||
(Only (Text.pack profile))
|
(Only (Text.pack profile))
|
||||||
pure ()
|
pure ()
|
||||||
withRedis config $ \redis -> do
|
withRedis (cfgRedis config) $ \redis -> do
|
||||||
cacheSet redis "lane-canary" 60 (B8.pack profile)
|
cacheSet redis "lane-canary" 60 (B8.pack profile)
|
||||||
cacheGet redis "lane-canary" >>=
|
cacheGet redis "lane-canary" >>=
|
||||||
assertEqual "coordination lane canary" (Just (B8.pack profile))
|
assertEqual "coordination lane canary" (Just (B8.pack profile))
|
||||||
|
|
@ -42,7 +42,7 @@ main = do
|
||||||
discovery <- curl [issuer <> "/.well-known/openid-configuration"]
|
discovery <- curl [issuer <> "/.well-known/openid-configuration"]
|
||||||
assertEqual "OIDC discovery status" 200 (curlStatus discovery)
|
assertEqual "OIDC discovery status" 200 (curlStatus discovery)
|
||||||
assertContains "OIDC discovery issuer" "\"issuer\"" (curlBody discovery)
|
assertContains "OIDC discovery issuer" "\"issuer\"" (curlBody discovery)
|
||||||
store <- newS3 config
|
store <- newS3 (cfgS3 config)
|
||||||
let key = ObjectKey "lane-canary"
|
let key = ObjectKey "lane-canary"
|
||||||
body = BL.fromStrict (B8.pack profile)
|
body = BL.fromStrict (B8.pack profile)
|
||||||
digest =
|
digest =
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ import qualified Data.Text.Encoding as TE
|
||||||
import Database.PostgreSQL.Simple (Only (..))
|
import Database.PostgreSQL.Simple (Only (..))
|
||||||
import Nuthatch.Config
|
import Nuthatch.Config
|
||||||
( Config (..)
|
( Config (..)
|
||||||
|
, ProductConfig (..)
|
||||||
|
, S3Config (..)
|
||||||
, loadConfig
|
, loadConfig
|
||||||
, revealSecret
|
, revealSecret
|
||||||
, secret
|
, secret
|
||||||
|
|
@ -33,13 +35,13 @@ import Nuthatch.Types
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = do
|
main = do
|
||||||
config <- loadConfig
|
config <- loadConfig
|
||||||
withDb config $ \database -> do
|
withDb (cfgPostgres config) $ \database -> do
|
||||||
migrate database "sql" config
|
migrate database "sql" (cfgProduct config)
|
||||||
migrate database "sql" config
|
migrate database "sql" (cfgProduct config)
|
||||||
verifySchema database
|
verifySchema database
|
||||||
verifyAdmission database config
|
verifyAdmission database config
|
||||||
verifyRowDecoder database
|
verifyRowDecoder database
|
||||||
store <- newS3 config
|
store <- newS3 (cfgS3 config)
|
||||||
verifyStorage config store
|
verifyStorage config store
|
||||||
putStrLn "Storage foundation integration gate passed"
|
putStrLn "Storage foundation integration gate passed"
|
||||||
|
|
||||||
|
|
@ -112,18 +114,19 @@ verifyAdmission database config = do
|
||||||
|
|
||||||
let personalUser = UserId "personal-limit-user"
|
let personalUser = UserId "personal-limit-user"
|
||||||
personal = personalPool personalUser
|
personal = personalPool personalUser
|
||||||
|
productCfg = cfgProduct config
|
||||||
ensurePersonalPool database
|
ensurePersonalPool database
|
||||||
config { cfgPersonalQuotaBytes = 30 }
|
productCfg { cfgPersonalQuotaBytes = 30 }
|
||||||
personalUser
|
personalUser
|
||||||
ensurePersonalPool database
|
ensurePersonalPool database
|
||||||
config { cfgPersonalQuotaBytes = 40 }
|
productCfg { cfgPersonalQuotaBytes = 40 }
|
||||||
personalUser
|
personalUser
|
||||||
_ <- Db.execute database
|
_ <- Db.execute database
|
||||||
"UPDATE pools SET reserved_bytes = 35 WHERE pool_id = ?"
|
"UPDATE pools SET reserved_bytes = 35 WHERE pool_id = ?"
|
||||||
(Only (unPoolId personal))
|
(Only (unPoolId personal))
|
||||||
personalLower <- try
|
personalLower <- try
|
||||||
(ensurePersonalPool database
|
(ensurePersonalPool database
|
||||||
config { cfgPersonalQuotaBytes = 34 }
|
productCfg { cfgPersonalQuotaBytes = 34 }
|
||||||
personalUser)
|
personalUser)
|
||||||
:: IO (Either SomeException ())
|
:: IO (Either SomeException ())
|
||||||
assert "unsafe personal limit reduction rejected" (isLeft personalLower)
|
assert "unsafe personal limit reduction rejected" (isLeft personalLower)
|
||||||
|
|
@ -326,9 +329,10 @@ verifyStorage config store = do
|
||||||
|
|
||||||
missing <- getObjectBytes store (ObjectKey "shared/missing")
|
missing <- getObjectBytes store (ObjectKey "shared/missing")
|
||||||
assertEqual "typed not-found error" (Left S3NotFound) missing
|
assertEqual "typed not-found error" (Left S3NotFound) missing
|
||||||
deniedStore <- newS3 config
|
let s3Cfg = cfgS3 config
|
||||||
|
deniedStore <- newS3 s3Cfg
|
||||||
{ cfgS3SecretKey =
|
{ cfgS3SecretKey =
|
||||||
secret (revealSecret (cfgS3SecretKey config) <> "-invalid")
|
secret (revealSecret (cfgS3SecretKey s3Cfg) <> "-invalid")
|
||||||
}
|
}
|
||||||
denied <- listObjects deniedStore "" Nothing Nothing 1
|
denied <- listObjects deniedStore "" Nothing Nothing 1
|
||||||
case denied of
|
case denied of
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue