{- git credential interface - - Copyright 2019-2022 Joey Hess - - Licensed under the GNU AGPL version 3 or higher. -} {-# LANGUAGE OverloadedStrings #-} module Git.Credential where import Common import Git import Git.Types import Git.Command import qualified Git.Config as Config import Utility.Url import Utility.Url.Parse import qualified Data.Map as M import Network.URI import Control.Concurrent.STM data Credential = Credential { fromCredential :: M.Map String String } credentialUsername :: Credential -> Maybe String credentialUsername = M.lookup "username" . fromCredential credentialPassword :: Credential -> Maybe String credentialPassword = M.lookup "password" . fromCredential credentialBasicAuth :: Credential -> Maybe BasicAuth credentialBasicAuth cred = BasicAuth <$> credentialUsername cred <*> credentialPassword cred getBasicAuthFromCredential :: Repo -> TMVar CredentialCache -> GetBasicAuth getBasicAuthFromCredential r ccv u = do (CredentialCache cc) <- atomically $ readTMVar ccv case mkCredentialBaseURL r u of Just bu -> case M.lookup bu cc of Just c -> go (const noop) c Nothing -> do let storeincache = \c -> atomically $ do CredentialCache cc' <- takeTMVar ccv putTMVar ccv (CredentialCache (M.insert bu c cc')) go storeincache =<< getUrlCredential u r Nothing -> go (const noop) =<< getUrlCredential u r where go storeincache c = case credentialBasicAuth c of Just ba -> return $ Just (ba, signalsuccess) Nothing -> do signalsuccess False return Nothing where signalsuccess True = do () <- storeincache c approveUrlCredential c r signalsuccess False = rejectUrlCredential c r -- | This may prompt the user for the credential, or get a cached -- credential from git. getUrlCredential :: URLString -> Repo -> IO Credential getUrlCredential = runCredential "fill" . urlCredential -- | Call if the credential the user entered works, and can be cached for -- later use if git is configured to do so. approveUrlCredential :: Credential -> Repo -> IO () approveUrlCredential c = void . runCredential "approve" c -- | Call if the credential the user entered does not work. rejectUrlCredential :: Credential -> Repo -> IO () rejectUrlCredential c = void . runCredential "reject" c urlCredential :: URLString -> Credential urlCredential = Credential . M.singleton "url" runCredential :: String -> Credential -> Repo -> IO Credential runCredential action input r = parseCredential . decodeBS <$> pipeWriteRead [ Param "credential" , Param action ] (Just (flip hPutStr formatinput)) r where formatinput = concat [ formatCredential input , "\n" -- blank line signifies end of input ] formatCredential :: Credential -> String formatCredential = unlines . map (\(k, v) -> k ++"=" ++ v) . M.toList . fromCredential parseCredential :: String -> Credential parseCredential = Credential . M.fromList . map go . lines where go l = case break (== '=') l of (k, _:v) -> (k, v) (k, []) -> (k, "") -- This is not the cache used by git, but is an in-process cache, -- allowing a process to avoid prompting repeatedly when accessing related -- urls even when git is not configured to cache credentials. data CredentialCache = CredentialCache (M.Map CredentialBaseURL Credential) -- An url with the uriPath empty when credential.useHttpPath is false. -- -- When credential.useHttpPath is true, no caching is done, since each -- distinct url would need a different credential to be cached, which -- could cause the CredentialCache to use a lot of memory. Presumably, -- when credential.useHttpPath is false, one Credential is cached -- for each git repo accessed, and there are a reasonably small number of -- those, so the cache will not grow too large. data CredentialBaseURL = CredentialBaseURI URI | CredentialBaseURL String deriving (Show, Eq, Ord) mkCredentialBaseURL :: Repo -> URLString -> Maybe CredentialBaseURL mkCredentialBaseURL r s = do u <- parseURIPortable s let usehttppath = fromMaybe False $ Config.isTrueFalse' $ Config.get (ConfigKey "credential.useHttpPath") (ConfigValue "") r if usehttppath then Nothing else Just $ CredentialBaseURI $ u { uriPath = "" }