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