2019-09-24 16:32:54 +00:00
|
|
|
{- git credential interface
|
|
|
|
-
|
2022-09-09 17:53:38 +00:00
|
|
|
- Copyright 2019-2022 Joey Hess <id@joeyh.name>
|
2019-09-24 16:32:54 +00:00
|
|
|
-
|
|
|
|
- Licensed under the GNU AGPL version 3 or higher.
|
|
|
|
-}
|
|
|
|
|
2022-09-09 17:53:38 +00:00
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
|
2019-09-24 16:32:54 +00:00
|
|
|
module Git.Credential where
|
|
|
|
|
|
|
|
import Common
|
|
|
|
import Git
|
2022-09-09 17:53:38 +00:00
|
|
|
import Git.Types
|
2019-09-24 16:32:54 +00:00
|
|
|
import Git.Command
|
2022-09-09 17:53:38 +00:00
|
|
|
import qualified Git.Config as Config
|
2019-09-24 16:32:54 +00:00
|
|
|
import Utility.Url
|
|
|
|
|
|
|
|
import qualified Data.Map as M
|
2022-09-09 17:53:38 +00:00
|
|
|
import Network.URI
|
|
|
|
import Control.Concurrent.STM
|
2019-09-24 16:32:54 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2020-01-22 20:13:48 +00:00
|
|
|
credentialBasicAuth :: Credential -> Maybe BasicAuth
|
|
|
|
credentialBasicAuth cred = BasicAuth
|
|
|
|
<$> credentialUsername cred
|
|
|
|
<*> credentialPassword cred
|
|
|
|
|
2022-09-09 17:53:38 +00:00
|
|
|
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
|
2020-01-22 20:13:48 +00:00
|
|
|
where
|
2022-09-09 17:53:38 +00:00
|
|
|
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
|
2020-01-22 20:13:48 +00:00
|
|
|
|
2022-09-09 17:53:38 +00:00
|
|
|
-- | This may prompt the user for the credential, or get a cached
|
|
|
|
-- credential from git.
|
2019-09-24 16:32:54 +00:00
|
|
|
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 ()
|
2019-09-24 18:34:15 +00:00
|
|
|
approveUrlCredential c = void . runCredential "approve" c
|
2019-09-24 16:32:54 +00:00
|
|
|
|
|
|
|
-- | 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 =
|
2020-04-06 21:14:49 +00:00
|
|
|
parseCredential . decodeBS <$> pipeWriteRead
|
2019-09-24 16:32:54 +00:00
|
|
|
[ 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, "")
|
2022-09-09 17:53:38 +00:00
|
|
|
|
|
|
|
-- 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 = CredentialBaseURL URI
|
|
|
|
deriving (Show, Eq, Ord)
|
|
|
|
|
|
|
|
mkCredentialBaseURL :: Repo -> URLString -> Maybe CredentialBaseURL
|
|
|
|
mkCredentialBaseURL r s = do
|
|
|
|
u <- parseURI s
|
|
|
|
let usehttppath = fromMaybe False $ Config.isTrueFalse' $
|
|
|
|
Config.get (ConfigKey "credential.useHttpPath") (ConfigValue "") r
|
|
|
|
if usehttppath
|
|
|
|
then Nothing
|
|
|
|
else Just $ CredentialBaseURL $ u { uriPath = "" }
|