Skip to content

Commit

Permalink
Merge pull request #23189 from JuliaLang/cv/credential-payload
Browse files Browse the repository at this point in the history
Add LibGit2 CredentialPayload struct
  • Loading branch information
omus authored Aug 16, 2017
2 parents 3b51323 + 5cd220c commit be74e58
Show file tree
Hide file tree
Showing 4 changed files with 331 additions and 55 deletions.
102 changes: 64 additions & 38 deletions base/libgit2/callbacks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ function user_abort()
return Cint(Error.EAUTH)
end

function authenticate_ssh(creds::SSHCredentials, libgit2credptr::Ptr{Ptr{Void}},
username_ptr, schema, host)
function authenticate_ssh(libgit2credptr::Ptr{Ptr{Void}}, p::CredentialPayload, username_ptr)
creds = Base.get(p.credential)::SSHCredentials
isusedcreds = checkused!(creds)

# Note: The same SSHCredentials can be used to authenticate separate requests using the
Expand All @@ -75,7 +75,7 @@ function authenticate_ssh(creds::SSHCredentials, libgit2credptr::Ptr{Ptr{Void}},
username = username_ptr != Cstring(C_NULL) ? unsafe_string(username_ptr) : ""
if isempty(username)
uname = creds.user # check if credentials were already used
prompt_url = git_url(scheme=schema, host=host)
prompt_url = git_url(scheme=p.scheme, host=p.host)
if !isusedcreds
username = uname
else
Expand All @@ -85,7 +85,7 @@ function authenticate_ssh(creds::SSHCredentials, libgit2credptr::Ptr{Ptr{Void}},
end
end

prompt_url = git_url(scheme=schema, host=host, username=username)
prompt_url = git_url(scheme=p.scheme, host=p.host, username=username)

# For SSH we need a private key location
privatekey = if haskey(ENV,"SSH_KEY_PATH")
Expand Down Expand Up @@ -167,28 +167,28 @@ function authenticate_ssh(creds::SSHCredentials, libgit2credptr::Ptr{Ptr{Void}},
libgit2credptr, creds.user, creds.pubkey, creds.prvkey, creds.pass)
end

function authenticate_userpass(creds::UserPasswordCredentials, libgit2credptr::Ptr{Ptr{Void}},
schema, host, urlusername)
function authenticate_userpass(libgit2credptr::Ptr{Ptr{Void}}, p::CredentialPayload)
creds = Base.get(p.credential)::UserPasswordCredentials
isusedcreds = checkused!(creds)

if creds.prompt_if_incorrect
username = creds.user
userpass = creds.pass
prompt_url = git_url(scheme=schema, host=host)
prompt_url = git_url(scheme=p.scheme, host=p.host)
if Sys.iswindows()
if isempty(username) || isempty(userpass) || isusedcreds
response = Base.winprompt("Please enter your credentials for '$prompt_url'", "Credentials required",
isempty(username) ? urlusername : username; prompt_username = true)
isempty(username) ? p.username : username; prompt_username = true)
isnull(response) && return user_abort()
username, userpass = unsafe_get(response)
end
elseif isusedcreds
response = Base.prompt("Username for '$prompt_url'",
default=isempty(username) ? urlusername : username)
default=isempty(username) ? p.username : username)
isnull(response) && return user_abort()
username = unsafe_get(response)

prompt_url = git_url(scheme=schema, host=host, username=username)
prompt_url = git_url(scheme=p.scheme, host=p.host, username=username)
response = Base.prompt("Password for '$prompt_url'", password=true)
isnull(response) && return user_abort()
userpass = unsafe_get(response)
Expand All @@ -211,7 +211,7 @@ end
"""Credentials callback function
Function provides different credential acquisition functionality w.r.t. a connection protocol.
If a payload is provided then `payload_ptr` should contain a `LibGit2.AbstractCredentials` object.
If a payload is provided then `payload_ptr` should contain a `LibGit2.CredentialPayload` object.
For `LibGit2.Consts.CREDTYPE_USERPASS_PLAINTEXT` type, if the payload contains fields:
`user` & `pass`, they are used to create authentication credentials.
Expand All @@ -238,48 +238,74 @@ function credentials_callback(libgit2credptr::Ptr{Ptr{Void}}, url_ptr::Cstring,
username_ptr::Cstring,
allowed_types::Cuint, payload_ptr::Ptr{Void})
err = Cint(0)
url = unsafe_string(url_ptr)
explicit = false

# parse url for schema and host
urlparts = match(URL_REGEX, url)
schema = urlparts[:scheme] === nothing ? "" : urlparts[:scheme]
urlusername = urlparts[:user] === nothing ? "" : urlparts[:user]
host = urlparts[:host]

# get credentials object from payload pointer
# get `CredentialPayload` object from payload pointer
@assert payload_ptr != C_NULL
creds = unsafe_pointer_to_objref(payload_ptr)
explicit = !isnull(creds[]) && !isa(Base.get(creds[]), CachedCredentials)
p = unsafe_pointer_to_objref(payload_ptr)[]::CredentialPayload

# Parse URL only during the first call to this function. Future calls will use the
# information cached inside the payload.
if isempty(p.host)
url = match(URL_REGEX, unsafe_string(url_ptr))

p.scheme = url[:scheme] === nothing ? "" : url[:scheme]
p.username = url[:user] === nothing ? "" : url[:user]
p.host = url[:host]
p.path = url[:path]

# When an explicit credential is supplied we will make sure to use the given
# credential during the first callback by modifying the allowed types. The
# modification only is in effect for the first callback since `allowed_types` cannot
# be mutated.
if !isnull(p.credential)
explicit = true
cred = unsafe_get(p.credential)
if isa(cred, SSHCredentials)
allowed_types &= Cuint(Consts.CREDTYPE_SSH_KEY)
elseif isa(cred, UserPasswordCredentials)
allowed_types &= Cuint(Consts.CREDTYPE_USERPASS_PLAINTEXT)
else
allowed_types &= Cuint(0) # Unhandled credential type
end
end
end

# use ssh key or ssh-agent
if isset(allowed_types, Cuint(Consts.CREDTYPE_SSH_KEY))
sshcreds = get_creds!(creds, "ssh://$host", reset!(SSHCredentials(true), -1))
if isa(sshcreds, SSHCredentials)
err = authenticate_ssh(sshcreds, libgit2credptr, username_ptr, schema, host)
err == 0 && return err
if isnull(p.credential) || !isa(unsafe_get(p.credential), SSHCredentials)
creds = reset!(SSHCredentials(p.username, "", true), -1)
if !isnull(p.cache)
credid = "ssh://$(p.host)"
creds = get_creds!(unsafe_get(p.cache), credid, creds)
end
p.credential = Nullable(creds)
end
err = authenticate_ssh(libgit2credptr, p, username_ptr)
err == 0 && return err
end

if isset(allowed_types, Cuint(Consts.CREDTYPE_USERPASS_PLAINTEXT))
defaultcreds = reset!(UserPasswordCredentials(true), -1)
credid = "$(isempty(schema) ? "ssh" : schema)://$host"
upcreds = get_creds!(creds, credid, defaultcreds)
# If there were stored SSH credentials, but we ended up here that must
# mean that something went wrong. Replace the SSH credentials by user/pass
# credentials
if !isa(upcreds, UserPasswordCredentials)
upcreds = defaultcreds
isa(Base.get(creds[]), CachedCredentials) && (Base.get(creds[]).creds[credid] = upcreds)
if isnull(p.credential) || !isa(unsafe_get(p.credential), UserPasswordCredentials)
creds = reset!(UserPasswordCredentials(p.username, "", true), -1)
if !isnull(p.cache)
credid = "$(isempty(p.scheme) ? "ssh" : p.scheme)://$(p.host)"
creds = get_creds!(unsafe_get(p.cache), credid, creds)
end
p.credential = Nullable(creds)
end
return authenticate_userpass(upcreds, libgit2credptr, schema, host, urlusername)
err = authenticate_userpass(libgit2credptr, p)
err == 0 && return err
end

# No authentication method we support succeeded. The most likely cause is
# that explicit credentials were passed in, but said credentials are incompatible
# with the remote host.
# with the requested authentication method.
if err == 0
if explicit
warn("The explicitly provided credentials were incompatible with " *
"the server's supported authentication methods")
ccall((:giterr_set_str, :libgit2), Void, (Cint, Cstring), Cint(Error.Callback),
"The explicitly provided credential is incompatible with the requested " *
"authentication methods.")
end
err = Cint(Error.EAUTH)
end
Expand Down
46 changes: 42 additions & 4 deletions base/libgit2/types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ Matches the [`git_checkout_options`](https://libgit2.github.com/libgit2/#HEAD/ty
perfdata_payload::Ptr{Void}
end

abstract type Payload end

"""
LibGit2.RemoteCallbacks
Expand All @@ -183,12 +185,16 @@ Matches the [`git_remote_callbacks`](https://libgit2.github.com/libgit2/#HEAD/ty
payload::Ptr{Void}
end

function RemoteCallbacks(credentials::Ptr{Void}, payload::Ref{Nullable{AbstractCredentials}})
RemoteCallbacks(credentials=credentials, payload=pointer_from_objref(payload))
function RemoteCallbacks(credentials_cb::Ptr{Void}, payload::Ref{<:Payload})
RemoteCallbacks(credentials=credentials_cb, payload=pointer_from_objref(payload))
end

function RemoteCallbacks(credentials_cb::Ptr{Void}, payload::Payload)
RemoteCallbacks(credentials_cb, Ref(payload))
end

function RemoteCallbacks(credentials::Ptr{Void}, payload::Nullable{<:AbstractCredentials})
RemoteCallbacks(credentials, Ref{Nullable{AbstractCredentials}}(payload))
function RemoteCallbacks(credentials_cb::Ptr{Void}, credentials)
RemoteCallbacks(credentials_cb, CredentialPayload(credentials))
end

"""
Expand Down Expand Up @@ -920,3 +926,35 @@ function securezero!(p::CachedCredentials)
foreach(securezero!, values(p.cred))
return p
end

"""
LibGit2.CredentialPayload
Retains state between multiple calls to the credential callback. A single
`CredentialPayload` instance will be used when authentication fails for a URL but different
instances will be used when the URL has changed.
"""
mutable struct CredentialPayload <: Payload
credential::Nullable{AbstractCredentials}
cache::Nullable{CachedCredentials}
scheme::String
username::String
host::String
path::String

function CredentialPayload(credential::Nullable{<:AbstractCredentials}, cache::Nullable{CachedCredentials})
new(credential, cache, "", "", "", "")
end
end

function CredentialPayload(credential::Nullable{<:AbstractCredentials})
CredentialPayload(credential, Nullable{CachedCredentials}())
end

function CredentialPayload(cache::Nullable{CachedCredentials})
CredentialPayload(Nullable{AbstractCredentials}(), cache)
end

function CredentialPayload()
CredentialPayload(Nullable{AbstractCredentials}(), Nullable{CachedCredentials}())
end
35 changes: 22 additions & 13 deletions test/libgit2-helpers.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# This file is a part of Julia. License is MIT: https://julialang.org/license

import Base.LibGit2: AbstractCredentials, UserPasswordCredentials, SSHCredentials, CachedCredentials
import Base.LibGit2: AbstractCredentials, UserPasswordCredentials, SSHCredentials,
CachedCredentials, CredentialPayload, Payload

"""
Emulates the LibGit2 credential loop to allows testing of the credential_callback function
Expand All @@ -11,10 +12,10 @@ function credential_loop(
url::AbstractString,
user::Nullable{<:AbstractString},
allowed_types::UInt32,
cache::CachedCredentials=CachedCredentials())
payload::CredentialPayload)
cb = Base.LibGit2.credentials_cb()
libgitcred_ptr_ptr = Ref{Ptr{Void}}(C_NULL)
payload_ptr = Ref(Nullable{AbstractCredentials}(cache))
payload_ptr = Ref(payload)

# Number of times credentials were authenticated against. With the real LibGit2
# credential loop this would be how many times we sent credentials to the remote.
Expand All @@ -29,7 +30,7 @@ function credential_loop(
num_authentications += 1

# Check if the callback provided us with valid credentials
if length(cache.cred) == 1 && first(values(cache.cred)) == valid_credential
if !isnull(payload.credential) && get(payload.credential) == valid_credential
break
end

Expand All @@ -44,38 +45,46 @@ end
function credential_loop(
valid_credential::UserPasswordCredentials,
url::AbstractString,
user::Nullable{<:AbstractString}=Nullable{String}())
credential_loop(valid_credential, url, user, 0x000001)
user::Nullable{<:AbstractString}=Nullable{String}(),
payload::CredentialPayload=CredentialPayload())
credential_loop(valid_credential, url, user, 0x000001, payload)
end

function credential_loop(
valid_credential::SSHCredentials,
url::AbstractString,
user::Nullable{<:AbstractString}=Nullable{String}();
user::Nullable{<:AbstractString}=Nullable{String}(),
payload::CredentialPayload=CredentialPayload();
use_ssh_agent::Bool=false)
cache = CachedCredentials()

if !use_ssh_agent
if isnull(payload.cache)
payload.cache = Nullable(CachedCredentials())
end
cache = get(payload.cache)

m = match(LibGit2.URL_REGEX, url)
default_cred = LibGit2.reset!(SSHCredentials(true), -1)
default_cred.usesshagent = "N"
LibGit2.get_creds!(cache, "ssh://$(m[:host])", default_cred)
end

credential_loop(valid_credential, url, user, 0x000046, cache)
credential_loop(valid_credential, url, user, 0x000046, payload)
end

function credential_loop(
valid_credential::UserPasswordCredentials,
url::AbstractString,
user::AbstractString)
credential_loop(valid_credential, url, Nullable(user))
user::AbstractString,
payload::CredentialPayload=CredentialPayload())
credential_loop(valid_credential, url, Nullable(user), payload)
end

function credential_loop(
valid_credential::SSHCredentials,
url::AbstractString,
user::AbstractString;
user::AbstractString,
payload::CredentialPayload=CredentialPayload();
use_ssh_agent::Bool=false)
credential_loop(valid_credential, url, Nullable(user), use_ssh_agent=use_ssh_agent)
credential_loop(valid_credential, url, Nullable(user), payload, use_ssh_agent=use_ssh_agent)
end
Loading

2 comments on commit be74e58

@nanosoldier
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Executing the daily benchmark build, I will reply here when finished:

@nanosoldier runbenchmarks(ALL, isdaily = true)

@nanosoldier
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your benchmark job has completed - possible performance regressions were detected. A full report can be found here. cc @ararslan

Please sign in to comment.