@@ -1,6 +1,7 @@
1{-# LANGUAGE FlexibleContexts #-}
1{-# LANGUAGE FlexibleContexts #-}
2{-# LANGUAGE LambdaCase #-}
2{-# LANGUAGE LambdaCase #-}
3{-# LANGUAGE OverloadedStrings #-}
3{-# LANGUAGE OverloadedStrings #-}
4+{-# LANGUAGE TemplateHaskell #-}
4module Obelisk.Command.Project
5module Obelisk.Command.Project
5 ( InitSource (..)
6 ( InitSource (..)
6 , initProject
7 , initProject
@@ -17,6 +18,7 @@ module Obelisk.Command.Project
17 , toImplDir
18 , toImplDir
21+import Control.Concurrent.MVar (MVar, newMVar, withMVarMasked)
20import Control.Monad
22import Control.Monad
21import Control.Monad.IO.Class (liftIO)
23import Control.Monad.IO.Class (liftIO)
22import Control.Monad.State
24import Control.Monad.State
@@ -25,12 +27,15 @@ import Data.Bits
25import Data.Function (on)
27import Data.Function (on)
26import qualified Data.Text as T
28import qualified Data.Text as T
27import System.Directory
29import System.Directory
30+import System.Environment (lookupEnv)
28import System.FilePath
31import System.FilePath
29import System.IO.Temp
32import System.IO.Temp
33+import System.IO.Unsafe (unsafePerformIO)
30−import System.Posix (FileStatus, UserID, deviceID, fileID, fileMode, fileOwner, getFileStatus, getRealUserID)
34+import System.Posix (FileStatus, FileMode, CMode (..), UserID, deviceID, fileID, fileMode, fileOwner, getFileStatus, getRealUserID)
31import System.Posix.Files
35import System.Posix.Files
32import System.Process (CreateProcess, cwd, proc, waitForProcess, delegate_ctlc)
36import System.Process (CreateProcess, cwd, proc, waitForProcess, delegate_ctlc)
33import Text.ShellEscape (Bash, bytes)
37import Text.ShellEscape (Bash, bytes)
38+import System.Which (staticWhich)
35import GitHub.Data.GitData (Branch)
40import GitHub.Data.GitData (Branch)
36import GitHub.Data.Name (Name)
41import GitHub.Data.Name (Name)
@@ -38,6 +43,7 @@ import GitHub.Data.Name (Name)
38import Obelisk.App (MonadObelisk)
43import Obelisk.App (MonadObelisk)
39import Obelisk.CliApp
44import Obelisk.CliApp
40import Obelisk.Command.Thunk
45import Obelisk.Command.Thunk
41--TODO: Make this module resilient to random exceptions
47--TODO: Make this module resilient to random exceptions
43--TODO: Don't hardcode this
49--TODO: Don't hardcode this
@@ -54,10 +60,10 @@ obeliskSourceWithBranch branch = ThunkSource_GitHub $ GitHubSource
56data InitSource
62data InitSource
57− = InitSource_Default
63+ = InitSource_Default
58− | InitSource_Branch (Name Branch)
64+ | InitSource_Branch (Name Branch)
59− | InitSource_Symlink FilePath
65+ | InitSource_Symlink FilePath
60− deriving Show
66+ deriving Show
62-- | Path to obelisk directory in given path
68-- | Path to obelisk directory in given path
63toObeliskDir :: FilePath -> FilePath
69toObeliskDir :: FilePath -> FilePath
@@ -118,15 +124,16 @@ initProject source force = withSystemTempDirectory "ob-init" $ \tmpDir -> do
118findProjectObeliskCommand :: MonadObelisk m => FilePath -> m (Maybe FilePath)
124findProjectObeliskCommand :: MonadObelisk m => FilePath -> m (Maybe FilePath)
119findProjectObeliskCommand target = do
125findProjectObeliskCommand target = do
120 myUid <- liftIO getRealUserID
126 myUid <- liftIO getRealUserID
127+ processUmask <- liftIO getUmask
121 targetStat <- liftIO $ getFileStatus target
128 targetStat <- liftIO $ getFileStatus target
122− (result, insecurePaths) <- flip runStateT [] $ walkToProjectRoot target targetStat myUid >>= \case
129+ (result, insecurePaths) <- flip runStateT [] $ walkToProjectRoot target targetStat processUmask myUid >>= \case
123 Nothing -> pure Nothing
130 Nothing -> pure Nothing
124 Just projectRoot -> liftIO (doesDirectoryExist $ toImplDir projectRoot) >>= \case
131 Just projectRoot -> liftIO (doesDirectoryExist $ toImplDir projectRoot) >>= \case
125 False -> do
132 False -> do
126 putLog Warning $ "Found obelisk directory in " <> T.pack projectRoot <> " but the implementation (impl) file is missing"
133 putLog Warning $ "Found obelisk directory in " <> T.pack projectRoot <> " but the implementation (impl) file is missing"
127 pure Nothing
134 pure Nothing
128 True -> do
135 True -> do
129− walkToImplDir projectRoot myUid -- For security check
136+ walkToImplDir projectRoot myUid processUmask -- For security check
130 return $ Just projectRoot
137 return $ Just projectRoot
131 case (result, insecurePaths) of
138 case (result, insecurePaths) of
132 (Just projDir, []) -> do
139 (Just projDir, []) -> do
@@ -137,16 +144,36 @@ findProjectObeliskCommand target = do
137 putLog Error $ T.unlines
144 putLog Error $ T.unlines
138 [ "Error: Found a project at " <> T.pack (normalise projDir) <> ", but had to traverse one or more insecure directories to get there:"
145 [ "Error: Found a project at " <> T.pack (normalise projDir) <> ", but had to traverse one or more insecure directories to get there:"
139 , T.unlines $ fmap (T.pack . normalise) insecurePaths
146 , T.unlines $ fmap (T.pack . normalise) insecurePaths
140− , "Please ensure that all of these directories are owned by you and are not writable by anyone else."
147+ , "Please ensure that all of these directories are owned by you, not world-writable, and no more group-writable than permitted by your umask."
142 return Nothing
149 return Nothing
151+-- | Get the umask for the Obelisk process.
154+-- http://man7.org/linux/man-pages/man2/umask.2.html#NOTES we have to set the
155+-- umask to read it. We are using 'withMVarMasked' to guarantee that setting and
156+-- reading isn't interrupted by any exception or interleaved with another thread.
157+getUmask :: IO FileMode
158+getUmask = withMVarMasked globalUmaskLock $ \() -> do
159+ initialMask <- setFileCreationMask safeUmask
160+ void (setFileCreationMask initialMask)
163+ safeUmask :: FileMode
164+ safeUmask = CMode 0o22
166+{-# NOINLINE globalUmaskLock #-}
167+globalUmaskLock :: MVar ()
168+globalUmaskLock = unsafePerformIO (newMVar ())
144-- | Get the FilePath to the containing project directory, if there is one
170-- | Get the FilePath to the containing project directory, if there is one
145findProjectRoot :: MonadObelisk m => FilePath -> m (Maybe FilePath)
171findProjectRoot :: MonadObelisk m => FilePath -> m (Maybe FilePath)
146findProjectRoot target = do
172findProjectRoot target = do
147 myUid <- liftIO getRealUserID
173 myUid <- liftIO getRealUserID
148 targetStat <- liftIO $ getFileStatus target
174 targetStat <- liftIO $ getFileStatus target
175+ umask <- liftIO $ getUmask
149− (result, _) <- liftIO $ runStateT (walkToProjectRoot target targetStat myUid) []
176+ (result, _) <- liftIO $ runStateT (walkToProjectRoot target targetStat umask myUid) []
150 return result
177 return result
152withProjectRoot :: MonadObelisk m => FilePath -> (FilePath -> m a) -> m a
179withProjectRoot :: MonadObelisk m => FilePath -> (FilePath -> m a) -> m a
@@ -159,15 +186,15 @@ withProjectRoot target f = findProjectRoot target >>= \case
159-- traversed in the process. Return the project root directory, if found.
186-- traversed in the process. Return the project root directory, if found.
160walkToProjectRoot
187walkToProjectRoot
161 :: (MonadState [FilePath] m, MonadIO m)
188 :: (MonadState [FilePath] m, MonadIO m)
162− => FilePath -> FileStatus -> UserID -> m (Maybe FilePath)
189+ => FilePath -> FileStatus -> FileMode -> UserID -> m (Maybe FilePath)
163−walkToProjectRoot this thisStat myUid = liftIO (doesDirectoryExist this) >>= \case
190+walkToProjectRoot this thisStat desiredUmask myUid = liftIO (doesDirectoryExist this) >>= \case
164 -- It's not a directory, so it can't be a project
191 -- It's not a directory, so it can't be a project
165 False -> do
192 False -> do
166 let dir = takeDirectory this
193 let dir = takeDirectory this
167 dirStat <- liftIO $ getFileStatus dir
194 dirStat <- liftIO $ getFileStatus dir
168− walkToProjectRoot dir dirStat myUid
195+ walkToProjectRoot dir dirStat desiredUmask myUid
169 True -> do
196 True -> do
170− when (not $ isWritableOnlyBy thisStat myUid) $ modify (this:)
197+ when (not $ isWellOwnedAndWellPermissioned thisStat myUid desiredUmask) $ modify (this:)
171 liftIO (doesDirectoryExist $ toObeliskDir this) >>= \case
198 liftIO (doesDirectoryExist $ toObeliskDir this) >>= \case
172 True -> return $ Just this
199 True -> return $ Just this
173 False -> do
200 False -> do
@@ -177,25 +204,38 @@ walkToProjectRoot this thisStat myUid = liftIO (doesDirectoryExist this) >>= \ca
177 isSameFileAs = (==) `on` fileIdentity
204 isSameFileAs = (==) `on` fileIdentity
178 if thisStat `isSameFileAs` nextStat
205 if thisStat `isSameFileAs` nextStat
179 then return Nothing -- Found a cycle; probably hit root directory
206 then return Nothing -- Found a cycle; probably hit root directory
180− else walkToProjectRoot next nextStat myUid
207+ else walkToProjectRoot next nextStat desiredUmask myUid
182-- | Walk from the given project root directory to its Obelisk implementation
209-- | Walk from the given project root directory to its Obelisk implementation
183-- directory, accumulating potentially insecure directories that were traversed
210-- directory, accumulating potentially insecure directories that were traversed
184-- in the process.
211-- in the process.
185−walkToImplDir :: (MonadState [FilePath] m, MonadIO m) => FilePath -> UserID -> m ()
212+walkToImplDir :: (MonadState [FilePath] m, MonadIO m) => FilePath -> UserID -> FileMode -> m ()
186−walkToImplDir projectRoot myUid = do
213+walkToImplDir projectRoot myUid umask = do
187 let obDir = toObeliskDir projectRoot
214 let obDir = toObeliskDir projectRoot
188 obDirStat <- liftIO $ getFileStatus obDir
215 obDirStat <- liftIO $ getFileStatus obDir
189− when (not $ isWritableOnlyBy obDirStat myUid) $ modify (obDir:)
216+ when (not $ isWellOwnedAndWellPermissioned obDirStat myUid umask) $ modify (obDir:)
190 let implThunk = obDir </> "impl"
217 let implThunk = obDir </> "impl"
191 implThunkStat <- liftIO $ getFileStatus implThunk
218 implThunkStat <- liftIO $ getFileStatus implThunk
192− when (not $ isWritableOnlyBy implThunkStat myUid) $ modify (implThunk:)
219+ when (not $ isWellOwnedAndWellPermissioned implThunkStat myUid umask) $ modify (implThunk:)
194−--TODO: Is there a better way to ask if anyone else can write things?
195−--E.g. what about ACLs?
196−-- | Check to see if directory is only writable by a user whose User ID matches the second argument provided
221+-- | Check to see if directory is writable by a user whose User ID matches the
222+-- second argument provided, and if the fact that other people can write to that
223+-- directory is in accordance with the umask of the system, passed as the third
197−isWritableOnlyBy :: FileStatus -> UserID -> Bool
225+isWellOwnedAndWellPermissioned :: FileStatus -> UserID -> FileMode -> Bool
226+isWellOwnedAndWellPermissioned s uid umask = isOwnedBy s uid && filePermissionIsSafe s umask
228+isOwnedBy :: FileStatus -> UserID -> Bool
198−isWritableOnlyBy s uid = fileOwner s == uid && fileMode s .&. 0o22 == 0
229+isOwnedBy s uid = fileOwner s == uid
231+-- | Check to see if a directory respect the umask, but check explicitly that
232+-- it's not world writable in any case.
233+filePermissionIsSafe :: FileStatus -> FileMode -> Bool
234+filePermissionIsSafe s umask = not fileWorldWritable && fileGroupWritable <= umaskGroupWritable
236+ fileWorldWritable = fileMode s .&. 0o002 == 0o002
237+ fileGroupWritable = fileMode s .&. 0o020 == 0o020
238+ umaskGroupWritable = umask .&. 0o020 == 0
200inProjectProc :: MonadObelisk m => String -> [Bash] -> m ()
240inProjectProc :: MonadObelisk m => String -> [Bash] -> m ()
201inProjectProc shellName command = withProjectRoot "." $ \root ->
241inProjectProc shellName command = withProjectRoot "." $ \root ->
@@ -218,16 +258,23 @@ projectProc root isPure shellName command =
219projectShell :: MonadObelisk m => FilePath -> Bool -> String -> Maybe String -> m ()
259projectShell :: MonadObelisk m => FilePath -> Bool -> String -> Maybe String -> m ()
220projectShell root isPure shellName command = do
260projectShell root isPure shellName command = do
261+ let nixPath = $(staticWhich "nix")
262+ nixpkgsPath <- fmap T.strip $ readProcessAndLogStderr Debug $ proc nixPath ["eval", "(import .obelisk/impl {}).nixpkgs.path"]
263+ nixRemote <- liftIO $ lookupEnv "NIX_REMOTE"
221 (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setCtlc $ setCwd (Just root) $ proc "nix-shell" $
264 (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setCtlc $ setCwd (Just root) $ proc "nix-shell" $
222 [ "default.nix"] <>
265 [ "default.nix"] <>
223− -- Keep $NIX_PATH in the env for --pure shells so '<nixpkgs>' works in sub-commands
266+ ["--pure" | isPure] <>
224− -- TODO: Don't use <nixpkgs> for anything!
225− (if isPure then [ "--pure", "--keep", "NIX_PATH" ] else []) <>
227 , "shells." <> shellName
268 , "shells." <> shellName
228 ] <> case command of
269 ] <> case command of
229 Nothing -> []
270 Nothing -> []
230− Just c -> ["--run", c]
271+ Just c ->
272+ -- TODO: Escape nixpkgsPath and nixRemote
274+ , "export NIX_PATH=nixpkgs=" <> T.unpack nixpkgsPath <> "; " <>
275+ maybe "" (\v -> "export NIX_REMOTE=" <> v <> "; ") nixRemote <>
231 void $ liftIO $ waitForProcess ph
278 void $ liftIO $ waitForProcess ph
233setCtlc :: CreateProcess -> CreateProcess
280setCtlc :: CreateProcess -> CreateProcess