module Propellor.Property.Sudo where

import Data.List

import Propellor.Base
import Propellor.Property.File
import qualified Propellor.Property.Apt as Apt
import Propellor.Property.User

-- | Allows a user to run any command with sudo.
-- If the user has a password, sudo is configured to require it.
-- If not, NOPASSWORD is enabled for the user.
--
-- Writes to the file /etc/sudoers.d/000users rather than the main sudoers
-- file. This file should come before other include files that may eg,
-- allow running more specific commands without a password, since sudo
-- uses the last matching configuration line.
--
-- If the main sudoers file contains a conflicting line for
-- the user for ALL commands, the line will be removed.
--
-- Also ensures that the main sudoers file includes /etc/sudoers.d/
enabledFor :: User -> RevertableProperty DebianLike DebianLike
enabledFor :: User -> RevertableProperty DebianLike DebianLike
enabledFor user :: User
user@(User UserName
u) = Property UnixLike
setup Property UnixLike
-> Property DebianLike
-> CombinedType (Property UnixLike) (Property DebianLike)
forall x y. Combines x y => x -> y -> CombinedType x y
`requires` [UserName] -> Property DebianLike
Apt.installed [UserName
"sudo"] Property DebianLike
-> Property DebianLike -> RevertableProperty DebianLike DebianLike
forall setupmetatypes undometatypes.
Property setupmetatypes
-> Property undometatypes
-> RevertableProperty setupmetatypes undometatypes
<!> Property DebianLike
cleanup
  where
	setup :: Property UnixLike
	setup :: Property UnixLike
setup = UserName
-> (OuterMetaTypesWitness
      '[ 'Targeting 'OSDebian, 'Targeting 'OSBuntish,
         'Targeting 'OSArchLinux, 'Targeting 'OSFreeBSD]
    -> Propellor Result)
-> Property UnixLike
forall k (metatypes :: k).
SingI metatypes =>
UserName
-> (OuterMetaTypesWitness metatypes -> Propellor Result)
-> Property (MetaTypes metatypes)
property' UserName
desc ((OuterMetaTypesWitness
    '[ 'Targeting 'OSDebian, 'Targeting 'OSBuntish,
       'Targeting 'OSArchLinux, 'Targeting 'OSFreeBSD]
  -> Propellor Result)
 -> Property UnixLike)
-> (OuterMetaTypesWitness
      '[ 'Targeting 'OSDebian, 'Targeting 'OSBuntish,
         'Targeting 'OSArchLinux, 'Targeting 'OSFreeBSD]
    -> Propellor Result)
-> Property UnixLike
forall a b. (a -> b) -> a -> b
$ \OuterMetaTypesWitness
  '[ 'Targeting 'OSDebian, 'Targeting 'OSBuntish,
     'Targeting 'OSArchLinux, 'Targeting 'OSFreeBSD]
w -> do
		Bool
locked <- IO Bool -> Propellor Bool
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO Bool -> Propellor Bool) -> IO Bool -> Propellor Bool
forall a b. (a -> b) -> a -> b
$ User -> IO Bool
isLockedPassword User
user
		OuterMetaTypesWitness
  '[ 'Targeting 'OSDebian, 'Targeting 'OSBuntish,
     'Targeting 'OSArchLinux, 'Targeting 'OSFreeBSD]
-> Property UnixLike -> Propellor Result
forall (inner :: [MetaType]) (outer :: [MetaType]).
EnsurePropertyAllowed inner outer =>
OuterMetaTypesWitness outer
-> Property (MetaTypes inner) -> Propellor Result
ensureProperty OuterMetaTypesWitness
  '[ 'Targeting 'OSDebian, 'Targeting 'OSBuntish,
     'Targeting 'OSArchLinux, 'Targeting 'OSFreeBSD]
w (Property UnixLike -> Propellor Result)
-> Property UnixLike -> Propellor Result
forall a b. (a -> b) -> a -> b
$ UserName -> Props UnixLike -> Property UnixLike
forall k (metatypes :: k).
SingI metatypes =>
UserName
-> Props (MetaTypes metatypes) -> Property (MetaTypes metatypes)
combineProperties UserName
desc (Props UnixLike -> Property UnixLike)
-> Props UnixLike -> Property UnixLike
forall a b. (a -> b) -> a -> b
$ Props UnixLike
props
			Props UnixLike
-> Property UnixLike
-> Props
     (MetaTypes
        (Combine
           '[ 'Targeting 'OSDebian, 'Targeting 'OSBuntish,
              'Targeting 'OSArchLinux, 'Targeting 'OSFreeBSD]
           '[ 'Targeting 'OSDebian, 'Targeting 'OSBuntish,
              'Targeting 'OSArchLinux, 'Targeting 'OSFreeBSD]))
forall a p (y :: [a]) (x :: [a]).
(IsProp p, MetaTypes y ~ GetMetaTypes p,
 CheckCombinableNote x y (NoteFor ('Text "&"))) =>
Props (MetaTypes x) -> p -> Props (MetaTypes (Combine x y))
& Property UnixLike
includessudoersd
			Props UnixLike
-> Property UnixLike
-> Props
     (MetaTypes
        (Combine
           '[ 'Targeting 'OSDebian, 'Targeting 'OSBuntish,
              'Targeting 'OSArchLinux, 'Targeting 'OSFreeBSD]
           '[ 'Targeting 'OSDebian, 'Targeting 'OSBuntish,
              'Targeting 'OSArchLinux, 'Targeting 'OSFreeBSD]))
forall a p (y :: [a]) (x :: [a]).
(IsProp p, MetaTypes y ~ GetMetaTypes p,
 CheckCombinableNote x y (NoteFor ('Text "&"))) =>
Props (MetaTypes x) -> p -> Props (MetaTypes (Combine x y))
& UserName
-> ([UserName] -> [UserName]) -> UserName -> Property UnixLike
forall c.
(FileContent c, Eq c) =>
UserName -> (c -> c) -> UserName -> Property UnixLike
fileProperty UserName
desc
				(Bool -> [UserName] -> [UserName]
modify Bool
locked ([UserName] -> [UserName])
-> ([UserName] -> [UserName]) -> [UserName] -> [UserName]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (UserName -> Bool) -> [UserName] -> [UserName]
forall a. (a -> Bool) -> [a] -> [a]
filter (Bool -> UserName -> Bool
wanted Bool
locked))
				UserName
dfile
			Props UnixLike
-> Property UnixLike
-> Props
     (MetaTypes
        (Combine
           '[ 'Targeting 'OSDebian, 'Targeting 'OSBuntish,
              'Targeting 'OSArchLinux, 'Targeting 'OSFreeBSD]
           '[ 'Targeting 'OSDebian, 'Targeting 'OSBuntish,
              'Targeting 'OSArchLinux, 'Targeting 'OSFreeBSD]))
forall a p (y :: [a]) (x :: [a]).
(IsProp p, MetaTypes y ~ GetMetaTypes p,
 CheckCombinableNote x y (NoteFor ('Text "&"))) =>
Props (MetaTypes x) -> p -> Props (MetaTypes (Combine x y))
& UserName -> Property UnixLike
removeconflicting UserName
sudoers
	  where
		desc :: UserName
desc = UserName
u UserName -> UserName -> UserName
forall a. [a] -> [a] -> [a]
++ UserName
" is sudoer"
	
	cleanup :: Property DebianLike
	cleanup :: Property DebianLike
cleanup = Property UnixLike -> Property DebianLike
forall (p :: * -> *) (untightened :: [MetaType])
       (tightened :: [MetaType]).
(TightenTargets p, TightenTargetsAllowed untightened tightened,
 SingI tightened) =>
p (MetaTypes untightened) -> p (MetaTypes tightened)
tightenTargets (Property UnixLike -> Property DebianLike)
-> Property UnixLike -> Property DebianLike
forall a b. (a -> b) -> a -> b
$ UserName -> Props UnixLike -> Property UnixLike
forall k (metatypes :: k).
SingI metatypes =>
UserName
-> Props (MetaTypes metatypes) -> Property (MetaTypes metatypes)
combineProperties UserName
desc (Props UnixLike -> Property UnixLike)
-> Props UnixLike -> Property UnixLike
forall a b. (a -> b) -> a -> b
$ Props UnixLike
props
		Props UnixLike
-> Property UnixLike
-> Props
     (MetaTypes
        (Combine
           '[ 'Targeting 'OSDebian, 'Targeting 'OSBuntish,
              'Targeting 'OSArchLinux, 'Targeting 'OSFreeBSD]
           '[ 'Targeting 'OSDebian, 'Targeting 'OSBuntish,
              'Targeting 'OSArchLinux, 'Targeting 'OSFreeBSD]))
forall a p (y :: [a]) (x :: [a]).
(IsProp p, MetaTypes y ~ GetMetaTypes p,
 CheckCombinableNote x y (NoteFor ('Text "&"))) =>
Props (MetaTypes x) -> p -> Props (MetaTypes (Combine x y))
& UserName -> Property UnixLike
removeconflicting UserName
sudoers
		Props UnixLike
-> Property UnixLike
-> Props
     (MetaTypes
        (Combine
           '[ 'Targeting 'OSDebian, 'Targeting 'OSBuntish,
              'Targeting 'OSArchLinux, 'Targeting 'OSFreeBSD]
           '[ 'Targeting 'OSDebian, 'Targeting 'OSBuntish,
              'Targeting 'OSArchLinux, 'Targeting 'OSFreeBSD]))
forall a p (y :: [a]) (x :: [a]).
(IsProp p, MetaTypes y ~ GetMetaTypes p,
 CheckCombinableNote x y (NoteFor ('Text "&"))) =>
Props (MetaTypes x) -> p -> Props (MetaTypes (Combine x y))
& UserName -> Property UnixLike
removeconflicting UserName
dfile
	  where
		desc :: UserName
desc = UserName
u UserName -> UserName -> UserName
forall a. [a] -> [a] -> [a]
++ UserName
" is not sudoer"
	
	removeconflicting :: UserName -> Property UnixLike
removeconflicting = UserName
-> ([UserName] -> [UserName]) -> UserName -> Property UnixLike
forall c.
(FileContent c, Eq c) =>
UserName -> (c -> c) -> UserName -> Property UnixLike
fileProperty UserName
"remove conflicting" ((UserName -> Bool) -> [UserName] -> [UserName]
forall a. (a -> Bool) -> [a] -> [a]
filter UserName -> Bool
notuserline)
	
	-- Not reverted because this line is included by default.
	includessudoersd :: Property UnixLike
includessudoersd = UserName
-> ([UserName] -> [UserName]) -> UserName -> Property UnixLike
forall c.
(FileContent c, Eq c) =>
UserName -> (c -> c) -> UserName -> Property UnixLike
fileProperty (UserName
sudoers UserName -> UserName -> UserName
forall a. [a] -> [a] -> [a]
++ UserName
" includes " UserName -> UserName -> UserName
forall a. [a] -> [a] -> [a]
++ UserName
sudoersd) [UserName] -> [UserName]
addl UserName
sudoers
	  where
		addl :: [UserName] -> [UserName]
addl [UserName]
content = [UserName]
content [UserName] -> [UserName] -> [UserName]
forall a. [a] -> [a] -> [a]
++ 
			if UserName
l UserName -> [UserName] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`notElem` [UserName]
content Bool -> Bool -> Bool
&& UserName
oldl UserName -> [UserName] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`notElem` [UserName]
content
				then [UserName
l]
				else []
		l :: UserName
l = UserName
"@includedir /etc/sudoers.d"
		oldl :: UserName
oldl = UserName
"#includedir /etc/sudoers.d"

	sudoers :: UserName
sudoers = UserName
"/etc/sudoers"
	sudoersd :: UserName
sudoersd = UserName
"/etc/sudoers.d"
	dfile :: UserName
dfile = UserName
"/etc/sudoers.d/000users"
	sudobaseline :: UserName
sudobaseline = UserName
u UserName -> UserName -> UserName
forall a. [a] -> [a] -> [a]
++ UserName
" ALL=(ALL:ALL)"
	notuserline :: UserName -> Bool
notuserline UserName
l = Bool -> Bool
not (UserName
sudobaseline UserName -> UserName -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`isPrefixOf` UserName
l)
	sudoline :: Bool -> UserName
sudoline Bool
True = UserName
sudobaseline UserName -> UserName -> UserName
forall a. [a] -> [a] -> [a]
++ UserName
" NOPASSWD:ALL"
	sudoline Bool
False = UserName
sudobaseline UserName -> UserName -> UserName
forall a. [a] -> [a] -> [a]
++ UserName
" ALL"
	wanted :: Bool -> UserName -> Bool
wanted Bool
locked UserName
l
		| UserName -> Bool
notuserline UserName
l = Bool
True
		| UserName
"NOPASSWD" UserName -> UserName -> Bool
forall a. Eq a => [a] -> [a] -> Bool
`isInfixOf` UserName
l = Bool
locked
		| Bool
otherwise = Bool
True
	modify :: Bool -> [UserName] -> [UserName]
modify Bool
locked [UserName]
ls
		| Bool -> UserName
sudoline Bool
locked UserName -> [UserName] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [UserName]
ls = [UserName]
ls
		| Bool
otherwise = [UserName]
ls [UserName] -> [UserName] -> [UserName]
forall a. [a] -> [a] -> [a]
++ [Bool -> UserName
sudoline Bool
locked]

-- | Sets up a file in /etc/sudoers.d/, which /etc/sudoers includes,
-- with the specified content.
--
-- The FilePath can be relative to that directory.
sudoersDFile :: FilePath -> [Line] -> RevertableProperty DebianLike Linux
sudoersDFile :: UserName -> [UserName] -> RevertableProperty DebianLike Linux
sudoersDFile UserName
dfile [UserName]
content = Property UnixLike
setup Property UnixLike
-> Property DebianLike
-> CombinedType (Property UnixLike) (Property DebianLike)
forall x y. Combines x y => x -> y -> CombinedType x y
`requires` [UserName] -> Property DebianLike
Apt.installed [UserName
"sudo"] Property DebianLike
-> Property Linux -> RevertableProperty DebianLike Linux
forall setupmetatypes undometatypes.
Property setupmetatypes
-> Property undometatypes
-> RevertableProperty setupmetatypes undometatypes
<!> Property Linux
cleanup
  where
	f :: UserName
f = UserName
"/etc/sudoers.d" UserName -> UserName -> UserName
</> UserName
dfile
	-- sudoers.d files should not be world readable
	setup :: Property UnixLike
setup = UserName -> [UserName] -> Property UnixLike
hasContentProtected UserName
f [UserName]
content
	cleanup :: Property Linux
cleanup = Property UnixLike -> Property Linux
forall (p :: * -> *) (untightened :: [MetaType])
       (tightened :: [MetaType]).
(TightenTargets p, TightenTargetsAllowed untightened tightened,
 SingI tightened) =>
p (MetaTypes untightened) -> p (MetaTypes tightened)
tightenTargets (Property UnixLike -> Property Linux)
-> Property UnixLike -> Property Linux
forall a b. (a -> b) -> a -> b
$ UserName -> Property UnixLike
notPresent UserName
f