module Ecluse.Core.Server.Pipeline.Publish (
servePublish,
) where
import Data.Aeson (Value (String))
import Data.Aeson qualified as Aeson
import Data.Aeson.KeyMap qualified as KeyMap
import Data.ByteString.Lazy qualified as LBS
import Lens.Micro ((^?))
import Lens.Micro.Aeson (key, _Object)
import Network.HTTP.Types (mkStatus, status403, status405, status500, status502)
import Network.Wai (Request, Response, ResponseReceived, consumeRequestBodyStrict)
import UnliftIO.Exception (tryAny)
import Ecluse.Core.Package (
PackageName,
Scope,
pkgNamespace,
renderPackageName,
)
import Ecluse.Core.Registry (PublishRelayResponse (PublishRelayResponse), UrlFormationError)
import Ecluse.Core.Server.Context (
Handler,
MountBinding (bindingPublishDeps, bindingRenderer),
PublishDeps (..),
ServeRuntime (srPrivateManager),
ctxMount,
ctxRuntime,
)
import Ecluse.Core.Server.Pipeline.Shared
import Ecluse.Core.Server.Response (
MountRenderer,
renderError,
)
servePublish ::
PackageName ->
Request ->
(Response -> IO ResponseReceived) ->
Handler ResponseReceived
servePublish :: PackageName
-> Request
-> (Response -> IO ResponseReceived)
-> Handler ResponseReceived
servePublish PackageName
name Request
request Response -> IO ResponseReceived
respond = do
renderer <- (RequestCtx -> MountRenderer) -> Handler MountRenderer
forall r (m :: * -> *) a. MonadReader r m => (r -> a) -> m a
asks (MountBinding -> MountRenderer
bindingRenderer (MountBinding -> MountRenderer)
-> (RequestCtx -> MountBinding) -> RequestCtx -> MountRenderer
forall b c a. (b -> c) -> (a -> b) -> a -> c
. RequestCtx -> MountBinding
ctxMount)
asks (bindingPublishDeps . ctxMount) >>= \case
Maybe PublishDeps
Nothing -> IO ResponseReceived -> Handler ResponseReceived
forall a. IO a -> Handler a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (Response -> IO ResponseReceived
respond (MountRenderer -> Response
publishDisabled MountRenderer
renderer))
Just PublishDeps
deps -> MountRenderer
-> PublishDeps
-> PackageName
-> Request
-> (Response -> IO ResponseReceived)
-> Handler ResponseReceived
publishWithDeps MountRenderer
renderer PublishDeps
deps PackageName
name Request
request Response -> IO ResponseReceived
respond
publishWithDeps ::
MountRenderer ->
PublishDeps ->
PackageName ->
Request ->
(Response -> IO ResponseReceived) ->
Handler ResponseReceived
publishWithDeps :: MountRenderer
-> PublishDeps
-> PackageName
-> Request
-> (Response -> IO ResponseReceived)
-> Handler ResponseReceived
publishWithDeps MountRenderer
renderer PublishDeps
deps PackageName
name Request
request Response -> IO ResponseReceived
respond
| Bool -> Bool
not (Maybe Secret -> Maybe Secret -> Bool
edgeTokenMatches (PublishDeps -> Maybe Secret
pubInboundToken PublishDeps
deps) Maybe Secret
clientToken) =
IO ResponseReceived -> Handler ResponseReceived
forall a. IO a -> Handler a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (Response -> IO ResponseReceived
respond (MountRenderer -> Response
edgeUnauthorised MountRenderer
renderer))
| Bool -> Bool
not ([Scope] -> PackageName -> Bool
inPublishScope (PublishDeps -> [Scope]
pubScopes PublishDeps
deps) PackageName
name) =
IO ResponseReceived -> Handler ResponseReceived
forall a. IO a -> Handler a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (Response -> IO ResponseReceived
respond (MountRenderer -> PublishDeps -> PackageName -> Response
outOfScope MountRenderer
renderer PublishDeps
deps PackageName
name))
| Bool
otherwise = do
rt <- (RequestCtx -> ServeRuntime) -> Handler ServeRuntime
forall r (m :: * -> *) a. MonadReader r m => (r -> a) -> m a
asks RequestCtx -> ServeRuntime
ctxRuntime
body <- liftIO (consumeRequestBodyStrict request)
case bodyNameDisagreement (pubCanonicaliseName deps) name body of
Just Text
declared -> IO ResponseReceived -> Handler ResponseReceived
forall a. IO a -> Handler a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (Response -> IO ResponseReceived
respond (MountRenderer -> PublishDeps -> PackageName -> Text -> Response
bodyNameMismatch MountRenderer
renderer PublishDeps
deps PackageName
name Text
declared))
Maybe Text
Nothing -> do
outcome <- Handler (Either UrlFormationError PublishRelayResponse)
-> Handler
(Either
SomeException (Either UrlFormationError PublishRelayResponse))
forall (m :: * -> *) a.
MonadUnliftIO m =>
m a -> m (Either SomeException a)
tryAny (IO (Either UrlFormationError PublishRelayResponse)
-> Handler (Either UrlFormationError PublishRelayResponse)
forall a. IO a -> Handler a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (PublishDeps
-> Limits
-> Manager
-> Text
-> Maybe Secret
-> PackageName
-> ByteString
-> IO (Either UrlFormationError PublishRelayResponse)
pubRelayPublish PublishDeps
deps (PublishDeps -> Limits
pubLimits PublishDeps
deps) (ServeRuntime -> Manager
srPrivateManager ServeRuntime
rt) (PublishDeps -> Text
pubTargetUrl PublishDeps
deps) (Maybe Secret
clientToken Maybe Secret -> Maybe Secret -> Maybe Secret
forall a. Maybe a -> Maybe a -> Maybe a
forall (f :: * -> *) a. Alternative f => f a -> f a -> f a
<|> PublishDeps -> Maybe Secret
pubStaticToken PublishDeps
deps) PackageName
name (ByteString -> ByteString
LBS.toStrict ByteString
body)))
liftIO (respond (renderRelay renderer deps outcome))
where
clientToken :: Maybe Secret
clientToken = Request -> Maybe Secret
forwardedToken Request
request
inPublishScope :: [Scope] -> PackageName -> Bool
inPublishScope :: [Scope] -> PackageName -> Bool
inPublishScope [Scope]
scopes PackageName
name = case PackageName -> Maybe Scope
pkgNamespace PackageName
name of
Just Scope
scope -> Scope
scope Scope -> [Scope] -> Bool
forall (f :: * -> *) a.
(Foldable f, DisallowElem f, Eq a) =>
a -> f a -> Bool
`elem` [Scope]
scopes
Maybe Scope
Nothing -> Bool
False
renderRelay ::
MountRenderer ->
PublishDeps ->
Either SomeException (Either UrlFormationError PublishRelayResponse) ->
Response
renderRelay :: MountRenderer
-> PublishDeps
-> Either
SomeException (Either UrlFormationError PublishRelayResponse)
-> Response
renderRelay MountRenderer
renderer PublishDeps
deps = \case
Right (Right (PublishRelayResponse Int
code ByteString
relayed)) ->
Status -> ResponseHeaders -> ByteString -> Response
jsonResponse (Int -> ByteString -> Status
mkStatus Int
code ByteString
"") [] ByteString
relayed
Right (Left UrlFormationError
_urlErr) ->
Status -> ResponseHeaders -> RenderedBody -> Response
renderedResponse Status
status500 [] (MountRenderer -> Maybe HelpMessage -> Text -> RenderedBody
renderError MountRenderer
renderer (PublishDeps -> Maybe HelpMessage
pubHelp PublishDeps
deps) Text
"the publication target URL is misconfigured")
Left SomeException
_exc ->
Status -> ResponseHeaders -> RenderedBody -> Response
renderedResponse Status
status502 [] (MountRenderer -> Maybe HelpMessage -> Text -> RenderedBody
renderError MountRenderer
renderer (PublishDeps -> Maybe HelpMessage
pubHelp PublishDeps
deps) Text
"the publication target could not be reached")
publishDisabled :: MountRenderer -> Response
publishDisabled :: MountRenderer -> Response
publishDisabled MountRenderer
renderer =
Status -> ResponseHeaders -> RenderedBody -> Response
renderedResponse Status
status405 [(HeaderName
"Allow", ByteString
"GET, HEAD")] (MountRenderer -> Maybe HelpMessage -> Text -> RenderedBody
renderError MountRenderer
renderer Maybe HelpMessage
forall a. Maybe a
Nothing Text
"publishing is not enabled on this proxy (no publication target is configured)")
outOfScope :: MountRenderer -> PublishDeps -> PackageName -> Response
outOfScope :: MountRenderer -> PublishDeps -> PackageName -> Response
outOfScope MountRenderer
renderer PublishDeps
deps PackageName
name =
Status -> ResponseHeaders -> RenderedBody -> Response
renderedResponse Status
status403 [] (MountRenderer -> Maybe HelpMessage -> Text -> RenderedBody
renderError MountRenderer
renderer (PublishDeps -> Maybe HelpMessage
pubHelp PublishDeps
deps) Text
message)
where
message :: Text
message :: Text
message =
Text
"refusing to publish '"
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> PackageName -> Text
renderPackageName PackageName
name
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"': its name is outside the configured publish-scope allow-list (the anti-shadowing guard against publishing a name that shadows a public package)"
bodyNameMismatch :: MountRenderer -> PublishDeps -> PackageName -> Text -> Response
bodyNameMismatch :: MountRenderer -> PublishDeps -> PackageName -> Text -> Response
bodyNameMismatch MountRenderer
renderer PublishDeps
deps PackageName
name Text
declared =
Status -> ResponseHeaders -> RenderedBody -> Response
renderedResponse Status
status403 [] (MountRenderer -> Maybe HelpMessage -> Text -> RenderedBody
renderError MountRenderer
renderer (PublishDeps -> Maybe HelpMessage
pubHelp PublishDeps
deps) Text
message)
where
message :: Text
message :: Text
message =
Text
"refusing to publish '"
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> PackageName -> Text
renderPackageName PackageName
name
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"': the document body declares the name '"
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
declared
Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"', which disagrees with the URL-path package name the scope guard authorised (the anti-shadowing guard against publishing a name the allow-list never saw)"
bodyNameDisagreement :: (Text -> Maybe PackageName) -> PackageName -> LByteString -> Maybe Text
bodyNameDisagreement :: (Text -> Maybe PackageName)
-> PackageName -> ByteString -> Maybe Text
bodyNameDisagreement Text -> Maybe PackageName
canonicalise PackageName
name ByteString
body =
case ByteString -> Maybe Value
forall a. FromJSON a => ByteString -> Maybe a
Aeson.decode ByteString
body of
Maybe Value
Nothing -> Maybe Text
forall a. Maybe a
Nothing
Just Value
document -> (Text -> Bool) -> [Text] -> Maybe Text
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Maybe a
find Text -> Bool
disagrees (Value -> [Text]
declaredNames Value
document)
where
disagrees :: Text -> Bool
disagrees :: Text -> Bool
disagrees Text
declared = case Text -> Maybe PackageName
canonicalise Text
declared of
Just PackageName
declaredName -> PackageName
declaredName PackageName -> PackageName -> Bool
forall a. Eq a => a -> a -> Bool
/= PackageName
name
Maybe PackageName
Nothing -> Bool
True
declaredNames :: Value -> [Text]
declaredNames :: Value -> [Text]
declaredNames Value
document =
[ Text
declared
| Maybe Value
slot <-
[Value
document Value -> Getting (First Value) Value Value -> Maybe Value
forall s a. s -> Getting (First a) s a -> Maybe a
^? Key -> Traversal' Value Value
forall t. AsValue t => Key -> Traversal' t Value
key Key
"_id", Value
document Value -> Getting (First Value) Value Value -> Maybe Value
forall s a. s -> Getting (First a) s a -> Maybe a
^? Key -> Traversal' Value Value
forall t. AsValue t => Key -> Traversal' t Value
key Key
"name"]
[Maybe Value] -> [Maybe Value] -> [Maybe Value]
forall a. Semigroup a => a -> a -> a
<> [ Value
versionDoc Value -> Getting (First Value) Value Value -> Maybe Value
forall s a. s -> Getting (First a) s a -> Maybe a
^? Key -> Traversal' Value Value
forall t. AsValue t => Key -> Traversal' t Value
key Key
"name"
| KeyMap Value
versions <- Maybe (KeyMap Value) -> [KeyMap Value]
forall a. Maybe a -> [a]
forall (t :: * -> *) a. Foldable t => t a -> [a]
toList (Value
document Value
-> Getting (First (KeyMap Value)) Value (KeyMap Value)
-> Maybe (KeyMap Value)
forall s a. s -> Getting (First a) s a -> Maybe a
^? Key -> Traversal' Value Value
forall t. AsValue t => Key -> Traversal' t Value
key Key
"versions" ((Value -> Const (First (KeyMap Value)) Value)
-> Value -> Const (First (KeyMap Value)) Value)
-> Getting (First (KeyMap Value)) Value (KeyMap Value)
-> Getting (First (KeyMap Value)) Value (KeyMap Value)
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Getting (First (KeyMap Value)) Value (KeyMap Value)
forall t. AsValue t => Traversal' t (KeyMap Value)
Traversal' Value (KeyMap Value)
_Object)
, Value
versionDoc <- KeyMap Value -> [Value]
forall v. KeyMap v -> [v]
KeyMap.elems KeyMap Value
versions
]
, Just (String Text
declared) <- [Maybe Value
slot]
]