    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 u :: 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 ["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
    $ \w :: 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]
    ++ " 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]
    ++ " 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 "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]
    ++ " includes " UserName -> UserName -> UserName
    forall a. [a] -> [a] -> [a]
    ++ UserName
    sudoersd) [UserName] -> [UserName]
    addl UserName
    sudoers
    	  where
    		addl :: [UserName] -> [UserName]
    addl content :: [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 = "@includedir /etc/sudoers.d"
    		oldl :: UserName
    oldl = "#includedir /etc/sudoers.d"

    	sudoers :: UserName
    sudoers = "/etc/sudoers"
    	sudoersd :: UserName
    sudoersd = "/etc/sudoers.d"
    	dfile :: UserName
    dfile = "/etc/sudoers.d/000users"
    	sudobaseline :: UserName
    sudobaseline = UserName
    u UserName -> UserName -> UserName
    forall a. [a] -> [a] -> [a]
    ++ " ALL=(ALL:ALL)"
    	notuserline :: UserName -> Bool
    notuserline l :: UserName
    l = Bool -> Bool
    not (UserName
    sudobaseline UserName -> UserName -> Bool
    forall a. Eq a => [a] -> [a] -> Bool
    `isPrefixOf` UserName
    l)
    	sudoline :: Bool -> UserName
    sudoline True = UserName
    sudobaseline UserName -> UserName -> UserName
    forall a. [a] -> [a] -> [a]
    ++ " NOPASSWD:ALL"
    	sudoline False = UserName
    sudobaseline UserName -> UserName -> UserName
    forall a. [a] -> [a] -> [a]
    ++ " ALL"
    	wanted :: Bool -> UserName -> Bool
    wanted locked :: Bool
    locked l :: UserName
    l
    		| UserName -> Bool
    notuserline UserName
    l = Bool
    True
    		| "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 locked :: Bool
    locked ls :: [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 dfile :: UserName
    dfile content :: [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 ["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 = "/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