hackage-server-0.4: Distribution/Server/Features/Upload.hs
{-# LANGUAGE DoRec, RankNTypes, NamedFieldPuns, RecordWildCards #-}
module Distribution.Server.Features.Upload (
UploadFeature(..),
UploadResource(..),
initUploadFeature,
UploadResult(..),
) where
import Distribution.Server.Framework
import Distribution.Server.Framework.BackupDump
import Distribution.Server.Features.Upload.State
import Distribution.Server.Features.Upload.Backup
import Distribution.Server.Features.Core
import Distribution.Server.Features.Users
import Distribution.Server.Users.Backup
import Distribution.Server.Packages.Types
import qualified Distribution.Server.Users.Types as Users
import qualified Distribution.Server.Users.Group as Group
import Distribution.Server.Users.Group (UserGroup(..), GroupDescription(..), nullDescription)
import qualified Distribution.Server.Framework.BlobStorage as BlobStorage
import qualified Distribution.Server.Packages.Unpack as Upload
import Distribution.Server.Packages.PackageIndex (PackageIndex)
import qualified Distribution.Server.Packages.PackageIndex as PackageIndex
import Data.Maybe (fromMaybe)
import Data.Time.Clock (getCurrentTime)
import Data.Function (fix)
import Data.ByteString.Lazy (ByteString)
import Distribution.Package
import Distribution.PackageDescription (GenericPackageDescription)
import Distribution.Text (display)
import qualified Distribution.Server.Util.GZip as GZip
data UploadFeature = UploadFeature {
uploadFeatureInterface :: HackageFeature,
uploadResource :: UploadResource,
uploadPackage :: ServerPartE UploadResult,
--TODO: consider moving the trustee and/or per-package maintainer groups
-- lower down in the feature hierarchy; many other features want to
-- use the trustee group purely for auth decisions
trusteesGroup :: UserGroup,
uploadersGroup :: UserGroup,
maintainersGroup :: PackageName -> UserGroup,
guardAuthorisedAsMaintainer :: PackageName -> ServerPartE (),
guardAuthorisedAsMaintainerOrTrustee :: PackageName -> ServerPartE (),
extractPackage :: (Users.UserId -> UploadResult -> IO (Maybe ErrorResponse))
-> ServerPartE (Users.UserId, UploadResult, PkgTarball)
}
instance IsHackageFeature UploadFeature where
getFeatureInterface = uploadFeatureInterface
data UploadResource = UploadResource {
uploadIndexPage :: Resource,
deletePackagePage :: Resource,
maintainersGroupResource :: GroupResource,
trusteesGroupResource :: GroupResource,
uploadersGroupResource :: GroupResource,
packageMaintainerUri :: String -> PackageId -> String,
trusteeUri :: String -> String,
uploaderUri :: String -> String
}
data UploadResult = UploadResult {
uploadDesc :: !GenericPackageDescription,
uploadCabal :: !ByteString,
uploadWarnings :: ![String]
}
initUploadFeature :: ServerEnv -> CoreFeature -> UserFeature -> IO UploadFeature
initUploadFeature env@ServerEnv{serverStateDir}
core@CoreFeature{..} user@UserFeature{..} = do
-- Canonical state
trusteesState <- trusteesStateComponent serverStateDir
uploadersState <- uploadersStateComponent serverStateDir
maintainersState <- maintainersStateComponent serverStateDir
-- Recusively tie the knot: the feature contains new user group resources
-- but we make the functions needed to create those resources along with
-- the feature
rec let (feature,
getTrusteesGroup, getUploadersGroup, makeMaintainersGroup)
= uploadFeature env core user
trusteesState trusteesGroup trusteesGroupResource
uploadersState uploadersGroup uploadersGroupResource
maintainersState maintainersGroup maintainersGroupResource
(trusteesGroup, trusteesGroupResource) <-
groupResourceAt "/packages/trustees" (getTrusteesGroup [adminGroup])
(uploadersGroup, uploadersGroupResource) <-
groupResourceAt "/packages/uploaders" (getUploadersGroup [adminGroup])
pkgNames <- PackageIndex.packageNames <$> queryGetPackageIndex
(maintainersGroup, maintainersGroupResource) <-
groupResourcesAt "/package/:package/maintainers"
(makeMaintainersGroup [adminGroup, trusteesGroup])
(\pkgname -> [("package", display pkgname)])
(packageInPath coreResource)
pkgNames
return feature
trusteesStateComponent :: FilePath -> IO (StateComponent AcidState HackageTrustees)
trusteesStateComponent stateDir = do
st <- openLocalStateFrom (stateDir </> "db" </> "HackageTrustees") initialHackageTrustees
return StateComponent {
stateDesc = "Trustees"
, stateHandle = st
, getState = query st GetHackageTrustees
, putState = update st . ReplaceHackageTrustees . trusteeList
, backupState = \(HackageTrustees trustees) -> [csvToBackup ["trustees.csv"] $ groupToCSV trustees]
, restoreState = HackageTrustees <$> groupBackup ["trustees.csv"]
, resetState = trusteesStateComponent
}
uploadersStateComponent :: FilePath -> IO (StateComponent AcidState HackageUploaders)
uploadersStateComponent stateDir = do
st <- openLocalStateFrom (stateDir </> "db" </> "HackageUploaders") initialHackageUploaders
return StateComponent {
stateDesc = "Uploaders"
, stateHandle = st
, getState = query st GetHackageUploaders
, putState = update st . ReplaceHackageUploaders . uploaderList
, backupState = \(HackageUploaders uploaders) -> [csvToBackup ["uploaders.csv"] $ groupToCSV uploaders]
, restoreState = HackageUploaders <$> groupBackup ["uploaders.csv"]
, resetState = uploadersStateComponent
}
maintainersStateComponent :: FilePath -> IO (StateComponent AcidState PackageMaintainers)
maintainersStateComponent stateDir = do
st <- openLocalStateFrom (stateDir </> "db" </> "PackageMaintainers") initialPackageMaintainers
return StateComponent {
stateDesc = "Package maintainers"
, stateHandle = st
, getState = query st AllPackageMaintainers
, putState = update st . ReplacePackageMaintainers
, backupState = \(PackageMaintainers mains) -> [maintToExport mains]
, restoreState = maintainerBackup
, resetState = maintainersStateComponent
}
uploadFeature :: ServerEnv
-> CoreFeature
-> UserFeature
-> StateComponent AcidState HackageTrustees -> UserGroup -> GroupResource
-> StateComponent AcidState HackageUploaders -> UserGroup -> GroupResource
-> StateComponent AcidState PackageMaintainers -> (PackageName -> UserGroup) -> GroupResource
-> (UploadFeature,
[UserGroup] -> UserGroup,
[UserGroup] -> UserGroup,
[UserGroup] -> PackageName -> UserGroup)
uploadFeature ServerEnv{serverBlobStore = store}
CoreFeature{ coreResource
, queryGetPackageIndex
, updateAddPackage
}
UserFeature{..}
trusteesState trusteesGroup trusteesGroupResource
uploadersState uploadersGroup uploadersGroupResource
maintainersState maintainersGroup maintainersGroupResource
= ( UploadFeature {..}
, getTrusteesGroup, getUploadersGroup, makeMaintainersGroup)
where
uploadFeatureInterface = (emptyHackageFeature "upload") {
featureDesc = "Support for package uploads, and define groups for trustees, uploaders, and package maintainers"
, featureResources =
[ uploadIndexPage uploadResource
, groupResource maintainersGroupResource
, groupUserResource maintainersGroupResource
, groupResource trusteesGroupResource
, groupUserResource trusteesGroupResource
, groupResource uploadersGroupResource
, groupUserResource uploadersGroupResource
]
, featureState = [
abstractAcidStateComponent trusteesState
, abstractAcidStateComponent uploadersState
, abstractAcidStateComponent maintainersState
]
}
uploadResource = UploadResource
{ uploadIndexPage = (extendResource (corePackagesPage coreResource)) { resourcePost = [] }
, deletePackagePage = (extendResource (corePackagePage coreResource)) { resourceDelete = [] }
, maintainersGroupResource = maintainersGroupResource
, trusteesGroupResource = trusteesGroupResource
, uploadersGroupResource = uploadersGroupResource
, packageMaintainerUri = \format pkgname -> renderResource
(groupResource maintainersGroupResource) [display pkgname, format]
, trusteeUri = \format -> renderResource (groupResource trusteesGroupResource) [format]
, uploaderUri = \format -> renderResource (groupResource uploadersGroupResource) [format]
}
--------------------------------------------------------------------------------
-- User groups and authentication
getTrusteesGroup :: [UserGroup] -> UserGroup
getTrusteesGroup canModify = fix $ \u -> UserGroup {
groupDesc = trusteeDescription,
queryUserList = queryState trusteesState GetTrusteesList,
addUserList = updateState trusteesState . AddHackageTrustee,
removeUserList = updateState trusteesState . RemoveHackageTrustee,
canAddGroup = [u] ++ canModify,
canRemoveGroup = canModify
}
getUploadersGroup :: [UserGroup] -> UserGroup
getUploadersGroup canModify = UserGroup {
groupDesc = uploaderDescription,
queryUserList = queryState uploadersState GetUploadersList,
addUserList = updateState uploadersState . AddHackageUploader,
removeUserList = updateState uploadersState . RemoveHackageUploader,
canAddGroup = canModify,
canRemoveGroup = canModify
}
makeMaintainersGroup :: [UserGroup] -> PackageName -> UserGroup
makeMaintainersGroup canModify name = fix $ \u -> UserGroup {
groupDesc = maintainerDescription name,
queryUserList = queryState maintainersState $ GetPackageMaintainers name,
addUserList = updateState maintainersState . AddPackageMaintainer name,
removeUserList = updateState maintainersState . RemovePackageMaintainer name,
canAddGroup = [u] ++ canModify,
canRemoveGroup = [u] ++ canModify
}
maintainerDescription :: PackageName -> GroupDescription
maintainerDescription pkgname = GroupDescription
{ groupTitle = "Maintainers"
, groupEntity = Just (pname, Just $ "/package/" ++ pname)
, groupPrologue = "Maintainers for a package can upload new versions and adjust other attributes in the package database."
}
where pname = display pkgname
trusteeDescription :: GroupDescription
trusteeDescription = nullDescription { groupTitle = "Package trustees", groupPrologue = "Package trustees are essentially maintainers for the entire package database. They can edit package maintainer groups and upload any package." }
uploaderDescription :: GroupDescription
uploaderDescription = nullDescription { groupTitle = "Package uploaders", groupPrologue = "Package uploaders allowed to upload packages. If a package already exists then you also need to be in the maintainer group for that package." }
guardAuthorisedAsMaintainer :: PackageName -> ServerPartE ()
guardAuthorisedAsMaintainer pkgname =
guardAuthorised_ [InGroup (maintainersGroup pkgname)]
guardAuthorisedAsMaintainerOrTrustee :: PackageName -> ServerPartE ()
guardAuthorisedAsMaintainerOrTrustee pkgname =
guardAuthorised_ [InGroup (maintainersGroup pkgname), InGroup trusteesGroup]
----------------------------------------------------
-- This is the upload function. It returns a generic result for multiple formats.
uploadPackage :: ServerPartE UploadResult
uploadPackage = do
guardAuthorised_ [InGroup uploadersGroup]
pkgIndex <- queryGetPackageIndex
(uid, uresult, tarball) <- extractPackage $ \uid info ->
processUpload pkgIndex uid info
now <- liftIO getCurrentTime
let (UploadResult pkg pkgStr _) = uresult
pkgid = packageId pkg
cabalfile = CabalFileText pkgStr
uploadinfo = (now, uid)
success <- updateAddPackage pkgid cabalfile uploadinfo (Just tarball)
if success
then do
-- make package maintainers group for new package
let existedBefore = packageExists pkgIndex pkgid
when (not existedBefore) $
liftIO $ addUserList (maintainersGroup (packageName pkgid)) uid
return uresult
-- this is already checked in processUpload, and race conditions are highly unlikely but imaginable
else errForbidden "Upload failed" [MText "Package already exists."]
-- This is a processing funtion for extractPackage that checks upload-specific requirements.
-- Does authentication, though not with requirePackageAuth, because it has to be IO.
-- Some other checks can be added, e.g. if a package with a later version exists
processUpload :: PackageIndex PkgInfo -> Users.UserId -> UploadResult -> IO (Maybe ErrorResponse)
processUpload state uid res = do
let pkg = packageId (uploadDesc res)
pkgGroup <- queryUserList (maintainersGroup (packageName pkg))
if packageIdExists state pkg
then uploadError versionExists --allow trustees to do this?
else if packageExists state pkg && not (uid `Group.member` pkgGroup)
then uploadError (notMaintainer pkg)
else return Nothing
where
uploadError = return . Just . ErrorResponse 403 [] "Upload failed" . return . MText
versionExists = "This version of the package has already been uploaded.\n\nAs a matter of "
++ "policy we do not allow package tarballs to be changed after a release "
++ "(so we can guarantee stable md5sums etc). The usual recommendation is "
++ "to upload a new version, and if necessary blacklist the existing one. "
++ "In extraordinary circumstances, contact the administrators."
notMaintainer pkg = "You are not authorised to upload new versions of this package. The "
++ "package '" ++ display (packageName pkg) ++ "' exists already and you "
++ "are not a member of the maintainer group for this package.\n\n"
++ "If you believe you should be a member of the maintainer group for this "
++ "package, then ask an existing maintainer to add you to the group. If "
++ "this is a package name clash, please pick another name or talk to the "
++ "maintainers of the existing package."
-- This function generically extracts a package, useful for uploading, checking,
-- and anything else in the standard user-upload pipeline.
extractPackage :: (Users.UserId -> UploadResult -> IO (Maybe ErrorResponse))
-> ServerPartE (Users.UserId, UploadResult, PkgTarball)
extractPackage processFunc =
withDataFn (lookInput "package") $ \input ->
case inputValue input of -- HS6 this has been updated to use the new file upload support in HS6, but has not been tested at all
(Right _) -> errBadRequest "Upload failed" [MText "package field in form data is not a file."]
(Left file) ->
let fileName = (fromMaybe "noname" $ inputFilename input)
in upload fileName file
where
upload name file =
do -- initial check to ensure logged in.
--FIXME: this should have been covered earlier
uid <- guardAuthenticated
let processPackage :: ByteString -> IO (Either ErrorResponse (UploadResult, BlobStorage.BlobId))
processPackage content' = do
-- as much as it would be nice to do requirePackageAuth in here,
-- processPackage is run in a handle bracket
case Upload.unpackPackage name content' of
Left err -> return . Left $ ErrorResponse 400 [] "Invalid package" [MText err]
Right ((pkg, pkgStr), warnings) -> do
let uresult = UploadResult pkg pkgStr warnings
res <- processFunc uid uresult
case res of
Nothing ->
do let decompressedContent = GZip.decompressNamed file content'
blobIdDecompressed <- BlobStorage.add store decompressedContent
return . Right $ (uresult, blobIdDecompressed)
Just err -> return . Left $ err
mres <- liftIO $ BlobStorage.consumeFileWith store file processPackage
case mres of
Left err -> throwError err
Right ((res, blobIdDecompressed), blobId) ->
return (uid, res, tarball)
where
tarball = PkgTarball { pkgTarballGz = blobId,
pkgTarballNoGz = blobIdDecompressed }