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