Skip to content

Commit

Permalink
feat: add method to auth with a token directly (#58)
Browse files Browse the repository at this point in the history
* feat: add method to auth with a token directly

* fix tests

* fix formatting

* fix docstring

---------

Co-authored-by: Sebastian Pfitzner <pfitzseb@gmail.com>
  • Loading branch information
mortenpi and pfitzseb authored Jun 25, 2024
1 parent a54b672 commit 115f14e
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 27 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.Authentication`](@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
16 changes: 4 additions & 12 deletions test/runtests-live.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,15 @@
TESTID = Random.randstring(8)

# Authenticate the test session
JULIAHUB_SERVER = let
juliahub_server = get(ENV, "JULIAHUB_SERVER") do
error("JULIAHUB_SERVER environment variable must be set for these tests to work")
end
JuliaHub._sanitize_juliahub_uri(juliahub_server) do _, msg
error("JULIAHUB_SERVER is invalid: $msg")
end
JULIAHUB_SERVER = get(ENV, "JULIAHUB_SERVER") do
error("JULIAHUB_SERVER environment variable must be set for these tests to work")
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))
@show JuliaHub.authenticate(JULIAHUB_SERVER)
end
# manually set global auth ref
JuliaHub.__AUTH__[] = auth
@info "Authentication / API version: $(auth._api_version)"

@testset "JuliaHub.jl LIVE tests" begin
Expand Down

0 comments on commit 115f14e

Please sign in to comment.