Skip to content

Commit

Permalink
feat: add method to auth with a token directly
Browse files Browse the repository at this point in the history
  • Loading branch information
mortenpi committed Jun 13, 2024
1 parent 367b873 commit 3277f79
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 17 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Added

* The `JuliaHub.authenticate` function now supports a two-argument form, where you can pass the JuliaHub token in directly, bypassing interactive authentication. (??)

## Version v0.1.10 - 2024-05-31

### Changed
Expand Down
56 changes: 42 additions & 14 deletions src/authentication.jl
Original file line number Diff line number Diff line change
Expand Up @@ -157,23 +157,34 @@ function _authheaders(token::Secret; hasura=false)
end

"""
JuliaHub.authenticate(server = Pkg.pkg_server(); force::Bool = false, maxcount::Integer = $(_DEFAULT_authenticate_maxcount), [hook::Base.Callable])
JuliaHub.authenticate(
server::AbstractString = Pkg.pkg_server();
force::Bool = false,
maxcount::Integer = $(_DEFAULT_authenticate_maxcount),
[hook::Base.Callable]
) -> JuliaHub.Authentication
JuliaHub.authenticate(server::AbstractString, token::Union{AbstractString, JuliaHub.Secret}) -> JuliaHub.Authentication
Authenticates with a JuliaHub server, returning a [`JuliaHub.Authenticate`](@ref) object and
setting the global authentication session (see [`JuliaHub.current_authentication`](@ref)).
May throw an [`AuthenticationError`](@ref) if the authentication fails (e.g. expired token).
The zero- and one-argument methods will attempt to read the token from the current Julia depot.
If a valid authentication token does not exist in the Julia depot, a new token is acquired via an
interactive browser based prompt. By default, it attemps to connect to the currently configured Julia
package server URL (configured e.g. via the `JULIA_PKG_SERVER` environment variable), but this
can be overridden by passing the `server` argument.
The two-argument method can be used when you do not want to read the token from the `auth.toml`
file (e.g. when using a long-term token via an environment variable). In this case, you also have
to explicitly set the server URL and `JULIA_PKG_SERVER` is ignored.
Authenticates with a JuliaHub server. If a valid authentication token does not exist in
the Julia depot, a new token is acquired via an interactive browser based prompt.
Returns an [`Authentication`](@ref) object if the authentication was successful, or throws an
[`AuthenticationError`](@ref) if authentication fails.
# Extended help
The interactive prompts tries to authenticate for a maximum of `maxcount` times.
If `force` is set to `true`, an existing authentication token is first deleted. This can be
useful when the existing authentication token is causing the authentication to fail.
# Extended help
By default, it attemps to connect to the currently configured Julia package server URL
(configured e.g. via the `JULIA_PKG_SERVER` environment variable). However, this can
be overridden by passing the `server` argument.
`hook` can be set to a function taking a single string-type argument, and will be passed the
authorization URL the user should interact with in the browser. This can be used to override the default
behavior coming from [PkgAuthentication](https://github.com/JuliaComputing/PkgAuthentication.jl).
Expand All @@ -183,6 +194,17 @@ cached authentications), making it unnecessary to pass the returned object manua
function calls. This is useful for interactive use, but should not be used in library code,
as different authentication calls may clash.
"""
function authenticate end

function authenticate(server::AbstractString, token::Union{AbstractString, Secret})
auth = _authentication(
_juliahub_uri(server);
token = isa(token, Secret) ? token : Secret(token)
)
global __AUTH__[] = auth
return auth
end

function authenticate(
server::Union{AbstractString, Nothing}=nothing;
force::Bool=false,
Expand All @@ -197,6 +219,13 @@ function authenticate(
),
)
end
server_uri = _juliahub_uri(server)
auth = Mocking.@mock _authenticate(server_uri; force, maxcount, hook)
global __AUTH__[] = auth
return auth
end

function _juliahub_uri(server::Union{AbstractString, Nothing})
# PkgAuthentication.token_path can not handle server values that do not
# prepend `https://`, so we use Pkg.pkg_server() to normalize it, just in case.
server_uri_string = if isnothing(server)
Expand All @@ -217,9 +246,8 @@ function authenticate(
isnothing(server) ? ("Pkg.pkg_server()", Pkg.pkg_server()) : ("server", server)
throw(AuthenticationError("Invalid $name value '$value' ($error_msg)"))
end
auth = Mocking.@mock _authenticate(server_uri; force, maxcount, hook)
global __AUTH__[] = auth
return auth

return server_uri
end

function _authenticate(
Expand Down
52 changes: 51 additions & 1 deletion test/authentication.jl
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ end
expires=1234,
email="authfile@example.org",
)
# Missing username in /api/v1 -- succes, but with a warning
# Missing username in /api/v1 -- success, but with a warning
delete!(MOCK_JULIAHUB_STATE, :auth_v1_status)
MOCK_JULIAHUB_STATE[:auth_v1_username] = nothing
let a = @test_logs (:warn,) JuliaHub._authentication(
Expand All @@ -124,3 +124,53 @@ end
end
end
end

# The two-argument JuliaHub.authenticate does not trigger PkgAuthentication, but
# it does do the REST calls, like JuliaHub._authentication() above
@testset "JuliaHub.authenticate(server, token)" begin
empty!(MOCK_JULIAHUB_STATE)
server = "https://juliahub.example.org"
token = JuliaHub.Secret("")
Mocking.apply(mocking_patch) do
let a = JuliaHub.authenticate(server, token)
@test a isa JuliaHub.Authentication
@test a.server == URIs.URI(server)
@test a.username == MOCK_USERNAME
@test a.token == token
@test a._api_version == v"0.0.1"
@test a._email === nothing
@test a._expires === nothing
end
# On old instances, we handle if /api/v1 404s
MOCK_JULIAHUB_STATE[:auth_v1_status] = 404
let a = JuliaHub.authenticate(server, token)
@test a isa JuliaHub.Authentication
@test a.server == URIs.URI(server)
@test a.username == MOCK_USERNAME
@test a._api_version == JuliaHub._MISSING_API_VERSION
@test a._email === "testuser@example.org"
@test a._expires === nothing
end
# .. but on a 500, it will actually throw
MOCK_JULIAHUB_STATE[:auth_v1_status] = 500
@test_throws JuliaHub.AuthenticationError JuliaHub.authenticate(server, token)
# Testing the fallback to legacy GQL endpoint
MOCK_JULIAHUB_STATE[:auth_v1_status] = 404
let a = JuliaHub.authenticate(server, token)
@test a isa JuliaHub.Authentication
@test a.server == URIs.URI(server)
@test a.username == MOCK_USERNAME
@test a._api_version == JuliaHub._MISSING_API_VERSION
@test a._email === "testuser@example.org"
@test a._expires === nothing
end
# Error when the fallback also 500s
MOCK_JULIAHUB_STATE[:auth_gql_fail] = true
@test_throws JuliaHub.AuthenticationError JuliaHub.authenticate(server, token)
# Missing username in /api/v1 -- throws an AuthenticationError, since there is
# no auth.toml file to fall back to.
delete!(MOCK_JULIAHUB_STATE, :auth_v1_status)
MOCK_JULIAHUB_STATE[:auth_v1_username] = nothing
@test_throws JuliaHub.AuthenticationError JuliaHub.authenticate(server, token)
end
end
3 changes: 1 addition & 2 deletions test/runtests-live.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ JULIAHUB_SERVER = let
end
end
auth = if haskey(ENV, "JULIAHUB_TOKEN")
JULIAHUB_TOKEN = JuliaHub.Secret(ENV["JULIAHUB_TOKEN"])
JuliaHub._authentication(JULIAHUB_SERVER; token=JULIAHUB_TOKEN)
JuliaHub.authenticate(JULIAHUB_SERVER, ENV["JULIAHUB_TOKEN"])
else
@warn "JULIAHUB_TOKEN not set, attempting interactive authentication."
@show JuliaHub.authenticate(string(JULIAHUB_SERVER))
Expand Down

0 comments on commit 3277f79

Please sign in to comment.