packages feed

cachix-0.5.0: src/Cachix/Client/InstallationMode.hs

{-# LANGUAGE QuasiQuotes #-}

module Cachix.Client.InstallationMode
  ( InstallationMode (..),
    NixEnv (..),
    getInstallationMode,
    addBinaryCache,
    isTrustedUser,
    getUser,
    fromString,
    toString,
    UseOptions (..),
  )
where

import Cachix.Client.Config (Config)
import qualified Cachix.Client.Config as Config
import Cachix.Client.Exception (CachixException (..))
import qualified Cachix.Client.NetRc as NetRc
import qualified Cachix.Client.NixConf as NixConf
import qualified Cachix.Types.BinaryCache as BinaryCache
import qualified Data.Maybe
import Data.String.Here
import qualified Data.Text as T
import Protolude
import System.Directory (Permissions, createDirectoryIfMissing, getPermissions, writable)
import System.Environment (lookupEnv)
import System.FilePath ((</>), replaceFileName)
import System.Process (readProcessWithExitCode)
import Prelude (String)

data NixEnv
  = NixEnv
      { isTrusted :: Bool,
        isRoot :: Bool,
        isNixOS :: Bool
      }

-- NOTE: update the list of options for --mode argument in OptionsParser.hs
data InstallationMode
  = Install NixConf.NixConfLoc
  | WriteNixOS
  | UntrustedRequiresSudo
  | UntrustedNixOS
  deriving (Show, Eq)

data UseOptions
  = UseOptions
      { useMode :: Maybe InstallationMode,
        useNixOSFolder :: FilePath,
        useOutputDirectory :: Maybe FilePath
      }
  deriving (Show)

fromString :: String -> Maybe InstallationMode
fromString "root-nixconf" = Just $ Install NixConf.Global
fromString "user-nixconf" = Just $ Install NixConf.Local
fromString "nixos" = Just WriteNixOS
fromString "untrusted-requires-sudo" = Just UntrustedRequiresSudo
fromString _ = Nothing

toString :: InstallationMode -> String
toString (Install NixConf.Global) = "root-nixconf"
toString (Install NixConf.Local) = "user-nixconf"
toString (Install (NixConf.Custom _)) = "custom-nixconf"
toString WriteNixOS = "nixos"
toString UntrustedRequiresSudo = "untrusted-requires-sudo"
toString UntrustedNixOS = "untrusted-nixos"

getInstallationMode :: NixEnv -> UseOptions -> InstallationMode
getInstallationMode nixenv useOptions
  | (isRoot nixenv || isTrusted nixenv) && isJust (useOutputDirectory useOptions) = Install (NixConf.Custom $ Data.Maybe.fromJust $ useOutputDirectory useOptions)
  | isJust (useMode useOptions) = Data.Maybe.fromJust $ useMode useOptions
  | isNixOS nixenv && isRoot nixenv = WriteNixOS
  | not (isNixOS nixenv) && isRoot nixenv = Install NixConf.Global
  | isTrusted nixenv = Install NixConf.Local
  | isNixOS nixenv = UntrustedNixOS
  | otherwise = UntrustedRequiresSudo

-- | Add a Binary cache to nix.conf, print nixos config or fail
addBinaryCache :: Maybe Config -> BinaryCache.BinaryCache -> UseOptions -> InstallationMode -> IO ()
addBinaryCache _ _ _ UntrustedNixOS = do
  user <- getUser
  throwIO $
    MustBeRoot
      [i|This user doesn't have permissions to configure binary caches.

You can either:

a) Run the same command as root to write NixOS configuration.

b) Add the following to your configuration.nix to add your user as trusted 
   and then try again:

  trustedUsers = [ "root" "${user}" ];

    |]
addBinaryCache _ _ _ UntrustedRequiresSudo = do
  user <- getUser
  throwIO $
    MustBeRoot
      [i|This user doesn't have permissions to configure binary caches.

You can either:

a) Run the same command as root to configure them globally.

b) Run the following command to add your user as trusted 
   and then try again:

  echo "trusted-users = root ${user}" | sudo tee -a /etc/nix/nix.conf && sudo pkill nix-daemon
    |]
addBinaryCache maybeConfig bc useOptions WriteNixOS =
  nixosBinaryCache maybeConfig bc useOptions
addBinaryCache maybeConfig bc _ (Install ncl) = do
  -- TODO: might need locking one day
  gnc <- NixConf.read NixConf.Global
  (input, output) <-
    case ncl of
      NixConf.Global -> return ([gnc], gnc)
      NixConf.Local -> do
        lnc <- NixConf.read NixConf.Local
        return ([gnc, lnc], lnc)
      NixConf.Custom _ -> do
        lnc <- NixConf.read ncl
        return ([lnc], lnc)
  let nixconf = fromMaybe (NixConf.NixConf []) output
  netrcLocMaybe <- forM (guard $ not (BinaryCache.isPublic bc)) $ const $ addPrivateBinaryCacheNetRC maybeConfig bc ncl
  let addNetRCLine :: NixConf.NixConf -> NixConf.NixConf
      addNetRCLine = fromMaybe identity $ do
        netrcLoc <- netrcLocMaybe :: Maybe FilePath
        -- We only add the netrc line for local user configs for now.
        -- On NixOS we assume it will be picked up from the default location.
        guard (ncl == NixConf.Local)
        pure (NixConf.setNetRC $ toS netrcLoc)
  NixConf.write ncl $ addNetRCLine $ NixConf.add bc (catMaybes input) nixconf
  filename <- NixConf.getFilename ncl
  putStrLn $ "Configured " <> BinaryCache.uri bc <> " binary cache in " <> toS filename

nixosBinaryCache :: Maybe Config -> BinaryCache.BinaryCache -> UseOptions -> IO ()
nixosBinaryCache maybeConfig bc UseOptions {useNixOSFolder = baseDirectory} = do
  _ <- try $ createDirectoryIfMissing True $ toS toplevel :: IO (Either SomeException ())
  eitherPermissions <- try $ getPermissions (toS toplevel) :: IO (Either SomeException Permissions)
  case eitherPermissions of
    Left _ -> throwIO $ NixOSInstructions $ noEtcPermissionInstructions $ toS baseDirectory
    Right permissions
      | writable permissions -> installFiles
      | otherwise -> throwIO $ NixOSInstructions $ noEtcPermissionInstructions $ toS baseDirectory
  where
    installFiles = do
      writeFile (toS glueModuleFile) glueModule
      writeFile (toS cacheModuleFile) cacheModule
      unless (BinaryCache.isPublic bc) $ void $ addPrivateBinaryCacheNetRC maybeConfig bc NixConf.Global
      putText instructions
    configurationNix :: Text
    configurationNix = toS $ toS baseDirectory </> "configuration.nix"
    namespace :: Text
    namespace = "cachix"
    toplevel :: Text
    toplevel = toS $ toS baseDirectory </> toS namespace
    glueModuleFile :: Text
    glueModuleFile = toplevel <> ".nix"
    cacheModuleFile :: Text
    cacheModuleFile = toplevel <> "/" <> toS (BinaryCache.name bc) <> ".nix"
    noEtcPermissionInstructions :: Text -> Text
    noEtcPermissionInstructions dir =
      [iTrim|
Could not install NixOS configuration to ${dir} due to lack of write permissions.

Pass `--nixos-folder /etc/mynixos/` as an alternative location with write permissions.
      |]
    instructions :: Text
    instructions =
      [iTrim|
Cachix configuration written to ${glueModuleFile}.
Binary cache ${BinaryCache.name bc} configuration written to ${cacheModuleFile}.

To start using cachix add the following to your ${configurationNix}:

    imports = [ ./cachix.nix ];

Then run:

    $ sudo nixos-rebuild switch
    |]
    glueModule :: Text
    glueModule =
      [i|
# WARN: this file will get overwritten by $ cachix use <name>
{ pkgs, lib, ... }:

let
  folder = ./cachix;
  toImport = name: value: folder + ("/" + name);
  filterCaches = key: value: value == "regular" && lib.hasSuffix ".nix" key;
  imports = lib.mapAttrsToList toImport (lib.filterAttrs filterCaches (builtins.readDir folder));
in {
  inherit imports;
  nix.binaryCaches = ["https://cache.nixos.org/"];
}
    |]
    cacheModule :: Text
    cacheModule =
      [i|
{
  nix = {
    binaryCaches = [
      "${BinaryCache.uri bc}"
    ];
    binaryCachePublicKeys = [
      ${T.intercalate " " (map (\s -> "\"" <> s <> "\"") (BinaryCache.publicSigningKeys bc))}
    ];
  };
}
    |]

-- TODO: allow overriding netrc location
addPrivateBinaryCacheNetRC :: Maybe Config -> BinaryCache.BinaryCache -> NixConf.NixConfLoc -> IO FilePath
addPrivateBinaryCacheNetRC maybeConfig bc nixconf = do
  filename <- (`replaceFileName` "netrc") <$> NixConf.getFilename nixconf
  case maybeConfig of
    Nothing -> throwIO $ NoConfig Config.noAuthTokenError
    Just config -> do
      let netrcfile = fromMaybe filename Nothing -- TODO: get netrc from nixconf
      NetRc.add config [bc] netrcfile
      putErrText $ "Configured private read access credentials in " <> toS filename
      pure filename

isTrustedUser :: [Text] -> IO Bool
isTrustedUser users = do
  user <- getUser
  -- to detect single user installations
  permissions <- getPermissions "/nix/store"
  isInAGroup <- userInAnyGroup user
  return $ writable permissions || user `elem` users || isInAGroup
  where
    groups :: [Text]
    groups = map T.tail $ filter (\u -> T.head u == '@') users
    userInAnyGroup :: Text -> IO Bool
    userInAnyGroup user = do
      isIn <- for groups $ checkUserInGroup user
      return $ any identity isIn
    checkUserInGroup :: Text -> Text -> IO Bool
    checkUserInGroup user groupName = do
      (_exitcode, out, _err) <- readProcessWithExitCode "id" ["-Gn", toS user] mempty
      return $ groupName `T.isInfixOf` toS out

getUser :: IO Text
getUser = do
  maybeUser <- lookupEnv "USER"
  case maybeUser of
    Nothing -> throwIO $ UserEnvNotSet "$USER must be set"
    Just user -> return $ toS user