-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Integrate Git credential helper support #23824
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,289 @@ | ||
""" | ||
GitCredential | ||
|
||
Git credential information used in communication with git credential helpers. The field are | ||
named using the [input/output key specification](https://git-scm.com/docs/git-credential#IOFMT). | ||
""" | ||
mutable struct GitCredential | ||
protocol::Nullable{String} | ||
host::Nullable{String} | ||
path::Nullable{String} | ||
username::Nullable{String} | ||
password::Nullable{String} | ||
|
||
function GitCredential( | ||
protocol::Nullable{<:AbstractString}, | ||
host::Nullable{<:AbstractString}, | ||
path::Nullable{<:AbstractString}, | ||
username::Nullable{<:AbstractString}, | ||
password::Nullable{<:AbstractString}) | ||
c = new(protocol, host, path, username, password) | ||
finalizer(c, securezero!) | ||
return c | ||
end | ||
end | ||
|
||
function GitCredential( | ||
protocol::Union{AbstractString,Void}=nothing, | ||
host::Union{AbstractString,Void}=nothing, | ||
path::Union{AbstractString,Void}=nothing, | ||
username::Union{AbstractString,Void}=nothing, | ||
password::Union{AbstractString,Void}=nothing) | ||
GitCredential( | ||
Nullable{String}(protocol), | ||
Nullable{String}(host), | ||
Nullable{String}(path), | ||
Nullable{String}(username), | ||
Nullable{String}(password)) | ||
end | ||
|
||
function GitCredential(cfg::GitConfig, url::AbstractString) | ||
fill!(cfg, parse(GitCredential, url)) | ||
end | ||
|
||
function GitCredential(cred::UserPasswordCredentials, url::AbstractString) | ||
git_cred = parse(GitCredential, url) | ||
git_cred.username = Nullable{String}(cred.user) | ||
git_cred.password = Nullable{String}(cred.pass) | ||
return git_cred | ||
end | ||
|
||
function securezero!(cred::GitCredential) | ||
!isnull(cred.protocol) && securezero!(unsafe_get(cred.protocol)) | ||
!isnull(cred.host) && securezero!(unsafe_get(cred.host)) | ||
!isnull(cred.path) && securezero!(unsafe_get(cred.path)) | ||
!isnull(cred.username) && securezero!(unsafe_get(cred.username)) | ||
!isnull(cred.password) && securezero!(unsafe_get(cred.password)) | ||
return cred | ||
end | ||
|
||
function Base.:(==)(a::GitCredential, b::GitCredential) | ||
isequal(a.protocol, b.protocol) && | ||
isequal(a.host, b.host) && | ||
isequal(a.path, b.path) && | ||
isequal(a.username, b.username) && | ||
isequal(a.password, b.password) | ||
end | ||
|
||
""" | ||
ismatch(url, git_cred) -> Bool | ||
|
||
Checks if the `git_cred` is valid for the given `url`. | ||
""" | ||
function ismatch(url::AbstractString, git_cred::GitCredential) | ||
isempty(url) && return true | ||
|
||
m = match(URL_REGEX, url) | ||
m === nothing && error("Unable to parse URL") | ||
|
||
# Note: missing URL groups match anything | ||
(m[:scheme] === nothing ? true : isequal(Nullable(m[:scheme]), git_cred.protocol)) && | ||
(m[:host] === nothing ? true : isequal(Nullable(m[:host]), git_cred.host)) && | ||
(m[:path] === nothing ? true : isequal(Nullable(m[:path]), git_cred.path)) && | ||
(m[:user] === nothing ? true : isequal(Nullable(m[:user]), git_cred.username)) | ||
end | ||
|
||
function isfilled(cred::GitCredential) | ||
!isnull(cred.username) && !isnull(cred.password) | ||
end | ||
|
||
function Base.parse(::Type{GitCredential}, url::AbstractString) | ||
m = match(URL_REGEX, url) | ||
m === nothing && error("Unable to parse URL") | ||
return GitCredential( | ||
m[:scheme], | ||
m[:host], | ||
m[:path], | ||
m[:user], | ||
m[:password], | ||
) | ||
end | ||
|
||
function Base.copy!(a::GitCredential, b::GitCredential) | ||
# Note: deepcopy calls avoid issues with securezero! | ||
a.protocol = deepcopy(b.protocol) | ||
a.host = deepcopy(b.host) | ||
a.path = deepcopy(b.path) | ||
a.username = deepcopy(b.username) | ||
a.password = deepcopy(b.password) | ||
return a | ||
end | ||
|
||
function Base.write(io::IO, cred::GitCredential) | ||
!isnull(cred.protocol) && println(io, "protocol=", unsafe_get(cred.protocol)) | ||
!isnull(cred.host) && println(io, "host=", unsafe_get(cred.host)) | ||
!isnull(cred.path) && println(io, "path=", unsafe_get(cred.path)) | ||
!isnull(cred.username) && println(io, "username=", unsafe_get(cred.username)) | ||
!isnull(cred.password) && println(io, "password=", unsafe_get(cred.password)) | ||
nothing | ||
end | ||
|
||
function Base.read!(io::IO, cred::GitCredential) | ||
# https://git-scm.com/docs/git-credential#IOFMT | ||
while !eof(io) | ||
key, value = split(readline(io), '=') | ||
|
||
if key == "url" | ||
# Any components which are missing from the URL will be set to empty | ||
# https://git-scm.com/docs/git-credential#git-credential-codeurlcode | ||
copy!(cred, parse(GitCredential, value)) | ||
else | ||
setfield!(cred, Symbol(key), Nullable(String(value))) | ||
end | ||
end | ||
|
||
return cred | ||
end | ||
|
||
function fill!(cfg::GitConfig, cred::GitCredential) | ||
# When the username is missing default to using the username set in the configuration | ||
if isnull(cred.username) | ||
cred.username = default_username(cfg, cred) | ||
end | ||
|
||
for helper in credential_helpers(cfg, cred) | ||
fill!(helper, cred) | ||
|
||
# "Once Git has acquired both a username and a password, no more helpers will be | ||
# tried." – https://git-scm.com/docs/gitcredentials#gitcredentials-helper | ||
!isfilled(cred) && break | ||
end | ||
|
||
return cred | ||
end | ||
|
||
struct GitCredentialHelper | ||
cmd::Cmd | ||
end | ||
|
||
function Base.parse(::Type{GitCredentialHelper}, helper::AbstractString) | ||
# The helper string can take on different behaviors depending on the value: | ||
# - "Code after `!` evaluated in shell" – https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage | ||
# - "If the helper name is not an absolute path, then the string `git credential-` is | ||
# prepended." – https://git-scm.com/docs/gitcredentials#gitcredentials-helper | ||
if startswith(helper, '!') | ||
cmd_str = helper[2:end] | ||
elseif isabspath(first(Base.shell_split(helper))) | ||
cmd_str = helper | ||
else | ||
cmd_str = "git credential-$helper" | ||
end | ||
|
||
GitCredentialHelper(`$(Base.shell_split(cmd_str)...)`) | ||
end | ||
|
||
function Base.:(==)(a::GitCredentialHelper, b::GitCredentialHelper) | ||
a.cmd == b.cmd | ||
end | ||
|
||
function run!(helper::GitCredentialHelper, operation::AbstractString, cred::GitCredential) | ||
cmd = `$(helper.cmd) $operation` | ||
output, input, p = readandwrite(cmd) | ||
|
||
# Provide the helper with the credential information we know | ||
write(input, cred) | ||
write(input, "\n") | ||
t = @async close(input) | ||
|
||
# Process the response from the helper | ||
Base.read!(output, cred) | ||
close(output) | ||
wait(t) | ||
|
||
return cred | ||
end | ||
|
||
function run(helper::GitCredentialHelper, operation::AbstractString, cred::GitCredential) | ||
run!(helper, operation, deepcopy(cred)) | ||
end | ||
|
||
# The available actions between using `git credential` and helpers are slightly different. | ||
# We will directly interact with the helpers as that way we can request credential | ||
# information without a prompt (helper `get` vs. git credential `fill`). | ||
# https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage | ||
|
||
fill!(helper::GitCredentialHelper, cred::GitCredential) = run!(helper, "get", cred) | ||
approve(helper::GitCredentialHelper, cred::GitCredential) = run(helper, "store", cred) | ||
reject(helper::GitCredentialHelper, cred::GitCredential) = run(helper, "erase", cred) | ||
|
||
""" | ||
credential_helpers(config, git_cred) -> Vector{GitCredentialHelper} | ||
|
||
Return all of the `GitCredentialHelper`s found within the provided `config` which are valid | ||
for the specified `git_cred`. | ||
""" | ||
function credential_helpers(cfg::GitConfig, cred::GitCredential) | ||
helpers = GitCredentialHelper[] | ||
|
||
# https://git-scm.com/docs/gitcredentials#gitcredentials-helper | ||
for entry in GitConfigIter(cfg, r"credential.*\.helper") | ||
section, url, name, value = split(entry) | ||
@assert name == "helper" | ||
|
||
# Only use configuration settings where the URL applies to the git credential | ||
ismatch(url, cred) || continue | ||
|
||
# An empty credential.helper resets the list to empty | ||
isempty(value) && empty!(helpers) | ||
|
||
# Due to a bug in libgit2 iteration we may read credential helpers out of order. | ||
# See: https://github.com/libgit2/libgit2/issues/4361 | ||
# | ||
# Typically the ordering doesn't matter but does in this particular case. Disabling | ||
# credential helpers avoids potential issues with using the wrong credentials or | ||
# writing credentials to the wrong helper. | ||
if isempty(value) | ||
Base.warn_once("Resetting the helper list is currently unsupported: " * | ||
"ignoring all git credential helpers.") | ||
return GitCredentialHelper[] | ||
end | ||
|
||
push!(helpers, parse(GitCredentialHelper, value)) | ||
end | ||
|
||
return helpers | ||
end | ||
|
||
""" | ||
default_username(config, git_cred) -> Nullable{String} | ||
|
||
Return the default username, if any, provided by the `config` which is valid for the | ||
specified `git_cred`. | ||
""" | ||
function default_username(cfg::GitConfig, cred::GitCredential) | ||
# https://git-scm.com/docs/gitcredentials#gitcredentials-username | ||
for entry in GitConfigIter(cfg, r"credential.*\.username") | ||
section, url, name, value = split(entry) | ||
@assert name == "username" | ||
|
||
# Only use configuration settings where the URL applies to the git credential | ||
ismatch(url, cred) || continue | ||
return Nullable{String}(value) | ||
end | ||
|
||
return Nullable{String}() | ||
end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I need to think of a better name for this function. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about just There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My only issue is that those are good variable names. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I renamed these to |
||
|
||
approve(cfg::GitConfig, cred::AbstractCredentials, url::AbstractString) = nothing | ||
reject(cfg::GitConfig, cred::AbstractCredentials, url::AbstractString) = nothing | ||
|
||
function approve(cfg::GitConfig, cred::UserPasswordCredentials, url::AbstractString) | ||
git_cred = GitCredential(cred, url) | ||
for helper in credential_helpers(cfg, git_cred) | ||
approve(helper, git_cred) | ||
end | ||
|
||
nothing | ||
end | ||
|
||
function reject(cfg::GitConfig, cred::UserPasswordCredentials, url::AbstractString) | ||
git_cred = GitCredential(cred, url) | ||
for helper in credential_helpers(cfg, git_cred) | ||
reject(helper, git_cred) | ||
end | ||
|
||
securezero!(git_cred) | ||
securezero!(cred) | ||
|
||
nothing | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Due to a bug with libgit2 the configuration iterator may not return results in the order in which they were written in the configuration file. For credential helpers this is only an issue when trying to use the special empty string helper which indicates that all previous helpers should be ignored.
See: libgit2/libgit2#4361
Note that this warning does show up when running the tests.