Group configuration by component ownership

This commit is contained in:
ms2k 2026-07-20 02:21:33 +09:00
commit 67cc0cb802
27 changed files with 486 additions and 258 deletions

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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,49 +209,67 @@ 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
{ cfgListenHost = host { cfgRuntime = RuntimeConfig
, cfgListenPort = port { cfgStartupAttempts = startupAttempts
, cfgTlsCertPath = tlsCertPath , cfgStartupDelayMs = startupDelayMs
, cfgTlsKeyPath = tlsKeyPath }
, cfgPublicBaseUrl = publicBase , cfgHttp = HttpConfig
, cfgJwtSecret = jwtSecret { cfgListenHost = host
, cfgSessionHours = sessionHours , cfgListenPort = port
, cfgCookieName = cookieName , cfgTlsCertPath = tlsCertPath
, cfgCsrfCookieName = csrfCookie , cfgTlsKeyPath = tlsKeyPath
, cfgOidcTransactionCookieName = transactionCookie , cfgPublicBaseUrl = publicBase
, cfgOidcIssuer = oidcIssuer , cfgJwtSecret = jwtSecret
, cfgOidcClientId = oidcClientId , cfgSessionHours = sessionHours
, cfgOidcAdditionalAudiences = oidcAdditionalAudiences , cfgCookieName = cookieName
, cfgOidcClientSecret = oidcClientSecret , cfgCsrfCookieName = csrfCookie
, cfgOidcRedirectUri = oidcRedirectUri , cfgOidcTransactionCookieName = transactionCookie
, cfgOidcTransactionMinutes = oidcTransactionMinutes }
, cfgOidcMaxIdTokenSeconds = oidcMaxIdTokenSeconds , cfgOidc = OidcConfig
, cfgPgConn = pgConn { cfgOidcIssuer = oidcIssuer
, cfgRedisHost = redisHost , cfgOidcClientId = oidcClientId
, cfgRedisPort = redisPort , cfgOidcAdditionalAudiences = oidcAdditionalAudiences
, cfgRedisDb = redisDb , cfgOidcClientSecret = oidcClientSecret
, cfgS3Endpoint = s3Endpoint , cfgOidcRedirectUri = oidcRedirectUri
, cfgS3Region = s3Region , cfgOidcTransactionMinutes = oidcTransactionMinutes
, cfgS3Bucket = s3Bucket , cfgOidcMaxIdTokenSeconds = oidcMaxIdTokenSeconds
, cfgS3AccessKey = s3Access , cfgOidcTransactionSecret = jwtSecret
, cfgS3SecretKey = s3Secret }
, cfgS3PathStyle = pathStyle , cfgPostgres = PostgresConfig
, cfgPersonalQuotaBytes = personalQuota { cfgPgConn = pgConn
, cfgSharedQuotaBytes = sharedQuota }
, cfgMaxObjectBytes = maxObject , cfgRedis = RedisConfig
, cfgThrottleBytesPerSec = throttleRate { cfgRedisHost = redisHost
, cfgThrottleBurstBytes = throttleBurst , cfgRedisPort = redisPort
, cfgListCacheTtlSec = listTtl , cfgRedisDb = redisDb
, cfgBrowsePageSize = browsePageSize , cfgThrottleBytesPerSec = throttleRate
, cfgSearchMaxKeys = searchMax , cfgThrottleBurstBytes = throttleBurst
, cfgSearchTimeoutMs = searchTimeoutMs }
, cfgDeletePageSize = deletePageSize , cfgS3 = S3Config
, cfgRecoveryGraceSec = recoveryGrace { cfgS3Endpoint = s3Endpoint
, cfgIdempotencyWindowSec = idempotencyWindow , cfgS3Region = s3Region
, cfgObjectRequestTimeoutSec = objectRequestTimeout , cfgS3Bucket = s3Bucket
, cfgStartupAttempts = startupAttempts , cfgS3AccessKey = s3Access
, cfgStartupDelayMs = startupDelayMs , cfgS3SecretKey = s3Secret
, cfgAuditObjectKeys = auditObjectKeys , cfgS3PathStyle = pathStyle
, cfgS3RequestTimeoutSec = objectRequestTimeout
}
, cfgProduct = ProductConfig
{ cfgPersonalQuotaBytes = personalQuota
, cfgSharedQuotaBytes = sharedQuota
, cfgMaxObjectBytes = maxObject
, cfgListCacheTtlSec = listTtl
, cfgBrowsePageSize = browsePageSize
, cfgSearchMaxKeys = searchMax
, cfgSearchTimeoutMs = searchTimeoutMs
, cfgDeletePageSize = deletePageSize
, cfgIdempotencyWindowSec = idempotencyWindow
, cfgAuditObjectKeys = auditObjectKeys
}
, cfgRecovery = RecoveryConfig
{ cfgRecoveryGraceSec = recoveryGrace
, cfgObjectRequestTimeoutSec = objectRequestTimeout
}
} }
luaExactIntegerMax :: Integer luaExactIntegerMax :: Integer

View file

@ -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)

View file

@ -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 $

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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")

View file

@ -0,0 +1,10 @@
module Nuthatch.Product.Limits
( multipartPartBytes
, maximumObjectBytes
) where
multipartPartBytes :: Int
multipartPartBytes = 5 * 1024 * 1024
maximumObjectBytes :: Integer
maximumObjectBytes = toInteger multipartPartBytes * 10000

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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]

View file

@ -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 =

View file

@ -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 =

View file

@ -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