Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* Print warnings to standard error instead of standard output, keeping stdout clean for machine-readable output such as `mix hex.outdated --json`. Warning-colored lines that are part of a command's regular output, such as retirement notices in `mix hex.info` and the `mix deps.get` dependency listing, remain on standard output
* Add `--format sarif` and `--output PATH` options to `mix hex.audit` to render the audit result as a SARIF v2.1.0 document that can be uploaded to GitHub code scanning and other SARIF consumers. Findings are anchored to the dependency's `mix.lock` entry and ignored findings are included as suppressed results. Requires OTP 27 or later
* Link to the hex.pm diffs page (`https://hex.pm/diffs`) in `mix hex.outdated` now that package diffs have moved from diff.hex.pm into hex.pm
* Record whether the key given to `mix hex.organization auth ORGANIZATION --key KEY` is owned by the organization or by a user. Organization keys no longer trigger the stored-key deprecation warning, while user keys warn that they will stop working in Hex 2.6. Re-run the command on this Hex version to record the owner of an already stored organization key and silence the warning

## v2.5.1 (2026-07-09)

Expand Down
45 changes: 34 additions & 11 deletions lib/hex/repo.ex
Original file line number Diff line number Diff line change
Expand Up @@ -349,29 +349,52 @@ defmodule Hex.Repo do
if Hex.Server.should_warn?({:deprecated_repos_key, organization}) do
Hex.Shell.warn("""
Authenticating to the #{organization} repository with HEX_REPOS_KEY is deprecated \
and will be removed.
and will stop working in Hex 2.6.

For development authenticate as a user with `mix hex.user auth`. For CI \
authenticate per organization with `mix hex.organization auth #{organization} --key KEY`.
""")
end

:config ->
if Hex.Server.should_warn?({:deprecated_repo_key, organization}) do
Hex.Shell.warn("""
Authenticating to the #{organization} repository with a stored key is deprecated \
and will be removed.

For development authenticate as a user with `mix hex.user auth`. For CI generate an \
organization key with `mix hex.organization key #{organization} generate` and pass it \
with `mix hex.organization auth #{organization} --key KEY`.
""")
end
warn_deprecated_stored_key(organization, Map.get(repo_config, :auth_key_owner))
end
end

defp maybe_warn_deprecated_repo_key(_repo_name, _organization, _repo_config), do: :ok

defp warn_deprecated_stored_key(_organization, :organization), do: :ok

defp warn_deprecated_stored_key(organization, :user) do
if Hex.Server.should_warn?({:deprecated_repo_key, organization}) do
Hex.Shell.warn("""
Authenticating to the #{organization} repository with a key owned by your user \
account is deprecated and will stop working in Hex 2.6.

For development authenticate as a user with `mix hex.user auth`. For CI generate an \
organization key with `mix hex.organization key #{organization} generate` and pass it \
with `mix hex.organization auth #{organization} --key KEY`.
""")
end
end

defp warn_deprecated_stored_key(organization, _owner) do
if Hex.Server.should_warn?({:deprecated_repo_key, organization}) do
Hex.Shell.warn("""
Authenticating to the #{organization} repository with a stored key is deprecated \
and will stop working in Hex 2.6.

For development authenticate as a user with `mix hex.user auth`. For CI generate an \
organization key with `mix hex.organization key #{organization} generate` and pass it \
with `mix hex.organization auth #{organization} --key KEY`.

If you already authenticate with an organization key, re-run \
`mix hex.organization auth #{organization} --key KEY` on this Hex version to record it \
and silence this warning.
""")
end
end

# HEX_REPOS_KEY is exposed as the hexpm source `auth_key` and inherited by
# `hexpm:*` repos, so an `auth_key` equal to `repos_key` came from the
# environment, while any other value was stored in the local config.
Expand Down
6 changes: 5 additions & 1 deletion lib/mix/tasks/hex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -277,15 +277,19 @@ defmodule Mix.Tasks.Hex do
end

@doc false
def auth_organization(name, key) do
def auth_organization(name, key, owner) do
repo = Hex.Repo.get_repo(name) || Hex.Repo.default_hexpm_repo()
repo = Map.put(repo, :auth_key, key)
repo = put_auth_key_owner(repo, owner)

Hex.State.fetch!(:repos)
|> Map.put(name, repo)
|> Hex.Config.update_repos()
end

defp put_auth_key_owner(repo, nil), do: Map.delete(repo, :auth_key_owner)
defp put_auth_key_owner(repo, owner), do: Map.put(repo, :auth_key_owner, owner)

@doc false
def required_opts(opts, required) do
Enum.map(required, fn req ->
Expand Down
77 changes: 48 additions & 29 deletions lib/mix/tasks/hex.organization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -165,43 +165,56 @@ defmodule Mix.Tasks.Hex.Organization do
end

defp auth(organization, opts) do
key = opts[:key]
if key = opts[:key] do
owner = test_key(key, organization)

key =
if key do
test_key(key, organization)
key
else
if owner == :user do
Hex.Shell.warn("""
Authorizing an organization without --key is deprecated and will be removed.
The given key is owned by your user account, not the organization. Authenticating \
to organization repositories with user keys is deprecated and will stop working \
in Hex 2.6.

For development authenticate as a user instead, which gives you access to \
all your organizations:

mix hex.user auth
For development authenticate as a user with `mix hex.user auth`. For CI generate \
an organization key with `mix hex.organization key #{organization} generate` and \
pass it with `--key`.
""")
end

For CI generate an organization key and pass it with --key:
Mix.Tasks.Hex.auth_organization("hexpm:#{organization}", key, owner)

mix hex.organization key #{organization} generate
mix hex.organization auth #{organization} --key KEY
if owner == :organization do
Hex.Shell.info("""
Starting with Hex 2.6 this command will exchange the organization key for a \
short-lived token instead of storing the key. In CI, run it right before \
fetching dependencies.
""")
end
else
Hex.Shell.warn("""
Authorizing an organization without --key is deprecated and will be removed.

key_name = Mix.Tasks.Hex.repository_key_name(organization, opts[:key_name])
permissions = [%{"domain" => "repository", "resource" => organization}]
For development authenticate as a user instead, which gives you access to \
all your organizations:

case Hex.API.Key.new(key_name, permissions) do
{:ok, {201, _, body}} ->
body["secret"]
mix hex.user auth

other ->
Mix.shell().error("Generation of key failed")
Hex.Utils.print_error_result(other)
nil
end
end
For CI generate an organization key and pass it with --key:

mix hex.organization key #{organization} generate
mix hex.organization auth #{organization} --key KEY
""")

if key do
Mix.Tasks.Hex.auth_organization("hexpm:#{organization}", key)
key_name = Mix.Tasks.Hex.repository_key_name(organization, opts[:key_name])
permissions = [%{"domain" => "repository", "resource" => organization}]

case Hex.API.Key.new(key_name, permissions) do
{:ok, {201, _, body}} ->
Mix.Tasks.Hex.auth_organization("hexpm:#{organization}", body["secret"], :user)

other ->
Mix.shell().error("Generation of key failed")
Hex.Utils.print_error_result(other)
end
end
end

Expand Down Expand Up @@ -287,20 +300,26 @@ defmodule Mix.Tasks.Hex.Organization do

defp test_key(key, name) do
case Hex.API.Auth.get("repository", name, key: key) do
{:ok, {code, _, _body}} when code in 200..299 ->
:ok
{:ok, {code, _, body}} when code in 200..299 ->
key_owner(body)

{:ok, {code, _, %{"message" => message}}} when code in [401, 403] ->
Hex.Shell.error(
"Failed to authenticate against organization repository with given key because of: #{message}"
)

Mix.Tasks.Hex.set_exit_code(1)
nil

other ->
Hex.Utils.print_error_result(other)
Hex.Shell.error("Failed to verify authentication key")
Mix.Tasks.Hex.set_exit_code(1)
nil
end
end

defp key_owner(%{"key" => %{"owner" => %{"type" => "organization"}}}), do: :organization
defp key_owner(%{"key" => %{"owner" => %{"type" => "user"}}}), do: :user
defp key_owner(_body), do: nil
end
89 changes: 87 additions & 2 deletions test/hex/repo_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,27 @@ defmodule Hex.RepoTest do
end)
end

test "auth_key_owner persists through config round-trip" do
in_tmp(fn ->
Hex.State.put(:config_home, File.cwd!())

repos = Hex.State.fetch!(:repos)

org_repo = %{auth_key: "org_key_value", auth_key_owner: :organization}

repos
|> Map.put("hexpm:roundtriporg", org_repo)
|> Hex.Config.update_repos()

# Reload config from disk to simulate a fresh session
config = Hex.Config.read()
repos = Hex.Config.read_repos(config)

assert repos["hexpm:roundtriporg"].auth_key == "org_key_value"
assert repos["hexpm:roundtriporg"].auth_key_owner == :organization
end)
end

test "warns about deprecation when a stored key authenticates to an organization repository" do
Hex.Server.reset()

Expand All @@ -120,8 +141,70 @@ defmodule Hex.RepoTest do
end

output = Case.shell_output()
assert output =~ "stored key is deprecated"
assert output =~ "stored key is deprecated and will stop working in Hex 2.6"
assert output =~ "mix hex.user auth"

assert output =~
"re-run `mix hex.organization auth storedkeyorg --key KEY` on this Hex version"
end

test "warns about deprecation when a user-owned key authenticates to an organization repository" do
Hex.Server.reset()

repos = Hex.State.fetch!(:repos)
hexpm = repos["hexpm"]

org_repo = %{
url: hexpm.url <> "/repos/userkeyorg",
public_key: hexpm.public_key,
auth_key: "user_key_value",
auth_key_owner: :user,
oauth_exchange: true,
trusted: true
}

Hex.State.put(:repos, Map.put(repos, "hexpm:userkeyorg", org_repo))

try do
Hex.Repo.get_package("hexpm:userkeyorg", "pkg", "")
rescue
_ -> :ok
end

output = Case.shell_output()

assert output =~
"key owned by your user account is deprecated and will stop working in Hex 2.6"

assert output =~ "mix hex.user auth"
refute output =~ "re-run"
end

test "does not warn when an organization-owned key authenticates to an organization repository" do
Hex.Server.reset()

repos = Hex.State.fetch!(:repos)
hexpm = repos["hexpm"]

org_repo = %{
url: hexpm.url <> "/repos/orgkeyorg",
public_key: hexpm.public_key,
auth_key: "org_key_value",
auth_key_owner: :organization,
oauth_exchange: true,
trusted: true
}

Hex.State.put(:repos, Map.put(repos, "hexpm:orgkeyorg", org_repo))

try do
Hex.Repo.get_package("hexpm:orgkeyorg", "pkg", "")
rescue
_ -> :ok
end

output = Case.shell_output()
refute output =~ "deprecated"
end

test "warns about deprecation when HEX_REPOS_KEY authenticates to an organization repository" do
Expand All @@ -148,7 +231,9 @@ defmodule Hex.RepoTest do
end

output = Case.shell_output()
assert output =~ "the reposkeyorg repository with HEX_REPOS_KEY is deprecated"

assert output =~
"the reposkeyorg repository with HEX_REPOS_KEY is deprecated and will stop working in Hex 2.6"
end

test "does not warn for the base hexpm repository authenticated with HEX_REPOS_KEY" do
Expand Down
Loading
Loading