Skip to content

Commit 5f41d21

Browse files
committed
Merge pull request #1801 from sjakobi/471-once-again
Add 'allow-different-user' flag and configuration option #471, fix #1777
2 parents 631e99b + 95fce22 commit 5f41d21

File tree

10 files changed

+202
-32
lines changed

10 files changed

+202
-32
lines changed

ChangeLog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ Other enhancements:
2626
work [#1358](https://github.com/commercialhaskell/stack/issues/1358)
2727
* Docker: strip suffix from docker --version
2828
[#1653](https://github.com/commercialhaskell/stack/issues/1653)
29+
* On each run, stack will test the stack root directory (~/.stack), and the
30+
project and package work directories (.stack-work) for whether they are
31+
owned by the current user and abort if they are not. This precaution can
32+
be disabled with the `--allow-different-user` flag or `allow-different-user`
33+
option in the global config (~/.stack/config.yaml).
34+
[#471](https://github.com/commercialhaskell/stack/issues/471)
2935

3036
Bug fixes:
3137

doc/yaml_configuration.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,22 @@ allow-newer: true
435435
Note that this also ignores lower bounds. The name "allow-newer" is chosen to
436436
match the commonly used cabal option.
437437

438+
### allow-different-user
439+
440+
(Since 1.0.1)
441+
442+
Allow users other than the owner of the stack root directory (typically `~/.stack`)
443+
to use the stack installation. The default is `false`. POSIX systems only.
444+
445+
```yaml
446+
allow-different-user: true
447+
```
448+
449+
The intention of this option is to prevent file permission problems, for example
450+
as the result of a `stack` command executed under `sudo`.
451+
452+
The option is automatically enabled when `stack` is re-spawned in a Docker process.
453+
438454
### templates
439455

440456
Templates used with `stack new` have a number of parameters that affect the generated code. These can be set for all new projects you create. The result of them can be observed in the generated LICENSE and cabal files.

src/Path/Find.hs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
module Path.Find
66
(findFileUp
77
,findDirUp
8-
,findFiles)
8+
,findFiles
9+
,findInParents)
910
where
1011

1112
import Control.Monad
@@ -66,3 +67,16 @@ findFiles dir p traversep =
6667
then findFiles entry p traversep
6768
else return [])
6869
return (concat (filter p files : subResults))
70+
71+
-- | @findInParents f path@ applies @f@ to @path@ and its 'parent's until
72+
-- it finds a 'Just' or reaches the root directory.
73+
findInParents :: MonadIO m => (Path Abs Dir -> m (Maybe a)) -> Path Abs Dir -> m (Maybe a)
74+
findInParents f path = do
75+
mres <- f path
76+
case mres of
77+
Just res -> return (Just res)
78+
Nothing -> do
79+
let next = parent path
80+
if next == path
81+
then return Nothing
82+
else findInParents f next

src/Stack/Build/Execute.hs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import Stack.Build.Cache
7070
import Stack.Build.Haddock
7171
import Stack.Build.Installed
7272
import Stack.Build.Source
73+
import Stack.Config
7374
import Stack.Constants
7475
import Stack.Coverage
7576
import Stack.Fetch as Fetch
@@ -769,6 +770,10 @@ withSingleContext runInBase ActionContext {..} ExecuteEnv {..} task@Task {..} md
769770

770771
withCabal package pkgDir mlogFile inner = do
771772
config <- asks getConfig
773+
774+
unless (configAllowDifferentUser config) $
775+
checkOwnership (pkgDir </> configWorkDir config)
776+
772777
let envSettings = EnvSettings
773778
{ esIncludeLocals = taskLocation task == Local
774779
, esIncludeGhcPackagePath = False

src/Stack/Config.hs

Lines changed: 103 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{-# LANGUAGE CPP #-}
12
{-# LANGUAGE ScopedTypeVariables #-}
23
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
34
{-# LANGUAGE TemplateHaskell #-}
@@ -30,6 +31,8 @@ module Stack.Config
3031
,getIsGMP4
3132
,getSnapshots
3233
,makeConcreteResolver
34+
,checkOwnership
35+
,getInContainer
3336
) where
3437

3538
import qualified Codec.Archive.Tar as Tar
@@ -40,6 +43,7 @@ import Control.Arrow ((***))
4043
import Control.Exception (assert)
4144
import Control.Monad (liftM, unless, when, filterM)
4245
import Control.Monad.Catch (MonadThrow, MonadCatch, catchAll, throwM)
46+
import Control.Monad.Extra (firstJustM)
4347
import Control.Monad.IO.Class
4448
import Control.Monad.Logger hiding (Loc)
4549
import Control.Monad.Reader (MonadReader, ask, asks, runReaderT)
@@ -49,6 +53,7 @@ import Data.Aeson.Extended
4953
import qualified Data.ByteString as S
5054
import qualified Data.ByteString.Base16 as B16
5155
import qualified Data.ByteString.Lazy as L
56+
import Data.Foldable (forM_)
5257
import qualified Data.IntMap as IntMap
5358
import qualified Data.Map as Map
5459
import Data.Maybe
@@ -66,6 +71,7 @@ import Network.HTTP.Download (download, downloadJSON)
6671
import Options.Applicative (Parser, strOption, long, help)
6772
import Path
6873
import Path.Extra (toFilePathNoTrailingSep)
74+
import Path.Find (findInParents)
6975
import Path.IO
7076
import qualified Paths_stack as Meta
7177
import Safe (headMay)
@@ -77,9 +83,10 @@ import qualified Stack.Image as Image
7783
import Stack.PackageIndex
7884
import Stack.Types
7985
import Stack.Types.Internal
80-
import qualified System.Directory as D
8186
import System.Environment
8287
import System.IO
88+
import System.PosixCompat.Files (fileOwner, getFileStatus)
89+
import System.PosixCompat.User (getEffectiveUserID)
8390
import System.Process.Read
8491

8592
-- | If deprecated path exists, use it and print a warning.
@@ -289,6 +296,11 @@ configFromConfigMonoid configStackRoot configUserConfigPath mresolver mproject c
289296
configAllowNewer = fromMaybe False configMonoidAllowNewer
290297
configDefaultTemplate = configMonoidDefaultTemplate
291298

299+
configAllowDifferentUser <-
300+
case configMonoidAllowDifferentUser of
301+
Just True -> return True
302+
_ -> getInContainer
303+
292304
return Config {..}
293305

294306
-- | Get the default 'GHCVariant'. On older Linux systems with libgmp4, returns 'GHCGMP4'.
@@ -365,7 +377,7 @@ loadConfig :: (MonadLogger m,MonadIO m,MonadCatch m,MonadThrow m,MonadBaseContro
365377
-- ^ Override resolver
366378
-> m (LoadConfig m)
367379
loadConfig configArgs mstackYaml mresolver = do
368-
stackRoot <- determineStackRoot
380+
(stackRoot, userOwnsStackRoot) <- determineStackRootAndOwnership
369381
userConfigPath <- getDefaultUserConfigPath stackRoot
370382
extraConfigs0 <- getExtraConfigs userConfigPath >>= mapM loadYaml
371383
let extraConfigs =
@@ -387,10 +399,18 @@ loadConfig configArgs mstackYaml mresolver = do
387399
Just (_, _, projectConfig) -> configArgs : projectConfig : extraConfigs
388400
unless (fromCabalVersion Meta.version `withinRange` configRequireStackVersion config)
389401
(throwM (BadStackVersionException (configRequireStackVersion config)))
402+
403+
let mprojectRoot = fmap (\(_, fp, _) -> parent fp) mproject
404+
unless (configAllowDifferentUser config) $ do
405+
unless userOwnsStackRoot $
406+
throwM (UserDoesn'tOwnDirectory stackRoot)
407+
forM_ mprojectRoot $ \dir ->
408+
checkOwnership (dir </> configWorkDir config)
409+
390410
return LoadConfig
391411
{ lcConfig = config
392412
, lcLoadBuildConfig = loadBuildConfig mproject config mresolver
393-
, lcProjectRoot = fmap (\(_, fp, _) -> parent fp) mproject
413+
, lcProjectRoot = mprojectRoot
394414
}
395415

396416
-- | Load the build configuration, adds build-specific values to config loaded by @loadConfig@.
@@ -611,15 +631,83 @@ resolvePackageLocation menv projRoot (PLRemote url remotePackageType) = do
611631
throwM $ UnexpectedArchiveContents dirs files
612632
_ -> return dir
613633

614-
-- | Get the stack root, e.g. ~/.stack
615-
determineStackRoot :: (MonadIO m, MonadThrow m) => m (Path Abs Dir)
616-
determineStackRoot = do
617-
env <- liftIO getEnvironment
618-
case lookup stackRootEnvVar env of
619-
Nothing -> getAppUserDataDir $(mkRelDir stackProgName)
620-
Just x -> do
621-
liftIO $ D.createDirectoryIfMissing True x
622-
resolveDir' x
634+
-- | Get the stack root, e.g. @~/.stack@, and determine whether the user owns it.
635+
--
636+
-- On Windows, the second value is always 'True'.
637+
determineStackRootAndOwnership
638+
:: (MonadIO m, MonadCatch m)
639+
=> m (Path Abs Dir, Bool)
640+
determineStackRootAndOwnership = do
641+
stackRoot <- do
642+
mstackRoot <- liftIO $ lookupEnv stackRootEnvVar
643+
case mstackRoot of
644+
Nothing -> getAppUserDataDir $(mkRelDir stackProgName)
645+
Just x -> parseAbsDir x
646+
647+
(existingStackRootOrParentDir, userOwnsIt) <- do
648+
mdirAndOwnership <- findInParents getDirAndOwnership stackRoot
649+
case mdirAndOwnership of
650+
Just x -> return x
651+
Nothing -> throwM (BadStackRootEnvVar stackRoot)
652+
653+
when (existingStackRootOrParentDir /= stackRoot) $
654+
if userOwnsIt
655+
then liftIO $ ensureDir stackRoot
656+
else throwM $
657+
Won'tCreateStackRootInDirectoryOwnedByDifferentUser
658+
stackRoot
659+
existingStackRootOrParentDir
660+
661+
stackRoot' <- canonicalizePath stackRoot
662+
return (stackRoot', userOwnsIt)
663+
664+
-- | @'checkOwnership' dir@ throws 'UserDoesn'tOwnDirectory' if @dir@
665+
-- isn't owned by the current user.
666+
--
667+
-- If @dir@ doesn't exist, its parent directory is checked instead.
668+
-- If the parent directory doesn't exist either, @'NoSuchDirectory' ('parent' dir)@
669+
-- is thrown.
670+
checkOwnership :: (MonadIO m, MonadCatch m) => Path Abs Dir -> m ()
671+
checkOwnership dir = do
672+
mdirAndOwnership <- firstJustM getDirAndOwnership [dir, parent dir]
673+
case mdirAndOwnership of
674+
Just (_, True) -> return ()
675+
Just (dir', False) -> throwM (UserDoesn'tOwnDirectory dir')
676+
Nothing ->
677+
(throwM . NoSuchDirectory) $ (toFilePathNoTrailingSep . parent) dir
678+
679+
-- | @'getDirAndOwnership' dir@ returns @'Just' (dir, 'True')@ when @dir@
680+
-- exists and the current user owns it in the sense of 'isOwnedByUser'.
681+
getDirAndOwnership
682+
:: (MonadIO m, MonadCatch m)
683+
=> Path Abs Dir
684+
-> m (Maybe (Path Abs Dir, Bool))
685+
getDirAndOwnership dir = forgivingAbsence $ do
686+
ownership <- isOwnedByUser dir
687+
return (dir, ownership)
688+
689+
-- | Check whether the current user (determined with 'getEffectiveUserId') is
690+
-- the owner for the given path.
691+
--
692+
-- Will always return 'True' on Windows.
693+
isOwnedByUser :: MonadIO m => Path Abs t -> m Bool
694+
isOwnedByUser path = liftIO $ do
695+
if osIsWindows
696+
then return True
697+
else do
698+
fileStatus <- getFileStatus (toFilePath path)
699+
user <- getEffectiveUserID
700+
return (user == fileOwner fileStatus)
701+
where
702+
#ifdef WINDOWS
703+
osIsWindows = True
704+
#else
705+
osIsWindows = False
706+
#endif
707+
708+
-- | 'True' if we are currently running inside a Docker container.
709+
getInContainer :: (MonadIO m) => m Bool
710+
getInContainer = liftIO (isJust <$> lookupEnv inContainerEnvVar)
623711

624712
-- | Determine the extra config file locations which exist.
625713
--
@@ -665,21 +753,16 @@ getProjectConfig Nothing = do
665753
liftM Just $ resolveFile' fp
666754
Nothing -> do
667755
currDir <- getCurrentDir
668-
search currDir
756+
findInParents getStackDotYaml currDir
669757
where
670-
search dir = do
758+
getStackDotYaml dir = do
671759
let fp = dir </> stackDotYaml
672760
fp' = toFilePath fp
673761
$logDebug $ "Checking for project config at: " <> T.pack fp'
674762
exists <- doesFileExist fp
675763
if exists
676764
then return $ Just fp
677-
else do
678-
let dir' = parent dir
679-
if dir == dir'
680-
-- fully traversed, give up
681-
then return Nothing
682-
else search dir'
765+
else return Nothing
683766

684767
-- | Find the project config file location, respecting environment variables
685768
-- and otherwise traversing parents. If no config is found, we supply a default

src/Stack/Constants.hs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module Stack.Constants
1818
,rawGithubUrl
1919
,stackDotYaml
2020
,stackRootEnvVar
21+
,inContainerEnvVar
2122
,userDocsDir
2223
,configCacheFile
2324
,configCabalMod
@@ -300,6 +301,10 @@ stackDotYaml = $(mkRelFile "stack.yaml")
300301
stackRootEnvVar :: String
301302
stackRootEnvVar = "STACK_ROOT"
302303

304+
-- | Environment variable used to indicate stack is running in container.
305+
inContainerEnvVar :: String
306+
inContainerEnvVar = stackProgNameUpper ++ "_IN_CONTAINER"
307+
303308
-- See https://downloads.haskell.org/~ghc/7.10.1/docs/html/libraries/ghc/src/Module.html#integerPackageKey
304309
wiredInPackages :: HashSet PackageName
305310
wiredInPackages =

src/Stack/Constants.hs-boot

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module Stack.Constants where
2+
3+
stackRootEnvVar :: String

src/Stack/Docker.hs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,14 @@ import Path.Extra (toFilePathNoTrailingSep)
5959
import Path.IO hiding (canonicalizePath)
6060
import qualified Paths_stack as Meta
6161
import Prelude -- Fix redundant import warnings
62+
import Stack.Config (getInContainer)
6263
import Stack.Constants
6364
import Stack.Docker.GlobalDB
6465
import Stack.Types
6566
import Stack.Types.Internal
6667
import Stack.Setup (ensureDockerStackExe)
6768
import System.Directory (canonicalizePath,getHomeDirectory)
68-
import System.Environment (getEnv,getEnvironment,getProgName,getArgs,getExecutablePath
69-
,lookupEnv)
69+
import System.Environment (getEnv,getEnvironment,getProgName,getArgs,getExecutablePath)
7070
import System.Exit (exitSuccess, exitWith)
7171
import qualified System.FilePath as FP
7272
import System.IO (stderr,stdin,stdout,hIsTerminalDevice)
@@ -238,10 +238,6 @@ preventInContainer inner =
238238
then throwM OnlyOnHostException
239239
else inner
240240

241-
-- | 'True' if we are currently running inside a Docker container.
242-
getInContainer :: (MonadIO m) => m Bool
243-
getInContainer = liftIO (isJust <$> lookupEnv inContainerEnvVar)
244-
245241
-- | Run a command in a new Docker container, then exit the process.
246242
runContainerAndExit :: M env m
247243
=> GetCmdArgs env m
@@ -878,10 +874,6 @@ fromMaybeProjectRoot = fromMaybe (throw CannotDetermineProjectRootException)
878874
oldSandboxIdEnvVar :: String
879875
oldSandboxIdEnvVar = "DOCKER_SANDBOX_ID"
880876

881-
-- | Environment variable used to indicate stack is running in container.
882-
inContainerEnvVar :: String
883-
inContainerEnvVar = stackProgNameUpper ++ "_IN_CONTAINER"
884-
885877
-- | Command-line argument for "docker"
886878
dockerCmdName :: String
887879
dockerCmdName = "docker"

src/Stack/Options.hs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ cleanOptsParser = CleanTargets <$> packages <|> CleanFull <$> doFullClean
288288
-- | Command-line arguments parser for configuration.
289289
configOptsParser :: Bool -> Parser ConfigMonoid
290290
configOptsParser hide0 =
291-
(\workDir dockerOpts nixOpts systemGHC installGHC arch os ghcVariant jobs includes libs skipGHCCheck skipMsys localBin modifyCodePage -> mempty
291+
(\workDir dockerOpts nixOpts systemGHC installGHC arch os ghcVariant jobs includes libs skipGHCCheck skipMsys localBin modifyCodePage allowDifferentUser -> mempty
292292
{ configMonoidWorkDir = workDir
293293
, configMonoidDockerOpts = dockerOpts
294294
, configMonoidNixOpts = nixOpts
@@ -304,6 +304,7 @@ configOptsParser hide0 =
304304
, configMonoidSkipMsys = skipMsys
305305
, configMonoidLocalBinPath = localBin
306306
, configMonoidModifyCodePage = modifyCodePage
307+
, configMonoidAllowDifferentUser = allowDifferentUser
307308
})
308309
<$> optional (strOption
309310
( long "work-dir"
@@ -371,6 +372,11 @@ configOptsParser hide0 =
371372
"modify-code-page"
372373
"setting the codepage to support UTF-8 (Windows only)"
373374
hide
375+
<*> maybeBoolFlags
376+
"allow-different-user"
377+
("permission for users other than the owner of the stack root " ++
378+
"directory to use a stack installation (POSIX only)")
379+
hide
374380
where hide = hideMods hide0
375381

376382
nixOptsParser :: Bool -> Parser NixOptsMonoid

0 commit comments

Comments
 (0)