Feature/admin allowed pkgs (#139)

* update admin endpoints to check if authorized to upload pkg

* add auth route for package to check in cli prior to upload

* fix param parsing

* remove uneeded log

* add case for when package is new

* move insert auth admin to pkg check

* send 200 response on successful insert

* invert logic

* retype AdminPkgs since PkgRecord id as key will not yet exist

* cleanup
This commit is contained in:
Lucy
2024-04-16 15:26:46 -04:00
committed by GitHub
parent 9af6e6dc76
commit 8834dd1d28
7 changed files with 141 additions and 42 deletions

View File

@@ -18,6 +18,7 @@
/package/#ApiVersion/version/#PkgId PkgVersionR GET -- get most recent appId version /package/#ApiVersion/version/#PkgId PkgVersionR GET -- get most recent appId version
-- ADMIN API V0 -- ADMIN API V0
/admin/v0/auth/#PkgId CheckPkgAuthR POST !admin
/admin/v0/upload PkgUploadR POST !admin /admin/v0/upload PkgUploadR POST !admin
/admin/v0/eos-upload EosUploadR POST !admin /admin/v0/eos-upload EosUploadR POST !admin
/admin/v0/index PkgIndexR POST !admin /admin/v0/index PkgIndexR POST !admin

View File

@@ -184,6 +184,7 @@ import Handler.Admin (
postPkgDeindexR, postPkgDeindexR,
postPkgIndexR, postPkgIndexR,
postPkgUploadR, postPkgUploadR,
postCheckPkgAuthR
) )
import Handler.Eos (getEosR, getEosVersionR) import Handler.Eos (getEosR, getEosVersionR)
import Handler.Root(getRootR, getMarketplaceR) import Handler.Root(getRootR, getMarketplaceR)

View File

@@ -53,7 +53,7 @@ import Data.HashMap.Internal.Strict (
import Data.String.Interpolate.IsString ( import Data.String.Interpolate.IsString (
i, i,
) )
import Data.Text (toLower) import Data.Text (toLower, splitOn, unpack)
import Dhall ( import Dhall (
Encoder (embed), Encoder (embed),
FromDhall (..), FromDhall (..),
@@ -207,6 +207,8 @@ import Yesod (
import Prelude (read) import Prelude (read)
import Options.Applicative (some) import Options.Applicative (some)
import Control.Applicative.HT (lift4) import Control.Applicative.HT (lift4)
import Data.List (last)
import Data.List (head)
data Upload = Upload data Upload = Upload
@@ -526,6 +528,19 @@ upload (Upload name mpkg shouldIndex arches) = do
for_ pkgs $ \f -> $logWarn (fromString f) for_ pkgs $ \f -> $logWarn (fromString f)
exitWith $ ExitFailure 1 exitWith $ ExitFailure 1
Just s -> pure s Just s -> pure s
let pkgId_ = head $ splitOn "." $ last $ splitOn "/" $ show pkg
pkgAuthBody <-
parseRequest ("POST " <> show publishCfgRepoLocation <> "/admin/v0/auth/" <> unpack pkgId_)
<&> setRequestHeaders [("accept", "text/plain")]
<&> setRequestResponseTimeout (responseTimeoutMicro (90_000_000))
<&> applyBasicAuth (B8.pack publishCfgRepoUser) (B8.pack publishCfgRepoPass)
manager <- newTlsManager
pkgAuthRes <- runReaderT (httpLbs pkgAuthBody) manager
if getResponseStatus pkgAuthRes == status200
then pure () -- no output is successful
else do
$logError (decodeUtf8 . LB.toStrict $ getResponseBody pkgAuthRes)
exitWith $ ExitFailure 1
noBody <- noBody <-
parseRequest ("POST " <> show publishCfgRepoLocation <> "/admin/v0/upload") parseRequest ("POST " <> show publishCfgRepoLocation <> "/admin/v0/upload")
<&> setRequestHeaders [("accept", "text/plain")] <&> setRequestHeaders [("accept", "text/plain")]
@@ -535,7 +550,6 @@ upload (Upload name mpkg shouldIndex arches) = do
bar <- newProgressBar defStyle 30 (Progress 0 (fromIntegral size) ()) bar <- newProgressBar defStyle 30 (Progress 0 (fromIntegral size) ())
body <- observedStreamFile (updateProgress bar . const . sfs2prog) pkg body <- observedStreamFile (updateProgress bar . const . sfs2prog) pkg
let withBody = setRequestBody body noBody let withBody = setRequestBody body noBody
manager <- newTlsManager
res <- runReaderT (httpLbs withBody) manager res <- runReaderT (httpLbs withBody) manager
if getResponseStatus res == status200 if getResponseStatus res == status200
then -- no output is successful then -- no output is successful

View File

@@ -19,7 +19,7 @@ import Model (
Metric (Metric), Metric (Metric),
PkgDependency (..), PkgDependency (..),
PkgRecord (PkgRecord), PkgRecord (PkgRecord),
VersionRecord (VersionRecord), VersionPlatform (VersionPlatform), EntityField (VersionPlatformPkgId, VersionPlatformVersionNumber, VersionPlatformArch), PkgRecordId, VersionRecord (VersionRecord), VersionPlatform (VersionPlatform), EntityField (VersionPlatformPkgId, VersionPlatformVersionNumber, VersionPlatformArch, AdminPkgsPkgId, AdminPkgsAdmin), PkgRecordId, AdminPkgs, AdminId,
) )
import Orphans.Emver () import Orphans.Emver ()
import Startlude ( import Startlude (
@@ -328,3 +328,20 @@ getVersionPlatform pkgId arches = do
where_ (v ^. VersionPlatformArch `in_` (valList arches)) where_ (v ^. VersionPlatformArch `in_` (valList arches))
pure v pure v
pure $ entityVal <$> vps pure $ entityVal <$> vps
getAllowedPkgs :: (Monad m, MonadIO m) => PkgId -> AdminId -> ReaderT SqlBackend m [AdminPkgs]
getAllowedPkgs pkgId adminId = do
pkgs <- select $ do
p <- from $ table @AdminPkgs
where_ $ p ^. AdminPkgsPkgId ==. val pkgId
where_ $ p ^. AdminPkgsAdmin ==. val adminId
pure p
pure $ entityVal <$> pkgs
getPkg:: (Monad m, MonadIO m) => PkgRecordId -> ReaderT SqlBackend m [PkgRecord]
getPkg pkgId = do
pkg <- select $ do
p <- from $ table @PkgRecord
where_ $ p ^. PkgRecordId ==. val pkgId
pure p
pure $ entityVal <$> pkg

View File

@@ -58,7 +58,7 @@ import Handler.Util (
getHashFromQuery, getHashFromQuery,
getVersionFromQuery, getVersionFromQuery,
orThrow, orThrow,
sendResponseText, sendResponseText, checkAdminAllowedPkgs, checkAdminAuth,
) )
import Lib.PkgRepository ( import Lib.PkgRepository (
PkgRepo (PkgRepo, pkgRepoFileRoot), PkgRepo (PkgRepo, pkgRepoFileRoot),
@@ -79,7 +79,7 @@ import Model (
Unique (UniqueName, UniquePkgCategory), Unique (UniqueName, UniquePkgCategory),
Upload (..), Upload (..),
VersionRecord (versionRecordNumber, versionRecordPkgId), VersionRecord (versionRecordNumber, versionRecordPkgId),
unPkgRecordKey, unPkgRecordKey, AdminPkgs (AdminPkgs),
) )
import Network.HTTP.Types ( import Network.HTTP.Types (
status400, status400,
@@ -150,6 +150,28 @@ import Yesod.Core.Types (JSONResponse (JSONResponse))
import Database.Persist.Sql (runSqlPool) import Database.Persist.Sql (runSqlPool)
import Data.List (elem, length) import Data.List (elem, length)
import Database.Persist ((==.)) import Database.Persist ((==.))
import Network.HTTP.Types.Status (status401)
import Network.HTTP.Types (status200)
postCheckPkgAuthR :: PkgId -> Handler ()
postCheckPkgAuthR pkgId = do
whitelist <- getsYesod $ whitelist . appSettings
maybeAuthId >>= \case
Nothing -> do
sendResponseText status401 "User not an authorized admin."
Just name -> do
if ((length whitelist > 0 && (pkgId `elem` whitelist)) || length whitelist <= 0)
then do
(authorized, newPkg) <- checkAdminAllowedPkgs pkgId name
if authorized && not newPkg
then sendResponseText status200 "User authorized to upload this package."
else if authorized && newPkg
-- if pkg is whitelisted and a new upload, add as authorized for this admin user
then do
runDB $ insert_ (AdminPkgs (AdminKey name) pkgId)
sendResponseText status200 "User authorized to upload this package."
else sendResponseText status401 "User not authorized to upload this package."
else sendResponseText status500 "Package does not belong on this registry."
postPkgUploadR :: Handler () postPkgUploadR :: Handler ()
postPkgUploadR = do postPkgUploadR = do
@@ -175,14 +197,12 @@ postPkgUploadR = do
removePathForcibly targetPath removePathForcibly targetPath
createDirectoryIfMissing True targetPath createDirectoryIfMissing True targetPath
renameDirectory dir targetPath renameDirectory dir targetPath
maybeAuthId >>= \case (authorized, name) <- checkAdminAuth packageManifestId
Nothing -> do if authorized
$logError then do
"The Impossible has happened, an unauthenticated user has managed to upload a pacakge to this registry"
pure ()
Just name -> do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
runDB $ insert_ (Upload (AdminKey name) (PkgRecordKey packageManifestId)packageManifestVersion now) runDB $ insert_ (Upload (AdminKey name) (PkgRecordKey packageManifestId)packageManifestVersion now)
else sendResponseText status401 "User not authorized to upload this package."
else sendResponseText status500 "Package does not belong on this registry." else sendResponseText status500 "Package does not belong on this registry."
where where
retry m = runMaybeT . asum $ replicate 3 (MaybeT $ hush <$> try @_ @SomeException m) retry m = runMaybeT . asum $ replicate 3 (MaybeT $ hush <$> try @_ @SomeException m)
@@ -230,6 +250,9 @@ instance ToJSON IndexPkgReq where
postPkgIndexR :: Handler () postPkgIndexR :: Handler ()
postPkgIndexR = do postPkgIndexR = do
IndexPkgReq{..} <- requireCheckJsonBody IndexPkgReq{..} <- requireCheckJsonBody
(admin, _) <- checkAdminAuth indexPkgReqId
if admin
then do
manifest <- getManifestLocation indexPkgReqId indexPkgReqVersion manifest <- getManifestLocation indexPkgReqId indexPkgReqVersion
man <- man <-
liftIO (decodeFileStrict manifest) liftIO (decodeFileStrict manifest)
@@ -239,15 +262,20 @@ postPkgIndexR = do
pool <- getsYesod appConnPool pool <- getsYesod appConnPool
runSqlPoolNoTransaction (upsertPackageVersion man) pool Nothing runSqlPoolNoTransaction (upsertPackageVersion man) pool Nothing
runSqlPool (upsertPackageVersionPlatform indexPkgReqArches man) pool runSqlPool (upsertPackageVersionPlatform indexPkgReqArches man) pool
else sendResponseText status401 "User not authorized to index this package."
postPkgDeindexR :: Handler () postPkgDeindexR :: Handler ()
postPkgDeindexR = do postPkgDeindexR = do
IndexPkgReq{..} <- requireCheckJsonBody IndexPkgReq{..} <- requireCheckJsonBody
(admin, _) <- checkAdminAuth indexPkgReqId
if admin
then do
case indexPkgReqArches of case indexPkgReqArches of
Nothing -> runDB $ delete (VersionRecordKey (PkgRecordKey indexPkgReqId) indexPkgReqVersion) Nothing -> runDB $ delete (VersionRecordKey (PkgRecordKey indexPkgReqId) indexPkgReqVersion)
Just a -> do Just a -> do
_ <- traverse (deleteArch indexPkgReqId indexPkgReqVersion) a _ <- traverse (deleteArch indexPkgReqId indexPkgReqVersion) a
pure () pure ()
else sendResponseText status401 "User not authorized to deindex this package."
where where
deleteArch :: PkgId -> Version -> OsArch -> Handler () deleteArch :: PkgId -> Version -> OsArch -> Handler ()
deleteArch id v a = runDB $ deleteWhere [VersionPlatformArch ==. a, VersionPlatformVersionNumber ==. v, VersionPlatformPkgId ==. PkgRecordKey id] deleteArch id v a = runDB $ deleteWhere [VersionPlatformArch ==. a, VersionPlatformVersionNumber ==. v, VersionPlatformPkgId ==. PkgRecordKey id]
@@ -298,7 +326,10 @@ deleteCategoryR cat = runDB $ deleteBy (UniqueName cat)
postPkgCategorizeR :: Text -> PkgId -> Handler () postPkgCategorizeR :: Text -> PkgId -> Handler ()
postPkgCategorizeR cat pkg = runDB $ do postPkgCategorizeR cat pkg = do
(admin, _) <- checkAdminAuth pkg
if admin
then runDB $ do
catEnt <- getBy (UniqueName cat) `orThrow` sendResponseText status404 [i|Category "#{cat}" does not exist|] catEnt <- getBy (UniqueName cat) `orThrow` sendResponseText status404 [i|Category "#{cat}" does not exist|]
_pkgEnt <- get (PkgRecordKey pkg) `orThrow` sendResponseText status404 [i|Package "#{pkg}" does not exist|] _pkgEnt <- get (PkgRecordKey pkg) `orThrow` sendResponseText status404 [i|Package "#{pkg}" does not exist|]
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
@@ -308,8 +339,13 @@ postPkgCategorizeR cat pkg = runDB $ do
status403 status403
[i|Package "#{pkg}" is already assigned to category "#{cat}"|] [i|Package "#{pkg}" is already assigned to category "#{cat}"|]
else sendResponseText status401 "User not authorized to categorize this package."
deletePkgCategorizeR :: Text -> PkgId -> Handler () deletePkgCategorizeR :: Text -> PkgId -> Handler ()
deletePkgCategorizeR cat pkg = runDB $ do deletePkgCategorizeR cat pkg = do
(admin, _) <- checkAdminAuth pkg
if admin
then runDB $ do
catEnt <- getBy (UniqueName cat) `orThrow` sendResponseText status404 [i|Category "#{cat}" does not exist|] catEnt <- getBy (UniqueName cat) `orThrow` sendResponseText status404 [i|Category "#{cat}" does not exist|]
deleteBy (UniquePkgCategory (PkgRecordKey pkg) (entityKey catEnt)) deleteBy (UniquePkgCategory (PkgRecordKey pkg) (entityKey catEnt))
else sendResponseText status401 "User not authorized to uncategorize this package."

View File

@@ -18,7 +18,7 @@ import Data.String.Interpolate.IsString (
import Data.Text qualified as T import Data.Text qualified as T
import Data.Text.Lazy qualified as TL import Data.Text.Lazy qualified as TL
import Data.Text.Lazy.Builder qualified as TB import Data.Text.Lazy.Builder qualified as TB
import Database.Queries (fetchAllPkgVersions, getVersionPlatform) import Database.Queries (fetchAllPkgVersions, getVersionPlatform, getAllowedPkgs, getPkg)
import Foundation import Foundation
import Lib.PkgRepository ( import Lib.PkgRepository (
PkgRepo, PkgRepo,
@@ -32,7 +32,7 @@ import Lib.Types.Emver (
) )
import Model ( import Model (
UserActivity (..), UserActivity (..),
VersionRecord (versionRecordOsVersion, versionRecordDeprecatedAt, versionRecordPkgId), VersionPlatform (versionPlatformDevice), VersionRecord (versionRecordOsVersion, versionRecordDeprecatedAt, versionRecordPkgId), VersionPlatform (versionPlatformDevice), AdminId, Key (PkgRecordKey, AdminKey),
) )
import Network.HTTP.Types ( import Network.HTTP.Types (
Status, Status,
@@ -61,6 +61,7 @@ import Startlude (
void, void,
($), ($),
(.), (.),
(>),
(<$>), (<$>),
(>>=), note, (=<<), catMaybes, all, encodeUtf8, toS, fmap, traceM, show, trace, any, or, (++), IO, putStrLn, map (>>=), note, (=<<), catMaybes, all, encodeUtf8, toS, fmap, traceM, show, trace, any, or, (++), IO, putStrLn, map
) )
@@ -88,6 +89,9 @@ import Data.Aeson (eitherDecodeStrict)
import Data.Bifunctor (Bifunctor(first)) import Data.Bifunctor (Bifunctor(first))
import qualified Data.MultiMap as MM import qualified Data.MultiMap as MM
import Startlude (bimap) import Startlude (bimap)
import Data.List (length)
import Control.Monad.Logger (logError)
import Yesod.Auth (YesodAuth(maybeAuthId))
orThrow :: MonadHandler m => m (Maybe a) -> m a -> m a orThrow :: MonadHandler m => m (Maybe a) -> m a -> m a
orThrow action other = orThrow action other =
@@ -253,3 +257,24 @@ areRegexMatchesEqual textMap (PackageDevice regexMap) =
checkMatch (key, regexPattern) = checkMatch (key, regexPattern) =
case MM.lookup key textMap of case MM.lookup key textMap of
val -> or $ regexMatch regexPattern <$> val val -> or $ regexMatch regexPattern <$> val
checkAdminAllowedPkgs :: PkgId -> Text -> Handler (Bool, Bool) -- (authorized, newPkg)
checkAdminAllowedPkgs pkgId adminId = do
-- if pkg does not exist yet, allow, because authorized by whitelist
pkg <- runDB $ getPkg (PkgRecordKey pkgId)
if length pkg > 0
then do
res <- runDB $ getAllowedPkgs pkgId (AdminKey adminId)
pure $ if length res > 0 then (True, False) else (False, False)
else pure (True, True)
checkAdminAuth :: PkgId -> Handler (Bool, Text)
checkAdminAuth pkgId = do
maybeAuthId >>= \case
Nothing -> do
$logError
"Impossible: an unauthenticated user has accessed an authenticated endpoint."
pure (False, "")
Just name -> do
(authorized, _) <- checkAdminAllowedPkgs pkgId name
pure (authorized, name)

View File

@@ -153,6 +153,11 @@ Admin
passHash (Digest SHA256) passHash (Digest SHA256)
deletedAt UTCTime Maybe deletedAt UTCTime Maybe
AdminPkgs
admin AdminId
pkgId PkgId
UniqueAdminPkg pkgId admin
Upload Upload
uploader AdminId uploader AdminId
pkgId PkgRecordId pkgId PkgRecordId