Remove pre-release HTTP compatibility surface
This commit is contained in:
parent
15eb0038d2
commit
f28810ea33
8 changed files with 104 additions and 329 deletions
|
|
@ -32,8 +32,7 @@ framework route and decoding failures retain Servant's response format.
|
|||
| Surface | Contract |
|
||||
| --- | --- |
|
||||
| Runtime | `GET /healthz` reports process liveness. `GET /readyz` reports declared traffic safety. |
|
||||
| Browser | Public `GET /nuthatch.js`, `GET /login`, and `GET /oidc/callback`; authenticated `GET /app` and `/assets/*`; and CSRF-protected authenticated `POST /logout` implement the browser session boundary. |
|
||||
| Legacy product | Authenticated `GET /`, `GET /download`, `POST /upload`, `POST /delete`, and `POST /delete-prefix` remain supported. |
|
||||
| Browser | Public `GET /login` and `GET /oidc/callback`; authenticated `GET /app` and `/assets/*`; and CSRF-protected authenticated `POST /logout` implement the browser session boundary. |
|
||||
| Programmatic product | Authenticated `/api/session`, `/api/browse`, `/api/download`, `/api/upload`, `/api/delete`, `/api/delete-plan`, and `/api/delete-batch` expose the JSON product surface. |
|
||||
|
||||
Product paths are valid UTF-8, NFC-normalized, relative, and canonical. The
|
||||
|
|
|
|||
|
|
@ -113,7 +113,6 @@ library
|
|||
Nuthatch.Session
|
||||
Nuthatch.Streaming
|
||||
Nuthatch.Types
|
||||
Nuthatch.Ui
|
||||
|
||||
executable nuthatch
|
||||
import: shared
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ module Nuthatch.Api
|
|||
( Api
|
||||
, HealthEndpoint
|
||||
, ReadyEndpoint
|
||||
, BrowserScriptEndpoint
|
||||
, AppEndpoint
|
||||
, StaticAssetsEndpoint
|
||||
, ProductApi
|
||||
|
|
@ -12,17 +11,10 @@ module Nuthatch.Api
|
|||
, LoginSuccess
|
||||
, LogoutEndpoint
|
||||
, LogoutSuccess
|
||||
, BrowseEndpoint
|
||||
, DownloadEndpoint
|
||||
, DownloadSuccess
|
||||
, UploadEndpoint
|
||||
, DeleteEndpoint
|
||||
, DeletePrefixEndpoint
|
||||
, api
|
||||
, HTML
|
||||
, JavaScript
|
||||
, HtmlPage (..)
|
||||
, BrowserScript (..)
|
||||
, SessionContext (..)
|
||||
, SessionAuth
|
||||
, BrowsePrefix (..)
|
||||
|
|
@ -32,10 +24,7 @@ module Nuthatch.Api
|
|||
, HealthResponse (..)
|
||||
, ReadyResponse (..)
|
||||
, BrowseResponse (..)
|
||||
, BrowseView (..)
|
||||
, DeleteRequest (..)
|
||||
, DeletePrefixRequest (..)
|
||||
, DeletePrefixResponse (..)
|
||||
, SessionResponse (..)
|
||||
, DeletePlanResponse (..)
|
||||
, DeleteBatchItem (..)
|
||||
|
|
@ -81,26 +70,15 @@ import Servant.Server (HasServer (..))
|
|||
import Servant.Server.Experimental.Auth (AuthServerData)
|
||||
|
||||
data HTML
|
||||
data JavaScript
|
||||
|
||||
instance Accept HTML where
|
||||
contentType _ = "text" // "html" /: ("charset", "utf-8")
|
||||
|
||||
instance Accept JavaScript where
|
||||
contentType _ = "text" // "javascript" /: ("charset", "utf-8")
|
||||
|
||||
newtype HtmlPage = HtmlPage { unHtmlPage :: Text }
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
instance MimeRender HTML HtmlPage where
|
||||
mimeRender _ = BL.fromStrict . TE.encodeUtf8 . unHtmlPage
|
||||
|
||||
newtype BrowserScript = BrowserScript { unBrowserScript :: Text }
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
instance MimeRender JavaScript BrowserScript where
|
||||
mimeRender _ = BL.fromStrict . TE.encodeUtf8 . unBrowserScript
|
||||
|
||||
data SessionContext = SessionContext
|
||||
{ sessionUser :: SessionUser
|
||||
, sessionCsrf :: Maybe CsrfToken
|
||||
|
|
@ -152,18 +130,6 @@ instance ToJSON BrowseResponse where
|
|||
, "partial" .= browsePartial response
|
||||
]
|
||||
|
||||
data BrowseView = BrowseView
|
||||
{ browseViewHtml :: HtmlPage
|
||||
, browseViewJson :: BrowseResponse
|
||||
}
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
instance MimeRender HTML BrowseView where
|
||||
mimeRender proxy = mimeRender proxy . browseViewHtml
|
||||
|
||||
instance ToJSON BrowseView where
|
||||
toJSON = toJSON . browseViewJson
|
||||
|
||||
data DeleteRequest = DeleteRequest
|
||||
{ deleteRoot :: Root
|
||||
, deletePath :: RelativePath
|
||||
|
|
@ -180,28 +146,6 @@ instance FromJSON DeleteRequest where
|
|||
<*> o .: "operation_id"
|
||||
<*> o .: "csrf"
|
||||
|
||||
data DeletePrefixRequest = DeletePrefixRequest
|
||||
{ deletePrefixRoot :: Root
|
||||
, deletePrefixPath :: RelativePath
|
||||
, deletePrefixCsrf :: CsrfToken
|
||||
}
|
||||
deriving stock (Eq)
|
||||
|
||||
instance FromJSON DeletePrefixRequest where
|
||||
parseJSON = withObject "DeletePrefixRequest" $ \o ->
|
||||
DeletePrefixRequest
|
||||
<$> o .: "root"
|
||||
<*> o .: "path"
|
||||
<*> o .: "csrf"
|
||||
|
||||
newtype DeletePrefixResponse = DeletePrefixResponse
|
||||
{ deletedObjectCount :: Int
|
||||
}
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
instance ToJSON DeletePrefixResponse where
|
||||
toJSON response = object ["deleted" .= deletedObjectCount response]
|
||||
|
||||
data UploadResponse = UploadResponse
|
||||
{ uploadResponseOperationId :: OperationId
|
||||
, uploadResponseResult :: MutationResult
|
||||
|
|
@ -462,9 +406,6 @@ type HealthEndpoint =
|
|||
type ReadyEndpoint =
|
||||
"readyz" :> Throws ReadyError :> Get '[JSON] ReadyResponse
|
||||
|
||||
type BrowserScriptEndpoint =
|
||||
"nuthatch.js" :> Get '[JavaScript] BrowserScript
|
||||
|
||||
type AppEndpoint =
|
||||
"app"
|
||||
:> AuthProtect SessionAuth
|
||||
|
|
@ -524,16 +465,6 @@ type LogoutEndpoint =
|
|||
:> Header' '[Required, Strict] "X-CSRF-Token" CsrfToken
|
||||
:> Post '[JSON] LogoutSuccess
|
||||
|
||||
type BrowseEndpoint =
|
||||
AuthProtect SessionAuth
|
||||
:> Throws AuthenticationError
|
||||
:> Throws BrowseError
|
||||
:> QueryParam "root" Root
|
||||
:> QueryParam "prefix" BrowsePrefix
|
||||
:> QueryParam "q" SearchQuery
|
||||
:> QueryParam "continuation" PaginationToken
|
||||
:> Get '[HTML, JSON] BrowseView
|
||||
|
||||
type DownloadSuccess =
|
||||
Headers
|
||||
'[ Header "Content-Disposition" Text
|
||||
|
|
@ -543,46 +474,6 @@ type DownloadSuccess =
|
|||
]
|
||||
(SourceIO BS.ByteString)
|
||||
|
||||
type DownloadEndpoint =
|
||||
"download"
|
||||
:> AuthProtect SessionAuth
|
||||
:> Throws AuthenticationError
|
||||
:> Throws DownloadError
|
||||
:> QueryParam' '[Required, Strict] "root" Root
|
||||
:> QueryParam' '[Required, Strict] "path" RelativePath
|
||||
:> Header "Range" Text
|
||||
:> StreamGet NoFraming OctetStream DownloadSuccess
|
||||
|
||||
type UploadEndpoint =
|
||||
"upload"
|
||||
:> AuthProtect SessionAuth
|
||||
:> Throws AuthenticationError
|
||||
:> Throws UploadError
|
||||
:> QueryParam' '[Required, Strict] "root" Root
|
||||
:> QueryParam' '[Required, Strict] "path" RelativePath
|
||||
:> QueryParam' '[Required, Strict] "operation_id" OperationId
|
||||
:> Header' '[Required, Strict] "X-Declared-Size" DeclaredSize
|
||||
:> Header' '[Required, Strict] "X-Content-SHA256" ContentSha256
|
||||
:> Header' '[Required, Strict] "X-CSRF-Token" CsrfToken
|
||||
:> StreamBody NoFraming OctetStream (SourceIO BS.ByteString)
|
||||
:> Post '[JSON] UploadResponse
|
||||
|
||||
type DeleteEndpoint =
|
||||
"delete"
|
||||
:> AuthProtect SessionAuth
|
||||
:> Throws AuthenticationError
|
||||
:> Throws DeleteError
|
||||
:> ReqBody '[JSON] DeleteRequest
|
||||
:> Post '[JSON] DeleteResponse
|
||||
|
||||
type DeletePrefixEndpoint =
|
||||
"delete-prefix"
|
||||
:> AuthProtect SessionAuth
|
||||
:> Throws AuthenticationError
|
||||
:> Throws DeletePrefixError
|
||||
:> ReqBody '[JSON] DeletePrefixRequest
|
||||
:> Post '[JSON] DeletePrefixResponse
|
||||
|
||||
type ProductApi =
|
||||
"api" :>
|
||||
( "session"
|
||||
|
|
@ -597,7 +488,7 @@ type ProductApi =
|
|||
:> QueryParam "prefix" BrowsePrefix
|
||||
:> QueryParam "q" SearchQuery
|
||||
:> QueryParam "continuation" PaginationToken
|
||||
:> Get '[JSON] BrowseView
|
||||
:> Get '[JSON] BrowseResponse
|
||||
:<|> "download"
|
||||
:> AuthProtect SessionAuth
|
||||
:> Throws AuthenticationError
|
||||
|
|
@ -643,17 +534,11 @@ type ProductApi =
|
|||
type Api =
|
||||
HealthEndpoint
|
||||
:<|> ReadyEndpoint
|
||||
:<|> BrowserScriptEndpoint
|
||||
:<|> AppEndpoint
|
||||
:<|> StaticAssetsEndpoint
|
||||
:<|> LoginEndpoint
|
||||
:<|> OidcCallbackEndpoint
|
||||
:<|> LogoutEndpoint
|
||||
:<|> BrowseEndpoint
|
||||
:<|> DownloadEndpoint
|
||||
:<|> UploadEndpoint
|
||||
:<|> DeleteEndpoint
|
||||
:<|> DeletePrefixEndpoint
|
||||
:<|> ProductApi
|
||||
|
||||
api :: Proxy Api
|
||||
|
|
|
|||
|
|
@ -94,7 +94,6 @@ import Nuthatch.Streaming
|
|||
, UploadStreamError (..)
|
||||
)
|
||||
import Nuthatch.Types
|
||||
import Nuthatch.Ui (browserScript, productPage)
|
||||
import Servant
|
||||
import Servant.Server.Experimental.Auth
|
||||
( AuthHandler
|
||||
|
|
@ -157,17 +156,11 @@ server :: Env -> Server Api
|
|||
server env =
|
||||
pure (HealthResponse True)
|
||||
:<|> handleReady env
|
||||
:<|> pure (BrowserScript browserScript)
|
||||
:<|> handleAppRequest
|
||||
:<|> staticAssetsServer
|
||||
:<|> handleLoginRequest env
|
||||
:<|> handleOidcCallback env
|
||||
:<|> handleLogoutRequest env
|
||||
:<|> handleBrowseRequest env
|
||||
:<|> handleDownloadRequest env
|
||||
:<|> handleUploadRequest env
|
||||
:<|> handleDeleteRequest env
|
||||
:<|> handleDeletePrefixRequest env
|
||||
:<|>
|
||||
( handleSessionRequest
|
||||
:<|> handleBrowseRequest env
|
||||
|
|
@ -396,7 +389,7 @@ handleBrowseRequest
|
|||
-> Maybe BrowsePrefix
|
||||
-> Maybe SearchQuery
|
||||
-> Maybe PaginationToken
|
||||
-> Handler BrowseView
|
||||
-> Handler BrowseResponse
|
||||
handleBrowseRequest env session selectedRoot selectedPrefix search continuation = do
|
||||
let user = sessionUser session
|
||||
root = fromMaybe Personal selectedRoot
|
||||
|
|
@ -414,14 +407,10 @@ handleBrowseRequest env session selectedRoot selectedPrefix search continuation
|
|||
(throwOperationError status502Error . BrowseObjectStoreFailed)
|
||||
pure
|
||||
result
|
||||
let response = BrowseResponse
|
||||
{ browseEntries = entries
|
||||
, browseContinuation = PaginationToken <$> nextContinuation
|
||||
, browsePartial = partial
|
||||
}
|
||||
pure BrowseView
|
||||
{ browseViewHtml = HtmlPage (productPage entries)
|
||||
, browseViewJson = response
|
||||
pure BrowseResponse
|
||||
{ browseEntries = entries
|
||||
, browseContinuation = PaginationToken <$> nextContinuation
|
||||
, browsePartial = partial
|
||||
}
|
||||
|
||||
handleDownloadRequest
|
||||
|
|
@ -624,22 +613,6 @@ handleDeleteRequest env session request = do
|
|||
cfgIdempotencyWindowSec (envConfig env)
|
||||
}
|
||||
|
||||
handleDeletePrefixRequest
|
||||
:: Env
|
||||
-> SessionContext
|
||||
-> DeletePrefixRequest
|
||||
-> Handler DeletePrefixResponse
|
||||
handleDeletePrefixRequest env session request = do
|
||||
requireCsrf "delete_prefix" DeletePrefixCsrfRejected session (deletePrefixCsrf request)
|
||||
liftIO $ audit "delete_prefix" "rejected"
|
||||
(Just (unUserId (suUserId (sessionUser session))))
|
||||
([ ("root", A.toJSON (rootLabel (deletePrefixRoot request)))
|
||||
, ("reason", A.String "paged_delete_required")
|
||||
] <> objectKeyAudit (envConfig env) (deletePrefixPath request))
|
||||
throwOperationError err409
|
||||
(DeletePrefixMutationRejected
|
||||
"use delete-plan and delete-batch with client operation ids")
|
||||
|
||||
handleDeletePlanRequest
|
||||
:: Env
|
||||
-> SessionContext
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
module Nuthatch.Ui
|
||||
( browserScript
|
||||
, productPage
|
||||
, htmlResponse
|
||||
, escapeHtml
|
||||
) where
|
||||
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Lazy (fromStrict)
|
||||
import qualified Data.Text.Lazy.Encoding as TLE
|
||||
import Network.HTTP.Types (Status, hContentType)
|
||||
import Network.Wai (Response, responseLBS)
|
||||
import Nuthatch.Types
|
||||
|
||||
htmlResponse :: Status -> Text -> Response
|
||||
htmlResponse status body =
|
||||
responseLBS status
|
||||
[(hContentType, "text/html; charset=utf-8")]
|
||||
(TLE.encodeUtf8 (fromStrict body))
|
||||
|
||||
escapeHtml :: Text -> Text
|
||||
escapeHtml =
|
||||
T.concatMap $ \case
|
||||
'&' -> "&"
|
||||
'<' -> "<"
|
||||
'>' -> ">"
|
||||
'"' -> """
|
||||
'\'' -> "'"
|
||||
c -> T.singleton c
|
||||
|
||||
productPage :: [ListEntry] -> Text
|
||||
productPage entries =
|
||||
T.unlines
|
||||
[ "<!doctype html>"
|
||||
, "<html lang=\"en\"><head>"
|
||||
, "<meta charset=\"utf-8\">"
|
||||
, "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">"
|
||||
, "<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'self'; object-src 'none'; base-uri 'none'\">"
|
||||
, "<title>Nuthatch</title>"
|
||||
, "</head><body><main>"
|
||||
, "<h1>Nuthatch</h1>"
|
||||
, "<p>The browser application has moved to <a href=\"/app\">/app</a>.</p>"
|
||||
, "<ul>"
|
||||
, T.concat
|
||||
[ "<li>" <> escapeHtml (leRelativePath entry) <> "</li>"
|
||||
| entry <- entries
|
||||
]
|
||||
, "</ul>"
|
||||
, "</main></body></html>"
|
||||
]
|
||||
|
||||
browserScript :: Text
|
||||
browserScript =
|
||||
"const nuthatchApp = '/app'; if (window.location.pathname === '/') window.location.assign(nuthatchApp);"
|
||||
108
test/Spec.hs
108
test/Spec.hs
|
|
@ -13,6 +13,7 @@ import Control.Exception
|
|||
, throwIO
|
||||
, try
|
||||
)
|
||||
import Control.Monad (forM_)
|
||||
import Control.Monad.IO.Class (liftIO)
|
||||
import Control.Retry (simulatePolicy)
|
||||
import Control.Lens ((&), (?~))
|
||||
|
|
@ -129,7 +130,6 @@ import Nuthatch.Streaming
|
|||
, maximumMultipartBytes
|
||||
, multipartPartBytes
|
||||
)
|
||||
import Nuthatch.Ui (browserScript, productPage)
|
||||
import Servant
|
||||
import Servant.Server.Experimental.Auth (AuthHandler, mkAuthHandler)
|
||||
import Servant.Types.SourceT
|
||||
|
|
@ -623,8 +623,8 @@ apiTests = testGroup "typed HTTP boundary"
|
|||
("prefix_mutation_rejected" `BS.isInfixOf` BL.toStrict prefixError)
|
||||
, testCase "production authentication validates cookies and callback emits secure cookies"
|
||||
testAuthenticationBoundary
|
||||
, testCase "legacy browser entry directs safely to the static application"
|
||||
testBrowserBridge
|
||||
, testCase "only canonical product routes are exposed"
|
||||
testCanonicalProductRoutes
|
||||
, testCase "links derive from the type-level API" $
|
||||
show (linkURI (safeLink api (Proxy :: Proxy HealthEndpoint)))
|
||||
@?= "healthz"
|
||||
|
|
@ -648,7 +648,7 @@ testValidDecoding :: IO ()
|
|||
testValidDecoding = do
|
||||
calls <- newIORef 0
|
||||
response <- apiRequest calls methodGet
|
||||
"/?root=personal&prefix=docs%2Freports&q=quarterly&continuation=next"
|
||||
"/api/browse?root=personal&prefix=docs%2Freports&q=quarterly&continuation=next"
|
||||
[]
|
||||
""
|
||||
simpleStatus response @?= status200
|
||||
|
|
@ -657,16 +657,17 @@ testValidDecoding = do
|
|||
testInvalidDomains :: IO ()
|
||||
testInvalidDomains = do
|
||||
calls <- newIORef 0
|
||||
invalidRoot <- apiRequest calls methodGet "/?root=PERSONAL" [] ""
|
||||
invalidRoot <- apiRequest calls methodGet "/api/browse?root=PERSONAL" [] ""
|
||||
simpleStatus invalidRoot @?= status400
|
||||
invalidPath <- apiRequest calls methodGet "/?prefix=docs%2F..%2Fprivate" [] ""
|
||||
invalidPath <- apiRequest calls methodGet
|
||||
"/api/browse?prefix=docs%2F..%2Fprivate" [] ""
|
||||
simpleStatus invalidPath @?= status400
|
||||
readIORef calls >>= (@?= 0)
|
||||
|
||||
testMalformedUtf8 :: IO ()
|
||||
testMalformedUtf8 = do
|
||||
calls <- newIORef 0
|
||||
response <- apiRequest calls methodGet "/?q=%FF" [] ""
|
||||
response <- apiRequest calls methodGet "/api/browse?q=%FF" [] ""
|
||||
simpleStatus response @?= status400
|
||||
readIORef calls >>= (@?= 0)
|
||||
|
||||
|
|
@ -681,16 +682,17 @@ testUploadDecoding = do
|
|||
, ("X-CSRF-Token", csrf)
|
||||
]
|
||||
response <- apiRequest calls methodPost
|
||||
"/upload?root=personal&path=docs%2Ffile.txt&operation_id=bad%20id"
|
||||
"/api/upload?root=personal&path=docs%2Ffile.txt&operation_id=bad%20id"
|
||||
headers
|
||||
"body"
|
||||
simpleStatus response @?= status400
|
||||
invalidSize <- apiRequest calls methodPost
|
||||
"/upload?root=personal&path=docs%2Ffile.txt&operation_id=upload-1"
|
||||
"/api/upload?root=personal&path=docs%2Ffile.txt&operation_id=upload-1"
|
||||
(replaceHeader "X-Declared-Size" "zero" headers)
|
||||
"body"
|
||||
simpleStatus invalidSize @?= status400
|
||||
invalidPagination <- apiRequest calls methodGet "/?continuation=" [] ""
|
||||
invalidPagination <- apiRequest calls methodGet
|
||||
"/api/browse?continuation=" [] ""
|
||||
simpleStatus invalidPagination @?= status400
|
||||
readIORef calls >>= (@?= 0)
|
||||
|
||||
|
|
@ -705,7 +707,7 @@ testStreamingEndpoints = do
|
|||
, ("X-CSRF-Token", csrf)
|
||||
]
|
||||
upload <- apiRequest calls methodPost
|
||||
"/upload?root=personal&path=docs%2Ffile.txt&operation_id=upload-1"
|
||||
"/api/upload?root=personal&path=docs%2Ffile.txt&operation_id=upload-1"
|
||||
headers
|
||||
"body"
|
||||
simpleStatus upload @?= status200
|
||||
|
|
@ -713,7 +715,7 @@ testStreamingEndpoints = do
|
|||
("\"idempotency_window_seconds\":3600"
|
||||
`BS.isInfixOf` BL.toStrict (simpleBody upload))
|
||||
download <- apiRequest calls methodGet
|
||||
"/download?root=shared&path=docs%2Ffile.txt"
|
||||
"/api/download?root=shared&path=docs%2Ffile.txt"
|
||||
[]
|
||||
""
|
||||
simpleStatus download @?= status200
|
||||
|
|
@ -735,10 +737,10 @@ testOperationRepresentations = do
|
|||
[("Cookie", "nuthatch_oidc=transaction")]
|
||||
""
|
||||
simpleStatus callback @?= status303
|
||||
legacyLogin <- apiRequest calls methodPost "/login" [] ""
|
||||
simpleStatus legacyLogin @?= status405
|
||||
wrongLoginMethod <- apiRequest calls methodPost "/login" [] ""
|
||||
simpleStatus wrongLoginMethod @?= status405
|
||||
deletion <- apiRequest calls methodPost
|
||||
"/delete"
|
||||
"/api/delete"
|
||||
[("Content-Type", "application/json")]
|
||||
"{\"root\":\"shared\",\"path\":\"docs/file.txt\",\"operation_id\":\"delete-1\",\"csrf\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}"
|
||||
simpleStatus deletion @?= status200
|
||||
|
|
@ -748,7 +750,7 @@ testOperationRepresentations = do
|
|||
("\"idempotency_window_seconds\":3600"
|
||||
`BS.isInfixOf` BL.toStrict (simpleBody deletion))
|
||||
malformed <- apiRequest calls methodPost
|
||||
"/delete"
|
||||
"/api/delete"
|
||||
[("Content-Type", "application/json")]
|
||||
"{\"root\":\"shared\",\"path\":\"../escape\"}"
|
||||
simpleStatus malformed @?= status400
|
||||
|
|
@ -815,7 +817,6 @@ typedTestServer :: IORef Int -> Server Api
|
|||
typedTestServer calls =
|
||||
counted (HealthResponse True)
|
||||
:<|> counted (ReadyResponse True)
|
||||
:<|> counted (BrowserScript "script")
|
||||
:<|> (\_ -> counted (addHeader "default-src 'none'" (HtmlPage "app")))
|
||||
:<|> (\_ -> Tagged $ \_ sendResponse ->
|
||||
sendResponse (responseLBS status404 [] ""))
|
||||
|
|
@ -831,29 +832,6 @@ typedTestServer calls =
|
|||
(addHeader "session=expired"
|
||||
(addHeader "csrf=expired"
|
||||
(addHeader "transaction=expired" NoContent))))
|
||||
:<|> (\_ _ _ _ _ -> counted
|
||||
(BrowseView
|
||||
(HtmlPage "browse")
|
||||
BrowseResponse
|
||||
{ browseEntries = []
|
||||
, browseContinuation = Nothing
|
||||
, browsePartial = False
|
||||
}))
|
||||
:<|> (\_ _ _ _ -> counted
|
||||
(addHeader "attachment; filename=\"file.txt\""
|
||||
(addHeader "nosniff"
|
||||
(addHeader "sandbox" (addHeader 4 (source ["body"]))))))
|
||||
:<|> (\_ _ _ operation _ _ _ _ -> counted UploadResponse
|
||||
{ uploadResponseOperationId = operation
|
||||
, uploadResponseResult = MutationSucceeded
|
||||
, uploadResponseIdempotencyWindowSeconds = 3600
|
||||
})
|
||||
:<|> (\_ request -> counted DeleteResponse
|
||||
{ deleteResponseOperationId = deleteOperationId request
|
||||
, deleteResponseResult = MutationSucceeded
|
||||
, deleteResponseIdempotencyWindowSeconds = 3600
|
||||
})
|
||||
:<|> (\_ _ -> counted (DeletePrefixResponse 1))
|
||||
:<|>
|
||||
( (\session -> counted SessionResponse
|
||||
{ sessionResponseUserId = suUserId (sessionUser session)
|
||||
|
|
@ -883,13 +861,11 @@ typedTestServer calls =
|
|||
counted :: a -> Handler a
|
||||
counted value = liftIO (modifyIORef' calls (+ 1)) >> pure value
|
||||
emptyBrowse =
|
||||
BrowseView
|
||||
(HtmlPage "browse")
|
||||
BrowseResponse
|
||||
{ browseEntries = []
|
||||
, browseContinuation = Nothing
|
||||
, browsePartial = False
|
||||
}
|
||||
BrowseResponse
|
||||
{ browseEntries = []
|
||||
, browseContinuation = Nothing
|
||||
, browsePartial = False
|
||||
}
|
||||
downloadResponse =
|
||||
addHeader "attachment; filename=\"file.txt\"" $
|
||||
addHeader "nosniff" $
|
||||
|
|
@ -932,7 +908,7 @@ testAuthenticationBoundary = do
|
|||
"nuthatch_session=" <> TE.encodeUtf8 (unSessionToken token)
|
||||
<> "; nuthatch_csrf=" <> TE.encodeUtf8 (unCsrfToken csrf)
|
||||
authenticated <- apiRequestWith app methodGet
|
||||
"/?root=personal"
|
||||
"/api/browse?root=personal"
|
||||
[("Cookie", cookie)]
|
||||
""
|
||||
simpleStatus authenticated @?= status200
|
||||
|
|
@ -940,7 +916,7 @@ testAuthenticationBoundary = do
|
|||
|
||||
writeIORef calls 0
|
||||
malformed <- apiRequestWith app methodGet
|
||||
"/?root=personal"
|
||||
"/api/browse?root=personal"
|
||||
[("Cookie", "nuthatch_session=" <> BS.singleton 255
|
||||
<> "; nuthatch_csrf=" <> TE.encodeUtf8 (unCsrfToken csrf))]
|
||||
""
|
||||
|
|
@ -948,7 +924,7 @@ testAuthenticationBoundary = do
|
|||
readIORef calls >>= (@?= 0)
|
||||
|
||||
malformedGrammar <- apiRequestWith app methodGet
|
||||
"/?root=personal"
|
||||
"/api/browse?root=personal"
|
||||
[("Cookie", "nuthatch_session")]
|
||||
""
|
||||
simpleStatus malformedGrammar @?= status400
|
||||
|
|
@ -961,33 +937,33 @@ testAuthenticationBoundary = do
|
|||
| otherwise -> prefix <> "a"
|
||||
Nothing -> "invalid"
|
||||
tampered <- apiRequestWith app methodGet
|
||||
"/?root=personal"
|
||||
"/api/browse?root=personal"
|
||||
[("Cookie", "nuthatch_session=" <> TE.encodeUtf8 tamperedToken)]
|
||||
""
|
||||
simpleStatus tampered @?= status303
|
||||
readIORef calls >>= (@?= 0)
|
||||
|
||||
missingCsrf <- apiRequestWith app methodGet
|
||||
"/?root=personal"
|
||||
"/api/browse?root=personal"
|
||||
[("Cookie", "nuthatch_session=" <> TE.encodeUtf8 (unSessionToken token))]
|
||||
""
|
||||
simpleStatus missingCsrf @?= status200
|
||||
readIORef calls >>= (@?= 1)
|
||||
|
||||
testBrowserBridge :: IO ()
|
||||
testBrowserBridge = do
|
||||
let entry = ListEntry
|
||||
{ leName = "folder"
|
||||
, leIsPrefix = True
|
||||
, leSize = Nothing
|
||||
, leRelativePath = "folder<script>"
|
||||
}
|
||||
browse = productPage [entry]
|
||||
assertBool "legacy page directs browsers to the static application"
|
||||
("/app" `T.isInfixOf` browse && "/app" `T.isInfixOf` browserScript)
|
||||
assertBool "legacy page escapes object paths"
|
||||
("folder<script>" `T.isInfixOf` browse
|
||||
&& not ("folder<script>" `T.isInfixOf` browse))
|
||||
testCanonicalProductRoutes :: IO ()
|
||||
testCanonicalProductRoutes = do
|
||||
calls <- newIORef 0
|
||||
forM_
|
||||
[ (methodGet, "/")
|
||||
, (methodGet, "/nuthatch.js")
|
||||
, (methodGet, "/download?root=personal&path=file")
|
||||
, (methodPost, "/upload")
|
||||
, (methodPost, "/delete")
|
||||
, (methodPost, "/delete-prefix")
|
||||
] $ \(method, path) -> do
|
||||
response <- apiRequest calls method path [] ""
|
||||
simpleStatus response @?= status404
|
||||
readIORef calls >>= (@?= 0)
|
||||
|
||||
productionAuthTestApplication :: Config -> IORef Int -> Application
|
||||
productionAuthTestApplication cfg calls =
|
||||
|
|
|
|||
|
|
@ -278,17 +278,6 @@ spec:
|
|||
grep -F '"ok":true'
|
||||
curl --insecure --fail --silent --show-error "$base/readyz" |
|
||||
grep -F '"ready":true'
|
||||
curl --insecure --fail --silent --show-error "$base/nuthatch.js" |
|
||||
grep -F 'nuthatch'
|
||||
{
|
||||
curl --insecure --fail --silent --show-error "$base/nuthatch.js"
|
||||
curl --insecure --silent --show-error "$base/login"
|
||||
} > /tmp/browser-visible
|
||||
if grep -Ei \
|
||||
'NUTHATCH_S3|AWS_ACCESS_KEY|AWS_SECRET_ACCESS_KEY|client_secret' \
|
||||
/tmp/browser-visible; then
|
||||
exit 1
|
||||
fi
|
||||
test "$(
|
||||
curl --insecure --silent --output /dev/null \
|
||||
--write-out '%{http_code}' \
|
||||
|
|
@ -299,11 +288,11 @@ spec:
|
|||
curl --insecure --silent --output /dev/null \
|
||||
--write-out '%{http_code}' \
|
||||
"$base/"
|
||||
)" = 303
|
||||
)" = 404
|
||||
test "$(
|
||||
curl --insecure --silent --output /dev/null \
|
||||
--write-out '%{http_code}' \
|
||||
"$base/download?root=personal&path=unauthenticated"
|
||||
"$base/api/download?root=personal&path=unauthenticated"
|
||||
)" = 303
|
||||
unauthenticated_digest="$(
|
||||
printf x | sha256sum | cut -d' ' -f1
|
||||
|
|
@ -315,14 +304,14 @@ spec:
|
|||
--header "X-Content-SHA256: $unauthenticated_digest" \
|
||||
--header 'X-CSRF-Token: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' \
|
||||
--data x \
|
||||
"$base/upload?root=personal&path=unauthenticated&operation_id=40000000-0000-4000-8000-000000000001"
|
||||
"$base/api/upload?root=personal&path=unauthenticated&operation_id=40000000-0000-4000-8000-000000000001"
|
||||
)" = 303
|
||||
test "$(
|
||||
curl --insecure --silent --output /dev/null \
|
||||
--write-out '%{http_code}' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{"root":"personal","path":"unauthenticated","operation_id":"40000000-0000-4000-8000-000000000002","csrf":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}' \
|
||||
"$base/delete"
|
||||
"$base/api/delete"
|
||||
)" = 303
|
||||
test "$(
|
||||
curl --insecure --silent --output /dev/null \
|
||||
|
|
@ -330,7 +319,7 @@ spec:
|
|||
--header 'Content-Type: application/json' \
|
||||
--data '{"root":"personal","path":"unauthenticated","csrf":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}' \
|
||||
"$base/delete-prefix"
|
||||
)" = 303
|
||||
)" = 404
|
||||
test "$(
|
||||
curl --insecure --silent --output /dev/null \
|
||||
--write-out '%{http_code}' \
|
||||
|
|
@ -348,14 +337,14 @@ spec:
|
|||
curl --insecure --silent --output /tmp/authenticated.body \
|
||||
--write-out '%{http_code}' \
|
||||
--cookie "$cookies" \
|
||||
"$base/"
|
||||
"$base/api/browse"
|
||||
)"
|
||||
printf 'Authenticated browse status=%s\n' "$authenticated_status"
|
||||
test "$authenticated_status" = 200
|
||||
curl --insecure --fail --silent --show-error \
|
||||
--cookie "$cookies" \
|
||||
--header 'Accept: application/json' \
|
||||
"$base/" >/tmp/authenticated.json
|
||||
"$base/api/browse" >/tmp/authenticated.json
|
||||
if grep -Ei \
|
||||
'NUTHATCH_S3|AWS_ACCESS_KEY|AWS_SECRET_ACCESS_KEY|client_secret' \
|
||||
/tmp/authenticated.body /tmp/authenticated.json; then
|
||||
|
|
@ -514,6 +503,11 @@ spec:
|
|||
/tmp/product-asset.headers >/dev/null
|
||||
grep -i '^x-content-type-options: *nosniff' \
|
||||
/tmp/product-asset.headers >/dev/null
|
||||
if grep -Ei \
|
||||
'NUTHATCH_S3|AWS_ACCESS_KEY|AWS_SECRET_ACCESS_KEY|client_secret' \
|
||||
/tmp/product-app.html /tmp/product-asset.js; then
|
||||
exit 1
|
||||
fi
|
||||
for marker in \
|
||||
'Drop a file here' \
|
||||
'Shared root' \
|
||||
|
|
@ -768,7 +762,7 @@ spec:
|
|||
first_size="$(wc -c </tmp/epoch-one | tr -d ' ')"
|
||||
first_digest="$(sha256sum /tmp/epoch-one | cut -d' ' -f1)"
|
||||
operation=73000000-0000-4000-8000-000000000001
|
||||
mutation_url="$base/upload?root=personal&path=idempotency.txt&operation_id=$operation"
|
||||
mutation_url="$base/api/upload?root=personal&path=idempotency.txt&operation_id=$operation"
|
||||
pod_curl "$NUTHATCH_IDEMPOTENCY_IP_A" \
|
||||
--fail --silent --show-error \
|
||||
--cookie "$cookies" \
|
||||
|
|
@ -880,7 +874,7 @@ spec:
|
|||
pod_curl "$NUTHATCH_IDEMPOTENCY_IP_A" \
|
||||
--fail --silent --show-error \
|
||||
--cookie "$cookies" \
|
||||
"$base/download?root=personal&path=idempotency.txt" \
|
||||
"$base/api/download?root=personal&path=idempotency.txt" \
|
||||
>/tmp/epoch-download
|
||||
cmp "$epoch_two_body" /tmp/epoch-download
|
||||
|
||||
|
|
@ -890,7 +884,7 @@ spec:
|
|||
sha256sum /tmp/active-body | cut -d' ' -f1
|
||||
)"
|
||||
active_operation=73000000-0000-4000-8000-000000000002
|
||||
active_url="$base/upload?root=personal&path=active-window.bin&operation_id=$active_operation"
|
||||
active_url="$base/api/upload?root=personal&path=active-window.bin&operation_id=$active_operation"
|
||||
(
|
||||
dd if=/tmp/active-body bs=65536 count=1 status=none
|
||||
sleep 5
|
||||
|
|
@ -929,7 +923,7 @@ spec:
|
|||
--header "X-Content-SHA256: $first_digest" \
|
||||
--header "X-CSRF-Token: $csrf" \
|
||||
--data-binary @/tmp/epoch-one \
|
||||
"$base/upload?root=personal&path=active-window.bin&operation_id=73000000-0000-4000-8000-000000000003"
|
||||
"$base/api/upload?root=personal&path=active-window.bin&operation_id=73000000-0000-4000-8000-000000000003"
|
||||
)"
|
||||
printf 'Active key lock status=%s body=' "$lock_status"
|
||||
cat /tmp/active-lock.json
|
||||
|
|
@ -983,7 +977,7 @@ spec:
|
|||
--header "X-CSRF-Token: $csrf" \
|
||||
--request POST \
|
||||
--upload-file - \
|
||||
"$base/upload?root=personal&path=streaming-large.bin&operation_id=70000000-0000-4000-8000-000000000001"
|
||||
"$base/api/upload?root=personal&path=streaming-large.bin&operation_id=70000000-0000-4000-8000-000000000001"
|
||||
)"
|
||||
printf 'Large upload status=%s\n' "$status"
|
||||
test "$status" = 200
|
||||
|
|
@ -992,7 +986,7 @@ spec:
|
|||
download_digest="$(
|
||||
curl --fail --silent --show-error \
|
||||
--cookie "$cookies" \
|
||||
"$base/download?root=personal&path=streaming-large.bin" |
|
||||
"$base/api/download?root=personal&path=streaming-large.bin" |
|
||||
sha256sum |
|
||||
cut -d' ' -f1
|
||||
)"
|
||||
|
|
@ -1013,7 +1007,7 @@ spec:
|
|||
--header "X-Content-SHA256: $large_digest" \
|
||||
--header "X-CSRF-Token: $csrf" \
|
||||
--data-binary @/tmp/tiny \
|
||||
"$base/upload?root=personal&path=streaming-large.bin&operation_id=70000000-0000-4000-8000-000000000007"
|
||||
"$base/api/upload?root=personal&path=streaming-large.bin&operation_id=70000000-0000-4000-8000-000000000007"
|
||||
)"
|
||||
test "$status" = 200
|
||||
grep -F '"result":"succeeded"' \
|
||||
|
|
@ -1029,7 +1023,7 @@ spec:
|
|||
--header "X-Content-SHA256: $tiny_digest" \
|
||||
--header "X-CSRF-Token: $csrf" \
|
||||
--data-binary @/tmp/tiny \
|
||||
"$base/upload?root=personal&path=size-mismatch.bin&operation_id=70000000-0000-4000-8000-000000000002"
|
||||
"$base/api/upload?root=personal&path=size-mismatch.bin&operation_id=70000000-0000-4000-8000-000000000002"
|
||||
)"
|
||||
printf 'Size mismatch status=%s body=' "$status"
|
||||
cat /tmp/size-mismatch.json
|
||||
|
|
@ -1046,7 +1040,7 @@ spec:
|
|||
--header "X-Content-SHA256: $(printf '%064d' 0)" \
|
||||
--header "X-CSRF-Token: $csrf" \
|
||||
--data-binary @/tmp/tiny \
|
||||
"$base/upload?root=personal&path=identity-mismatch.bin&operation_id=70000000-0000-4000-8000-000000000003"
|
||||
"$base/api/upload?root=personal&path=identity-mismatch.bin&operation_id=70000000-0000-4000-8000-000000000003"
|
||||
)"
|
||||
printf 'Identity mismatch status=%s body=' "$status"
|
||||
cat /tmp/identity-mismatch.json
|
||||
|
|
@ -1069,7 +1063,7 @@ spec:
|
|||
--header "X-CSRF-Token: $csrf" \
|
||||
--request POST \
|
||||
--upload-file - \
|
||||
"$base/upload?root=personal&path=too-large.bin&operation_id=70000000-0000-4000-8000-000000000004"
|
||||
"$base/api/upload?root=personal&path=too-large.bin&operation_id=70000000-0000-4000-8000-000000000004"
|
||||
)"
|
||||
printf 'Over-limit status=%s body=' "$status"
|
||||
cat /tmp/too-large.json
|
||||
|
|
@ -1089,7 +1083,7 @@ spec:
|
|||
--header "X-CSRF-Token: $csrf" \
|
||||
--request POST \
|
||||
--upload-file - \
|
||||
"$base/upload?root=personal&path=aborted-upload.bin&operation_id=70000000-0000-4000-8000-000000000005" \
|
||||
"$base/api/upload?root=personal&path=aborted-upload.bin&operation_id=70000000-0000-4000-8000-000000000005" \
|
||||
>/tmp/aborted-upload.json; then
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -1097,7 +1091,7 @@ spec:
|
|||
curl --silent --output /dev/null \
|
||||
--write-out '%{http_code}' \
|
||||
--cookie "$cookies" \
|
||||
"$base/download?root=personal&path=aborted-upload.bin"
|
||||
"$base/api/download?root=personal&path=aborted-upload.bin"
|
||||
)"
|
||||
printf 'Aborted upload lookup status=%s\n' \
|
||||
"$aborted_upload_status"
|
||||
|
|
@ -1112,7 +1106,7 @@ spec:
|
|||
--header "X-Content-SHA256: $large_digest" \
|
||||
--header "X-CSRF-Token: $csrf" \
|
||||
--data-binary @/tmp/tiny \
|
||||
"$base/upload?root=personal&path=aborted-upload.bin&operation_id=70000000-0000-4000-8000-000000000005"
|
||||
"$base/api/upload?root=personal&path=aborted-upload.bin&operation_id=70000000-0000-4000-8000-000000000005"
|
||||
)"
|
||||
printf 'Aborted upload replay status=%s body=' "$status"
|
||||
cat /tmp/aborted-upload-replay.json
|
||||
|
|
@ -1132,14 +1126,14 @@ spec:
|
|||
--header "X-CSRF-Token: $csrf" \
|
||||
--request POST \
|
||||
--upload-file - \
|
||||
"$base/upload?root=personal&path=streaming-large.bin&operation_id=70000000-0000-4000-8000-000000000006" \
|
||||
"$base/api/upload?root=personal&path=streaming-large.bin&operation_id=70000000-0000-4000-8000-000000000006" \
|
||||
>/tmp/aborted-overwrite.json; then
|
||||
exit 1
|
||||
fi
|
||||
preserved_digest="$(
|
||||
curl --fail --silent --show-error \
|
||||
--cookie "$cookies" \
|
||||
"$base/download?root=personal&path=streaming-large.bin" |
|
||||
"$base/api/download?root=personal&path=streaming-large.bin" |
|
||||
sha256sum |
|
||||
cut -d' ' -f1
|
||||
)"
|
||||
|
|
@ -1148,7 +1142,7 @@ spec:
|
|||
--max-time 1 \
|
||||
--limit-rate 64k \
|
||||
--cookie "$cookies" \
|
||||
"$base/download?root=personal&path=streaming-large.bin" \
|
||||
"$base/api/download?root=personal&path=streaming-large.bin" \
|
||||
>/tmp/aborted-download; then
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -1178,7 +1172,7 @@ spec:
|
|||
--header "X-Content-SHA256: $joint_digest" \
|
||||
--header "X-CSRF-Token: $csrf" \
|
||||
--data-binary @/tmp/joint-upload \
|
||||
"$base/upload?root=personal&path=joint-throttle.bin&operation_id=70000000-0000-4000-8000-000000000013"
|
||||
"$base/api/upload?root=personal&path=joint-throttle.bin&operation_id=70000000-0000-4000-8000-000000000013"
|
||||
)"
|
||||
test "$status" = 200
|
||||
grep -F '"result":"succeeded"' /tmp/joint-upload.json
|
||||
|
|
@ -1197,7 +1191,7 @@ spec:
|
|||
--header "X-Content-SHA256: $upload_digest" \
|
||||
--header "X-CSRF-Token: $csrf_b" \
|
||||
--data-binary @/tmp/throttle-upload \
|
||||
"$base/upload?root=personal&path=throttled-upload.bin&operation_id=71000000-0000-4000-8000-000000000001"
|
||||
"$base/api/upload?root=personal&path=throttled-upload.bin&operation_id=71000000-0000-4000-8000-000000000001"
|
||||
)"
|
||||
test "$status" = 429
|
||||
grep -F '"error":"bandwidth_exceeded"' /tmp/throttle-upload.json
|
||||
|
|
@ -1211,15 +1205,15 @@ spec:
|
|||
--header "X-Content-SHA256: $upload_digest" \
|
||||
--header "X-CSRF-Token: $csrf_b" \
|
||||
--data-binary @/tmp/tiny \
|
||||
"$base/upload?root=personal&path=prestart.bin&operation_id=71000000-0000-4000-8000-000000000002"
|
||||
"$base/api/upload?root=personal&path=prestart.bin&operation_id=71000000-0000-4000-8000-000000000002"
|
||||
)" = 429
|
||||
|
||||
curl --silent --show-error --cookie "$cookies" \
|
||||
"$base/download?root=personal&path=streaming-large.bin" \
|
||||
"$base/api/download?root=personal&path=streaming-large.bin" \
|
||||
>/tmp/parallel-a &
|
||||
first_pid=$!
|
||||
curl --silent --show-error --cookie "$cookies" \
|
||||
"$base/download?root=personal&path=streaming-large.bin" \
|
||||
"$base/api/download?root=personal&path=streaming-large.bin" \
|
||||
>/tmp/parallel-b &
|
||||
second_pid=$!
|
||||
first_ok=true
|
||||
|
|
@ -1244,7 +1238,7 @@ spec:
|
|||
curl --silent --output /tmp/prestart-download.json \
|
||||
--write-out '%{http_code}' \
|
||||
--cookie "$cookies" \
|
||||
"$base/download?root=personal&path=streaming-large.bin"
|
||||
"$base/api/download?root=personal&path=streaming-large.bin"
|
||||
)"
|
||||
printf 'Post-exhaustion download status=%s\n' "$prestart_status"
|
||||
test "$prestart_status" = 429
|
||||
|
|
@ -1275,7 +1269,7 @@ spec:
|
|||
--header "X-Content-SHA256: $refill_digest" \
|
||||
--header "X-CSRF-Token: $csrf" \
|
||||
--data-binary @/tmp/refill-upload \
|
||||
"$base/upload?root=personal&path=refill-$suffix.bin&operation_id=$refill_operation"
|
||||
"$base/api/upload?root=personal&path=refill-$suffix.bin&operation_id=$refill_operation"
|
||||
)"
|
||||
test "$status" = 200
|
||||
grep -F '"result":"succeeded"' "/tmp/refill-$suffix.json"
|
||||
|
|
@ -1289,7 +1283,7 @@ spec:
|
|||
--header "X-Content-SHA256: $refill_digest" \
|
||||
--header "X-CSRF-Token: $csrf" \
|
||||
--data-binary @/tmp/refill-upload \
|
||||
"$base/upload?root=personal&path=refill-exhausted.bin&operation_id=72000000-0000-4000-8000-000000000003"
|
||||
"$base/api/upload?root=personal&path=refill-exhausted.bin&operation_id=72000000-0000-4000-8000-000000000003"
|
||||
)" = 429
|
||||
fi
|
||||
done
|
||||
|
|
@ -1329,7 +1323,7 @@ spec:
|
|||
--header "X-Content-SHA256: $tiny_digest" \
|
||||
--header "X-CSRF-Token: $csrf" \
|
||||
--data-binary @/tmp/tiny \
|
||||
"$base/upload?root=personal&path=unavailable.bin&operation_id=72000000-0000-4000-8000-000000000001"
|
||||
"$base/api/upload?root=personal&path=unavailable.bin&operation_id=72000000-0000-4000-8000-000000000001"
|
||||
)" = 503
|
||||
grep -F '"error":"throttle_unavailable"' \
|
||||
/tmp/unavailable-upload.json
|
||||
|
|
@ -1337,7 +1331,7 @@ spec:
|
|||
curl --silent --output /tmp/unavailable-download.json \
|
||||
--write-out '%{http_code}' \
|
||||
--cookie "$cookies" \
|
||||
"$base/download?root=personal&path=$unavailable_path"
|
||||
"$base/api/download?root=personal&path=$unavailable_path"
|
||||
)" = 503
|
||||
grep -F '"error":"throttle_unavailable"' \
|
||||
/tmp/unavailable-download.json
|
||||
|
|
@ -1363,7 +1357,7 @@ spec:
|
|||
--header "X-Content-SHA256: $digest" \
|
||||
--header "X-CSRF-Token: $csrf" \
|
||||
--data-binary @/tmp/body \
|
||||
"$base/upload?root=$root&path=smoke-$suffix.txt&operation_id=00000000-0000-4000-8000-0000000000${suffix}"
|
||||
"$base/api/upload?root=$root&path=smoke-$suffix.txt&operation_id=00000000-0000-4000-8000-0000000000${suffix}"
|
||||
)"
|
||||
printf 'Upload %s status=%s\n' "$root" "$upload_status"
|
||||
test "$upload_status" = 200
|
||||
|
|
@ -1371,11 +1365,11 @@ spec:
|
|||
curl --fail --silent --show-error \
|
||||
--cookie "$cookies" \
|
||||
--header 'Accept: application/json' \
|
||||
"$base/?root=$root" |
|
||||
"$base/api/browse?root=$root" |
|
||||
grep -F "smoke-$suffix.txt"
|
||||
curl --fail --silent --show-error \
|
||||
--cookie "$cookies" \
|
||||
"$base/download?root=$root&path=smoke-$suffix.txt" >/tmp/download
|
||||
"$base/api/download?root=$root&path=smoke-$suffix.txt" >/tmp/download
|
||||
test "$(cat /tmp/download)" = "$first"
|
||||
|
||||
printf '%s' "$second" >/tmp/body
|
||||
|
|
@ -1388,22 +1382,22 @@ spec:
|
|||
--header "X-Content-SHA256: $digest" \
|
||||
--header "X-CSRF-Token: $csrf" \
|
||||
--data-binary @/tmp/body \
|
||||
"$base/upload?root=$root&path=smoke-$suffix.txt&operation_id=10000000-0000-4000-8000-0000000000${suffix}" |
|
||||
"$base/api/upload?root=$root&path=smoke-$suffix.txt&operation_id=10000000-0000-4000-8000-0000000000${suffix}" |
|
||||
grep -F '"result":"succeeded"'
|
||||
curl --fail --silent --show-error \
|
||||
--cookie "$cookies" \
|
||||
"$base/download?root=$root&path=smoke-$suffix.txt" >/tmp/download
|
||||
"$base/api/download?root=$root&path=smoke-$suffix.txt" >/tmp/download
|
||||
test "$(cat /tmp/download)" = "$second"
|
||||
|
||||
curl --fail --silent --show-error \
|
||||
--cookie "$cookies" \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{\"root\":\"$root\",\"path\":\"smoke-$suffix.txt\",\"operation_id\":\"20000000-0000-4000-8000-0000000000${suffix}\",\"csrf\":\"$csrf\"}" \
|
||||
"$base/delete" |
|
||||
"$base/api/delete" |
|
||||
grep -F '"result":"succeeded"'
|
||||
if curl --fail --silent \
|
||||
--cookie "$cookies" \
|
||||
"$base/download?root=$root&path=smoke-$suffix.txt" >/dev/null; then
|
||||
"$base/api/download?root=$root&path=smoke-$suffix.txt" >/dev/null; then
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
|
@ -1420,7 +1414,7 @@ spec:
|
|||
--header "X-Content-SHA256: $digest" \
|
||||
--header "X-CSRF-Token: $csrf" \
|
||||
--data-binary @/tmp/body \
|
||||
"$base/upload?root=personal&path=cross-user.txt&operation_id=30000000-0000-4000-8000-000000000001" |
|
||||
"$base/api/upload?root=personal&path=cross-user.txt&operation_id=30000000-0000-4000-8000-000000000001" |
|
||||
grep -F '"result":"succeeded"'
|
||||
curl --insecure --fail --silent --show-error \
|
||||
--cookie "$cookies" \
|
||||
|
|
@ -1429,7 +1423,7 @@ spec:
|
|||
--header "X-Content-SHA256: $digest" \
|
||||
--header "X-CSRF-Token: $csrf" \
|
||||
--data-binary @/tmp/body \
|
||||
"$base/upload?root=shared&path=cross-user.txt&operation_id=30000000-0000-4000-8000-000000000002" |
|
||||
"$base/api/upload?root=shared&path=cross-user.txt&operation_id=30000000-0000-4000-8000-000000000002" |
|
||||
grep -F '"result":"succeeded"'
|
||||
|
||||
csrf_b="$(login "$NUTHATCH_OIDC_USER_B" b)"
|
||||
|
|
@ -1472,14 +1466,14 @@ spec:
|
|||
if curl --insecure --fail --silent --show-error \
|
||||
--cookie /tmp/cookies-b \
|
||||
--header 'Accept: application/json' \
|
||||
"$base/?root=personal" |
|
||||
"$base/api/browse?root=personal" |
|
||||
grep -F cross-user.txt; then
|
||||
exit 1
|
||||
fi
|
||||
curl --insecure --fail --silent --show-error \
|
||||
--cookie /tmp/cookies-b \
|
||||
--header 'Accept: application/json' \
|
||||
"$base/?root=shared" |
|
||||
"$base/api/browse?root=shared" |
|
||||
grep -F cross-user.txt
|
||||
|
||||
if curl --insecure --fail --silent \
|
||||
|
|
@ -1507,7 +1501,7 @@ spec:
|
|||
curl --insecure --silent --output /dev/null \
|
||||
--write-out '%{http_code}' \
|
||||
--cookie "$cookies" \
|
||||
"$base/"
|
||||
"$base/api/browse"
|
||||
)" = 303
|
||||
printf 'OIDC app smoke passed\n'
|
||||
env:
|
||||
|
|
|
|||
|
|
@ -51,7 +51,11 @@ if grep -Eiq '\b(TODO|TBD|unresolved|open question)\b' "$document"; then
|
|||
fi
|
||||
|
||||
grep -F 'e0240ab33521a1882fb647a79f7fd3c74c9b6348b29bdb14e572204517882859' "$document" >/dev/null
|
||||
grep -F 'Public `GET /nuthatch.js`' "$document" >/dev/null
|
||||
grep -F 'Authenticated `/api/session`, `/api/browse`, `/api/download`' "$document" >/dev/null
|
||||
if grep -Fq '/nuthatch.js' "$document"; then
|
||||
printf '%s\n' "Durable contracts contain a removed browser bridge" >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -F '`nuthatch-sha256`' "$document" >/dev/null
|
||||
grep -F '`x-amz-meta-nuthatch-sha256`' "$document" >/dev/null
|
||||
grep -F 'always streams the full body and computes its digest' "$document" >/dev/null
|
||||
|
|
|
|||
Loading…
Reference in a new issue