Group configuration by component ownership

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

View file

@ -6,47 +6,67 @@ import Nuthatch.Config
testConfig :: Config
testConfig = Config
{ cfgListenHost = "127.0.0.1"
, cfgListenPort = 8080
, cfgTlsCertPath = "/tls/tls.crt"
, cfgTlsKeyPath = "/tls/tls.key"
, cfgPublicBaseUrl = "https://nuthatch.example"
, cfgJwtSecret = secret "unit-test-session-secret-material-32"
, cfgSessionHours = 1
, cfgCookieName = "nuthatch_session"
, cfgCsrfCookieName = "nuthatch_csrf"
, cfgOidcTransactionCookieName = "nuthatch_oidc"
, cfgOidcIssuer = "https://issuer.example"
, cfgOidcClientId = "nuthatch"
, cfgOidcAdditionalAudiences = []
, cfgOidcClientSecret = secret "unit-test-client-secret"
, cfgOidcRedirectUri = "https://nuthatch.example/oidc/callback"
, cfgOidcTransactionMinutes = 5
, cfgOidcMaxIdTokenSeconds = 3600
, cfgPgConn = secret ""
, cfgRedisHost = ""
, cfgRedisPort = 6379
, cfgRedisDb = 0
, cfgS3Endpoint = "https://s3.example"
, cfgS3Region = "test"
, cfgS3Bucket = "test"
, cfgS3AccessKey = secret "test"
, cfgS3SecretKey = secret "test"
, cfgS3PathStyle = True
, cfgPersonalQuotaBytes = 1024
, cfgSharedQuotaBytes = 1024
, cfgMaxObjectBytes = 1024
, cfgThrottleBytesPerSec = 1024
, cfgThrottleBurstBytes = 1024
, cfgListCacheTtlSec = 30
, cfgBrowsePageSize = 100
, cfgSearchMaxKeys = 100
, cfgSearchTimeoutMs = 1000
, cfgDeletePageSize = 100
, cfgRecoveryGraceSec = 30
, cfgIdempotencyWindowSec = 3600
, cfgObjectRequestTimeoutSec = 300
, cfgStartupAttempts = 3
, cfgStartupDelayMs = 10
, cfgAuditObjectKeys = False
{ cfgRuntime = RuntimeConfig
{ cfgStartupAttempts = 3
, cfgStartupDelayMs = 10
}
, cfgHttp = HttpConfig
{ cfgListenHost = "127.0.0.1"
, cfgListenPort = 8080
, cfgTlsCertPath = "/tls/tls.crt"
, cfgTlsKeyPath = "/tls/tls.key"
, cfgPublicBaseUrl = "https://nuthatch.example"
, cfgJwtSecret = sessionSecret
, cfgSessionHours = 1
, cfgCookieName = "nuthatch_session"
, cfgCsrfCookieName = "nuthatch_csrf"
, cfgOidcTransactionCookieName = "nuthatch_oidc"
}
, cfgOidc = OidcConfig
{ cfgOidcIssuer = "https://issuer.example"
, cfgOidcClientId = "nuthatch"
, cfgOidcAdditionalAudiences = []
, cfgOidcClientSecret = secret "unit-test-client-secret"
, cfgOidcRedirectUri = "https://nuthatch.example/oidc/callback"
, cfgOidcTransactionMinutes = 5
, cfgOidcMaxIdTokenSeconds = 3600
, cfgOidcTransactionSecret = sessionSecret
}
, cfgPostgres = PostgresConfig
{ cfgPgConn = secret ""
}
, cfgRedis = RedisConfig
{ cfgRedisHost = ""
, cfgRedisPort = 6379
, cfgRedisDb = 0
, cfgThrottleBytesPerSec = 1024
, cfgThrottleBurstBytes = 1024
}
, cfgS3 = S3Config
{ cfgS3Endpoint = "https://s3.example"
, cfgS3Region = "test"
, cfgS3Bucket = "test"
, cfgS3AccessKey = secret "test"
, cfgS3SecretKey = secret "test"
, cfgS3PathStyle = True
, cfgS3RequestTimeoutSec = 300
}
, cfgProduct = ProductConfig
{ cfgPersonalQuotaBytes = 1024
, cfgSharedQuotaBytes = 1024
, cfgMaxObjectBytes = 1024
, cfgListCacheTtlSec = 30
, cfgBrowsePageSize = 100
, cfgSearchMaxKeys = 100
, cfgSearchTimeoutMs = 1000
, cfgDeletePageSize = 100
, cfgIdempotencyWindowSec = 3600
, cfgAuditObjectKeys = False
}
, cfgRecovery = RecoveryConfig
{ cfgRecoveryGraceSec = 30
, cfgObjectRequestTimeoutSec = 300
}
}
where
sessionSecret = secret "unit-test-session-secret-material-32"

View file

@ -30,7 +30,7 @@ import Network.Wai.Test
, srequest
)
import Nuthatch.Api
import Nuthatch.Config (Config)
import Nuthatch.Config (Config (..), HttpConfig)
import Nuthatch.Http.Auth
( authenticationHandler
, loginResponse
@ -79,6 +79,9 @@ tests = testGroup "typed HTTP boundary"
@?= "healthz"
]
httpConfig :: HttpConfig
httpConfig = cfgHttp testConfig
testRouteSelection :: IO ()
testRouteSelection = do
calls <- newIORef 0
@ -324,11 +327,11 @@ typedTestServer calls =
testAuthenticationBoundary :: IO ()
testAuthenticationBoundary = do
tokenResult <- mintSession testConfig "issuer" "subject" Nothing
tokenResult <- mintSession httpConfig "issuer" "subject" Nothing
token <- either (fail . T.unpack) pure tokenResult
let csrf = CsrfToken "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
responseHeaders = getHeaders
(loginResponse testConfig token csrf)
(loginResponse httpConfig token csrf)
cookieHeaders =
[ value
| (name, value) <- responseHeaders
@ -353,7 +356,7 @@ testAuthenticationBoundary = do
(length (filter ("HttpOnly" `BS.isInfixOf`) cookieHeaders) == 2)
calls <- newIORef 0
let app = productionAuthTestApplication testConfig calls
let app = productionAuthTestApplication httpConfig calls
cookie =
"nuthatch_session=" <> TE.encodeUtf8 (unSessionToken token)
<> "; nuthatch_csrf=" <> TE.encodeUtf8 (unCsrfToken csrf)
@ -415,7 +418,7 @@ testCanonicalProductRoutes = do
simpleStatus response @?= status404
readIORef calls >>= (@?= 0)
productionAuthTestApplication :: Config -> IORef Int -> Application
productionAuthTestApplication :: HttpConfig -> IORef Int -> Application
productionAuthTestApplication cfg calls =
utf8Boundary $
serveWithContext

View file

@ -31,7 +31,7 @@ import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Data.Time.Clock (UTCTime, addUTCTime, getCurrentTime)
import Nuthatch.Config (Config (..))
import Nuthatch.Config (Config (..), OidcConfig (..))
import Nuthatch.Oidc (OidcIdentity (..), verifyIdToken)
import Nuthatch.Test.Config (testConfig)
import Test.Tasty (TestTree, testGroup)
@ -47,6 +47,9 @@ tests = testGroup "OIDC validation"
testIdTokenTimeAndSerializationRejections
]
oidcConfig :: OidcConfig
oidcConfig = cfgOidc testConfig
data TestIdTokenClaims = TestIdTokenClaims
{ testIdTokenRegistered :: ClaimsSet
, testIdTokenNonce :: Text
@ -98,7 +101,7 @@ testValidIdToken = do
compact <- signTestIdToken key RS256
(testIdClaims now "https://issuer.example" ["nuthatch"] "nonce")
result <- verifyIdToken
testConfig
oidcConfig
(JWKSet [key])
now
"nonce"
@ -119,13 +122,13 @@ testConfiguredAudiences = do
missingAuthorizedParty <- signTestIdToken key RS256
(testIdClaims now "https://issuer.example" ["project", "nuthatch"] "nonce")
result <- verifyIdToken
testConfig { cfgOidcAdditionalAudiences = ["project"] }
oidcConfig { cfgOidcAdditionalAudiences = ["project"] }
(JWKSet [key])
now
"nonce"
compact
rejected <- verifyIdToken
testConfig { cfgOidcAdditionalAudiences = ["project"] }
oidcConfig { cfgOidcAdditionalAudiences = ["project"] }
(JWKSet [key])
now
"nonce"
@ -151,12 +154,12 @@ testIdTokenCryptographicRejections = do
multipleAudience <- signTestIdToken key RS256
(testIdClaims now "https://issuer.example" ["nuthatch", "other"] "nonce")
let rejected =
[ verifyIdToken testConfig (JWKSet [key]) now "other-nonce" valid
, verifyIdToken testConfig (JWKSet [key]) now "nonce" wrongAlgorithm
, verifyIdToken testConfig (JWKSet [key]) now "nonce" wrongIssuer
, verifyIdToken testConfig (JWKSet [key]) now "nonce" wrongSignature
, verifyIdToken testConfig (JWKSet [key]) now "nonce" wrongAudience
, verifyIdToken testConfig (JWKSet [key]) now "nonce" multipleAudience
[ verifyIdToken oidcConfig (JWKSet [key]) now "other-nonce" valid
, verifyIdToken oidcConfig (JWKSet [key]) now "nonce" wrongAlgorithm
, verifyIdToken oidcConfig (JWKSet [key]) now "nonce" wrongIssuer
, verifyIdToken oidcConfig (JWKSet [key]) now "nonce" wrongSignature
, verifyIdToken oidcConfig (JWKSet [key]) now "nonce" wrongAudience
, verifyIdToken oidcConfig (JWKSet [key]) now "nonce" multipleAudience
]
results <- sequence rejected
assertBool "every invalid cryptographic or binding case is rejected"
@ -197,11 +200,11 @@ testIdTokenTimeAndSerializationRejections = do
["nuthatch"]
"nonce")
results <- sequence
[ verifyIdToken testConfig (JWKSet [key]) now "nonce" "not.compact"
, verifyIdToken testConfig (JWKSet [key]) now "nonce" missingIssuedAt
, verifyIdToken testConfig (JWKSet [key]) now "nonce" future
, verifyIdToken testConfig (JWKSet [key]) now "nonce" expired
, verifyIdToken testConfig (JWKSet [key]) now "nonce" overlong
[ verifyIdToken oidcConfig (JWKSet [key]) now "nonce" "not.compact"
, verifyIdToken oidcConfig (JWKSet [key]) now "nonce" missingIssuedAt
, verifyIdToken oidcConfig (JWKSet [key]) now "nonce" future
, verifyIdToken oidcConfig (JWKSet [key]) now "nonce" expired
, verifyIdToken oidcConfig (JWKSet [key]) now "nonce" overlong
]
assertBool "every invalid serialization or time case is rejected"
(all isLeft results)