git-annex-10.20261026: Logs/VerifiedRemote.hs
{- git-annex verified remote log
-
- This is similar to the git-annex remote log, but only remote configs
- that have been verified by the user to be safe to use.
-
- Copyright 2026 Joey Hess <id@joeyh.name>
-
- Licensed under the GNU AGPL version 3 or higher.
-}
{-# LANGUAGE TupleSections #-}
module Logs.VerifiedRemote (
remoteConfigMap,
readRemoteLog,
configSet,
configUpdate,
omitUnverifiableFields,
addUnverifiableFields,
VerifyingRemoteConfigId(..),
UnverifiedRemoteAmelioration(..),
unverifiedRemoteAmelioration,
readVerifyingRemoteLog,
storeVerifyingRemoteLog,
removeVerifyingLog
) where
import Annex.Common
import qualified Annex
import Types.Remote
import Types.VectorClock
import Logs.File
import Logs.UUIDBased
import Logs.Remote.Pure
import Logs.MapLog
import Annex.SpecialRemote.Config
import qualified Utility.FileIO as F
import qualified Data.Map as M
import qualified Data.ByteString.Lazy as L
import qualified Data.Attoparsec.ByteString.Char8 as A8
import Data.ByteString.Builder
configSet :: UUID -> RemoteConfig -> Annex ()
configSet u c = configSetMulti [(u, c)]
{- Only sets the RemoteConfig if the log already contains a RemoteConfig
- for this UUID. -}
configUpdate :: UUID -> RemoteConfig -> Annex ()
configUpdate u c = configSetMulti' (M.member u . fromMapLog) [(u, c)]
configSetMulti :: [(UUID, RemoteConfig)] -> Annex ()
configSetMulti = configSetMulti' (const True)
configSetMulti'
:: (Log RemoteConfig -> Bool)
-> [(UUID, RemoteConfig)]
-> Annex ()
configSetMulti' shouldset ucfgs = do
c <- currentVectorClock
logf <- fromRepo gitAnnexVerifiedRemoteLog
lck <- fromRepo gitAnnexVerifiedRemoteLock
old <- parseRemoteConfigLog . fromMaybe mempty
<$> readLogFile logf lck
writeLogFile' logf $ \tmp -> F.writeFile tmp $
toLazyByteString $ buildRemoteConfigLog $
foldl' (accumchange c) old ucfgs
Annex.changeState $ \s ->
s { Annex.verifiedremoteconfigmap = Nothing }
where
accumchange c old (u, cfg)
| shouldset old =
changeLog c u (omitUnverifiableFields cfg) old
| otherwise = old
{- Hidden encryption fields cannot be verified by the user, and in any
- case do not configure the location that the remote accesses, and so
- are omitted from this log file.
-}
omitUnverifiableFields :: RemoteConfig -> RemoteConfig
omitUnverifiableFields c = M.withoutKeys c hiddenEncryptionFields
addUnverifiableFields
:: M.Map UUID RemoteConfig
-> M.Map UUID RemoteConfig
-> M.Map UUID RemoteConfig
addUnverifiableFields unverifiedconfigs verifiedconfigs =
M.mapWithKey go verifiedconfigs
where
go u verifiedconfig = case M.lookup u unverifiedconfigs of
Just unverifiedconfig ->
let unverifiable = M.restrictKeys
unverifiedconfig
hiddenEncryptionFields
in M.union unverifiable verifiedconfig
Nothing -> verifiedconfig
{- Map of remotes by uuid with their RemoteConfigs. Cached for speed.
-
- Nothing when the verified remote config log file does not exist yet.
-}
remoteConfigMap :: Annex (Maybe (M.Map UUID RemoteConfig))
remoteConfigMap =
maybe load return =<< Annex.getState Annex.verifiedremoteconfigmap
where
load = do
m <- readRemoteLog
Annex.changeState $ \s ->
s { Annex.verifiedremoteconfigmap = Just m }
return m
readRemoteLog :: Annex (Maybe (M.Map UUID RemoteConfig))
readRemoteLog = do
logf <- fromRepo gitAnnexVerifiedRemoteLog
lck <- fromRepo gitAnnexVerifiedRemoteLock
fmap calcRemoteConfigMapBase <$> readLogFile logf lck
data UnverifiedRemoteAmelioration = UnverifiedRemoteAmelioration
{ preventGitPush :: Maybe String
-- ^ Prevent git push to all remotes.
, preventAnnexWrite :: Maybe String
-- ^ Prevent any git-annex object modification of all remotes.
, preventAnnexAccess :: UUID -> Maybe String
-- ^ Prevent any git-annex object access to remotes whose UUID
-- the function selects. The returned message explains why.
}
{- Given a list of UUIDs of all currently enabled remotes, check if any of
- them are not in the verified remote log, and depending on their
- remote.name.unverified configuration, returns the necessary
- amelioration to prevent exfiltation of data from an unverified remote
- to an attacker controlled remote.
-}
unverifiedRemoteAmelioration
:: [(UUID, RemoteGitConfig, Maybe RemoteName)]
-> (M.Map UUID RemoteConfig)
-- ^ Logs.Remote.remoteConfigMap
-> Annex (Maybe UnverifiedRemoteAmelioration)
unverifiedRemoteAmelioration rs configmap = remoteConfigMap >>= \case
Just m -> return $
let l = mapMaybe (notinmap m) rs
in Just $ UnverifiedRemoteAmelioration
{ preventGitPush =
checkprevented pushblocker l "Push"
, preventAnnexWrite =
checkprevented writeblocker l "Write"
, preventAnnexAccess =
accessblocker l
}
-- The log file does not exist yet. This is either a newly
-- initialized repository, or git-annex has been upgraded from a
-- version that did not use this log.
--
-- Handle upgrade by populating the log with the
-- current configs of all the currently enabled remotes.
--
-- In a newly initialized repository, there are not yet
-- any enabled remotes, and this creates an empty log file.
Nothing -> do
configSetMulti $ mapMaybe (copyfromconfigmap configmap) rs
return Nothing
where
-- Any special remote that is unverified prevents git push to all
-- remotes.
pushblocker (_, gc, remotename)
| remoteAnnexVerified gc /= RemoteConfigVerified = remotename
| otherwise = Nothing
-- Any autoenabled special remote that is unverified prevents
-- annex object write to all remotes.
writeblocker (_, gc, remotename)
| remoteAnnexVerified gc == RemoteConfigUnverifiedAutoEnabled = remotename
| remoteAnnexVerified gc == RemoteConfigUnverified = remotename
| otherwise = Nothing
-- Prevent annex object access to any special remote that was
-- enabled manually but is not yet verified.
accessblocker l u = case filter (\(u',_, _) -> u' == u) l of
((_, gc, Just remotename):_)
| remoteAnnexVerified gc == RemoteConfigUnverifiedManualEnabled ->
preventedunverified remotename
| remoteAnnexVerified gc == RemoteConfigUnverified ->
preventedunverified remotename
_ -> Nothing
prevented remotename msg = Just $ unwords
[ msg
, "Run this command to verify it:"
, "git-annex verifyremote"
, remotename
]
preventdueto remotetoverify action =
prevented remotetoverify $ unwords
[ action
, "prevented due to an unverified special"
, "remote."
]
preventedunverified remotename =
prevented remotename $
"This special remote cannot be used since " ++
"it's not verified."
checkprevented blocker l action =
case mapMaybe blocker l of
[] -> Nothing
(remotename:_) -> preventdueto remotename action
notinmap m i@(u, gc, _)
| M.notMember u' configmap = Nothing
| M.member u' m = Nothing
| otherwise = Just i
where
u' = getcu u gc
copyfromconfigmap m (u, gc, _) =
(getcu u gc, ) <$> M.lookup u m
getcu u gc = fromMaybe u (remoteAnnexConfigUUID gc)
data VerifyingRemoteConfigId = VerifyingRemoteConfigId
{ fromVerifyingRemoteConfigId :: Integer }
deriving (Eq, Ord)
readVerifyingRemoteLog :: Annex (M.Map VerifyingRemoteConfigId RemoteConfig)
readVerifyingRemoteLog = do
logf <- fromRepo gitAnnexVerifyingRemoteLog
lck <- fromRepo gitAnnexVerifyingRemoteLock
simpleMap . parseVerifyingRemoteLog . fromMaybe mempty
<$> readLogFile logf lck
storeVerifyingRemoteLog :: RemoteConfig -> Annex VerifyingRemoteConfigId
storeVerifyingRemoteLog cfg =
modifyVerifyingRemoteLog $ \c n ->
changeMapLog c n (omitUnverifiableFields cfg)
removeVerifyingLog :: VerifyingRemoteConfigId -> Annex ()
removeVerifyingLog n =
void $ modifyVerifyingRemoteLog
(\_ _ (MapLog m) -> MapLog (M.delete n m))
modifyVerifyingRemoteLog
:: (CandidateVectorClock -> VerifyingRemoteConfigId -> MapLog VerifyingRemoteConfigId RemoteConfig -> MapLog VerifyingRemoteConfigId RemoteConfig)
-> Annex VerifyingRemoteConfigId
modifyVerifyingRemoteLog f = do
c <- currentVectorClock
logf <- fromRepo gitAnnexVerifyingRemoteLog
lck <- fromRepo gitAnnexVerifyingRemoteLock
old <- parseVerifyingRemoteLog . fromMaybe mempty
<$> readLogFile logf lck
let nextcid = VerifyingRemoteConfigId $ succ $
maybe 0 fromVerifyingRemoteConfigId $
headMaybe $ reverse $ M.keys $ simpleMap old
writeLogFile' logf $ \tmp -> F.writeFile tmp $
toLazyByteString $ buildVerifyingRemoteLog (f c nextcid old)
return nextcid
parseVerifyingRemoteLog
:: L.ByteString
-> MapLog VerifyingRemoteConfigId RemoteConfig
parseVerifyingRemoteLog =
parseMapLog parseVerifyingRemoteConfigId remoteConfigParser
buildVerifyingRemoteLog
:: MapLog VerifyingRemoteConfigId RemoteConfig
-> Builder
buildVerifyingRemoteLog =
buildMapLog buildVerifyingRemoteConfigId buildRemoteConfig
parseVerifyingRemoteConfigId :: A8.Parser VerifyingRemoteConfigId
parseVerifyingRemoteConfigId = VerifyingRemoteConfigId <$> A8.decimal
buildVerifyingRemoteConfigId :: VerifyingRemoteConfigId -> Builder
buildVerifyingRemoteConfigId = integerDec . fromVerifyingRemoteConfigId