Type ordinary integration probe HTTP traffic
This commit is contained in:
parent
390434bed1
commit
cc5c47f2ce
1 changed files with 1022 additions and 661 deletions
|
|
@ -8,28 +8,27 @@ import qualified Data.ByteString.Char8 as B8
|
|||
import Data.List (find)
|
||||
import Nuthatch.Probe.Assert
|
||||
import Nuthatch.Probe.Curl
|
||||
import Nuthatch.Probe.Http
|
||||
import Nuthatch.Probe.Oidc
|
||||
import Nuthatch.Probe.ProductClient
|
||||
import System.Entropy (getEntropy)
|
||||
import System.IO.Temp (withSystemTempDirectory)
|
||||
|
||||
main :: IO ()
|
||||
main =
|
||||
withSystemTempDirectory "nuthatch-auth-probe" $ \directory -> do
|
||||
context <- newProbeContext directory
|
||||
runId <- B8.unpack . B16.encode <$> getEntropy 8
|
||||
verifyCredentialScope
|
||||
waitForApplication context
|
||||
verifyPublicBoundary context
|
||||
userA <- requireEnv "NUTHATCH_OIDC_USER_A"
|
||||
sessionA <- login context True userA "a"
|
||||
verifyPkceBoundary context userA
|
||||
verifyAuthenticatedBoundary context sessionA
|
||||
exerciseRoot context sessionA runId "personal" "01"
|
||||
exerciseRoot context sessionA runId "shared" "02"
|
||||
verifyCrossUserBoundary context sessionA runId
|
||||
verifyLogout context sessionA
|
||||
putStrLn "OIDC application boundary passed"
|
||||
main = do
|
||||
context <- newProbeContext
|
||||
runId <- B8.unpack . B16.encode <$> getEntropy 8
|
||||
verifyCredentialScope
|
||||
waitForApplication context
|
||||
verifyPublicBoundary context
|
||||
userA <- requireEnv "NUTHATCH_OIDC_USER_A"
|
||||
sessionA <- login context True userA "a"
|
||||
verifyPkceBoundary context userA
|
||||
verifyAuthenticatedBoundary context sessionA
|
||||
exerciseRoot context sessionA runId "personal" "01"
|
||||
exerciseRoot context sessionA runId "shared" "02"
|
||||
verifyCrossUserBoundary context sessionA runId
|
||||
verifyLogout context sessionA
|
||||
putStrLn "OIDC application boundary passed"
|
||||
|
||||
waitForApplication :: ProbeContext -> IO ()
|
||||
waitForApplication context = go 30
|
||||
|
|
@ -37,16 +36,16 @@ waitForApplication context = go 30
|
|||
go :: Int -> IO ()
|
||||
go remaining = do
|
||||
result <-
|
||||
try (curl
|
||||
[ "--connect-timeout", "1"
|
||||
, "--max-time", "2"
|
||||
, probeBase context <> "/readyz"
|
||||
])
|
||||
:: IO (Either CurlTransportFailure CurlResponse)
|
||||
try
|
||||
(getWith
|
||||
(probeClient context)
|
||||
defaultRequestOptions { optionTimeoutMicros = Just 2000000 }
|
||||
(probeBase context <> "/readyz"))
|
||||
:: IO (Either HttpException ProbeResponse)
|
||||
case result of
|
||||
Right _ -> pure ()
|
||||
Left failure
|
||||
| curlTransportExitCode failure == 7 && remaining > 1 ->
|
||||
| isConnectionFailure failure && remaining > 1 ->
|
||||
threadDelay 1000000 >> go (remaining - 1)
|
||||
| otherwise -> throwIO failure
|
||||
|
||||
|
|
@ -63,9 +62,9 @@ verifyCredentialScope = do
|
|||
denied label arguments = do
|
||||
response <- curl (signed <> arguments)
|
||||
assertEqual
|
||||
(label <> ": " <> B8.unpack (curlBody response))
|
||||
(label <> ": " <> B8.unpack (probeBody response))
|
||||
403
|
||||
(curlStatus response)
|
||||
(probeStatus response)
|
||||
emptyPayload =
|
||||
"X-Amz-Content-Sha256:e3b0c44298fc1c149afbf4c8996fb924"
|
||||
<> "27ae41e4649b934ca495991b7852b855"
|
||||
|
|
@ -93,73 +92,89 @@ verifyCredentialScope = do
|
|||
|
||||
verifyPublicBoundary :: ProbeContext -> IO ()
|
||||
verifyPublicBoundary context = do
|
||||
health <- curl [probeBase context <> "/healthz"]
|
||||
assertEqual "health status" 200 (curlStatus health)
|
||||
assertContains "health response" "\"ok\":true" (curlBody health)
|
||||
ready <- curl [probeBase context <> "/readyz"]
|
||||
assertEqual "readiness status" 200 (curlStatus ready)
|
||||
assertContains "readiness response" "\"ready\":true" (curlBody ready)
|
||||
loginPost <- curl ["--request", "POST", probeBase context <> "/login"]
|
||||
assertEqual "login method boundary" 405 (curlStatus loginPost)
|
||||
root <- curl [probeBase context <> "/"]
|
||||
assertEqual "root route boundary" 404 (curlStatus root)
|
||||
downloadResponse <- curl
|
||||
[ probeBase context
|
||||
<> "/api/download?root=personal&path=unauthenticated"
|
||||
]
|
||||
assertEqual "unauthenticated download" 303 (curlStatus downloadResponse)
|
||||
uploadResponse <- curl
|
||||
[ "--header", "X-Declared-Size: 1"
|
||||
, "--header", "X-Content-SHA256: " <> B8.unpack (sha256Hex "x")
|
||||
, "--header"
|
||||
, "X-CSRF-Token: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
, "--data", "x"
|
||||
, probeBase context
|
||||
health <- get (probeClient context) (probeBase context <> "/healthz")
|
||||
assertEqual "health status" 200 (probeStatus health)
|
||||
assertContains "health response" "\"ok\":true" (probeBody health)
|
||||
ready <- get (probeClient context) (probeBase context <> "/readyz")
|
||||
assertEqual "readiness status" 200 (probeStatus ready)
|
||||
assertContains "readiness response" "\"ready\":true" (probeBody ready)
|
||||
loginPost <- postWith
|
||||
(probeClient context)
|
||||
defaultRequestOptions
|
||||
(probeBase context <> "/login")
|
||||
assertEqual "login method boundary" 405 (probeStatus loginPost)
|
||||
root <- get (probeClient context) (probeBase context <> "/")
|
||||
assertEqual "root route boundary" 404 (probeStatus root)
|
||||
downloadResponse <- get
|
||||
(probeClient context)
|
||||
(probeBase context <> "/api/download?root=personal&path=unauthenticated")
|
||||
assertEqual "unauthenticated download" 303 (probeStatus downloadResponse)
|
||||
uploadResponse <- postWith
|
||||
(probeClient context)
|
||||
defaultRequestOptions
|
||||
{ optionHeaders =
|
||||
[ ("X-Declared-Size", "1")
|
||||
, ("X-Content-SHA256", sha256Hex "x")
|
||||
, ( "X-CSRF-Token"
|
||||
, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
)
|
||||
]
|
||||
, optionBody = ByteBody "x"
|
||||
}
|
||||
(probeBase context
|
||||
<> "/api/upload?root=personal&path=unauthenticated"
|
||||
<> "&operation_id=40000000-0000-4000-8000-000000000001"
|
||||
]
|
||||
assertEqual "unauthenticated upload" 303 (curlStatus uploadResponse)
|
||||
deleteResponse <- curl
|
||||
[ "--header", "Content-Type: application/json"
|
||||
, "--data"
|
||||
, "{\"root\":\"personal\",\"path\":\"unauthenticated\","
|
||||
<> "&operation_id=40000000-0000-4000-8000-000000000001")
|
||||
assertEqual "unauthenticated upload" 303 (probeStatus uploadResponse)
|
||||
deleteResponse <- postWith
|
||||
(probeClient context)
|
||||
defaultRequestOptions
|
||||
{ optionHeaders = [("Content-Type", "application/json")]
|
||||
, optionBody = ByteBody $
|
||||
"{\"root\":\"personal\",\"path\":\"unauthenticated\","
|
||||
<> "\"operation_id\":\"40000000-0000-4000-8000-000000000002\","
|
||||
<> "\"csrf\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}"
|
||||
, probeBase context <> "/api/delete"
|
||||
]
|
||||
assertEqual "unauthenticated delete" 303 (curlStatus deleteResponse)
|
||||
removedCompatibility <- curl
|
||||
[ "--header", "Content-Type: application/json"
|
||||
, "--data"
|
||||
, "{\"root\":\"personal\",\"path\":\"unauthenticated\","
|
||||
}
|
||||
(probeBase context <> "/api/delete")
|
||||
assertEqual "unauthenticated delete" 303 (probeStatus deleteResponse)
|
||||
removedCompatibility <- postWith
|
||||
(probeClient context)
|
||||
defaultRequestOptions
|
||||
{ optionHeaders = [("Content-Type", "application/json")]
|
||||
, optionBody = ByteBody $
|
||||
"{\"root\":\"personal\",\"path\":\"unauthenticated\","
|
||||
<> "\"csrf\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}"
|
||||
, probeBase context <> "/delete-prefix"
|
||||
]
|
||||
assertEqual "removed compatibility route" 404 (curlStatus removedCompatibility)
|
||||
logoutResponse <- curl
|
||||
[ "--header"
|
||||
, "X-CSRF-Token: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
, "--request", "POST"
|
||||
, probeBase context <> "/logout"
|
||||
]
|
||||
assertEqual "unauthenticated logout" 303 (curlStatus logoutResponse)
|
||||
}
|
||||
(probeBase context <> "/delete-prefix")
|
||||
assertEqual "removed compatibility route" 404 (probeStatus removedCompatibility)
|
||||
logoutResponse <- postWith
|
||||
(probeClient context)
|
||||
defaultRequestOptions
|
||||
{ optionHeaders =
|
||||
[ ( "X-CSRF-Token"
|
||||
, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
)
|
||||
]
|
||||
}
|
||||
(probeBase context <> "/logout")
|
||||
assertEqual "unauthenticated logout" 303 (probeStatus logoutResponse)
|
||||
|
||||
verifyAuthenticatedBoundary :: ProbeContext -> Session -> IO ()
|
||||
verifyAuthenticatedBoundary context session = do
|
||||
html <- curl
|
||||
[ "--cookie", sessionCookies session
|
||||
, probeBase context <> "/api/browse"
|
||||
]
|
||||
assertEqual "authenticated HTML browse" 200 (curlStatus html)
|
||||
html <- getWith
|
||||
(probeClient context)
|
||||
defaultRequestOptions
|
||||
{ optionCookies = Just (sessionCookies session) }
|
||||
(probeBase context <> "/api/browse")
|
||||
assertEqual "authenticated HTML browse" 200 (probeStatus html)
|
||||
json <- browse context session ""
|
||||
assertEqual "authenticated JSON browse" 200 (curlStatus json)
|
||||
assertEqual "authenticated JSON browse" 200 (probeStatus json)
|
||||
let prohibited =
|
||||
[ "NUTHATCH_S3"
|
||||
, "AWS_ACCESS_KEY"
|
||||
, "AWS_SECRET_ACCESS_KEY"
|
||||
, "client_secret"
|
||||
]
|
||||
bodies = curlBody html <> curlBody json
|
||||
bodies = probeBody html <> probeBody json
|
||||
mapM_
|
||||
(\value ->
|
||||
assert ("browser response exposed " <> value) $
|
||||
|
|
@ -182,18 +197,18 @@ exerciseRoot context session runId root suffix = do
|
|||
firstUpload <- upload context session root path (operation "0") first
|
||||
requireSucceeded "first upload" firstUpload
|
||||
listing <- browse context session ("?root=" <> root)
|
||||
assertContains "uploaded object listing" (B8.pack path) (curlBody listing)
|
||||
assertContains "uploaded object listing" (B8.pack path) (probeBody listing)
|
||||
firstDownload <- download context session root path
|
||||
assertEqual "first download status" 200 (curlStatus firstDownload)
|
||||
assertEqual "first download body" first (curlBody firstDownload)
|
||||
assertEqual "first download status" 200 (probeStatus firstDownload)
|
||||
assertEqual "first download body" first (probeBody firstDownload)
|
||||
overwrite <- upload context session root path (operation "1") second
|
||||
requireSucceeded "overwrite" overwrite
|
||||
secondDownload <- download context session root path
|
||||
assertEqual "overwrite body" second (curlBody secondDownload)
|
||||
assertEqual "overwrite body" second (probeBody secondDownload)
|
||||
deleted <- deleteObject context session root path (operation "2")
|
||||
requireSucceeded "delete" deleted
|
||||
missing <- download context session root path
|
||||
assertEqual "deleted object" 404 (curlStatus missing)
|
||||
assertEqual "deleted object" 404 (probeStatus missing)
|
||||
|
||||
verifyCrossUserBoundary :: ProbeContext -> Session -> String -> IO ()
|
||||
verifyCrossUserBoundary context sessionA runId = do
|
||||
|
|
@ -208,27 +223,32 @@ verifyCrossUserBoundary context sessionA runId = do
|
|||
sessionB <- login context False userB "b"
|
||||
personalB <- browse context sessionB "?root=personal"
|
||||
assert "another user observed a personal object" $
|
||||
not ("cross-user.txt" `BS.isInfixOf` curlBody personalB)
|
||||
not ("cross-user.txt" `BS.isInfixOf` probeBody personalB)
|
||||
sharedB <- browse context sessionB "?root=shared"
|
||||
assertContains "shared object visibility" "cross-user.txt" (curlBody sharedB)
|
||||
assertContains "shared object visibility" "cross-user.txt" (probeBody sharedB)
|
||||
|
||||
verifyLogout :: ProbeContext -> Session -> IO ()
|
||||
verifyLogout context session = do
|
||||
rejected <- curl
|
||||
[ "--cookie", sessionCookies session
|
||||
, "--header"
|
||||
, "X-CSRF-Token: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
, "--request", "POST"
|
||||
, probeBase context <> "/logout"
|
||||
]
|
||||
assert "logout accepted the wrong CSRF token" (curlStatus rejected >= 400)
|
||||
completed <- curl
|
||||
[ "--cookie", sessionCookies session
|
||||
, "--header", "X-CSRF-Token: " <> B8.unpack (sessionCsrf session)
|
||||
, "--request", "POST"
|
||||
, probeBase context <> "/logout"
|
||||
]
|
||||
assertEqual "logout status" 200 (curlStatus completed)
|
||||
rejected <- postWith
|
||||
(probeClient context)
|
||||
defaultRequestOptions
|
||||
{ optionCookies = Just (sessionCookies session)
|
||||
, optionHeaders =
|
||||
[ ( "X-CSRF-Token"
|
||||
, "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
)
|
||||
]
|
||||
}
|
||||
(probeBase context <> "/logout")
|
||||
assert "logout accepted the wrong CSRF token" (probeStatus rejected >= 400)
|
||||
completed <- postWith
|
||||
(probeClient context)
|
||||
defaultRequestOptions
|
||||
{ optionCookies = Just (sessionCookies session)
|
||||
, optionHeaders = [("X-CSRF-Token", sessionCsrf session)]
|
||||
}
|
||||
(probeBase context <> "/logout")
|
||||
assertEqual "logout status" 200 (probeStatus completed)
|
||||
mapM_
|
||||
(\name ->
|
||||
let matching =
|
||||
|
|
@ -240,13 +260,14 @@ verifyLogout context session = do
|
|||
(BS.isInfixOf "Max-Age=0")
|
||||
matching)
|
||||
["nuthatch_session", "nuthatch_csrf", "nuthatch_oidc"]
|
||||
after <- curl
|
||||
[ "--header", "Accept: application/json"
|
||||
, probeBase context <> "/api/browse"
|
||||
]
|
||||
assertEqual "session after logout" 303 (curlStatus after)
|
||||
after <- getWith
|
||||
(probeClient context)
|
||||
defaultRequestOptions
|
||||
{ optionHeaders = [("Accept", "application/json")] }
|
||||
(probeBase context <> "/api/browse")
|
||||
assertEqual "session after logout" 303 (probeStatus after)
|
||||
|
||||
requireSucceeded :: String -> CurlResponse -> IO ()
|
||||
requireSucceeded :: String -> ProbeResponse -> IO ()
|
||||
requireSucceeded label response = do
|
||||
assertEqual (label <> " status") 200 (curlStatus response)
|
||||
assertContains label "\"result\":\"succeeded\"" (curlBody response)
|
||||
assertEqual (label <> " status") 200 (probeStatus response)
|
||||
assertContains label "\"result\":\"succeeded\"" (probeBody response)
|
||||
|
|
|
|||
Loading…
Reference in a new issue