diff --git a/Distribution/Server.hs b/Distribution/Server.hs index 98203b1b0..8ddef4d9b 100644 --- a/Distribution/Server.hs +++ b/Distribution/Server.hs @@ -7,8 +7,8 @@ module Distribution.Server ( checkpoint, -- * Server configuration - Config(..), - defaultConfig, + ServerConfig(..), + defaultServerConfig, hasSavedState, -- * First time initialisation of the database @@ -17,29 +17,23 @@ module Distribution.Server ( initState, ) where -import Distribution.Package (packageName) import Happstack.Server hiding (port, host) import qualified Happstack.Server import Happstack.State hiding (Version) -import Distribution.Server.ServerParts (guardAuth) -import qualified Distribution.Server.Import as Import ( importTar ) +import qualified Distribution.Server.Import as Import (importTar) import Distribution.Server.Packages.ServerParts import Distribution.Server.Users.ServerParts -import Distribution.Server.Distributions.ServerParts -import Distribution.Server.Users.Permissions (GroupName(..)) +--import Distribution.Server.Distributions.ServerParts -- this will take some effort to revamp import qualified Distribution.Server.Feature as Feature import qualified Distribution.Server.Features as Features import Distribution.Server.State as State -import Distribution.Server.Packages.State as State hiding (buildReports, bulkImport) +import Distribution.Server.Packages.State as State hiding (bulkImport) import Distribution.Server.Users.State as State import qualified Distribution.Server.Cache as Cache -import Distribution.Server.Packages.Types - ( PkgInfo(..) ) -import qualified Distribution.Server.ResourceTypes as Resource import qualified Distribution.Server.Util.BlobStorage as BlobStorage import Distribution.Server.Util.BlobStorage (BlobStorage) import qualified Distribution.Server.BulkImport as BulkImport @@ -49,44 +43,43 @@ import qualified Distribution.Server.Users.Users as Users import qualified Distribution.Server.Users.Types as Users import Distribution.Server.Export.ServerParts (export) -import Distribution.Server.Auth.Types (PasswdPlain(..)) +import qualified Distribution.Server.Auth.Types as Auth +import qualified Distribution.Server.Auth.Basic as Auth +import qualified Distribution.Server.Auth.Crypt as Auth -import Distribution.Server.Resource (addResponse, serverTreeEmpty, renderServerTree, spiffyResources) -import Data.List (foldl') +import Distribution.Server.Resource --(addResponse, serverTreeEmpty, renderServerTree) +--import Data.List (foldl') import System.FilePath (()) -import System.Directory - ( createDirectoryIfMissing, doesDirectoryExist ) +import System.Directory (createDirectoryIfMissing, doesDirectoryExist) import Control.Concurrent.MVar (MVar) import Control.Monad.Trans import Control.Monad (when, msum) import Data.ByteString.Lazy.Char8 (ByteString) -import Network.URI - ( URIAuth(URIAuth) ) -import Network.BSD - ( getHostName ) -import qualified Data.Map as Map (empty) +import Network.URI (URIAuth(URIAuth)) +import Network.BSD (getHostName) +import Data.Char (toUpper) import qualified Data.ByteString.Lazy.Char8 as BS import Paths_hackage_server (getDataDir) -data Config = Config { +data ServerConfig = ServerConfig { confHostName :: String, confPortNum :: Int, confStateDir :: FilePath, confStaticDir :: FilePath } deriving (Show) -confHappsStateDir, confBlobStoreDir :: Config -> FilePath +confHappsStateDir, confBlobStoreDir :: ServerConfig -> FilePath confHappsStateDir config = confStateDir config "db" confBlobStoreDir config = confStateDir config "blobs" -defaultConfig :: IO Config -defaultConfig = do +defaultServerConfig :: IO ServerConfig +defaultServerConfig = do hostName <- getHostName dataDir <- getDataDir - return Config { + return ServerConfig { confHostName = hostName, confPortNum = 8080, confStateDir = "state", @@ -94,17 +87,16 @@ defaultConfig = do } data Server = Server { - serverTxControl :: MVar TxControl, - serverFeatureConfig :: Feature.Config, - serverPort :: Int, - serverCache :: Cache.Cache + serverTxControl :: MVar TxControl, + serverPort :: Int, + serverConfig :: Config } -- | If we made a server instance from this 'Config', would we find some -- existing saved state or would it be a totally clean instance with no -- existing state. -- -hasSavedState :: Config -> IO Bool +hasSavedState :: ServerConfig -> IO Bool hasSavedState = doesDirectoryExist . confHappsStateDir -- | Make a server instance from the server configuration. @@ -115,9 +107,8 @@ hasSavedState = doesDirectoryExist . confHappsStateDir -- Note: the server instance must eventually be 'shutdown' or you'll end up -- with stale lock files. -- -initialise :: Config -> IO Server -initialise config@(Config hostName portNum stateDir staticDir) = do - +initialise :: ServerConfig -> IO Server +initialise config@(ServerConfig hostName portNum stateDir staticDir) = do exists <- doesDirectoryExist staticDir when (not exists) $ fail $ "The static files directory " ++ staticDir ++ " does not exist." @@ -126,17 +117,20 @@ initialise config@(Config hostName portNum stateDir staticDir) = do store <- BlobStorage.open blobStoreDir txCtl <- runTxSystem (Queue (FileSaver happsStateDir)) hackageEntryPoint - cache <- Cache.new =<< stateToCache hostURI =<< query GetPackagesState + cache <- do + packages <- query GetPackagesState + users <- query GetUserDb + Cache.new =<< stateToCache hostURI packages users return Server { serverTxControl = txCtl, - serverFeatureConfig = Feature.Config { - Feature.serverStore = store, - Feature.serverStaticDir = staticDir, - Feature.serverURI = hostURI - }, serverPort = portNum, - serverCache = cache + serverConfig = Config { + serverStore = store, + serverStaticDir = staticDir, + serverURI = hostURI, + serverCache = cache + } } where @@ -156,13 +150,20 @@ run server = simpleHTTP conf $ mungeRequest $ impl server where conf = nullConf { Happstack.Server.port = serverPort server } mungeRequest = localRq mungeMethod + -- this is not restful. mungeMethod req = case (rqMethod req, lookup "_method" (rqInputs req)) of - (POST, Just input) -> case reads (BS.unpack (inputValue input)) of + (POST, Just input) -> case reads . map toUpper . BS.unpack $ inputValue input of [(newMethod, "")] -> req { rqMethod = newMethod } _ -> req _ -> req -- todo: given a .json or .html suffix, munge it into an Accept header + -- can use MessageWrap.pathEls to reparse rqPath + +{-case lookup "_patharg" (rqInputs req) of + Just param -> req' { rqUri = rqUri req SURI.escape param, rqPath = rqPath req ++ [param] } + _ -> req' + where req' = -} -- | Perform a clean shutdown of the server. -- shutdown :: Server -> IO () @@ -181,7 +182,7 @@ bulkImport :: Server -> Maybe String -- users -> Maybe String -- admin users -> IO [UploadLog.Entry] -bulkImport (Server _ (Feature.Config store _ host) _ cache) +bulkImport (Server _ _ (Config store _ host cache)) indexFile logFile archiveFile htPasswdFile adminsFile = do pkgIndex <- either fail return (BulkImport.importPkgIndex indexFile) uploadLog <- either fail return (BulkImport.importUploadLog logFile) @@ -192,19 +193,18 @@ bulkImport (Server _ (Feature.Config store _ host) _ cache) (pkgsInfo, users, badLogEntries) <- either fail return (BulkImport.mergePkgInfo pkgIndex uploadLog tarballs accounts) - update $ BulkImport pkgsInfo users + update $ BulkImport pkgsInfo + update $ ReplaceUserDb users - admPerms <- case admins of - Nothing -> return [] + case admins of + Nothing -> return () Just adminUsers -> do - state <- query GetPackagesState - uids <- either fail return $ lookupUsers (userDb state) adminUsers - return $ map (\uid -> (uid, Administrator)) uids - - let uploadPerms - = map (\pkg -> (pkgUploadUser pkg, PackageMaintainer (packageName pkg))) pkgsInfo + userDb <- query GetUserDb + uids <- either fail return $ lookupUsers userDb adminUsers + mapM_ (\uid -> update $ AddHackageAdmin uid) uids - update $ BulkImportPermissions (admPerms ++ uploadPerms) + --let uploadPerms = map (\pkg -> (pkgUploadUser pkg, PackageMaintainer (packageName pkg))) pkgsInfo + --update $ BulkImportPermissions (admPerms ++ uploadPerms) updateCache cache host @@ -221,7 +221,7 @@ bulkImport (Server _ (Feature.Config store _ host) _ cache) Just uid -> Right uid importTar :: Server -> ByteString -> IO (Maybe String) -importTar (Server _ (Feature.Config store _ host) _ cache) tar = do +importTar (Server _ _ (Config store _ host cache)) tar = do res <- Import.importTar store tar case res of Nothing -> updateCache cache host @@ -231,53 +231,43 @@ importTar (Server _ (Feature.Config store _ host) _ cache) tar = do -- An alternative to an import. -- Starts the server off to a sane initial state. initState :: MonadIO m => Server -> m () -initState (Server _ (Feature.Config _ _ host) _ cache) = do +initState (Server _ _ (Config _ _ host cache)) = do -- clear off existing state - update $ BulkImport [] Users.empty - update $ BulkImportPermissions [] + update $ BulkImport [] + update $ ReplaceUserDb Users.empty +--update $ BulkImportPermissions [] -- create default admin user let userName = Users.UserName "admin" - userAuth <- newPasswd (PasswdPlain "admin") - res <- update $ AddUser userName userAuth + userAuth = Auth.newDigestPass userName (Auth.PasswdPlain "admin") "hackage" + res <- update $ AddUser userName (Users.UserAuth userAuth Auth.DigestAuth) case res of - Just user -> update $ AddToGroup Administrator user + Just user -> update $ State.AddHackageAdmin user _ -> fail "Failed to create admin user!" updateCache cache host impl :: Server -> ServerPart Response -impl server = flip renderServerTree Map.empty $ spiffyResources $ foldl' (flip $ uncurry addResponse) serverTreeEmpty $ ([], core server):concatMap (Feature.serverParts) Features.hackageFeatures +impl server = renderServerTree (serverConfig server) [] $ foldr (uncurry addResponse) serverTreeEmpty $ ([], \_ _ -> core server):concatMap Feature.locations Features.hackageFeatures core :: Server -> ServerPart Response -core (Server _ (Feature.Config store static host) _ cache) = msum - [ dir "packages" $ - methodSP GET $ - ok . Cache.packagesPage =<< Cache.get cache - , dir "package" $ msum +core (Server _ _ (Config store static _ cache)) = msum +{- [ dir "package" $ msum [ path $ msum . handlePackageById store , path $ servePackage store - ] - , dir "buildreports" $ msum (buildReports store) + ]-} + [ dir "buildreports" $ msum (buildReports store) -- , dir "groups" (groupInterface) , dir "recent.rss" $ msum [ methodSP GET $ ok . Cache.packagesFeed =<< Cache.get cache ] , dir "recent.html" $ msum [ methodSP GET $ ok . Cache.recentChanges =<< Cache.get cache ] - , dir "upload" $ msum - [ uploadPackage store cache host ] - , dir "00-index.tar.gz" $ msum - [ methodSP GET $ do - cacheState <- Cache.get cache - ok $ toResponse $ Resource.IndexTarball (Cache.indexTarball cacheState) - ] , dir "admin" $ admin static store , dir "check" checkPackage - , dir "htpasswd" $ msum - [ changePassword ] - , dir "distro" distros +-- , dir "htpasswd" $ msum [ changePassword ] +-- , dir "distro" distros , fileServe ["hackage.html"] static ] @@ -285,12 +275,12 @@ core (Server _ (Feature.Config store static host) _ cache) = msum -- directory admin :: FilePath -> BlobStorage -> ServerPart Response admin static storage = do - - guardAuth [Administrator] - - msum - [ dir "users" userAdmin - , dir "export.tar.gz" (export storage) - , adminDist - , fileServe ["admin.html"] static - ] + userDb <- query State.GetUserDb + let admins = Users.adminList userDb + Auth.requireHackageAuth userDb (Just admins) Nothing + msum + [ dir "users" userAdmin + , dir "export.tar.gz" (export storage) +-- , adminDist + , fileServe ["admin.html"] static + ] diff --git a/Distribution/Server/Auth/Basic.hs b/Distribution/Server/Auth/Basic.hs index 7d82a3915..b78219159 100644 --- a/Distribution/Server/Auth/Basic.hs +++ b/Distribution/Server/Auth/Basic.hs @@ -1,6 +1,6 @@ module Distribution.Server.Auth.Basic ( - hackageAuth, - hackageDigestAuth + getHackageAuth, + requireHackageAuth, ) where import Distribution.Server.Users.Types @@ -9,18 +9,16 @@ import qualified Distribution.Server.Users.Types as Users import qualified Distribution.Server.Users.Users as Users import qualified Distribution.Server.Users.Group as Group import qualified Distribution.Server.Auth.Crypt as Crypt +import Distribution.Server.Auth.Types import Happstack.Server --- ( ServerPartT(..), withRequest, getHeader, escape --- , unauthorized, addHeader, toResponse ) import qualified Happstack.Crypto.Base64 as Base64 import Control.Monad.Trans (MonadIO, liftIO) -import Control.Monad (guard) import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Lazy.Char8 as BS.Lazy -import Control.Monad (mzero, join, liftM2) +import Control.Monad (join, liftM2, mplus, when) import Data.Char (intToDigit, isAsciiLower) import System.Random (randomRs, newStdGen) import Data.Map (Map) @@ -29,46 +27,99 @@ import qualified Text.ParserCombinators.ReadP as Parse import Data.Maybe (maybe) import Data.List (find, intercalate) import Data.Digest.Pure.MD5 (md5) -import Debug.Trace -hackageAuth :: MonadIO m => Users.Users -> Maybe Group.UserGroup - -> ServerPartT m UserId -hackageAuth users authorisedGroup = genericBasicAuth realm cryptPasswdCheck - where - realm = "hackage" - cryptPasswdCheck userName passwd = do - userId <- Users.lookupName userName users - userInfo <- Users.lookupId userId users - guard $ case Users.userStatus userInfo of - Users.Enabled hash -> Crypt.checkPasswd passwd hash - _ -> False - guard (maybe True (Group.member userId) authorisedGroup) - return userId - --- This is directly ripped out of Happstack-Server and generalised --- -genericBasicAuth :: Monad m => String -> (UserName -> PasswdPlain -> Maybe a) - -> ServerPartT m a -genericBasicAuth realmName validLogin = withRequest $ \req -> - case getHeader "authorization" req of - Just h -> case checkAuth h of - Just res -> return res -- unServerPartT (multi (xs ok)) req - Nothing -> err - _ -> err +-- genericBasicAuth :: Request -> String -> (UserName -> Maybe (a, Users.UserAuth)) -> Either AuthError a +getHackageAuth :: Monad m => Users.Users -> ServerPartT m (Either AuthError UserId) +getHackageAuth users = askRq >>= \req -> return $ case getAuthType req of + Just BasicAuth -> genericBasicAuth req "hackage" (getPasswdInfo users) + Just DigestAuth -> genericDigestAuth req (getPasswdInfo users) --realm hashed in user database + Nothing -> Left NoAuthError + +getAuthType :: Request -> Maybe AuthType +getAuthType req = case getHeader "authorization" req of + Just h | BS.isPrefixOf (BS.pack "Digest ") h -> Just DigestAuth + | BS.isPrefixOf (BS.pack "Basic ") h -> Just BasicAuth + _ -> Nothing + +-- semantic ambiguity: does Nothing mean allow everyone, or only allow enabled? Currently, the former. +-- disabled users might want to perform some authorized action, like login or change their password +requireHackageAuth :: MonadIO m => Users.Users -> Maybe Group.UserList -> Maybe AuthType -> ServerPartT m UserId +requireHackageAuth users authorisedGroup forceType = getHackageAuth users >>= \res -> case res of + Right userId -> do + let forbid = escape $ forbidden $ toResponse "No access for this page." + isDisabled = case Users.userStatus `fmap` Users.lookupId userId users of + Just (Users.Active Users.Disabled _) -> True + _ -> False + when (maybe True (\group -> Group.member userId group && not isDisabled) authorisedGroup) $ forbid + return userId + Left NoAuthError -> makeAuthPage "No authorization provided." + Left UnrecognizedAuthError -> makeAuthPage "Authorization scheme not recognized." + Left NoSuchUserError -> makeAuthPage "Username or password incorrect." + Left PasswordMismatchError -> makeAuthPage "Username or password incorrect." + -- the complicated migrating case + Left AuthTypeMismatchError -> makeAuthPage "You can't use the more secure MD5 digest authentication because the server has already hashed your password in a different format. Try logging in using basic authentication and then submitting a password change request to let the server rehash your password in digest form (recommended)." where - checkAuth h = do - (user, pass) <- parseHeader h - validLogin user pass + makeAuthPage str = do + req <- askRq + let response = toResponse $ "401 Unathorized: " ++ str -- todo: render pretty XHTML + theAsk = case forceType `mplus` getAuthType req of + Just BasicAuth -> askBasicAuth + _ -> askDigestAuth + theAsk "hackage" response + +getPasswdInfo :: Users.Users -> UserName -> Maybe (UserId, Users.UserAuth) +getPasswdInfo users userName = do + userId <- Users.lookupName userName users + userInfo <- Users.lookupId userId users + auth <- case Users.userStatus userInfo of + Users.Active _ auth -> Just auth + _ -> Nothing + return (userId, auth) +( Maybe b -> Either a b +e String -> (UserName -> Maybe (a, Users.UserAuth)) -> Either AuthError a +genericBasicAuth req realmName userDetails = do + authHeader <- NoAuthError Crypt.checkPasswd pass hash + DigestAuth -> Crypt.newDigestPass userName pass realmName == hash + if matches then Right var else Left PasswordMismatchError + where parseHeader h = case splitHeader h of (name, ':':pass) -> Just (UserName name, PasswdPlain pass) _ -> Nothing splitHeader = break (':'==) . Base64.decode . BS.unpack . BS.drop 6 +-- assumes user is not already authorized +askBasicAuth :: MonadIO m => String -> Response -> ServerPartT m a +askBasicAuth realmName response = escape $ unauthorized $ + addHeader headerName headerValue $ response + where headerName = "WWW-Authenticate" headerValue = "Basic realm=\"" ++ realmName ++ "\"" - err = escape $ unauthorized $ - addHeader headerName headerValue $ toResponse "Not authorized" + +-------------------------------------------------------------------------------- +digestPasswdCheck :: Request -> Map String String -> PasswdHash -> Maybe Bool +digestPasswdCheck req authMap (PasswdHash hash1) = do + nonce <- Map.lookup "nonce" authMap + response <- Map.lookup "response" authMap + uri <- Map.lookup "uri" authMap + let qop = Map.lookup "qop" authMap + nonceCount = Map.lookup "nc" authMap + cnonce = Map.lookup "cnonce" authMap --insert (join traceShow) before intercalates to debug + hash2 = show . md5 . BS.Lazy.pack $ intercalate ":" [show (rqMethod req), uri] ++ case qop of + Just "auth-int" -> (':':) . show . md5 $ case rqBody req of Body body -> body + _ -> "" + responseString = show . md5 . BS.Lazy.pack . intercalate ":" $ case (qop, nonceCount, cnonce) of + (Just qop', Just nonceCount', Just cnonce') + | qop' == "auth" || qop' == "auth-int" -> [hash1, nonce, nonceCount', cnonce', qop', hash2] + _ -> [hash1, nonce, hash2] + return (responseString == response) -- This is the digest version of the above code, utilizing the UserInfo map as -- though it contained MD5 hashes of the form username:hackage:password. @@ -76,48 +127,21 @@ genericBasicAuth realmName validLogin = withRequest $ \req -> -- To use both systems at the same time, determine at the start of a client -- "session" whether to request Basic or Digest authentication, and then -- demultiplex when the client sends the Authorization header. -hackageDigestAuth :: MonadIO m => Users.Users -> Maybe Group.UserGroup -> ServerPartT m UserId -hackageDigestAuth users authorizedGroup = genericDigestAuth realm digestPasswdCheck - where - realm = "hackage" - digestPasswdCheck :: Request -> Map String String -> Maybe UserId - digestPasswdCheck req authMap = do - userName <- Map.lookup "username" authMap - userId <- Users.lookupName (UserName userName) users - guard (maybe True (Group.member userId) authorizedGroup) - userInfo <- Users.lookupId userId users - --split this off into a separate module that hides the MD5 business - case Users.userStatus userInfo of - Users.Enabled (Users.PasswdHash hash1) -> do - nonce <- Map.lookup "nonce" authMap - response <- Map.lookup "response" authMap - uri <- Map.lookup "uri" authMap - let qop = Map.lookup "qop" authMap - nonceCount = Map.lookup "nc" authMap - cnonce = Map.lookup "cnonce" authMap - hash2 = show . md5 . BS.Lazy.pack . join traceShow $ intercalate ":" [show (rqMethod req), uri] ++ case qop of - Just "auth-int" -> (':':) . show . md5 $ case rqBody req of Body body -> body - _ -> "" - responseString = show . md5 . BS.Lazy.pack . join traceShow . intercalate ":" $ case (qop, nonceCount, cnonce) of - (Just qop', Just nonceCount', Just cnonce') - | qop' == "auth" || qop' == "auth-int" -> [hash1, nonce, nonceCount', cnonce', qop', hash2] - _ -> [hash1, nonce, hash2] - guard (responseString == response) - return userId - _ -> mzero - -genericDigestAuth :: MonadIO m => String -> (Request -> Map String String -> Maybe a) -> ServerPartT m a -genericDigestAuth realmName validLogin = askRq >>= \req -> do - let authMap = case getHeader "authorization" req of - Nothing -> Map.empty - Just h -> parseDigestResponse . BS.unpack $ h - case validLogin req authMap of - Nothing -> setDigestChallenge realmName - Just res -> return res +genericDigestAuth :: Request -> (UserName -> Maybe (a, Users.UserAuth)) -> Either AuthError a +genericDigestAuth req userDetails = do + authHeader <- NoAuthError Left AuthTypeMismatchError + DigestAuth -> do + matches <- UnrecognizedAuthError Map String String -parseDigestResponse = maybe Map.empty (Map.fromList . fst) . find (null . snd) . +parseDigestResponse :: String -> Maybe (Map String String) +parseDigestResponse = fmap (Map.fromList . fst) . find (null . snd) . -- giving ReadS [(String, String)] = [([(String, String)], String)] Parse.readP_to_S (Parse.string "Digest " >> Parse.skipSpaces >> parser) where @@ -129,13 +153,14 @@ parseDigestResponse = maybe Map.empty (Map.fromList . fst) . find (null . snd) . quotedString = join Parse.between (Parse.char '"') (Parse.many $ (Parse.char '\\' >> Parse.get) Parse.<++ Parse.satisfy (/='"')) Parse.<++ (liftM2 (:) (Parse.satisfy (/='"')) (Parse.munch (/=','))) -setDigestChallenge :: MonadIO m => String -> ServerPartT m a -setDigestChallenge realmName = do +askDigestAuth :: MonadIO m => String -> Response -> ServerPartT m a +askDigestAuth realmName response = do nonce <- liftIO generateNonce - escape $ unauthorized $ addHeader headerName (headerValue nonce) $ toResponse "Not authorized under digest" + escape $ unauthorized $ addHeader headerName (headerValue nonce) $ response where headerName = "WWW-Authenticate" - -- I would use qop=\"auth,auth-int\", but Google Chrome seems to have problems choosing one... + -- I would use qop=\"auth,auth-int\", but Google Chrome seems to have problems choosing one + -- http://code.google.com/p/chromium/issues/detail?id=45194 headerValue nonce = "Digest realm=\"" ++ realmName ++ "\", qop=\"auth\", nonce=\"" ++ nonce ++ "\", opaque=\"\"" generateNonce = fmap (take 32 . map intToDigit . randomRs (0, 15)) newStdGen diff --git a/Distribution/Server/Auth/Crypt.hs b/Distribution/Server/Auth/Crypt.hs index 1e83f7f9c..f915151e6 100644 --- a/Distribution/Server/Auth/Crypt.hs +++ b/Distribution/Server/Auth/Crypt.hs @@ -2,7 +2,8 @@ module Distribution.Server.Auth.Crypt ( PasswdPlain(..), PasswdHash(..), - newPasswd, + newBasicPass, + newDigestPass, checkPasswd, -- * raw crypt @@ -10,14 +11,22 @@ module Distribution.Server.Auth.Crypt ( ) where import Distribution.Server.Auth.Types +import Distribution.Server.Users.Types (UserName(..)) import System.Random (Random(randomRs), RandomGen) +import Data.Digest.Pure.MD5 (md5) +import qualified Data.ByteString.Lazy.Char8 as BS.Lazy +import Data.List (intercalate) import Foreign (unsafePerformIO) import Foreign.C (CString, withCAString, peekCAString) -newPasswd :: RandomGen rnd => rnd -> PasswdPlain -> PasswdHash -newPasswd rnd (PasswdPlain plain) = PasswdHash (crypt plain salt) +newDigestPass :: UserName -> PasswdPlain -> String -> PasswdHash +newDigestPass (UserName userName) (PasswdPlain passwd) realm = + PasswdHash . show . md5 . BS.Lazy.pack $ intercalate ":" [userName, realm, passwd] + +newBasicPass :: RandomGen rnd => rnd -> PasswdPlain -> PasswdHash +newBasicPass rnd (PasswdPlain plain) = PasswdHash (crypt plain salt) where salt :: [Char] salt = take 2 (randomRs ('\1', '\255') rnd) diff --git a/Distribution/Server/Auth/Types.hs b/Distribution/Server/Auth/Types.hs index 7dfbd4d32..ad4b6002f 100644 --- a/Distribution/Server/Auth/Types.hs +++ b/Distribution/Server/Auth/Types.hs @@ -1,11 +1,28 @@ {-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving #-} module Distribution.Server.Auth.Types where +import qualified Data.Binary as Binary import Data.Binary (Binary) import Data.Typeable (Typeable) +import Control.Monad.Error.Class (Error, noMsg) newtype PasswdPlain = PasswdPlain String deriving (Eq, Ord, Show, Binary, Typeable) newtype PasswdHash = PasswdHash String deriving (Eq, Ord, Show, Binary, Typeable) + +data AuthType = BasicAuth | DigestAuth + deriving (Show, Enum, Eq, Typeable) + +data AuthError = NoAuthError | UnrecognizedAuthError | NoSuchUserError + | PasswordMismatchError | AuthTypeMismatchError + deriving (Enum, Eq, Show, Typeable) + +instance Binary AuthType where + put t = Binary.put (t == DigestAuth) + get = fmap (\b -> if b then DigestAuth else BasicAuth) Binary.get + +instance Error AuthError where + noMsg = NoAuthError + diff --git a/Distribution/Server/BulkImport.hs b/Distribution/Server/BulkImport.hs index 355be4735..88f406011 100644 --- a/Distribution/Server/BulkImport.hs +++ b/Distribution/Server/BulkImport.hs @@ -26,6 +26,7 @@ import qualified Codec.Archive.Tar.Entry as Tar ( Entry(..), entryPath, EntryContent(..) ) import qualified Distribution.Server.BulkImport.UploadLog as UploadLog import qualified Distribution.Server.Auth.HtPasswdDb as HtPasswdDb +import qualified Distribution.Server.Auth.Types as Auth import qualified Distribution.Server.Util.BlobStorage as BlobStorage import Distribution.Server.Util.BlobStorage (BlobStorage) import Distribution.Server.Packages.Types (PkgInfo(..)) @@ -72,11 +73,10 @@ newPkgInfo pkgid (cabalFilePath, cabalFile) (UploadLog.Entry time user _) pkgInfoId = pkgid, pkgDesc = pkg, pkgData = cabalFile, - pkgTarball = Nothing, - pkgUploadTime = time, - pkgUploadUser = Users.nameToId users user, - pkgUploadOld = [ (time', Users.nameToId users user') - | UploadLog.Entry time' user' _ <- others] + pkgTarball = [], + pkgUploadData = (time, Users.nameToId users user), + pkgDataOld = []--[ (time', Users.nameToId users user') + -- | UploadLog.Entry time' user' _ <- others] } where parse = parsePackageDescription . fromUTF8 . BS.unpack @@ -114,7 +114,7 @@ importUsers (Just htpasswdFile) = importUsers' Users.empty where importUsers' users [] = Right users importUsers' users ((userName, userAuth):rest) = - case Users.add userName userAuth users of + case Users.add userName (Users.UserAuth userAuth Auth.BasicAuth) users of Nothing -> Left (alreadyPresent userName) Just (users', _userId) -> importUsers' users' rest @@ -138,10 +138,10 @@ mergeDeletedUsers logEntries users0 = where addUser (users, added) (UploadLog.Entry _ userName _) = case Users.add userName dummyAuth users of - Nothing -> (users , added) -- already present - Just (users', userId) -> (users', userId:added) + Nothing -> (users , added) -- are already present + Just (users', userId) -> (users', userId:added) -- shall not be spared from the delete-fold - dummyAuth = Users.PasswdHash "" + dummyAuth = Users.UserAuth (Auth.PasswdHash "") (Auth.BasicAuth) deleteUser users userId = users' where Just users' = Users.delete userId users @@ -215,7 +215,7 @@ mergeTarballs tarballInfo pkgs = mergePkgs merged [] = Right merged mergePkgs merged (next:remaining) = case next of InBoth (_, blobid) pkginfo -> mergePkgs (pkginfo':merged) remaining - where pkginfo' = pkginfo { pkgTarball = Just blobid } + where pkginfo' = pkginfo { pkgTarball = (blobid, pkgUploadData pkginfo):pkgTarball pkginfo } OnlyInLeft (pkgid, _) -> Left missing where missing = "Package tarball missing metadata " ++ display pkgid OnlyInRight pkginfo -> mergePkgs (pkginfo:merged) remaining diff --git a/Distribution/Server/Distributions/Distributions.hs b/Distribution/Server/Distributions/Distributions.hs index 44274602e..d718d7b27 100644 --- a/Distribution/Server/Distributions/Distributions.hs +++ b/Distribution/Server/Distributions/Distributions.hs @@ -18,12 +18,16 @@ module Distribution.Server.Distributions.Distributions , removeDistroVersions , distroStatus , packageStatus + , getDistroMaintainers + , modifyDistroMaintainers ) where import qualified Data.Map as Map import qualified Data.Set as Set import Distribution.Server.Distributions.Types +import qualified Distribution.Server.Users.Group as Group +import Distribution.Server.Users.Group (UserList) import Distribution.Package @@ -31,30 +35,29 @@ import Data.List (foldl') import Data.Maybe (fromJust) emptyDistributions :: Distributions -emptyDistributions = Distributions Set.empty +emptyDistributions = Distributions Map.empty emptyDistroVersions :: DistroVersions emptyDistroVersions = DistroVersions Map.empty Map.empty - --- Distribution updating isDistribution :: DistroName -> Distributions -> Bool isDistribution distro Distributions{..} - = Set.member distro name_map + = Map.member distro name_map -- | Add a distribution. Returns 'Nothing' if the -- name is already in use. addDistro :: DistroName -> Distributions -> Maybe Distributions addDistro name d@Distributions{..} | isDistribution name d = Nothing - | otherwise = Just . Distributions $ Set.insert name name_map + | otherwise = Just . Distributions $ Map.insert name Group.empty name_map -- | List all known distributions enumerate :: Distributions -> [DistroName] enumerate Distributions{..} - = Set.toList name_map + = Map.keys name_map --- Queries @@ -62,8 +65,8 @@ enumerate Distributions{..} -- at which version. distroStatus :: DistroName -> DistroVersions -> [(PackageName, DistroPackageInfo)] distroStatus distro DistroVersions{..} - = let packageNames = maybe [] Set.toList (Map.lookup distro distro_map) - f package = let infoMap = fromJust $ Map.lookup package package_map + = let packageNames = maybe [] Set.toList (Map.lookup distro distroMap) + f package = let infoMap = fromJust $ Map.lookup package packageDistroMap info = fromJust $ Map.lookup distro infoMap in (package, info) in map f packageNames @@ -71,22 +74,18 @@ distroStatus distro DistroVersions{..} -- | For a particular package, which distributions contain it and at which -- version. packageStatus :: PackageName -> DistroVersions -> [(DistroName, DistroPackageInfo)] -packageStatus package DistroVersions{..} - = maybe [] Map.toList (Map.lookup package package_map) +packageStatus package DistroVersions{..} = maybe [] Map.toList (Map.lookup package packageDistroMap) --- Removing -- | Remove a distirbution from the list of known distirbutions removeDistro :: DistroName -> Distributions -> Distributions -removeDistro distro distros@Distributions{..} - = distros - { name_map = Set.delete distro name_map - } +removeDistro distro distros@Distributions{..} = distros { name_map = Map.delete distro name_map } -- | Drop all packages for a distribution. removeDistroVersions :: DistroName -> DistroVersions -> DistroVersions removeDistroVersions distro dv@DistroVersions{..} - = let packageNames = maybe [] Set.toList (Map.lookup distro distro_map) + = let packageNames = maybe [] Set.toList (Map.lookup distro distroMap) in foldl' (flip $ dropPackage distro) dv packageNames --- Updating @@ -95,8 +94,8 @@ removeDistroVersions distro dv@DistroVersions{..} dropPackage :: DistroName -> PackageName -> DistroVersions -> DistroVersions dropPackage distro package dv@DistroVersions{..} = dv - { package_map = Map.update pUpdate package package_map - , distro_map = Map.update dUpdate distro distro_map + { packageDistroMap = Map.update pUpdate package packageDistroMap + , distroMap = Map.update dUpdate distro distroMap } where pUpdate infoMap = case Map.delete distro infoMap of @@ -118,15 +117,23 @@ addPackage :: DistroName -> PackageName -> DistroPackageInfo -> DistroVersions -> DistroVersions addPackage distro package info dv@DistroVersions{..} = dv - { package_map = Map.insertWith' + { packageDistroMap = Map.insertWith' (const $ Map.insert distro info) package (Map.singleton distro info) - package_map + packageDistroMap - , distro_map = Map.insertWith -- should be insertWith'? + , distroMap = Map.insertWith -- should be insertWith'? (const $ Set.insert package) distro (Set.singleton package) - distro_map + distroMap } + +getDistroMaintainers :: DistroName -> Distributions -> Maybe UserList +getDistroMaintainers name = Map.lookup name . name_map + +modifyDistroMaintainers :: DistroName -> (UserList -> UserList) -> Distributions -> Distributions +modifyDistroMaintainers name func dists = dists {name_map = Map.update (Just . func) name (name_map dists) } + + diff --git a/Distribution/Server/Distributions/ServerParts.hs b/Distribution/Server/Distributions/ServerParts.hs index 99ecabe9e..172897a71 100644 --- a/Distribution/Server/Distributions/ServerParts.hs +++ b/Distribution/Server/Distributions/ServerParts.hs @@ -16,7 +16,6 @@ import Distribution.Server.Users.State import Distribution.Server.Distributions.Distributions import Distribution.Server.Users.Types -import Distribution.Server.Users.Permissions (GroupName(..)) import qualified Distribution.Server.ResourceTypes as Resources @@ -35,6 +34,16 @@ import System.FilePath.Posix (()) import Text.URI (escapeString, okInPath) +maintainersGroup :: DynamicPath -> Maybe (UserGroup GetDistroMaintainers AddDistroMaintainer RemoveDistroMaintainer) +maintainersGroup dpath = do + distroStr <- lookup "distro" dpath + let distroName = DistroName distroStr + return $ UserGroup { + groupName = "Maintainers for " ++ distroStr, + queryUserList = Dist.GetDistroMaintainers distroName, + addUserList = Dist.AddDistroMaintainer distroName, + removeUserList = Dist.RemoveDistroMaintainer distroName + } {-| Administrator entry points. Assumes that permissions have already been verified. These parts are for driving diff --git a/Distribution/Server/Distributions/State.hs b/Distribution/Server/Distributions/State.hs index 241540870..652306805 100644 --- a/Distribution/Server/Distributions/State.hs +++ b/Distribution/Server/Distributions/State.hs @@ -16,6 +16,11 @@ import qualified Distribution.Server.Distributions.Distributions as Dist import Distribution.Server.Distributions.Distributions (DistroName, Distributions, DistroVersions, DistroPackageInfo) +import Distribution.Server.Users.Group (UserList) +import Distribution.Server.Users.Types (UserId) +import qualified Distribution.Server.Users.Group as Group +import Distribution.Server.Users.State () + import Data.Typeable import Happstack.State @@ -23,11 +28,10 @@ import Happstack.State import Control.Monad.State.Class (get, put, modify) import Control.Monad.Reader.Class (ask, asks) -data Distros - = Distros - { dist_distros :: !Distributions - , dist_versions :: !DistroVersions - } +data Distros = Distros { + dist_distros :: !Distributions, + dist_versions :: !DistroVersions +} deriving (Typeable, Show) instance Version Distros @@ -44,17 +48,16 @@ addDistro name let distros = dist_distros state case Dist.addDistro name distros of Nothing -> return False - Just distros' - -> put state{dist_distros = distros'} >> return True + Just distros' -> put state{dist_distros = distros'} >> return True -- DELETES a distribution. The name may then be re-used. -- You should also clean up the permissions DB as well. removeDistro :: DistroName -> Update Distros () removeDistro distro = modify $ \state@Distros{..} -> - state{ dist_distros = Dist.removeDistro distro dist_distros - , dist_versions = Dist.removeDistroVersions distro dist_versions - } + state { dist_distros = Dist.removeDistro distro dist_distros + , dist_versions = Dist.removeDistroVersions distro dist_versions + } enumerate :: Query Distros [DistroName] enumerate = asks $ Dist.enumerate . dist_distros @@ -88,6 +91,17 @@ packageStatus :: PackageName -> Query Distros [(DistroName, DistroPackageInfo)] packageStatus package = asks $ Dist.packageStatus package . dist_versions +getDistroMaintainers :: DistroName -> Query Distros (Maybe UserList) +getDistroMaintainers name = fmap (Dist.getDistroMaintainers name) (asks dist_distros) + +modifyDistroMaintainers :: DistroName -> (UserList -> UserList) -> Update Distros () +modifyDistroMaintainers name func = modify (\distros -> distros {dist_distros = Dist.modifyDistroMaintainers name func (dist_distros distros) }) + +addDistroMaintainer :: DistroName -> UserId -> Update Distros () +addDistroMaintainer name uid = modifyDistroMaintainers name (Group.add uid) + +removeDistroMaintainer :: DistroName -> UserId -> Update Distros () +removeDistroMaintainer name uid = modifyDistroMaintainers name (Group.remove uid) $(mkMethods ''Distros @@ -110,5 +124,10 @@ $(mkMethods -- import/export , 'getDistributions , 'replaceDistributions + + -- distro maintainers + , 'getDistroMaintainers + , 'addDistroMaintainer + , 'removeDistroMaintainer ] ) diff --git a/Distribution/Server/Distributions/Types.hs b/Distribution/Server/Distributions/Types.hs index 1f6e46ec5..f395a97f3 100644 --- a/Distribution/Server/Distributions/Types.hs +++ b/Distribution/Server/Distributions/Types.hs @@ -9,6 +9,8 @@ module Distribution.Server.Distributions.Types where import Distribution.Server.Instances() +import Distribution.Server.Users.State() +import Distribution.Server.Users.Group (UserList) import qualified Data.Map as Map import qualified Data.Set as Set @@ -18,8 +20,7 @@ import Distribution.Package import Control.Applicative ((<$>)) -import Distribution.Text - ( Text(..) ) +import Distribution.Text (Text(..)) import qualified Distribution.Compat.ReadP as Parse import qualified Text.PrettyPrint as Disp @@ -40,22 +41,20 @@ instance Text DistroName where -- | Listing of known distirbutions -data Distributions = Distributions - { name_map :: !(Set.Set DistroName) - } +data Distributions = Distributions { + name_map :: !(Map.Map DistroName UserList) +} deriving (Typeable, Show) instance Version Distributions -- | Listing of which distirbutions have which version of particular -- packages. -data DistroVersions = DistroVersions - { package_map :: !(Map.Map PackageName (Map.Map DistroName DistroPackageInfo)) - , distro_map :: !(Map.Map DistroName (Set.Set PackageName)) - } - deriving (Typeable, Show) +data DistroVersions = DistroVersions { + packageDistroMap :: !(Map.Map PackageName (Map.Map DistroName DistroPackageInfo)), + distroMap :: !(Map.Map DistroName (Set.Set PackageName)) +} deriving (Typeable, Show) instance Version DistroVersions - data DistroPackageInfo = DistroPackageInfo { distro_version :: Version.Version diff --git a/Distribution/Server/Export.hs b/Distribution/Server/Export.hs index a107a7df0..612b246ee 100644 --- a/Distribution/Server/Export.hs +++ b/Distribution/Server/Export.hs @@ -51,8 +51,6 @@ import Distribution.Server.Distributions.Distributions import qualified Distribution.Server.Distributions.Distributions as Distros import Distribution.Server.Users.Users (Users) -import Distribution.Server.Users.Permissions (Permissions) -import qualified Distribution.Server.Users.Permissions as Permissions import Distribution.Server.Packages.Types import Distribution.Server.Packages.State @@ -75,7 +73,6 @@ import Data.Time export :: Users - -> Permissions.Permissions -> PackageIndex PkgInfo -> Documentation -> BuildReports @@ -83,14 +80,13 @@ export :: Users -> Distributions -> DistroVersions -> IO BSL.ByteString -export users permissions pkgs docs reports storage dists distInfo +export users pkgs docs reports storage dists distInfo = (compress . Tar.write) `fmap` tarball where tarball :: IO [Tar.Entry] - = mkExportEntries users permissions pkgs docs reports storage dists distInfo + = mkExportEntries users pkgs docs reports storage dists distInfo mkExportEntries :: Users - -> Permissions.Permissions -> PackageIndex PkgInfo -> Documentation -> BuildReports @@ -98,7 +94,7 @@ mkExportEntries :: Users -> Distributions -> DistroVersions -> IO [Tar.Entry] -mkExportEntries users perms pkgs docs reports storage dists distInfo +mkExportEntries users pkgs docs reports storage dists distInfo = do baseDir <- mkBaseDir `fmap` getCurrentTime @@ -106,7 +102,6 @@ mkExportEntries users perms pkgs docs reports storage dists distInfo return $ concat [ mkUserEntries baseDir users - , mkPermsEntries baseDir perms , mkDistroEntries baseDir dists distInfo , packageEntries ] @@ -178,9 +173,9 @@ mkPackageEntry baseDir storage docs reports pkgInfo mkSourceEntry :: PkgInfo -> BlobStorage -> IO (Maybe Tar.Entry) mkSourceEntry pkgInfo storage = case pkgTarball pkgInfo of - Nothing -> return Nothing - Just blob -> Just `fmap` (mkBlobEntry storage blob $ - display (packageName pkgInfo) <.> "tar" <.> "gz") + [] -> return Nothing + ((blob, _):_) -> Just `fmap` (mkBlobEntry storage blob $ + display (packageName pkgInfo) <.> "tar" <.> "gz") -- | Tar entry for the documentation for this package mkDocumentationEntry @@ -252,12 +247,6 @@ mkUserEntries baseDir users = return $ csvToEntry (usersToCSV users) $ baseDir "users" "auth" <.> "csv" --- | Tar entry for the permissions db -mkPermsEntries :: FilePath -> Permissions -> [Tar.Entry] -mkPermsEntries baseDir perms - = return $ csvToEntry (permsToCSV perms) $ - baseDir "users" "permissions" <.> "csv" - -- | Tar entries fr distribution packaging information. mkDistroEntries :: FilePath -> Distributions -> DistroVersions -> [Tar.Entry] mkDistroEntries baseDir dists distInfo diff --git a/Distribution/Server/Export/FlatFiles.hs b/Distribution/Server/Export/FlatFiles.hs index ca3c00639..60cb05f0b 100644 --- a/Distribution/Server/Export/FlatFiles.hs +++ b/Distribution/Server/Export/FlatFiles.hs @@ -9,7 +9,7 @@ -} module Distribution.Server.Export.FlatFiles ( usersToCSV - , permsToCSV +-- , permsToCSV , uploadsToCSV , distroToCSV ) where @@ -19,8 +19,6 @@ import Distribution.Server.Export.Utils import Distribution.Server.Users.Types as Users import Distribution.Server.Users.Users as Users -import Distribution.Server.Users.Group as Group -import Distribution.Server.Users.Permissions as Permissions import qualified Distribution.Server.Distributions.Distributions as Distros import Distribution.Server.Distributions.Distributions @@ -30,6 +28,7 @@ import Distribution.Server.Distributions.Distributions ) import Distribution.Server.Packages.Types +import Distribution.Server.Auth.Types import Distribution.Text @@ -85,48 +84,30 @@ usersToCSV users infoToStatus :: Users.UserInfo -> String infoToStatus userInfo = case Users.userStatus userInfo of - Users.Deleted{} -> "deleted" - Users.Disabled{} -> "disabled" - Users.Enabled{} -> "enabled" + Users.Deleted -> "deleted" + Users.Active Users.Disabled _ -> "disabled" + Users.Active Users.Enabled _ -> "enabled" - -- one of "none" or "basic" + -- one of "none", "basic", or "digest" infoToAuthType :: Users.UserInfo -> String infoToAuthType userInfo = case Users.userStatus userInfo of - Users.Deleted{} -> "none" - _ -> "basic" + Users.Deleted -> "none" + Users.Active _ (Users.UserAuth _ atype)-> case atype of + BasicAuth -> "basic" + DigestAuth -> "digest" -- may be null infoToAuth :: Users.UserInfo -> String infoToAuth userInfo = case Users.userStatus userInfo of Users.Deleted{} -> "" - Users.Disabled (PasswdHash hash) -> hash - Users.Enabled (PasswdHash hash) -> hash + Users.Active _ (UserAuth (PasswdHash hash) _) -> hash userCSVVer :: Version -userCSVVer = Version [0,1] ["unstable"] - --- permissions.csv -{-| User groups membership. - -} -permsToCSV :: Permissions.Permissions -> CSV -permsToCSV perms - = ([showVersion permsCSVVer]:) $ - -- (permsCSVKey:) $ - - flip map (Permissions.enumerate perms) . uncurry - $ \groupName group -> - (display groupName:) $ - ( - map display . Group.enumerate $ group - ) - - -permsCSVVer :: Version -permsCSVVer = userCSVVer +userCSVVer = Version [0,1] ["unstable"] -- uploads.csv {-| For a particular package, when and by whom was it @@ -153,10 +134,10 @@ uploadsCSVVer = userCSVVer uploadTimes :: PkgInfo -> [(UTCTime, UserId)] uploadTimes pkgInfo - = front : back + = [front] - where front = (pkgUploadTime pkgInfo, pkgUploadUser pkgInfo) - back = pkgUploadOld pkgInfo + where front = pkgUploadData pkgInfo +-- back = pkgUploadOld pkgInfo distroToCSV :: DistroName -> DistroVersions -> CSV distroToCSV distro distInfo @@ -176,4 +157,4 @@ distroToCSV distro distInfo ] distrosCSVVer :: Version -distrosCSVVer = userCSVVer \ No newline at end of file +distrosCSVVer = userCSVVer diff --git a/Distribution/Server/Export/ServerParts.hs b/Distribution/Server/Export/ServerParts.hs index a8a59e8a1..6ff940cd2 100644 --- a/Distribution/Server/Export/ServerParts.hs +++ b/Distribution/Server/Export/ServerParts.hs @@ -20,18 +20,17 @@ export :: BlobStorage -> ServerPart Response export storage = methodSP GET $ do state <- query GetPackagesState - perms <- query GetPermissions docs <- query GetDocumentation dist <- query GetDistributions + users <- query GetUserDb + rpts <- query GetBuildReports let pkgs = packageList state - rpts = buildReports state - users = userDb state dists = dist_distros dist distInfo = dist_versions dist tarball <- liftIO $ - Export.export users perms pkgs docs rpts storage dists distInfo + Export.export users pkgs docs rpts storage dists distInfo return $ toResponse . Resources.ExportTarball $ tarball diff --git a/Distribution/Server/Feature.hs b/Distribution/Server/Feature.hs index d82c14260..411d00fc2 100644 --- a/Distribution/Server/Feature.hs +++ b/Distribution/Server/Feature.hs @@ -1,9 +1,7 @@ module Distribution.Server.Feature where -import Distribution.Server.Util.BlobStorage (BlobStorage) import Distribution.Server.Resource import Happstack.Server -import qualified Network.URI as URI -- This module defines a plugin interface for hackage features. -- @@ -13,27 +11,12 @@ import qualified Network.URI as URI data HackageFeature = HackageFeature { featureName :: String, - resources :: [Resource], - serverParts :: [(BranchPath, ServerPart Response)], + locations :: [(BranchPath, ServerResponse)], dumpBackup :: IO [BackupEntry], restoreBackup :: [BackupEntry] -> IO () } -addFeatureResource :: Resource -> HackageFeature -> HackageFeature -addFeatureResource resource feature = feature { resources = resource:(resources feature) } - -addStaticURIPart :: [String] -> ServerPart Response -> HackageFeature -> HackageFeature -addStaticURIPart = addDynamicURIPart . map StaticBranch - -addDynamicURIPart :: BranchPath -> ServerPart Response -> HackageFeature -> HackageFeature -addDynamicURIPart bpath response feature = feature { serverParts = (bpath, response):(serverParts feature) } -- TODO: move this to a backup dump/restore module -- filesystem name + human readable content type BackupEntry = ([FilePath], {-Byte-}String) -data Config = Config { - serverStore :: BlobStorage, - serverStaticDir :: FilePath, - serverURI :: URI.URIAuth -} ---instance Eq SomeResource where (==) (SomeResource r1) (SomeResource r2) = typeRep r1 == typeRep r2 diff --git a/Distribution/Server/Features.hs b/Distribution/Server/Features.hs index dc825fc01..31ec25ed7 100644 --- a/Distribution/Server/Features.hs +++ b/Distribution/Server/Features.hs @@ -3,7 +3,8 @@ module Distribution.Server.Features where import Distribution.Server.Feature (HackageFeature) ---import Distribution.Server.Features.Users (usersFeature) +import Distribution.Server.Users.ServerParts (usersFeature) +import Distribution.Server.Packages.ServerParts (packagePagesFeature) --import Distribution.Server.Features.StaticFiles (staticFilesFeature) import Distribution.Server.Features.LegacyRedirects (legacyRedirectsFeature) --import Distribution.Server.Users.State (UsersStore) @@ -17,8 +18,9 @@ import Data.Typeable hackageFeatures :: [HackageFeature] hackageFeatures = [ legacyRedirectsFeature --- , usersFeature --- , staticFilesFeature + , usersFeature + , packagePagesFeature +--, staticFilesFeature ] -- For the sake of the happstack state system we need to give the list diff --git a/Distribution/Server/Features/LegacyRedirects.hs b/Distribution/Server/Features/LegacyRedirects.hs index 6e38e6d14..9d29c50ee 100644 --- a/Distribution/Server/Features/LegacyRedirects.hs +++ b/Distribution/Server/Features/LegacyRedirects.hs @@ -23,8 +23,7 @@ import Control.Monad (msum, mzero) legacyRedirectsFeature :: HackageFeature legacyRedirectsFeature = HackageFeature { featureName = "legacy redirects", - resources = [], - serverParts = [([], serveLegacyRedirects)], + locations = [([], \config dpath -> serveLegacyRedirects)], -- There is no persistent state for this feature, -- so nothing needs to be backed up. dumpBackup = return [], diff --git a/Distribution/Server/Import.hs b/Distribution/Server/Import.hs index 24e8bab2d..795ed2239 100644 --- a/Distribution/Server/Import.hs +++ b/Distribution/Server/Import.hs @@ -47,6 +47,7 @@ import Distribution.Server.Packages.State , PackagesState(..) , ReplacePackagesState(..) , ReplaceDocumentation(..) + , ReplaceBuildReports(..) ) import qualified Distribution.Server.TarIndex.State as TarIndexMap import qualified Distribution.Server.Util.Serve as TarIndex @@ -59,11 +60,10 @@ import qualified Distribution.Server.BuildReport.BuildReports as Reports import qualified Distribution.Server.Users.Users as Users import Distribution.Server.Users.Users (Users) +import Distribution.Server.Users.State (ReplaceUserDb(..)) import Distribution.Server.Users.Types -import qualified Distribution.Server.Users.Permissions as Permissions import Distribution.Server.Packages.Types -import Distribution.Server.Users.Permissions (Permissions,GroupName) -import Distribution.Server.Users.State (ReplacePermissions(..)) +import Distribution.Server.Auth.Types import qualified Distribution.Server.PackageIndex as PackageIndex import Distribution.Server.PackageIndex (PackageIndex) @@ -90,11 +90,10 @@ importTar storage tar -> do update $ ReplaceDocumentation isDocs update $ TarIndexMap.ReplaceTarIndexMap isTarIndex - update $ ReplacePackagesState $ PackagesState - isPackages - isBuildReps - isUsers - update $ ReplacePermissions isPerms + update $ ReplacePackagesState $ PackagesState isPackages + update $ ReplaceBuildReports $ isBuildReps + update $ ReplaceUserDb $ isUsers + -- update permissions here update $ ReplaceDistributions isDistributions isDistVersions return Nothing @@ -125,8 +124,7 @@ fromFile path contents go ["users","auth.csv"] = importAuth contents - go ["users","permissions.csv"] - = importPermissions contents +-- go ["users","permissions.csv"] = importPermissions contents go ["build-reports", repIdString, "report.txt"] = importReport repIdString contents @@ -164,15 +162,14 @@ impPackage [pkgName, pkgTarName] contents pkgDesc <- parsePackageDesc cabalContents case uploads of - (upTime,upUser):rest + updata:rest -- reuploads without data aren't significant -> addPackage $ PkgInfo { pkgInfoId = pkgId , pkgDesc = pkgDesc , pkgData = cabalContents - , pkgTarball = source - , pkgUploadTime = upTime - , pkgUploadUser = upUser - , pkgUploadOld = rest + , pkgTarball = case source of Just source' -> [(source', updata)]; Nothing -> [] + , pkgUploadData = updata + , pkgDataOld = [] } _ -> fail $ "Package " ++ show (display pkgId) ++ "requires at least one uploading user" @@ -272,33 +269,24 @@ importAuth contents insertUser user $ UserInfo name Deleted fromRecord - [nameStr, idStr, authType, "basic", auth] + [nameStr, idStr, isEnabled, authType, auth] = do name <- parse "user name" nameStr user <- parse "user id" idStr - authInfo <- parseAuth authType - insertUser user $ UserInfo name $ authInfo auth + authEn <- parseEnabled isEnabled + atype <- parseAuth authType + insertUser user $ UserInfo name (Active authEn $ UserAuth (PasswdHash auth) atype) fromRecord x = fail $ "Error processing auth record: " ++ show x -- parseAuth :: String -> Import (String -> UserAuth) - parseAuth "enabled" = return $ Enabled . PasswdHash - parseAuth "disabled" = return $ Disabled . PasswdHash - parseAuth sts = fail $ "unable to parse auth status: " ++ sts - -importPermissions :: ByteString -> Import () -importPermissions contents - = case customParseCSV "permissions.csv" (bytesToString contents) of - Left e -> fail e - Right csv -> mapM_ fromRecord (drop 1 csv) + parseEnabled "enabled" = return Enabled + parseEnabled "disabled" = return Disabled + parseEnabled sts = fail $ "unable to parse whether user enabled: " ++ sts - where fromRecord - (groupStr:users) - = do - groupName <- parse "group name" groupStr - forM_ users $ \userStr -> - parse "user id" userStr >>= addPermission groupName - fromRecord x = fail $ "Error handling permissions record: " ++ show x + parseAuth "digest" = return DigestAuth + parseAuth "basic" = return BasicAuth + parseAuth sts = fail $ "unable to parse auth status: " ++ sts importDistro :: String -> ByteString -> Import () importDistro filename contents @@ -350,7 +338,6 @@ strucuture, I'm okay with this. data IS = IS { isUsers :: !Users - , isPerms :: !Permissions , isPackages :: !(PackageIndex PkgInfo) , isDocs :: !Documentation , isTarIndex :: !TarIndexMap.TarIndexMap @@ -374,11 +361,6 @@ insertUser user info Just users' -> do put $ s {isUsers = users'} -addPermission :: GroupName -> UserId -> Import () -addPermission group user - = modify $ \is -> - is {isPerms = Permissions.addToGroup group user (isPerms is)} - addPackage :: PkgInfo -> Import () addPackage pkg = modify $ \is -> @@ -451,7 +433,6 @@ runImport storage imp = unImp imp err k initState initState = IS Users.empty - Permissions.empty (PackageIndex.fromList []) (Documentation Map.empty) TarIndexMap.emptyTarIndex @@ -514,3 +495,24 @@ customParseCSV filename inp chopLastRecord [] = [] chopLastRecord ([""]:[]) = [] chopLastRecord (x:xs) = x : chopLastRecord xs + +{-importPermissions :: ByteString -> Import () +importPermissions contents + = case customParseCSV "permissions.csv" (bytesToString contents) of + Left e -> fail e + Right csv -> mapM_ fromRecord (drop 1 csv) + + where fromRecord + (groupStr:users) + = do + groupName <- parse "group name" groupStr + forM_ users $ \userStr -> + parse "user id" userStr >>= addPermission groupName + fromRecord x = fail $ "Error handling permissions record: " ++ show x + +addPermission :: GroupName -> UserId -> Import () +addPermission group user + = modify $ \is -> + is {isPerms = Permissions.addToGroup group user (isPerms is)} +-} + diff --git a/Distribution/Server/Instances.hs b/Distribution/Server/Instances.hs index 0a76c5e68..13051b7aa 100644 --- a/Distribution/Server/Instances.hs +++ b/Distribution/Server/Instances.hs @@ -1,17 +1,16 @@ {-# LANGUAGE DeriveDataTypeable, StandaloneDeriving, FlexibleContexts #-} -- | 'Typeable' and 'Binary' instances for various types from Cabal +-- Major version changes may break this module. -- + module Distribution.Server.Instances () where import Distribution.Text -import Distribution.Package - ( PackageIdentifier(..), PackageName(..) ) -import Distribution.PackageDescription - ( GenericPackageDescription(..) ) -import Distribution.Version - ( Version ) +import Distribution.Package (PackageIdentifier(..), PackageName(..)) +import Distribution.PackageDescription (GenericPackageDescription(..)) +import Distribution.Version (Version) import qualified Data.Array.Unboxed as UA @@ -22,8 +21,9 @@ import Data.Time.Calendar (Day(..)) import qualified Data.Binary as Binary import Data.Binary (Binary) +import Happstack.State hiding (Version) import qualified Happstack.State as Happs -import qualified Happstack.Server as Happs +import Happstack.Server import Data.Maybe (fromJust) @@ -35,23 +35,28 @@ instance Binary PackageIdentifier where put = Binary.put . show get = fmap read Binary.get +instance Happs.Version PackageIdentifier +instance Serialize PackageIdentifier where + putCopy = contain . Binary.put . show + getCopy = contain $ fmap read Binary.get + instance Happs.Version PackageName -instance Happs.Serialize PackageName where - getCopy = Happs.contain textGet - putCopy = Happs.contain . textPut +instance Serialize PackageName where + getCopy = contain textGet + putCopy = contain . textPut instance Happs.Version Version -instance Happs.Serialize Version where - getCopy = Happs.contain textGet - putCopy = Happs.contain . textPut +instance Serialize Version where + getCopy = contain textGet + putCopy = contain . textPut -instance Happs.FromReqURI PackageIdentifier where +instance FromReqURI PackageIdentifier where fromReqURI = simpleParse -instance Happs.FromReqURI PackageName where +instance FromReqURI PackageName where fromReqURI = simpleParse -instance Happs.FromReqURI Version where +instance FromReqURI Version where fromReqURI = simpleParse -- These assume that the text representations @@ -63,17 +68,17 @@ textPut :: Text a => a -> Binary.Put textPut = Binary.put . display instance Happs.Version (UA.UArray ix e) where - mode = Happs.Primitive + mode = Primitive -instance (Happs.Serialize ix, Happs.Serialize e, UA.Ix ix, - UA.IArray UA.UArray e) => Happs.Serialize (UA.UArray ix e) where - getCopy = Happs.contain $ do - bounds <- Happs.safeGet - assocs <- Happs.safeGet +instance (Serialize ix, Serialize e, UA.Ix ix, + UA.IArray UA.UArray e) => Serialize (UA.UArray ix e) where + getCopy = contain $ do + bounds <- safeGet + assocs <- safeGet return $ UA.array bounds assocs - putCopy arr = Happs.contain - (Happs.safePut (UA.bounds arr) >> Happs.safePut (UA.assocs arr)) + putCopy arr = contain + (safePut (UA.bounds arr) >> safePut (UA.assocs arr)) instance Binary UTCTime where put time = do diff --git a/Distribution/Server/Packages/Index.hs b/Distribution/Server/Packages/Index.hs index d6a0f3b69..aa584634a 100644 --- a/Distribution/Server/Packages/Index.hs +++ b/Distribution/Server/Packages/Index.hs @@ -39,10 +39,10 @@ import Prelude hiding (read) write :: Users.Users -> PackageIndex PkgInfo -> ByteString write users = PackageIndex.write pkgData setModTime where - setModTime pkgInfo entry = entry { - Tar.entryTime = utcToUnixTime (pkgUploadTime pkgInfo), + setModTime pkgInfo entry = let (utime, uuser) = pkgUploadData pkgInfo in entry { + Tar.entryTime = utcToUnixTime utime, Tar.entryOwnership = Tar.Ownership { - Tar.ownerName = userName (pkgUploadUser pkgInfo), + Tar.ownerName = userName uuser, Tar.groupName = "HackageDB", Tar.ownerId = 0, Tar.groupId = 0 diff --git a/Distribution/Server/Packages/ServerParts.hs b/Distribution/Server/Packages/ServerParts.hs index 9efa99a13..c4e4ac897 100644 --- a/Distribution/Server/Packages/ServerParts.hs +++ b/Distribution/Server/Packages/ServerParts.hs @@ -1,7 +1,7 @@ module Distribution.Server.Packages.ServerParts ( + packagePagesFeature, updateCache, stateToCache, - handlePackageById, servePackage, checkPackage, @@ -9,28 +9,26 @@ module Distribution.Server.Packages.ServerParts ( buildReports, ) where -import Distribution.Package - ( PackageIdentifier(..), packageName, packageVersion , Package(packageId) ) +import Distribution.Package (PackageIdentifier(..), PackageId, packageName, packageVersion , Package(packageId)) import Distribution.Text (simpleParse, display) import Happstack.Server hiding (port, host) import Happstack.State hiding (Version) -import Distribution.Server.ServerParts (guardAuth) +--import Distribution.Server.ServerParts (guardAuth) import Distribution.Server.Instances () -import Distribution.Server.Packages.State as State hiding (buildReports) +import Distribution.Server.Packages.State as State import Distribution.Server.Users.State as State + import Distribution.Server.Distributions.State as State import qualified Distribution.Server.TarIndex.State as TarIndex import qualified Distribution.Server.Util.Serve as TarIndex -import Distribution.Server.Users.Permissions (GroupName(..)) import qualified Distribution.Server.Packages.State as State import qualified Distribution.Server.Cache as Cache import qualified Distribution.Server.PackageIndex as PackageIndex import qualified Distribution.Server.Auth.Basic as Auth -import Distribution.Server.Packages.Types - ( PkgInfo(..) ) +import Distribution.Server.Packages.Types (PkgInfo(..), pkgUploadUser, pkgUploadTime) import qualified Distribution.Server.ResourceTypes as Resource import qualified Distribution.Server.Pages.Index as Pages (packageIndex) import qualified Distribution.Server.Pages.Package as Pages @@ -45,14 +43,18 @@ import Distribution.Server.Util.Serve (serveTarball) import qualified Distribution.Server.BuildReport.BuildReport as BuildReport import qualified Distribution.Server.BuildReport.BuildReports as BuildReports +import Distribution.Server.Resource +import Distribution.Server.Feature + import qualified Distribution.Server.Users.Users as Users import qualified Distribution.Server.Users.Types as Users import qualified Distribution.Server.Users.Group as Groups +import Distribution.Server.Users.Group (UserGroup(..), UserList(..)) import Data.Maybe import Data.Version import Control.Monad.Trans -import Control.Monad (msum,mzero,unless) +import Control.Monad (msum, mzero, unless, guard) import Data.List (maximumBy, sortBy) import Data.Ord (comparing) import Data.Time.Clock @@ -63,40 +65,94 @@ import System.FilePath.Posix (()) import qualified Data.ByteString.Lazy.Char8 as BS.Char8 import qualified Codec.Compression.GZip as GZip -{- -TODO: change this module to support Resources that do: - dir "package" $ msum - [ path $ msum . handlePackageById store - , path $ servePackage store - ] - , dir "check" checkPackage - , dir "upload" $ msum - [ uploadPackage store cache host ] - -With these BranchPaths: -/packages/ -[StaticBranch "packages"] -/packages/index.tar.gz -[StaticBranch "index.tar.gz", StaticBranch "packages"] -/package/ -[DynamicBranch "package", StaticBranch "packages"] -/package//.cabal -[DynamicBranch "cabal", DynamicBranch "package", StaticBranch "packages"] -/package//.tar.gz -[DynamicBranch "tarball", DynamicBranch "package", StaticBranch "packages"] -/package//doc/ -[DynamicBranch "doctree", StaticBranch "doc", DynamicBranch "package", StaticBranch "packages"] -/package//maintainers -Automatic user group creation --} +packagePagesFeature :: HackageFeature +packagePagesFeature = HackageFeature { + featureName = "package pages", + -- todo: add checking + locations = map serveResource $ + [ (resourceAt "/packages/") { resourceGet = Just serveIndexPage, resourcePost = Just uploadPackageTarball } + , (resourceAt "/packages/index.tar.gz") { resourceGet = Just serveIndexTarball } + , (resourceAt "/package/:package") { resourceGet = Just servePackagePage, resourceDelete = Nothing, resourcePut = Nothing } + , (resourceAt "/package/:package/:cabal") { resourceGet = Just serveCabalFile, resourcePut = Nothing } + , (resourceAt "/package/:package/:tarball") { resourceGet = Just servePackageTarball, resourcePut = Nothing, resourceDelete = Nothing } + , (resourceAt "/package/:package/doc/:doctree") + , (resourceAt "/package/:package/buildreports/") { resourceGet = Just serveBuildReports } + ] ++ makeGroupResources (trunkAt "/package/:package/maintainers") maintainersGroup, + dumpBackup = return [], + restoreBackup = \_ -> return () +} +-- "/package/:package/candidate", "/package/:package/candidate/:cabal", "/package/:package/candidate/:tarball" + +maintainersGroup :: DynamicPath -> Maybe (UserGroup GetPackageMaintainers AddPackageMaintainer RemovePackageMaintainer) +maintainersGroup dpath = do + name <- fmap pkgName (simpleParse =<< lookup "package" dpath) + return $ UserGroup { + groupName = "Maintainers for " ++ display name, + queryUserList = GetPackageMaintainers name, + addUserList = AddPackageMaintainer name, + removeUserList = RemovePackageMaintainer name + } + +serveBuildReports, serveIndexPage, serveIndexTarball, servePackageTarball, + servePackagePage, serveCabalFile :: Config -> DynamicPath -> ServerPart Response + +serveBuildReports config dpath = withPackageId dpath $ \pkgid -> do + state <- query GetPackagesState + buildReports <- query GetBuildReports + case PackageIndex.lookupPackageId (packageList state) pkgid of + Nothing -> notFound $ toResponse "No such package" + Just _ -> do + let reports = BuildReports.lookupPackageReports + buildReports pkgid + ok $ toResponse $ Resource.XHtml $ + Pages.buildReportSummary pkgid reports + +serveIndexPage config dpath = do + cacheState <- Cache.get (serverCache config) + ok $ Cache.packagesPage cacheState + +uploadPackageTarball :: Config -> DynamicPath -> ServerPart Response +uploadPackageTarball config dpath = uploadPackage (serverStore config) (serverCache config) (serverURI config) + +serveIndexTarball config dpath = do + cacheState <- Cache.get (serverCache config) + ok $ toResponse $ Resource.IndexTarball (Cache.indexTarball cacheState) + +servePackageTarball config dpath = withPackageId dpath $ \pkgid -> + require (return $ lookup "tarball" dpath) $ \tarball -> do + -- FIXME: more accurate versioning. currently /package/foo-1.2/bar-3.14.tar.gz is possible + servePackage (serverStore config) tarball + +servePackagePage config dpath = withPackagePath dpath $ \state pkg pkgs -> do + let pkgid = pkgInfoId pkg + distributions <- query $ State.PackageStatus (packageName pkg) + hasDocs <- query $ State.HasDocumentation (packageId pkg) + let docURL | hasDocs = Just $ "/package" display pkgid "documentation" + | otherwise = Nothing + userDb <- query $ GetUserDb + ok $ toResponse $ Resource.XHtml $ + Pages.packagePage userDb (packageList state) pkg pkgs distributions docURL + +serveCabalFile config dpath = withPackagePath dpath $ \_ pkg _ -> do + guard (lookup "cabal" dpath == Just (display (packageName pkg) ++ ".cabal")) + ok $ toResponse (Resource.CabalFile (pkgData pkg)) + +withPackagePath :: DynamicPath -> (PackagesState -> PkgInfo -> [PkgInfo] -> ServerPart Response) -> ServerPart Response +withPackagePath dpath func = withPackageId dpath $ \pkgid -> withPackage pkgid func + +withPackageId :: DynamicPath -> (PackageId -> ServerPart Response) -> ServerPart Response +withPackageId dpath = require (return $ lookup "package" dpath >>= fromReqURI) --TODO: switch to new cache mechanism: updateCache :: MonadIO m => Cache.Cache -> URIAuth -> m () -updateCache cache host - = liftIO (Cache.put cache =<< stateToCache host =<< query GetPackagesState) - -stateToCache :: URIAuth -> PackagesState -> IO Cache.State -stateToCache host state = getCurrentTime >>= \now -> return +updateCache cache host = liftIO $ do + state <- query GetPackagesState + userDb <- query GetUserDb + cacheState <- stateToCache host state userDb + Cache.put cache cacheState + +stateToCache :: URIAuth -> PackagesState -> Users.Users -> IO Cache.State +stateToCache host state users = getCurrentTime >>= \now -> return Cache.State { Cache.packagesPage = toResponse $ Resource.XHtml $ Pages.packageIndex index, @@ -107,58 +163,24 @@ stateToCache host state = getCurrentTime >>= \now -> return Pages.recentFeed users host now recentChanges } where index = packageList state - users = userDb state - recentChanges = reverse $ sortBy (comparing pkgUploadTime) (PackageIndex.allPackages index) + recentChanges = reverse $ sortBy (comparing (fst . pkgUploadData)) (PackageIndex.allPackages index) -handlePackageById :: BlobStorage -> PackageIdentifier -> [ServerPart Response] +handlePackageById :: BlobStorage -> PackageId -> [ServerPart Response] handlePackageById store pkgid = - [ withPackage pkgid $ \state pkg pkgs -> - methodSP GET $ do - distributions <- query $ State.PackageStatus (packageName pkg) - hasDocs <- query $ State.HasDocumentation (packageId pkg) - let docURL | hasDocs = Just $ "/package" display pkgid "documentation" - | otherwise = Nothing - - ok $ toResponse $ Resource.XHtml $ - Pages.packagePage (userDb state) (packageList state) pkg pkgs distributions docURL - - , dir (display (packageName pkgid) ++ ".cabal") $ msum - [ withPackage pkgid $ \_ pkg _pkgs -> - methodSP GET $ - ok $ toResponse (Resource.CabalFile (pkgData pkg)) --- , methodSP PUT $ do ... - ] - , dir "buildreports" $ msum - [ methodSP GET $ do - state <- query GetPackagesState - case PackageIndex.lookupPackageId (packageList state) pkgid of - Nothing -> notFound $ toResponse "No such package" - Just _ -> do - let reports = BuildReports.lookupPackageReports - (State.buildReports state) pkgid - ok $ toResponse $ Resource.XHtml $ - Pages.buildReportSummary pkgid reports - ] - , dir "documentation" $ msum + [ dir "documentation" $ msum [ withPackage pkgid $ \state pkg _ -> let resolvedPkgId = packageId pkg in msum [ methodSP POST $ do - authGroup <- query $ LookupUserGroups - [Trustee, PackageMaintainer (packageName pkg)] - _user <- Auth.hackageAuth (userDb state) (Just authGroup) + requirePackageAuth pkgid withRequest $ \Request{rqBody = Body body} -> do - {- The order of operations: - - Insert new documentation into blob store - Generate the new index - Drop the index for the old tar-file - Link the new documentation to the package - -} - blob <- liftIO $ BlobStorage.add store (GZip.decompress body) tarIndex <- liftIO $ TarIndex.readTarIndex (BlobStorage.filepath store blob) update $ TarIndex.AddIndex blob tarIndex @@ -184,25 +206,24 @@ handlePackageById store pkgid = Just blob -> do update $ TarIndex.DropIndex blob -packageAdmin :: PackageIdentifier -> ServerPart Response -packageAdmin pkgid = - withPackage pkgid $ \_ pkg _ -> do - guardAuth [Trustee, PackageMaintainer (packageName pkg)] +packageAdmin :: PackageId -> ServerPart Response +packageAdmin pkgid = withPackage pkgid $ \_ pkg _ -> do + requirePackageAuth pkgid msum [ methodSP GET $ do - maintainers <- packageMaintainers pkg + mains <- packageMaintainers pkg ok $ toResponse $ Resource.XHtml $ - Pages.packageAdminPage maintainers pkg + Pages.packageAdminPage mains pkg , adminPost ] where packageMaintainers pkg = do - group <- query $ LookupUserGroup (PackageMaintainer (packageName pkg)) - let uids = Groups.enumerate group - state <- query GetPackagesState - return $ lookupUserNames (userDb state) uids + group <- query $ GetPackageMaintainers (packageName pkg) + let uids = Groups.enumerate (fromMaybe Groups.empty group) + userDb <- query GetUserDb + return $ lookupUserNames userDb uids -- this needs work, as it won't skip over deleted users. lookupUserNames users = map (Users.idToName users) @@ -221,19 +242,19 @@ packageAdmin pkgid = case userM of Nothing -> ok $ toResponse "Not a valid user!" Just user -> do - update $ AddToGroup (PackageMaintainer (packageName pkgid)) user + update $ AddPackageMaintainer (packageName pkgid) user ok $ toResponse "Ok!" , dir "removeMaintainer" $ methodSP POST $ do userM <- lookUser case userM of Nothing -> ok $ toResponse "Not a valid user!" Just user -> do - update $ RemoveFromGroup (PackageMaintainer (packageName pkgid)) user + update $ RemovePackageMaintainer (packageName pkgid) user ok $ toResponse "Ok!" ] -withPackage :: PackageIdentifier -> (PackagesState -> PkgInfo -> [PkgInfo] -> ServerPart Response) -> ServerPart Response +withPackage :: PackageId -> (PackagesState -> PkgInfo -> [PkgInfo] -> ServerPart Response) -> ServerPart Response withPackage pkgid action = do state <- query GetPackagesState let index = packageList state @@ -248,6 +269,7 @@ withPackage pkgid action = do Nothing -> anyRequest $ notFound $ toResponse "No such package version" Just pkg -> action state pkg pkgs + servePackage :: BlobStorage -> String -> ServerPart Response servePackage store pkgIdStr = methodSP GET $ do let (pkgVer,t) = splitAt (length pkgIdStr - length ext) pkgIdStr @@ -260,12 +282,10 @@ servePackage store pkgIdStr = methodSP GET $ do serve pkgId t | t /= ext = notFound $ toResponse "No such package in store" | otherwise = withPackage pkgId $ \_ pkg _ -> case pkgTarball pkg of - Nothing -> notFound $ toResponse "No tarball available" - Just blobId -> do + [] -> notFound $ toResponse "No tarball available" + ((blobId, _):_) -> do file <- liftIO $ BlobStorage.fetch store blobId - ok $ toResponse $ - Resource.PackageTarball file blobId (pkgUploadTime pkg) - + ok $ toResponse $ Resource.PackageTarball file blobId (pkgUploadTime pkg) checkPackage :: ServerPart Response checkPackage = methodSP POST $ do @@ -297,52 +317,51 @@ uploadPackage store cache host = case res of Left err -> badRequest $ toResponse err Right (((pkg, pkgStr), warnings), blobId) -> do - state <- query GetPackagesState - + state <- query GetPackagesState let pkgExists = packageExists state pkg - user <- uploadUser state pkg - - (realUser, realTime) <- do now <- liftIO getCurrentTime - return (user,now) + user <- uploadingUser state (packageId pkg) + uploadData <- do now <- liftIO getCurrentTime + return (now, user) success <- update $ Insert PkgInfo { pkgInfoId = packageId pkg, pkgDesc = pkg, pkgData = pkgStr, - pkgTarball = Just blobId, - pkgUploadTime = realTime, - pkgUploadUser = realUser, - pkgUploadOld = [] + pkgTarball = [(blobId, uploadData)], --does this merge properly? + pkgUploadData = uploadData, + pkgDataOld = [] -- what about this? } if success then do -- Update the package maintainers group. - unless pkgExists $ - update $ AddToGroup (PackageMaintainer (packageName pkg)) realUser - + unless pkgExists $ update $ AddPackageMaintainer (packageName pkg) user updateCache cache host ok $ toResponse $ unlines warnings else forbidden $ toResponse "Package already exists." - uploadUser state pkg = do - group <- uploadUserGroup state pkg - Auth.hackageAuth (userDb state) group - -- Auth group for uploading a package. -- A new package may be uped by anyone -- An existing package may only be uploaded by a maintainer of -- that package or a trustee. - uploadUserGroup state pkg = - if packageExists state pkg - then Just `fmap` query (LookupUserGroups [Trustee, PackageMaintainer (packageName pkg)]) - else return Nothing + uploadingUser state pkg = + if packageExists state pkg + then requirePackageAuth pkg + else query GetUserDb >>= \users -> Auth.requireHackageAuth users Nothing Nothing + + packageExists state pkg = not . null $ PackageIndex.lookupPackageName (packageList state) (packageName pkg) - packageExists state pkg = not . null $ PackageIndex.lookupPackageName (packageList state) (packageName pkg) +requirePackageAuth :: (MonadIO m, Package pkg) => pkg -> ServerPartT m Users.UserId +requirePackageAuth pkg = do + userDb <- query $ GetUserDb + pkgm <- query $ GetPackageMaintainers (packageName pkg) + let admins = Users.adminList userDb + groupSum = Groups.unions [admins, fromMaybe Groups.empty pkgm] + Auth.requireHackageAuth userDb (Just groupSum) Nothing buildReports :: BlobStorage -> [ServerPart Response] buildReports store = [ path $ \reportId -> msum [ methodSP GET $ do - reports <- return . State.buildReports =<< query GetPackagesState + reports <- query GetBuildReports case BuildReports.lookupReport reports reportId of Nothing -> notFound $ toResponse "No such report" Just report -> @@ -352,7 +371,7 @@ buildReports store = , dir "buildlog" $ msum [ methodSP GET $ do - reports <- return . State.buildReports =<< query GetPackagesState + reports <- query GetBuildReports case BuildReports.lookupBuildLog reports reportId of Nothing -> notFound $ toResponse "No build log available" Just (BuildReports.BuildLog blobId) -> do @@ -361,7 +380,7 @@ buildReports store = Resource.BuildLog file , methodSP PUT $ withRequest $ \Request { rqBody = Body body } -> do - reports <- return . State.buildReports =<< query GetPackagesState + reports <- query GetBuildReports case BuildReports.lookupReport reports reportId of Nothing -> notFound $ toResponse "No such report" Just _ -> do @@ -382,4 +401,4 @@ buildReports store = ] instance FromReqURI BuildReports.BuildReportId where - fromReqURI = simpleParse + fromReqURI = simpleParse diff --git a/Distribution/Server/Packages/State.hs b/Distribution/Server/Packages/State.hs index 39c404c51..6a52e8d5c 100644 --- a/Distribution/Server/Packages/State.hs +++ b/Distribution/Server/Packages/State.hs @@ -6,14 +6,12 @@ module Distribution.Server.Packages.State where import Distribution.Server.Instances () import Distribution.Server.Users.State () -import Distribution.Package (PackageIdentifier,Package(packageId)) +import Distribution.Package (PackageIdentifier,PackageName,Package(packageId)) import qualified Distribution.Server.PackageIndex as PackageIndex import Distribution.Server.Packages.Types (PkgInfo(..)) import qualified Distribution.Server.Users.Group as Group -import Distribution.Server.Users.Group (UserGroup) -import Distribution.Server.Users.Types (UserId,UserName,UserAuth) -import qualified Distribution.Server.Users.Users as Users -import Distribution.Server.Users.Users (Users) +import Distribution.Server.Users.Group (UserList) +import Distribution.Server.Users.Types (UserId) import Distribution.Server.Util.BlobStorage (BlobId) import qualified Distribution.Server.BuildReport.BuildReports as BuildReports import Distribution.Server.BuildReport.BuildReports (BuildReports,BuildReportId,BuildLog) @@ -25,91 +23,85 @@ import qualified Data.Binary as Binary import Data.Typeable import Control.Monad.Reader import qualified Control.Monad.State as State -import Data.Maybe (isJust) import Data.Monoid -import Data.Time.Clock (UTCTime(..)) import qualified Data.Map as Map -data Documentation = Documentation { - documentation :: Map.Map PackageIdentifier BlobId - } deriving (Typeable, Show) - -instance Component Documentation where - type Dependencies Documentation = End - initialValue = Documentation Map.empty - -instance Version Documentation where - mode = Versioned 0 Nothing -- Version 0, no previous types - -instance Serialize Documentation where - putCopy (Documentation m) = contain $ safePut m - getCopy = contain $ liftM Documentation safeGet - - - +---------------------------------- Index of metadata and tarballs data PackagesState = PackagesState { - packageList :: !(PackageIndex.PackageIndex PkgInfo), - buildReports :: !BuildReports, - userDb :: !Users + packageList :: !(PackageIndex.PackageIndex PkgInfo) } deriving (Typeable, Show) instance Component PackagesState where type Dependencies PackagesState = End initialValue = PackagesState { - packageList = mempty, - buildReports = BuildReports.empty, - userDb = Users.empty + packageList = mempty } instance Version PackagesState where mode = Versioned 0 Nothing instance Serialize PackagesState where - putCopy (PackagesState idx rpts users) = contain $ do + putCopy (PackagesState idx) = contain $ do safePut $ PackageIndex.allPackages idx - safePut rpts - safePut users getCopy = contain $ do packages <- safeGet - reports <- safeGet - users <- safeGet return PackagesState { - packageList = PackageIndex.fromList packages, - buildReports = reports, - userDb = users + packageList = PackageIndex.fromList packages } -instance Version BuildReports where +instance Version PkgInfo where mode = Versioned 0 Nothing -instance Serialize BuildReports where +instance Serialize PkgInfo where putCopy = contain . Binary.put getCopy = contain Binary.get -instance Version PackageIdentifier where - mode = Versioned 0 Nothing +insert :: PkgInfo -> Update PackagesState Bool +insert pkg + = do pkgsState <- State.get + case PackageIndex.lookupPackageId (packageList pkgsState) (packageId pkg) of + Nothing -> do State.put $ pkgsState { packageList = PackageIndex.insert pkg (packageList pkgsState) } + return True + Just{} -> do return False -instance Serialize PackageIdentifier where - putCopy = contain . Binary.put . show - getCopy = contain $ fmap read Binary.get +-- NOTE! overwrites any existing data +bulkImport :: [PkgInfo] -> Update PackagesState () +bulkImport newIndex = do + pkgsState <- State.get + State.put pkgsState { + packageList = PackageIndex.fromList newIndex + } -instance Version PkgInfo where - mode = Versioned 0 Nothing +-- |Replace all existing packages and reports +replacePackagesState :: PackagesState -> Update PackagesState () +replacePackagesState = State.put -instance Serialize PkgInfo where - putCopy = contain . Binary.put - getCopy = contain Binary.get +getPackagesState :: Query PackagesState PackagesState +getPackagesState = ask -{-- These are included in happstack-state 0.5.* -instance Version UTCTime where - mode = Versioned 0 Nothing -instance Serialize UTCTime where - putCopy = contain . Binary.put - getCopy = contain Binary.get --} +$(mkMethods ''PackagesState ['getPackagesState + ,'bulkImport + ,'replacePackagesState + ,'insert + ]) +---------------------------------- Documentation +data Documentation = Documentation { + documentation :: Map.Map PackageIdentifier BlobId + } deriving (Typeable, Show) + +instance Component Documentation where + type Dependencies Documentation = End + initialValue = Documentation Map.empty + +instance Version Documentation where + mode = Versioned 0 Nothing -- Version 0, no previous types + +instance Serialize Documentation where + putCopy (Documentation m) = contain $ safePut m + getCopy = contain $ liftM Documentation safeGet instance Version BlobId where mode = Versioned 0 Nothing @@ -118,9 +110,6 @@ instance Serialize BlobId where putCopy = contain . Binary.put getCopy = contain Binary.get - - - lookupDocumentation :: PackageIdentifier -> Query Documentation (Maybe BlobId) lookupDocumentation pkgId = do m <- asks documentation @@ -134,7 +123,7 @@ hasDocumentation pkgId insertDocumentation :: PackageIdentifier -> BlobId -> Update Documentation () insertDocumentation pkgId blob - = State.modify $ \doc -> doc{documentation = Map.insert pkgId blob (documentation doc)} + = State.modify $ \doc -> doc {documentation = Map.insert pkgId blob (documentation doc)} getDocumentation :: Query Documentation Documentation getDocumentation = ask @@ -143,126 +132,77 @@ getDocumentation = ask replaceDocumentation :: Documentation -> Update Documentation () replaceDocumentation = State.put -insert :: PkgInfo -> Update PackagesState Bool -insert pkg - = do pkgsState <- State.get - case PackageIndex.lookupPackageId (packageList pkgsState) (packageId pkg) of - Nothing -> do State.put $ pkgsState { packageList = PackageIndex.insert pkg (packageList pkgsState) } - return True - Just{} -> do return False +$(mkMethods ''Documentation ['insertDocumentation + ,'lookupDocumentation + ,'hasDocumentation + ,'getDocumentation + ,'replaceDocumentation + ]) +-------------------------------- Build reports +instance Version BuildReports where + mode = Versioned 0 Nothing --- NOTE! overwrites any existing data -bulkImport :: [PkgInfo] -> Users -> Update PackagesState () -bulkImport newIndex users = do - pkgsState <- State.get - State.put pkgsState { - packageList = PackageIndex.fromList newIndex, - userDb = users - } +instance Serialize BuildReports where + putCopy = contain . Binary.put + getCopy = contain Binary.get --- |Replace all existing packages, users and reports -replacePackagesState :: PackagesState -> Update PackagesState () -replacePackagesState = State.put +instance Component BuildReports where + type Dependencies BuildReports = End + initialValue = BuildReports.empty -addReport :: BuildReport -> Update PackagesState BuildReportId +addReport :: BuildReport -> Update BuildReports BuildReportId addReport report - = do pkgsState <- State.get - let (reports, reportId) = BuildReports.addReport (buildReports pkgsState) report - State.put pkgsState{buildReports = reports} + = do buildReports <- State.get + let (reports, reportId) = BuildReports.addReport buildReports report + State.put reports return reportId -addBuildLog :: BuildReportId -> BuildLog -> Update PackagesState Bool +addBuildLog :: BuildReportId -> BuildLog -> Update BuildReports Bool addBuildLog reportId buildLog - = do pkgsState <- State.get - case BuildReports.addBuildLog (buildReports pkgsState) reportId buildLog of + = do buildReports <- State.get + case BuildReports.addBuildLog buildReports reportId buildLog of Nothing -> return False - Just reports -> do State.put pkgsState{buildReports = reports} - return True - - -getPackagesState :: Query PackagesState PackagesState -getPackagesState = ask - --- Returns 'Nothing' if the user name is in use -addUser :: UserName -> UserAuth -> Update PackagesState (Maybe UserId) -addUser userName auth = updateUsers' updateFn formatFn - - where updateFn = Users.add userName auth - formatFn = id + Just reports -> State.put reports >> return True --- Disables the indicated user -disableUser :: UserId -> Update PackagesState Bool -disableUser = updateUsers . Users.disable +getBuildReports :: Query BuildReports BuildReports +getBuildReports = ask --- Enables the indicated previously disabled user -enableUser :: UserId -> Update PackagesState Bool -enableUser = updateUsers . Users.enable +replaceBuildReports :: BuildReports -> Update BuildReports () +replaceBuildReports = State.put --- Deletes the indicated user. Cannot be re-enabled. The associated --- user name is available for re-use -deleteUser :: UserId -> Update PackagesState Bool -deleteUser = updateUsers . Users.delete +$(mkMethods ''BuildReports ['addReport + ,'addBuildLog + ,'getBuildReports + ,'replaceBuildReports + ]) --- Re-set the user autenication info -replaceUserAuth :: UserId -> UserAuth -> Update PackagesState Bool -replaceUserAuth userId auth - = updateUsers $ \users -> - Users.replaceAuth users userId auth - +-------------------------------- Maintainer list +data PackageMaintainers = PackageMaintainers { + maintainers :: Map.Map PackageName UserList +} deriving (Show, Typeable) --- updates the user db with a simpler function -updateUsers :: (Users -> Maybe Users) -> Update PackagesState Bool -updateUsers f = updateUsers' updateFn isJust - - where updateFn users = fmap (swap . (,) ()) $ f users - swap (x,y) = (y,x) - --- Helper function for updating the users db -updateUsers' :: (Users -> Maybe (Users, a)) -> (Maybe a -> b) -> Update PackagesState b -updateUsers' f format = do - state <- State.get - let users = userDb state - result = f users +instance Version PackageMaintainers where + mode = Versioned 0 Nothing +$(deriveSerialize ''PackageMaintainers) - liftM format $ case result of - Nothing -> return Nothing - Just (users',a) -> do - State.put state { userDb = users' } - return (Just a) +instance Component PackageMaintainers where + type Dependencies PackageMaintainers = End + initialValue = PackageMaintainers Map.empty -lookupUserName :: UserName -> Query PackagesState (Maybe UserId) -lookupUserName = queryUsers . Users.lookupName +getPackageMaintainers :: PackageName -> Query PackageMaintainers (Maybe UserList) +getPackageMaintainers name = fmap (Map.lookup name) (asks maintainers) -queryUsers :: (Users -> a) -> Query PackagesState a -queryUsers queryFn = liftM queryFn (asks userDb) +modifyPackageMaintainers :: PackageName -> (UserList -> UserList) -> Update PackageMaintainers () +modifyPackageMaintainers name func = State.modify (\pm -> pm {maintainers = Map.update (Just . func) name (maintainers pm) }) -listGroupMembers :: UserGroup -> Query PackagesState [UserName] -listGroupMembers userGroup - = do users <- asks userDb - return [ Users.idToName users uid | uid <- Group.enumerate userGroup ] +addPackageMaintainer :: PackageName -> UserId -> Update PackageMaintainers () +addPackageMaintainer name uid = modifyPackageMaintainers name (Group.add uid) +removePackageMaintainer :: PackageName -> UserId -> Update PackageMaintainers () +removePackageMaintainer name uid = modifyPackageMaintainers name (Group.remove uid) -$(mkMethods ''Documentation ['insertDocumentation - ,'lookupDocumentation - ,'hasDocumentation - ,'getDocumentation - ,'replaceDocumentation - ]) +$(mkMethods ''PackageMaintainers ['getPackageMaintainers + ,'addPackageMaintainer + ,'removePackageMaintainer + ]) -$(mkMethods ''PackagesState ['getPackagesState - ,'listGroupMembers - ,'bulkImport - ,'replacePackagesState - ,'insert - ,'addReport - ,'addBuildLog - - --TODO: move these to a separate state component - -- User management - ,'addUser - ,'disableUser - ,'enableUser - ,'deleteUser - ,'replaceUserAuth - ,'lookupUserName - ]) diff --git a/Distribution/Server/Packages/Types.hs b/Distribution/Server/Packages/Types.hs index aea83454f..5fdf0ae0d 100644 --- a/Distribution/Server/Packages/Types.hs +++ b/Distribution/Server/Packages/Types.hs @@ -14,10 +14,8 @@ ----------------------------------------------------------------------------- module Distribution.Server.Packages.Types where -import Distribution.Server.Users.Types - ( UserId ) -import Distribution.Server.Util.BlobStorage - ( BlobId ) +import Distribution.Server.Users.Types (UserId) +import Distribution.Server.Util.BlobStorage (BlobId) import Distribution.Server.Instances () import Distribution.Package @@ -36,44 +34,46 @@ import Data.Time.Clock (UTCTime) import Data.Typeable (Typeable) -- | The information we keep about a particular version of a package. +-- +-- Previous versions of this package name and version may exist as well. +-- We normally disallow re-uploading but may make occasional exceptions, +-- such as , and there are some such old packages. data PkgInfo = PkgInfo { - pkgInfoId :: PackageIdentifier, - pkgDesc :: GenericPackageDescription, - + pkgInfoId :: !PackageIdentifier, + pkgDesc :: !GenericPackageDescription, -- | The .cabal file text. - pkgData :: ByteString, - - -- | The actual package .tar.gz file. It is optional for the moment - -- to make testing easier, eg using archives of just the latest packages. - pkgTarball :: Maybe BlobId, + pkgData :: !ByteString, + -- | The actual package .tar.gz file. It is optional for making an incomplete + -- mirror, e.g. using archives of just the latest packages, or perhaps for a + -- multipart upload process. + pkgTarball :: ![(BlobId, UploadInfo)], + -- | Previous data + pkgDataOld :: ![(ByteString, UploadInfo)], + -- | When the package was created. Imports will override this with time in their logs. + pkgUploadData :: !UploadInfo +} deriving (Typeable, Show) - -- | When the .tar.gz file was uploaded. - pkgUploadTime :: UTCTime, +type UploadInfo = (UTCTime, UserId) - -- | Who uploaded the .tar.gz file. - pkgUploadUser :: UserId, +pkgUploadTime :: PkgInfo -> UTCTime +pkgUploadTime = fst . pkgUploadData - -- | Previous upload times and users. We normally disallow re-uploading but - -- we may make occasional exceptions, and there are some such old packages. - pkgUploadOld :: [(UTCTime, UserId)] - } - deriving (Typeable, Show) +pkgUploadUser :: PkgInfo -> UserId +pkgUploadUser = snd . pkgUploadData instance Package PkgInfo where packageId = pkgInfoId instance Binary PkgInfo where put pkgInfo = do Binary.put (pkgInfoId pkgInfo) - Binary.put (pkgUploadTime pkgInfo) - Binary.put (pkgUploadUser pkgInfo) - Binary.put (pkgUploadOld pkgInfo) + Binary.put (pkgUploadData pkgInfo) + Binary.put (pkgDataOld pkgInfo) Binary.put (pkgTarball pkgInfo) Binary.put (pkgData pkgInfo) get = do infoId <- Binary.get - mtime <- Binary.get - user <- Binary.get + updata <- Binary.get old <- Binary.get tarball <- Binary.get bstring <- Binary.get @@ -83,10 +83,9 @@ instance Binary PkgInfo where -- XXX: Better error message? ParseFailed e -> error $ "Internal error: " ++ show e ParseOk _ x -> x, - pkgUploadTime = mtime, - pkgUploadUser = user, - pkgUploadOld = old, - pkgData = bstring, - pkgTarball= tarball + pkgUploadData = updata, + pkgDataOld = old, + pkgTarball = tarball, + pkgData = bstring } where parse = parsePackageDescription . fromUTF8 . BS.unpack diff --git a/Distribution/Server/Pages/Package.hs b/Distribution/Server/Pages/Package.hs index aff9dd9ee..fcdf4d71b 100644 --- a/Distribution/Server/Pages/Package.hs +++ b/Distribution/Server/Pages/Package.hs @@ -21,11 +21,7 @@ import Distribution.PackageDescription.Configuration ( flattenPackageDescription ) import Distribution.Package import Distribution.PackageDescription as P -import Distribution.Version - ( Version (..) - , VersionRange (..) - , withinRange - ) +import Distribution.Version (Version (..), VersionRange (..), withinRange) import Distribution.Text ( display ) import Text.XHtml.Strict hiding ( p, name ) @@ -48,14 +44,14 @@ packagePage :: Users.Users -> PackageIndex PkgInfo -> packagePage users pkgs pkg allVersions distributions docURL = let packageData = (emptyPackageData (pkgDesc pkg)) { pdAllVersions = sort (map packageVersion allVersions), - pdTags = [("upload date", showTime (pkgUploadTime pkg)) + pdTags = [("upload date", showTime (fst $ pkgUploadData pkg)) ,("uploaded by", display userName)], pdDependencies = pkgs, pdDistributions = distributions, pdDocURL = docURL } showTime = formatTime defaultTimeLocale "%c" - userName = Users.idToName users (pkgUploadUser pkg) + userName = Users.idToName users (snd $ pkgUploadData pkg) in hackagePage (display $ packageId pkg) (pkgBody packageData) -- | Data about a package used in the package page. @@ -368,6 +364,8 @@ fromVersionRange (UnionVersionRanges r1 r2) = fromVersionRange (IntersectVersionRanges r1 r2) = filter (not . nullInterval) $ liftM2 intersectInterval (fromVersionRange r1) (fromVersionRange r2) +-- temporary fix. TODO: change this entire module to use Distribution.Version.VersionInterval +fromVersionRange _ = [] intersectInterval :: Ord a => Interval a -> Interval a -> Interval a intersectInterval (Interval l1 u1) (Interval l2 u2) = diff --git a/Distribution/Server/Pages/PackageAdmin.hs b/Distribution/Server/Pages/PackageAdmin.hs index ece4ff478..a4b442acd 100644 --- a/Distribution/Server/Pages/PackageAdmin.hs +++ b/Distribution/Server/Pages/PackageAdmin.hs @@ -56,8 +56,9 @@ forms pkg = concat addMaintainer :: PkgInfo -> [Html] addMaintainer pkg = [ h3 << "Add Maintainer" - , gui (adminAction pkg "addMaintainer") ! [theclass "box"] << - [ p << [stringToHtml "User: ", textfield "user"] + , gui (adminAction pkg) ! [theclass "box"] << + [ p << [stringToHtml "User: ", textfield "_patharg"] + , hidden "_method" "PUT" , submit "submit" "Add maintainer" ] ] @@ -65,8 +66,9 @@ addMaintainer pkg = removeMaintainer :: PkgInfo -> [Html] removeMaintainer pkg = [ h3 << "Remove Maintainer" - , gui (adminAction pkg "removeMaintainer") ! [theclass "box"] << - [ p << [stringToHtml "User: ", textfield "user"] + , gui (adminAction pkg) ! [theclass "box"] << + [ p << [stringToHtml "User: ", textfield "_patharg"] + , hidden "_method" "DELETE" , submit "submit" "Remove maintainer" ] ] @@ -78,8 +80,8 @@ listMaintainers users = , p << unordList (map display users) ] -adminAction :: PkgInfo -> String -> String -adminAction pkg act =packageNameURL (packageId pkg) "admin" act +adminAction :: PkgInfo -> String +adminAction pkg = packageNameURL (packageId pkg) "maintainers" -- Break text into paragraphs (separated by blank lines) paragraphs :: String -> [String] diff --git a/Distribution/Server/Pages/Recent.hs b/Distribution/Server/Pages/Recent.hs index cc40f55d7..825ae6e60 100644 --- a/Distribution/Server/Pages/Recent.hs +++ b/Distribution/Server/Pages/Recent.hs @@ -55,8 +55,7 @@ recentPage users pkgs = makeRow :: Users -> PkgInfo -> Html makeRow users PkgInfo { pkgInfoId = pkgid - , pkgUploadTime = time - , pkgUploadUser = userId + , pkgUploadData = (time, userId) } = XHtml.tr << [XHtml.td ! [XHtml.align "right"] << @@ -115,8 +114,7 @@ releaseItem :: Users -> URIAuth -> PkgInfo -> [RSS.ItemElem] releaseItem users host PkgInfo { pkgInfoId = pkgId , pkgDesc = pkg - , pkgUploadTime = time - , pkgUploadUser = userId + , pkgUploadData = (time, userId) } = [ RSS.Title title , RSS.Link uri diff --git a/Distribution/Server/Resource.hs b/Distribution/Server/Resource.hs index 036ab79c3..a5d284b12 100644 --- a/Distribution/Server/Resource.hs +++ b/Distribution/Server/Resource.hs @@ -1,127 +1,159 @@ {-# LANGUAGE GeneralizedNewtypeDeriving, StandaloneDeriving, FlexibleContexts #-} -module Distribution.Server.Resource where +module Distribution.Server.Resource ( + Config(..), + Resource (..), + DynamicPath, + BranchComponent(..), + BranchPath, + ServerResponse, + ServerTree(..), + makeGroupResources, + serverTreeEmpty, + trunkAt, + resourceAt, + defaultResource, + serveResource, + renderServerTree, + addResponse + ) where import Happstack.Server -import qualified Data.ByteString.Char8 as BS -import qualified Network.URI as URI -import Data.Time.Clock (UTCTime) +import Distribution.Server.Util.BlobStorage (BlobStorage) +import Distribution.Server.Users.Group (UserGroup(..), UserList(..)) + import Data.Map (Map) import qualified Data.Map as Map -import Data.Set (Set) -import qualified Data.Set as Set -import Control.Monad (mplus, msum, when) +import Control.Monad (msum) import Data.Maybe (maybeToList) -import Data.List (intercalate) +import Data.List (intercalate, find) import Data.Monoid (mappend) +import qualified Text.ParserCombinators.ReadP as Parse +import qualified Network.URI as URI +import qualified Distribution.Server.Cache as Cache import Happstack.State (QueryEvent, UpdateEvent, query, update) +data Config = Config { + serverStore :: BlobStorage, + serverStaticDir :: FilePath, + serverURI :: URI.URIAuth, + serverCache :: Cache.Cache +} + data Resource = Resource { resourceLocation :: BranchPath, - resourceMethods :: Map Method (DynamicPath -> MediaType -> ServerPart (Response, Bool)), - resourceInvalidates :: [BranchPath] + resourceGet :: Maybe ServerResponse, + resourcePut :: Maybe ServerResponse, + resourcePost :: Maybe ServerResponse, + resourceDelete :: Maybe ServerResponse } -type DynamicPath = Map String String -type MediaType = Maybe BS.ByteString +resourceAt :: String -> Resource +resourceAt = defaultResource . trunkAt + +defaultResource :: BranchPath -> Resource +defaultResource bpath = Resource bpath Nothing Nothing Nothing Nothing + +-- to be used with functions like withPackage :: DynamicPath -> (PackagesState -> PkgInfo -> [PkgInfo] -> ServerPart Response) -> ServerPart Response + +type DynamicPath = [(String, String)] -- until Happstack 0.6.* deriving instance Ord Method -data BranchComponent = StaticBranch String | DynamicBranch String +data BranchComponent = StaticBranch String | DynamicBranch String deriving (Show, Eq, Ord) type BranchPath = [BranchComponent] -type ClientCache = Map URI.URI UTCTime + +type ServerResponse = Config -> DynamicPath -> ServerPart Response data ServerTree = ServerTree { - nodeResource :: Maybe (Resource), - nodeResponse :: Maybe (ServerPart Response), - dirForest :: DirForest, - resourceForest :: ResourceForest + nodeResponse :: Maybe ServerResponse, + nodeForest :: Map BranchComponent ServerTree } - -- Shared user group abstraction should probably go elsewhere, but the -- group-resource-factory is in this module for now -type UserList = Set Int -class (QueryEvent a UserList, UpdateEvent a ()) => UserGroup a where - -- call: query queryUserList - queryUserList :: a - -- call: update (\group -> ...) - updateUserList :: (UserList -> UserList) -> a - -{- --- Helper functions -removeUser :: UserGroup a => UserId -> a -removeUser user = updateUserList (Set.delete user) - -addUser :: UserGroup a => UserId -> a -addUser user = addUser (Set.insert user) --} +--data UserGroup a = UserGroup { +-- groupName :: String, +-- queryUserList :: a, +-- updateUserList :: (UserList -> UserList) -> a +--} -- TODO: implement -makeGroupResources :: UserGroup a => a -> BranchPath -> [Resource] -makeGroupResources group prefix = [] - -type DirForest = Map String ServerTree -type ResourceForest = Map String ServerTree +makeGroupResources :: (QueryEvent a (Maybe UserList), UpdateEvent b (), UpdateEvent c ()) => BranchPath -> (DynamicPath -> Maybe (UserGroup a b c)) -> [Resource] +makeGroupResources branch group = [] {-[viewList, modifyList] + where + viewList = defaultResource branch { resourceGet = getList, resourcePost = postUser } + getList dpath = do + userList <- liftIO (query $ queryList dpath) + return $ toResponse () + modifyList = defaultResource (DynamicBranch "user":branch) { resourceGet = getUser, resourceDelete = deleteUser } + putUser dpath = withUser dpath $ \userId -> + liftIO (update $ updateList dpath (Map.insert userId)) + deleteUser dpath = withUser dpath $ \userId -> + liftIO (update $ updateList dpath (Map.delete userId)) + -- require (return - group dpath) +--(UserGroup name queryList updateList)-} serverTreeEmpty :: ServerTree -serverTreeEmpty = ServerTree Nothing Nothing Map.empty Map.empty - -spiffyResources :: ServerTree -> ServerTree -spiffyResources (ServerTree mresource response sdirs resources) = ServerTree (fmap spiffyResource mresource) response - (Map.map spiffyResources sdirs) (Map.map spiffyResources resources) - where spiffyResource :: Resource -> Resource - spiffyResource resource = resource { resourceMethods = - addMissingWithMap (Just . makeOptions . Map.keys) OPTIONS - . addMissingWithMap (fmap makeHead . Map.lookup GET) HEAD - $ resourceMethods resource } - addMissingWithMap :: Ord k => (Map k a -> Maybe a) -> k -> Map k a -> Map k a - addMissingWithMap f key mmap = Map.alter (`mplus` f mmap) key mmap - makeHead responseForGET = \dpath mediaType -> do - (_, _) <- responseForGET dpath mediaType - noBody - -- one downside of the DynamicBranch String approach (as opposed to a more typeful - -- generic system) is that, out of multiple resources served from the same - -- ServerTree node, only the first one's options will be answered - makeOptions methods = \_ _ -> do - setHeaderM "Allow" (intercalate ", " . map show $ methods) - noBody - noBody = return $ (toResponse "", False) - -renderServerTree :: ServerTree -> DynamicPath -> ServerPart Response -renderServerTree (ServerTree resource response sdirs resources) dpath = msum $ maybeToList response ++ maybeToList (fmap renderResource resource) ++ renderDirs ++ [renderResources] +serverTreeEmpty = ServerTree Nothing Map.empty + +-- "/package/:package/doc/:doctree/" +trunkAt :: String -> BranchPath +trunkAt arg = fromTrunkJust . find (\(_, str) -> null str || str == "/") . Parse.readP_to_S parser $ arg + -- Parser gives ReadS BranchPath = [BranchPath, String)] + where + fromTrunkJust (Just a) = reverse (fst a) + -- bottom, not ideal, but this is one of the risks of using declarative parsers + -- alternatively return a dummy path, like [StaticBranch "error"] + fromTrunkJust Nothing = error $ "Distribution.Server.Resource.trunkLiteral: Could not parse trunk literal " ++ show arg + parser :: Parse.ReadP BranchPath -- = [BranchComponent] + parser = Parse.many $ do + Parse.char '/' + fmap DynamicBranch (Parse.char ':' >> Parse.munch1 (/='/')) Parse.<++ (fmap StaticBranch (Parse.munch1 (/='/'))) + +serveResource :: Resource -> (BranchPath, ServerResponse) +serveResource (Resource trunk rget rput rpost rdelete) = (,) trunk $ \config dpath -> msum $ + map (\func -> func config dpath) ([ (methodSP met .) . res | (Just res, met) <- zip methods methodsList] + ++ return (makeOptions $ concat [ met | (Just _, met) <- zip methods methodsList])) where - renderDirs = map (\(name, tree) -> dir name $ renderServerTree tree dpath) (Map.toList sdirs) - renderResources = path $ \pname -> msum $ map (\(name, tree) -> renderServerTree tree (Map.insert name pname dpath)) $ Map.toList resources - renderResource (Resource _ resourceMap resourceDepends) = do - -- use map instead of msum, slightly more efficient (?) - met <- fmap rqMethod askRq - require (return $ Map.lookup met resourceMap) $ \task -> methodSP met $ do - mediaType <- getHeaderM "accept" - (resResponse, isModified) <- task dpath mediaType - when isModified $ invalidate resourceDepends - return resResponse - invalidate _ = return () - ---withPackage :: DynamicPath -> (PackagesState -> PkgInfo -> [PkgInfo] -> ServerPart Response) -> ServerPart Response - -reinsert :: String -> ServerTree -> Map String ServerTree -> Map String ServerTree -reinsert key newTree pairMap = Map.insertWith combine key newTree pairMap + methods = [rget, rput, rpost, rdelete] + methodsList = [[GET, HEAD], [PUT], [POST], [DELETE]] + -- apparently Happstack can do HEAD on its own! plus we need the Content-Length + makeHead :: ServerResponse -> ServerResponse + makeHead responseGET = \config dpath -> do + _ <- responseGET config dpath + noBody + -- one downside of the DynamicBranch String approach (as opposed to a more typeful + -- generic system) is that, out of multiple resources served from the same + -- ServerTree node, only the first one's options will be answered + makeOptions :: [Method] -> ServerResponse + makeOptions methodList = \_ _ -> do + setHeaderM "Allow" (intercalate ", " . map show $ methodList) + noBody + noBody = return $ toResponse () + +renderServerTree :: Config -> DynamicPath -> ServerTree -> ServerPart Response +renderServerTree config dpath (ServerTree func forest) = msum $ maybeToList (fmap (\fun -> fun config dpath) func) ++ map (uncurry renderBranch) (Map.toList forest) + where + renderBranch :: BranchComponent -> ServerTree -> ServerPart Response + renderBranch (StaticBranch sdir) tree = dir sdir $ renderServerTree config dpath tree + renderBranch (DynamicBranch sdir) tree = path $ \pname -> renderServerTree config ((sdir, pname):dpath) tree + +reinsert :: BranchComponent -> ServerTree -> Map BranchComponent ServerTree -> Map BranchComponent ServerTree +-- combine will only be called if branchMap already contains the key +reinsert key newTree branchMap = Map.insertWith combine key newTree branchMap + -- combine new old combine :: ServerTree -> ServerTree -> ServerTree -combine (ServerTree resource response sdirs resources) (ServerTree resource' response' sdirs' resources') = +combine (ServerTree response forest) (ServerTree response' forest') = -- replace old resource with new resource, combine old and new responses - ServerTree (mplus resource resource') (mappend response response') - (Map.foldWithKey reinsert sdirs' sdirs) -- this combines them - (Map.foldWithKey reinsert resources resources') - -addResource :: Resource -> ServerTree -> ServerTree -addResource resource tree = snd $ treeFold (resourceLocation resource) (ServerTree (Just resource) Nothing Map.empty Map.empty) tree + -- reinsert will only be called if forest' is non-empty + ServerTree (mappend response response') (Map.foldWithKey reinsert forest forest') -addResponse :: BranchPath -> ServerPart Response -> ServerTree -> ServerTree -addResponse trunk response tree = snd $ treeFold trunk (ServerTree Nothing (Just response) Map.empty Map.empty) tree +addResponse :: BranchPath -> ServerResponse -> ServerTree -> ServerTree +addResponse trunk response tree = snd $ treeFold trunk (ServerTree (Just response) Map.empty) tree --this function takes a list whose head is the resource and traverses leftwards in the URI --this is due to the original design of specifying URI branches: if the resources are @@ -130,9 +162,9 @@ addResponse trunk response tree = snd $ treeFold trunk (ServerTree Nothing (Just --is nearly too 'clever' for me to even debug, though it works. treeFold :: BranchPath -> ServerTree -> ServerTree -> (ServerTree, ServerTree) treeFold [] newChild topLevel = (topLevel, combine newChild topLevel) -treeFold (StaticBranch sdir:otherTree) newChild topLevel = (Map.findWithDefault serverTreeEmpty sdir (dirForest tree), newTree) - where (tree, newTree) = treeFold otherTree (tree { dirForest = reinsert sdir newChild (dirForest tree) }) topLevel -treeFold (DynamicBranch sdir:otherTree) newChild topLevel = (Map.findWithDefault serverTreeEmpty sdir (resourceForest tree), newTree) - where (tree, newTree) = treeFold otherTree (tree { resourceForest = reinsert sdir newChild (resourceForest tree) }) topLevel +treeFold (sdir:otherTree) newChild topLevel = (Map.findWithDefault serverTreeEmpty sdir (nodeForest tree), newTree) + where (tree, newTree) = treeFold otherTree (tree { nodeForest = reinsert sdir newChild (nodeForest tree) }) topLevel +--treeFold (DynamicBranch sdir:otherTree) newChild topLevel = (Map.findWithDefault serverTreeEmpty sdir (resourceForest tree), newTree) +-- where (tree, newTree) = treeFold otherTree (tree { resourceForest = reinsert sdir newChild (resourceForest tree) }) topLevel diff --git a/Distribution/Server/ServerParts.hs b/Distribution/Server/ServerParts.hs index 01f74adc4..28b22ed08 100644 --- a/Distribution/Server/ServerParts.hs +++ b/Distribution/Server/ServerParts.hs @@ -1,10 +1,10 @@ module Distribution.Server.ServerParts - ( guardAuth + ( --guardAuth ) where -import Distribution.Server.Packages.State +{-import Distribution.Server.Packages.State import Distribution.Server.Users.State import Distribution.Server.Users.Permissions (GroupName) @@ -20,4 +20,4 @@ guardAuth gNames = do _ <- Auth.hackageAuth (userDb state) (Just group) return () - +-} diff --git a/Distribution/Server/State.hs b/Distribution/Server/State.hs index 60dcca7a0..3e3b9dd9f 100644 --- a/Distribution/Server/State.hs +++ b/Distribution/Server/State.hs @@ -6,7 +6,8 @@ module Distribution.Server.State where import Distribution.Server.Instances () import Distribution.Server.Packages.State -import Distribution.Server.Users.Permissions (Permissions) +import Distribution.Server.BuildReport.BuildReports (BuildReports) +import Distribution.Server.Users.Users (Users) import Distribution.Server.Distributions.State (Distros) import Distribution.Server.TarIndex.State (TarIndexMap) @@ -23,8 +24,9 @@ instance Serialize HackageEntryPoint where instance Component HackageEntryPoint where type Dependencies HackageEntryPoint - = PackagesState :+: Documentation :+: Permissions :+: - Distros :+: TarIndexMap :+: End + = PackagesState :+: Documentation :+: Users :+: + BuildReports :+: Distros :+: TarIndexMap :+: + PackageMaintainers :+: End initialValue = HackageEntryPoint diff --git a/Distribution/Server/Users/Group.hs b/Distribution/Server/Users/Group.hs index f9a8cef2d..bbf23b92d 100644 --- a/Distribution/Server/Users/Group.hs +++ b/Distribution/Server/Users/Group.hs @@ -1,6 +1,7 @@ -{-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving #-} +{-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving, ExistentialQuantification #-} module Distribution.Server.Users.Group ( - UserGroup, + UserList(..), + UserGroup(..), empty, add, remove, @@ -20,23 +21,31 @@ import Prelude hiding (id) -- | Some subset of users, eg those allowed to perform some action. -- -newtype UserGroup = UserGroup IntSet.IntSet +newtype UserList = UserList IntSet.IntSet deriving (Eq, Monoid, Binary, Typeable, Show) -empty :: UserGroup -empty = UserGroup IntSet.empty +--forall a b. QueryEvent a (Maybe UserList), UpdateEvent b (), UpdateEvent c () +data UserGroup a b c = UserGroup { + groupName :: String, + queryUserList :: a, + addUserList :: UserId -> b, + removeUserList :: UserId -> c +} -add :: UserId -> UserGroup -> UserGroup -add (UserId id) (UserGroup group) = UserGroup (IntSet.insert id group) +empty :: UserList +empty = UserList IntSet.empty -remove :: UserId -> UserGroup -> UserGroup -remove (UserId id) (UserGroup group) = UserGroup (IntSet.delete id group) +add :: UserId -> UserList -> UserList +add (UserId id) (UserList group) = UserList (IntSet.insert id group) -member :: UserId -> UserGroup -> Bool -member (UserId id) (UserGroup group) = IntSet.member id group +remove :: UserId -> UserList -> UserList +remove (UserId id) (UserList group) = UserList (IntSet.delete id group) -enumerate :: UserGroup -> [UserId] -enumerate (UserGroup group) = map UserId (IntSet.toList group) +member :: UserId -> UserList -> Bool +member (UserId id) (UserList group) = IntSet.member id group -unions :: [UserGroup] -> UserGroup -unions groups = UserGroup (IntSet.unions [ group | UserGroup group <- groups ]) +enumerate :: UserList -> [UserId] +enumerate (UserList group) = map UserId (IntSet.toList group) + +unions :: [UserList] -> UserList +unions groups = UserList (IntSet.unions [ group | UserList group <- groups ]) diff --git a/Distribution/Server/Users/Permissions.hs b/Distribution/Server/Users/Permissions.hs deleted file mode 100644 index 932551bfb..000000000 --- a/Distribution/Server/Users/Permissions.hs +++ /dev/null @@ -1,97 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} - -module Distribution.Server.Users.Permissions - ( GroupName(..) - , Permissions(..) - -- construction - , empty - , addToGroup - , removeFromGroup - , removeGroup - -- querying - , lookupUserGroup - , lookupUserGroups - , enumerate - ) where - -import Distribution.Package - ( PackageName ) -import Distribution.Server.Distributions.Types (DistroName) -import qualified Distribution.Server.Users.Group as Group -import Distribution.Server.Users.Group (UserGroup) -import Distribution.Server.Users.Types (UserId) - -import Distribution.Compat.ReadP -import Distribution.Text -import Text.PrettyPrint.HughesPJ hiding (empty) - -import qualified Data.Map as Map -import Data.Maybe (fromMaybe) -import Data.Typeable - - - -data GroupName - = Administrator - | Trustee - | PackageMaintainer PackageName - | DistroMaintainer DistroName - deriving (Read,Show,Ord,Typeable,Eq) - -instance Text GroupName where - disp Administrator = text "Administrator" - disp Trustee = text "Trustee" - disp (PackageMaintainer pkgName) - = text "PackageMaintainer" <+> disp pkgName - disp (DistroMaintainer distro) - = text "DistroMaintainer" <+> disp distro - - parse - = choice - [ string "Administrator" >> return Administrator - , string "Trustee" >> return Trustee - , string "PackageMaintainer" >> - skipSpaces >> - PackageMaintainer `fmap` parse - , string "DistroMaintainer" >> - skipSpaces >> - DistroMaintainer `fmap` parse - ] - - -data Permissions = Permissions - { permissions :: Map.Map GroupName UserGroup - } deriving (Typeable, Show) - -empty :: Permissions -empty = Permissions Map.empty - -enumerate :: Permissions -> [(GroupName, UserGroup)] -enumerate = Map.toList . permissions - -lookupUserGroup :: GroupName -> Permissions -> UserGroup -lookupUserGroup group = lookupUserGroups [group] - -lookupUserGroups :: [GroupName] -> Permissions -> UserGroup -lookupUserGroups groups perms - = Group.unions - [ Map.findWithDefault Group.empty groupName (permissions perms) - | groupName <- groups - ] - -addToGroup :: GroupName -> UserId -> Permissions -> Permissions -addToGroup groupName userId perms - = perms{permissions = Map.alter fn groupName (permissions perms)} - where fn mbGroup = Just $ Group.add userId (fromMaybe Group.empty mbGroup) - --- | Remove the indicated user from the indicated group -removeFromGroup :: GroupName -> UserId -> Permissions -> Permissions -removeFromGroup groupName userId perms - = perms{permissions = Map.alter fn groupName (permissions perms)} - where fn Nothing = Nothing - fn (Just group) = Just $ Group.remove userId group - --- | Drop all members from the indicated group -removeGroup :: GroupName -> Permissions -> Permissions -removeGroup groupName perms - = perms{permissions = Map.delete groupName (permissions perms)} diff --git a/Distribution/Server/Users/ServerParts.hs b/Distribution/Server/Users/ServerParts.hs index 3b9751ea6..34e1f8e40 100644 --- a/Distribution/Server/Users/ServerParts.hs +++ b/Distribution/Server/Users/ServerParts.hs @@ -1,7 +1,7 @@ module Distribution.Server.Users.ServerParts ( userAdmin, changePassword, - newPasswd, + usersFeature ) where import Happstack.Server hiding (port) @@ -13,148 +13,153 @@ import qualified Distribution.Server.Auth.Basic as Auth import qualified Distribution.Server.Auth.Types as Auth import qualified Distribution.Server.Auth.Crypt as Auth -import qualified Distribution.Server.Users.Types as Users -import Distribution.Server.Users.Permissions(GroupName(..)) -import Distribution.Server.Auth.Types (PasswdPlain(..)) +import qualified Distribution.Server.Users.Users as Users +import qualified Distribution.Server.Users.Group as Group +import Distribution.Server.Users.Types +import Distribution.Server.Feature +import Distribution.Server.Resource +import qualified Distribution.Server.Auth.Types as Auth +import qualified Distribution.Server.Auth.Crypt as Auth +import qualified Distribution.Server.Auth.Basic as Auth -import Distribution.Text (simpleParse) +import Distribution.Text (simpleParse, display) import System.Random (newStdGen) import Data.Maybe import Control.Monad.Trans -import Control.Monad (msum,liftM2,mplus) - - -data ChangePassword = ChangePassword { first, second :: String } deriving (Eq, Ord, Show) +import Control.Monad (msum, liftM3, mplus) + +{- +/users/ +GET: list of users +/users/admins/ +GET: list of admins and contact info +/users/admins/ +PUT: add admin (may not be provided by web interface for security reasons) +DELETE: delete admin (likewise) +/user/ +GET: return basic user info, perhaps packages uploaded +PUT: Add user, admin only. If free registration were allowed, it might be as a POST to /users/ +DELETE: Delete user +/user//password +PUT: change password. Admins can change anyone's password, but everyone else can only change their own. +-} + +usersFeature :: HackageFeature +usersFeature = HackageFeature { + featureName = "user pages", + -- todo: add checking + locations = map serveResource $ + [ (resourceAt "/users/") { resourceGet = Just serveUserList, resourcePost = Just adminAddUser } + , (resourceAt "/user/:username") { resourceGet = Just serveUserPage, resourceDelete = Nothing } + , (resourceAt "/user/:username/enabled") + , (resourceAt "/user/:username/password") { resourcePut = Just changePassword } + ] ++ makeGroupResources (trunkAt "/users/admins") (\_ -> Just $ Group.UserGroup "Site administrators" GetJustHackageAdmins AddHackageAdmin RemoveHackageAdmin), + dumpBackup = return [], + restoreBackup = \_ -> return () +} + +data ChangePassword = ChangePassword { first :: String, second :: String, newAuthType :: Auth.AuthType } deriving (Eq, Show) instance FromData ChangePassword where - fromData = liftM2 ChangePassword (look "new" `mplus` return "") (look "new2" `mplus` return "") - -changePassword :: ServerPart Response -changePassword = - methodSP POST $ do - state <- query GetPackagesState - let users = userDb state - uid <- Auth.hackageAuth users Nothing - pwd <- getData >>= maybe (return $ ChangePassword "not" "valid") return - if (first pwd == second pwd && first pwd /= "") - then do let passwd = PasswdPlain (first pwd) - auth <- newPasswd passwd - res <- update $ ReplaceUserAuth uid auth + fromData = liftM3 ChangePassword (look "password" `mplus` return "") (look "repeat-password" `mplus` return "") + (fmap (maybe Auth.BasicAuth (const Auth.DigestAuth) . lookup "auth") lookPairs) --checked: digest auth + +serveUserList :: Config -> DynamicPath -> ServerPart Response +serveUserList config dpath = do + users <- query GetUserDb + return . toResponse $ "Calling all users: " ++ show users + +serveUserPage :: Config -> DynamicPath -> ServerPart Response +serveUserPage config dpath = return . toResponse $ "Welcome to the illustrious user page of (" ++ show (lookup "username" dpath) ++ "), wherein you may view basic information and toggle settings and change passwords." + +changePassword :: Config -> DynamicPath -> ServerPart Response +changePassword config dpath = do + users <- query State.GetUserDb + uid <- Auth.requireHackageAuth users Nothing Nothing + let muserIdName = userName `fmap` Users.lookupId uid users + muserPathName = simpleParse =<< lookup "username" dpath + muserPathId = flip Users.lookupName users =<< muserPathName + case (muserPathId, muserPathName, muserIdName) of + (Just userPathId, Just userPathName, Just userIdName) -> + if uid == userPathId || (uid `Group.member` Users.adminList users) + then do + pwd <- maybe (return $ ChangePassword "not" "valid" Auth.BasicAuth) return =<< getData + if (first pwd == second pwd && first pwd /= "") + then do + let passwd = PasswdPlain (first pwd) + auth <- case newAuthType pwd of + Auth.BasicAuth -> newBasicPass passwd + Auth.DigestAuth -> return $ newDigestPass userPathName passwd + res <- update $ ReplaceUserAuth userPathId auth if res - then ok $ toResponse "Password Changed" - else ok $ toResponse "Error changing password" - else forbidden $ toResponse "Copies of new password do not match or is an invalid password (ex: blank)" - + then ok $ toResponse "Password Changed" + else ok $ toResponse "Error changing password" + else forbidden $ toResponse "Copies of new password do not match or is an invalid password (ex: blank)" + else forbidden . toResponse $ "Cannot change password for " ++ display userPathName + (Nothing, Just userPathName, _) -> notFound . toResponse $ "User " ++ display userPathName ++ " doesn't exist" + _ -> internalServerError . toResponse $ "Error in changePassword" + +newBasicPass :: MonadIO m => Auth.PasswdPlain -> m UserAuth +newBasicPass pwd = do + gen <- liftIO newStdGen + return $ UserAuth (Auth.newBasicPass gen pwd) Auth.BasicAuth + +newDigestPass :: UserName -> PasswdPlain -> UserAuth +newDigestPass name pwd = UserAuth (Auth.newDigestPass name pwd "hackage") Auth.DigestAuth + +adminAddUser :: Config -> DynamicPath -> ServerPart Response +adminAddUser config dpath = do + reqData <- getDataFn lookUserNamePasswords + case reqData of + Nothing -> ok $ toResponse "try to fill out all the fields" + Just (userName, pwd1, pwd2) -> doAdminAddUser userName + (Auth.PasswdPlain pwd1) (Auth.PasswdPlain pwd2) + where + lookUserNamePasswords = do + userName <- look "user-name" + pwd1 <- look "password" + pwd2 <- look "repeat-password" + return (userName, pwd1, pwd2) -- Assumes that the user has already been autheniticated -- and has proper permissions userAdmin :: ServerPart Response -userAdmin - = msum - [ dir "add" $ msum - [ methodSP POST $ do - reqData <- getDataFn lookUserNamePasswords - case reqData of - Nothing -> ok $ toResponse "try to fill out all the fields" - Just (userName, pwd1, pwd2) -> - - adminAddUser userName - (Auth.PasswdPlain pwd1) (Auth.PasswdPlain pwd2) - ] - , dir "change-password" $ msum - [ methodSP POST $ do - reqData <- getDataFn lookUserNamePasswords - case reqData of - Nothing -> ok $ toResponse "try to fill out all the fields" - Just (userName, pwd1, pwd2) -> - - adminChangePassword (Users.UserName userName) - (Auth.PasswdPlain pwd1) (Auth.PasswdPlain pwd2) - ] - -- , dir "disable" $ undefined - -- , dir "enable" $ undefined - -- , dir "delete" $ undefined - , dir "toggle-admin" $ msum +userAdmin = msum + [ dir "toggle-admin" $ msum [ methodSP POST $ do reqData <- getDataFn $ do - userName <- lookUserName + userName <- look "user-name" makeAdmin <- lookRead "admin" return (userName, makeAdmin) case reqData of Nothing -> ok $ toResponse "Bad inputs, somehow" Just (userName, makeAdmin) -> - adminToggleAdmin (Users.UserName userName) makeAdmin + adminToggleAdmin (UserName userName) makeAdmin ] ] - where lookUserNamePasswords = do - userName <- lookUserName - pwd1 <- look "password" - pwd2 <- look "repeat-password" - return (userName, pwd1, pwd2) - - lookUserName = look "user-name" - -adminToggleAdmin :: Users.UserName -> Bool -> ServerPart Response -adminToggleAdmin userName makeAdmin - = do - mUser <- query $ LookupUserName userName - - if isNothing mUser then ok $ toResponse "Unknown user name" else do - - let Just user = mUser - - if makeAdmin - then update $ AddToGroup Administrator user - else update $ RemoveFromGroup Administrator user - - ok $ toResponse "Success!" - - -adminChangePassword - :: Users.UserName -> Auth.PasswdPlain -> Auth.PasswdPlain - -> ServerPart Response -adminChangePassword _ pwd1 pwd2 +adminToggleAdmin :: UserName -> Bool -> ServerPart Response +adminToggleAdmin userName makeAdmin = do + mUser <- query $ LookupUserName userName + if isNothing mUser then ok $ toResponse "Unknown user name" else do + let Just user = mUser + if makeAdmin + then update $ AddHackageAdmin user + else update $ RemoveHackageAdmin user + ok $ toResponse "Success!" + +doAdminAddUser :: String -> Auth.PasswdPlain -> Auth.PasswdPlain -> ServerPart Response +doAdminAddUser _ pwd1 pwd2 | pwd1 /= pwd2 = ok $ toResponse "Entered passwords do not match" -adminChangePassword userName password _ - = do - - mUser <- query $ LookupUserName userName - - case mUser of - Nothing -> ok $ toResponse "Unknown user name" - Just user -> - do - auth <- newPasswd password - res <- update $ ReplaceUserAuth user auth - - ok $ toResponse $ - if res then "Success!" - else "Failure!" - - -adminAddUser :: String -> Auth.PasswdPlain -> Auth.PasswdPlain - -> ServerPart Response -adminAddUser _ pwd1 pwd2 - | pwd1 /= pwd2 - = ok $ toResponse "Entered passwords do not match" -adminAddUser userNameStr password _ +doAdminAddUser userNameStr password _ = case simpleParse userNameStr of Nothing -> ok $ toResponse "Not a valid user name!" Just userName -> do - userAuth <- newPasswd password - res <- update $ AddUser userName userAuth - + let userAuth = newDigestPass userName password + res <- update $ AddUser userName userAuth case res of Nothing -> ok $ toResponse "Failed!" Just _ -> ok $ toResponse "Ok!" - -newPasswd :: MonadIO m => Auth.PasswdPlain -> m Auth.PasswdHash -newPasswd pwd = - do - gen <- liftIO newStdGen - return $ Auth.newPasswd gen pwd - diff --git a/Distribution/Server/Users/State.hs b/Distribution/Server/Users/State.hs index ffbe5f3ca..4f2fc23b0 100644 --- a/Distribution/Server/Users/State.hs +++ b/Distribution/Server/Users/State.hs @@ -5,13 +5,11 @@ module Distribution.Server.Users.State where import Distribution.Server.Instances () -import Distribution.Server.Users.Group (UserGroup) -import qualified Distribution.Server.Users.Permissions as Permissions -import Distribution.Server.Users.Permissions (Permissions(..),GroupName) import Distribution.Server.Users.Types (UserId,UserName,UserAuth) +import Distribution.Server.Users.Group as Group (UserList(..), enumerate, add, remove) import Distribution.Server.Users.Users as Users -import qualified Data.Map as Map +import Data.Maybe (isJust) import Happstack.State import qualified Data.Binary as Binary @@ -19,18 +17,6 @@ import qualified Data.Binary as Binary import Control.Monad.Reader import qualified Control.Monad.State as State - -instance Component Permissions where - type Dependencies Permissions = End - initialValue = Permissions Map.empty - -instance Version Permissions where - mode = Versioned 0 Nothing - -instance Serialize Permissions where - putCopy (Permissions p) = contain $ safePut p - getCopy = contain $ liftM Permissions safeGet - instance Version Users where mode = Versioned 0 Nothing @@ -38,18 +24,12 @@ instance Serialize Users where putCopy = contain . Binary.put getCopy = contain Binary.get -instance Version UserGroup where +instance Version UserList where mode = Versioned 0 Nothing -instance Serialize UserGroup where +instance Serialize UserList where putCopy = contain . Binary.put getCopy = contain Binary.get -instance Version GroupName where - mode = Versioned 0 Nothing -instance Serialize GroupName where - putCopy = contain . Binary.put . show - getCopy = contain (liftM read Binary.get) - instance Version UserId where mode = Versioned 0 Nothing instance Serialize UserId where @@ -68,31 +48,94 @@ instance Serialize UserAuth where putCopy = contain . Binary.put getCopy = contain Binary.get - -lookupUserGroup :: GroupName -> Query Permissions UserGroup -lookupUserGroup group - = Permissions.lookupUserGroup group `fmap` ask - -lookupUserGroups :: [GroupName] -> Query Permissions UserGroup -lookupUserGroups groups = - Permissions.lookupUserGroups groups `fmap` ask - -addToGroup :: GroupName -> UserId -> Update Permissions () -addToGroup groupName userId - = State.modify $ Permissions.addToGroup groupName userId - -removeFromGroup :: GroupName -> UserId -> Update Permissions () -removeFromGroup groupName userId - = State.modify $ Permissions.removeFromGroup groupName userId - -removeGroup :: GroupName -> Update Permissions () -removeGroup groupName - = State.modify $ Permissions.removeGroup groupName - -getPermissions :: Query Permissions Permissions -getPermissions = ask - --- |overwrites existing permissions +instance Component Users where + type Dependencies Users = End + initialValue = Users.empty + +-------------------------------------------- + +-- Returns 'Nothing' if the user name is in use +addUser :: UserName -> UserAuth -> Update Users (Maybe UserId) +addUser userName auth = updateUsers' updateFn formatFn + where updateFn = Users.add userName auth + formatFn = id + +-- Disables the indicated user +setEnabledUser :: UserId -> Bool -> Update Users Bool +setEnabledUser uid en = updateUsers $ Users.setEnabled en uid + +-- Deletes the indicated user. Cannot be re-enabled. The associated +-- user name is available for re-use +deleteUser :: UserId -> Update Users Bool +deleteUser = updateUsers . Users.delete + +-- Re-set the user autenication info +replaceUserAuth :: UserId -> UserAuth -> Update Users Bool +replaceUserAuth userId auth + = updateUsers $ \users -> Users.replaceAuth users userId auth + +-- updates the user db with a simpler function +updateUsers :: (Users -> Maybe Users) -> Update Users Bool +updateUsers f = updateUsers' updateFn isJust + where updateFn users = fmap (swap . (,) ()) $ f users + swap (x,y) = (y,x) + +-- Helper function for updating the users db +updateUsers' :: (Users -> Maybe (Users, a)) -> (Maybe a -> b) -> Update Users b +updateUsers' f format = do + users <- State.get + liftM format $ case (f users) of + Nothing -> return Nothing + Just (users',a) -> do + State.put users' + return (Just a) + +lookupUserName :: UserName -> Query Users (Maybe UserId) +lookupUserName = queryUsers . Users.lookupName + +queryUsers :: (Users -> a) -> Query Users a +queryUsers queryFn = liftM queryFn ask + +getUserDb :: Query Users Users +getUserDb = ask + +replaceUserDb :: Users -> Update Users () +replaceUserDb = State.put + +listGroupMembers :: UserList -> Query Users [UserName] +listGroupMembers userList + = do users <- ask + return [ Users.idToName users uid | uid <- Group.enumerate userList ] + +getHackageAdmins :: Query Users UserList +getHackageAdmins = asks adminList + +getJustHackageAdmins :: Query Users (Maybe UserList) +getJustHackageAdmins = Just `fmap` asks adminList + +modifyHackageAdmins :: (UserList -> UserList) -> Update Users () +modifyHackageAdmins func = State.modify (\users -> users { adminList = func (adminList users) }) + +addHackageAdmin :: UserId -> Update Users () +addHackageAdmin uid = modifyHackageAdmins (Group.add uid) + +removeHackageAdmin :: UserId -> Update Users () +removeHackageAdmin uid = modifyHackageAdmins (Group.remove uid) + +$(mkMethods ''Users ['addUser + ,'setEnabledUser + ,'deleteUser + ,'replaceUserAuth + ,'lookupUserName + ,'getUserDb + ,'replaceUserDb + + ,'getHackageAdmins + ,'getJustHackageAdmins + ,'addHackageAdmin + ,'removeHackageAdmin + ]) +{--- |overwrites existing permissions bulkImportPermissions :: [(UserId, GroupName)] -> Update Permissions () bulkImportPermissions perms = do @@ -113,4 +156,4 @@ $(mkMethods ''Permissions ['lookupUserGroup -- Import ,'bulkImportPermissions ,'replacePermissions - ]) + ])-} diff --git a/Distribution/Server/Users/Types.hs b/Distribution/Server/Users/Types.hs index ff6bb7e9d..9e69b5a7f 100644 --- a/Distribution/Server/Users/Types.hs +++ b/Distribution/Server/Users/Types.hs @@ -26,37 +26,40 @@ newtype UserName = UserName String data UserInfo = UserInfo { userName :: UserName, - userStatus :: AccountStatus + userStatus :: UserStatus } deriving (Show) -data AccountStatus = Deleted - | Disabled UserAuth - | Enabled UserAuth - deriving (Show) +data UserStatus = Deleted + | Active !AccountEnabled UserAuth + deriving (Show) +data AccountEnabled = Enabled | Disabled deriving (Show, Enum, Eq) -type UserAuth = PasswdHash +data UserAuth = UserAuth PasswdHash AuthType deriving (Show, Eq, Typeable) instance Text UserId where - disp (UserId uid) = Disp.int uid - parse = UserId <$> Parse.int + disp (UserId uid) = Disp.int uid + parse = UserId <$> Parse.int instance Text UserName where - disp (UserName name) = Disp.text name - parse = UserName <$> Parse.munch1 Char.isAlphaNum - -instance Binary AccountStatus where - put Deleted = Binary.putWord8 1 - put (Disabled auth) = Binary.putWord8 2 >> Binary.put auth - put (Enabled auth) = Binary.putWord8 3 >> Binary.put auth - - get = do - w <- Binary.getWord8 - case w of - 1 -> pure Deleted - 2 -> Disabled <$> Binary.get - 3 -> Enabled <$> Binary.get - _ -> error "decoding AccountStatus" + disp (UserName name) = Disp.text name + parse = UserName <$> Parse.munch1 Char.isAlphaNum + +instance Binary UserAuth where + put (UserAuth hash atype) = Binary.put hash >> Binary.put atype + get = UserAuth <$> Binary.get <*> Binary.get + +instance Binary UserStatus where + put Deleted = Binary.putWord8 1 + put (Active Enabled auth) = Binary.putWord8 2 >> Binary.put auth + put (Active Disabled auth) = Binary.putWord8 3 >> Binary.put auth + get = do + w <- Binary.getWord8 + case w of + 1 -> pure Deleted + 2 -> Active Enabled <$> Binary.get + 3 -> Active Disabled <$> Binary.get + _ -> error "decoding AccountStatus" instance Binary UserInfo where - put (UserInfo a b) = Binary.put a >> Binary.put b - get = UserInfo <$> Binary.get <*> Binary.get + put (UserInfo a b) = Binary.put a >> Binary.put b + get = UserInfo <$> Binary.get <*> Binary.get diff --git a/Distribution/Server/Users/Users.hs b/Distribution/Server/Users/Users.hs index 5d07ac660..9bf611ad1 100644 --- a/Distribution/Server/Users/Users.hs +++ b/Distribution/Server/Users/Users.hs @@ -1,7 +1,7 @@ {-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving #-} module Distribution.Server.Users.Users ( -- * Users type - Users, + Users(..), -- * Construction empty, @@ -10,8 +10,7 @@ module Distribution.Server.Users.Users ( -- * Modification delete, - disable, - enable, + setEnabled, replaceAuth, -- * Lookup @@ -29,6 +28,7 @@ module Distribution.Server.Users.Users ( ) where import Distribution.Server.Users.Types +import qualified Distribution.Server.Users.Group as Group import Data.Typeable (Typeable) import qualified Data.Map as Map @@ -43,7 +43,8 @@ import Control.Applicative ((<$>), (<*>)) data Users = Users { userIdMap :: !(IntMap.IntMap UserInfo), userNameMap :: !(Map.Map UserName UserId), - nextId :: !UserId + nextId :: !UserId, + adminList :: !Group.UserList } deriving (Typeable, Show) @@ -62,13 +63,12 @@ data Users = Users { -- it also allows us to track historical info, like name of uploader -- even if that user name has been recycled, the user ids will be distinct. - - empty :: Users empty = Users { userIdMap = IntMap.empty, userNameMap = Map.empty, - nextId = UserId 0 + nextId = UserId 0, + adminList = Group.empty } -- | Add a new user account. @@ -86,7 +86,7 @@ add name auth users = UserId userId = nextId users userInfo = UserInfo { userName = name, - userStatus = Enabled auth + userStatus = Active Enabled auth } users' = users { userIdMap = IntMap.insert userId userInfo (userIdMap users), @@ -102,30 +102,22 @@ add name auth users = -- deleted user and the user name is already in use, -- 'Nothing' will be returned. insert :: UserId -> UserInfo -> Users -> Maybe Users -insert user@(UserId ident) info users - = let name = userName info - - isDeleted = case userStatus info of - Deleted{} -> True - _ -> False - idMap' - = intInsertMaybe ident info (userIdMap users) - - nameMap' - = insertMaybe name user (userNameMap users) - - nextIdent - | user >= nextId users = UserId (ident + 1) - | otherwise = nextId users - - in case idMap' of - Nothing -> Nothing -- Id clash, always fatal - Just idMap -> if isDeleted - then Just $ Users idMap (userNameMap users) nextIdent - else case nameMap' of - Nothing -> Nothing -- name clash, fatal if non-deleted user - Just nameMap -> Just $ Users idMap nameMap nextIdent - +insert user@(UserId ident) info users = + let name = userName info + isDeleted = case userStatus info of + Deleted -> True + _ -> False + idMap' = intInsertMaybe ident info (userIdMap users) + nameMap' = insertMaybe name user (userNameMap users) + nextIdent | user >= nextId users = UserId (ident + 1) + | otherwise = nextId users + in case idMap' of + Nothing -> Nothing -- Id clash, always fatal + Just idMap -> if isDeleted + then Just $ Users idMap (userNameMap users) nextIdent (adminList users) + else case nameMap' of + Nothing -> Nothing -- name clash, fatal if non-deleted user + Just nameMap -> Just $ Users idMap nameMap nextIdent (adminList users) -- | Delete a user account. -- @@ -150,7 +142,7 @@ delete (UserId userId) users = do userNameMap = Map.delete (userName userInfo) (userNameMap users) } --- | Disable a user account. +-- | Change the status of a user account to enabled or disabled. -- -- Prevents the given user from performing any authenticated operations. -- This operation is idempotent and reversable. Use 'enable' to re-enable a @@ -161,37 +153,17 @@ delete (UserId userId) users = do -- -- * Returns 'Nothing' if the user id does not exist or is deleted -- -disable :: UserId -> Users -> Maybe Users -disable (UserId userId) users = do - userInfo <- IntMap.lookup userId (userIdMap users) - userInfo' <- disableAccount userInfo - return $! users { - userIdMap = IntMap.insert userId userInfo' (userIdMap users) - } - where - disableAccount userInfo = case userStatus userInfo of - Deleted -> Nothing - Disabled _ -> Just userInfo - Enabled auth -> Just userInfo { userStatus = Disabled auth } - --- | Enable a user account. --- --- This operation is idempotent and reversable. The ordinary state of accounts --- is enabled. Accounts can be 'disable'd and this operation is used to --- re-enable them. --- -enable :: UserId -> Users -> Maybe Users -enable (UserId userId) users = do - userInfo <- IntMap.lookup userId (userIdMap users) - userInfo' <- enableAccount userInfo - return $! users { - userIdMap = IntMap.insert userId userInfo' (userIdMap users) - } +setEnabled :: Bool -> UserId -> Users -> Maybe Users +setEnabled newStatus (UserId userId) users = do + userInfo <- IntMap.lookup userId (userIdMap users) + userInfo' <- changeStatus userInfo + return $! users { + userIdMap = IntMap.insert userId userInfo' (userIdMap users) + } where - enableAccount userInfo = case userStatus userInfo of - Deleted -> Nothing - Disabled auth -> Just userInfo { userStatus = Enabled auth } - Enabled _ -> Just userInfo + changeStatus userInfo = case userStatus userInfo of + Deleted -> Nothing + Active _ auth -> Just userInfo { userStatus = Active (if newStatus then Enabled else Disabled) auth } lookupId :: UserId -> Users -> Maybe UserInfo lookupId (UserId userId) users = IntMap.lookup userId (userIdMap users) @@ -223,8 +195,7 @@ replaceAuth :: Users -> UserId -> UserAuth -> Maybe Users replaceAuth users userId newAuth = modifyUser users userId $ \userInfo -> case userStatus userInfo of - Disabled _ -> userInfo { userStatus = Disabled newAuth } - Enabled _ -> userInfo { userStatus = Enabled newAuth } + Active status _ -> userInfo { userStatus = Active status newAuth } Deleted -> userInfo -- | Modify a single user. Returns 'Nothing' if the user does not @@ -243,8 +214,7 @@ enumerateAll where mapFst f = map $ \(x,y) -> (f x, y) enumerateEnabled :: Users -> [(UserId, UserInfo)] -enumerateEnabled users = - [ x | x@(_, UserInfo { userStatus = Enabled _ }) <- enumerateAll users ] +enumerateEnabled users = [ x | x@(_, UserInfo { userStatus = Active Enabled _ }) <- enumerateAll users ] -- | Insertion fails if key is present @@ -263,5 +233,5 @@ intInsertMaybe k a m instance Binary Users where - put (Users a b c) = Binary.put a >> Binary.put b >> Binary.put c - get = Users <$> Binary.get <*> Binary.get <*> Binary.get + put (Users a b c d) = Binary.put a >> Binary.put b >> Binary.put c >> Binary.put d + get = Users <$> Binary.get <*> Binary.get <*> Binary.get <*> Binary.get diff --git a/Main.hs b/Main.hs index 543fbbcbb..ec66e4755 100644 --- a/Main.hs +++ b/Main.hs @@ -1,7 +1,7 @@ module Main (main) where import qualified Distribution.Server -import Distribution.Server (Config(..), Server) +import Distribution.Server (ServerConfig(..), Server) import Distribution.Text ( display ) @@ -46,7 +46,7 @@ main = topHandler $ do (optImportLog opts) (optImportArchive opts) (optImportHtPasswd opts) (optImportAdmins opts) - defaults <- Distribution.Server.defaultConfig + defaults <- Distribution.Server.defaultServerConfig port <- checkPortOpt defaults (optPort opts) let hostname = fromMaybe (confHostName defaults) (optHost opts) @@ -89,7 +89,7 @@ main = topHandler $ do Distribution.Server.run server where - withServer :: Config -> (Server -> IO ()) -> IO () + withServer :: ServerConfig -> (Server -> IO ()) -> IO () withServer config = bracket initialise shutdown where initialise = do @@ -184,6 +184,7 @@ main = topHandler $ do info "importing..." badLogEntries <- Distribution.Server.bulkImport server indexFile logFile tarballs htpasswd admins + info "done" unless (null badLogEntries) $ putStr $ "Warning: Upload log entries for non-existant packages:\n" ++ unlines (map display (sort badLogEntries)) diff --git a/hackage-server.cabal b/hackage-server.cabal index c87ebfb33..b7c338528 100644 --- a/hackage-server.cabal +++ b/hackage-server.cabal @@ -83,7 +83,6 @@ executable hackage-server Distribution.Server.State Distribution.Server.TarIndex.State Distribution.Server.Users.Group - Distribution.Server.Users.Permissions Distribution.Server.Users.ServerParts Distribution.Server.Users.State Distribution.Server.Users.Types