Skip to content

Commit 53328c2

Browse files
sampan-s-nayaksampan
andauthored
[Core] Token Auth minor UX Improvements (#58998)
## Description - convert debug logs in authentication_token_loader to info so that users are aware of where the token being used is being loaded from - When we raise the `AuthenticationError`, if RAY_AUTH_MODE is not set to token we should explicitly print that in the error message - in error messages suggest storing tokens in filesystem instead of env - add state api tests in test_token_auth_integration.py --------- Signed-off-by: sampan <sampan@anyscale.com> Co-authored-by: sampan <sampan@anyscale.com>
1 parent ed14974 commit 53328c2

File tree

9 files changed

+146
-33
lines changed

9 files changed

+146
-33
lines changed

doc/source/ray-security/token-auth.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,7 @@ $ export RAY_AUTH_MODE=token
9393
# First attempt - an error is raised if no token exists.
9494
$ ray start --head
9595
...
96-
RuntimeError: Token authentication is enabled but no authentication token was found. Please provide an authentication token using one of these methods:
97-
1. Set the RAY_AUTH_TOKEN environment variable
98-
2. Set the RAY_AUTH_TOKEN_PATH environment variable (pointing to a token file)
99-
3. Create a token file at the default location: ~/.ray/auth_token
96+
ray.exceptions.AuthenticationError: Token authentication is enabled but no authentication token was found. Ensure that the token for the cluster is available in a local file (e.g., ~/.ray/auth_token or via RAY_AUTH_TOKEN_PATH) or as the `RAY_AUTH_TOKEN` environment variable. To generate a token for local development, use `ray get-auth-token --generate` For remote clusters, ensure that the token is propagated to all nodes of the cluster when token authentication is enabled. For more information, see: https://docs.ray.io/en/latest/ray-security/auth.html
10097

10198
# Generate a token.
10299
$ ray get-auth-token --generate

python/ray/_private/authentication/authentication_constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Authentication error messages
22
TOKEN_AUTH_ENABLED_BUT_NO_TOKEN_FOUND_ERROR_MESSAGE = (
3-
"Token authentication is enabled but no authentication token was found."
3+
"Token authentication is enabled but no authentication token was found"
44
)
55

66
TOKEN_INVALID_ERROR_MESSAGE = "Token authentication is enabled but the authentication token is invalid or incorrect." # noqa: E501

python/ray/_private/authentication/authentication_utils.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,22 @@ def validate_request_token(auth_header: str) -> bool:
4141
# validate_authentication_token expects full "Bearer <token>" format
4242
# and performs equality comparison via C++ layer
4343
return validate_authentication_token(auth_header)
44+
45+
46+
def get_authentication_mode_name(mode: AuthenticationMode) -> str:
47+
"""Convert AuthenticationMode enum value to string name.
48+
49+
Args:
50+
mode: AuthenticationMode enum value from ray._raylet
51+
52+
Returns:
53+
String name: "disabled", "token", or "k8s"
54+
"""
55+
from ray._raylet import AuthenticationMode
56+
57+
_MODE_NAMES = {
58+
AuthenticationMode.DISABLED: "disabled",
59+
AuthenticationMode.TOKEN: "token",
60+
AuthenticationMode.K8S: "k8s",
61+
}
62+
return _MODE_NAMES.get(mode, "unknown")

python/ray/dashboard/http_server_head.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from ray._private.authentication.http_token_authentication import (
2828
get_token_auth_middleware,
2929
)
30-
from ray._raylet import AuthenticationMode, get_authentication_mode
30+
from ray._raylet import get_authentication_mode
3131
from ray.dashboard.dashboard_metrics import DashboardPrometheusMetrics
3232
from ray.dashboard.head import DashboardHeadModule
3333

@@ -171,12 +171,7 @@ async def get_timezone(self, req) -> aiohttp.web.Response:
171171
async def get_authentication_mode(self, req) -> aiohttp.web.Response:
172172
try:
173173
mode = get_authentication_mode()
174-
if mode == AuthenticationMode.TOKEN:
175-
mode_str = "token"
176-
elif mode == AuthenticationMode.K8S:
177-
mode_str = "k8s"
178-
else:
179-
mode_str = "disabled"
174+
mode_str = auth_utils.get_authentication_mode_name(mode)
180175

181176
response = aiohttp.web.json_response({"authentication_mode": mode_str})
182177

python/ray/exceptions.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -495,10 +495,29 @@ def __init__(self, message: str):
495495
super().__init__(message)
496496

497497
def __str__(self) -> str:
498-
return self.message + (
499-
". Ensure that you have `RAY_AUTH_MODE=token` set and the token for the cluster is available as the `RAY_AUTH_TOKEN` environment variable or a local file. "
498+
# Check if RAY_AUTH_MODE is set to token and add a heads-up if not
499+
auth_mode_note = ""
500+
501+
from ray._private.authentication.authentication_utils import (
502+
get_authentication_mode_name,
503+
)
504+
from ray._raylet import AuthenticationMode, get_authentication_mode
505+
506+
current_mode = get_authentication_mode()
507+
if current_mode != AuthenticationMode.TOKEN:
508+
mode_name = get_authentication_mode_name(current_mode)
509+
auth_mode_note = (
510+
f" Note: RAY_AUTH_MODE is currently '{mode_name}' (not 'token')."
511+
)
512+
513+
help_text = (
514+
" Ensure that the token for the cluster is available in a local file (e.g., ~/.ray/auth_token or via "
515+
"RAY_AUTH_TOKEN_PATH) or as the `RAY_AUTH_TOKEN` environment variable. "
516+
"To generate a token for local development, use `ray get-auth-token --generate` "
517+
"For remote clusters, ensure that the token is propagated to all nodes of the cluster when token authentication is enabled. "
500518
"For more information, see: https://docs.ray.io/en/latest/ray-security/auth.html"
501519
)
520+
return self.message + "." + auth_mode_note + help_text
502521

503522

504523
@DeveloperAPI

python/ray/scripts/scripts.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -942,12 +942,7 @@ def start(
942942
)
943943

944944
# Ensure auth token is available if authentication mode is token
945-
try:
946-
ensure_token_if_auth_enabled(system_config, create_token_if_missing=False)
947-
except ray.exceptions.AuthenticationError:
948-
raise RuntimeError(
949-
"Failed to load authentication token. To generate a token for local development, use `ray get-auth-token --generate`. For remote clusters, ensure that the token is propagated to all nodes of the cluster when token authentication is enabled."
950-
)
945+
ensure_token_if_auth_enabled(system_config, create_token_if_missing=False)
951946

952947
node = ray._private.node.Node(
953948
ray_params, head=True, shutdown_at_exit=block, spawn_reaper=block
Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,68 @@
11
"""Tests for Ray exceptions."""
22
import sys
3+
from enum import Enum
4+
from unittest.mock import MagicMock, patch
35

46
import pytest
57

68
from ray.exceptions import AuthenticationError, RayError
79

810

11+
class FakeAuthMode(Enum):
12+
DISABLED = 0
13+
TOKEN = 1
14+
K8S = 2
15+
16+
917
class TestAuthenticationError:
1018
"""Tests for AuthenticationError exception."""
1119

1220
auth_doc_url = "https://docs.ray.io/en/latest/ray-security/auth.html"
1321

1422
def test_basic_creation(self):
15-
"""Test basic AuthenticationError creation."""
23+
"""Test basic AuthenticationError creation and message format."""
1624
error = AuthenticationError("Token is missing")
1725
error_str = str(error)
1826

27+
# Original message preserved
1928
assert "Token is missing" in error_str
29+
# Doc URL included
2030
assert self.auth_doc_url in error_str
2131

2232
def test_is_ray_error_subclass(self):
2333
"""Test that AuthenticationError is a RayError subclass."""
2434
error = AuthenticationError("Test")
2535
assert isinstance(error, RayError)
2636

37+
@pytest.mark.parametrize(
38+
"auth_mode,expected_note",
39+
[
40+
(FakeAuthMode.DISABLED, "RAY_AUTH_MODE is currently 'disabled'"),
41+
(FakeAuthMode.K8S, "RAY_AUTH_MODE is currently 'k8s'"),
42+
(FakeAuthMode.TOKEN, None),
43+
],
44+
ids=["disabled", "k8s", "token"],
45+
)
46+
def test_auth_mode_note_in_message(self, auth_mode, expected_note):
47+
"""Test that error message includes auth mode note when not in token mode."""
48+
with patch.dict(
49+
"sys.modules",
50+
{
51+
"ray._raylet": MagicMock(
52+
AuthenticationMode=FakeAuthMode,
53+
get_authentication_mode=lambda: auth_mode,
54+
)
55+
},
56+
):
57+
error = AuthenticationError("Token is missing")
58+
error_str = str(error)
59+
60+
assert "Token is missing" in error_str
61+
if expected_note:
62+
assert expected_note in error_str
63+
else:
64+
assert "RAY_AUTH_MODE is currently" not in error_str
65+
2766

2867
if __name__ == "__main__":
2968
sys.exit(pytest.main(["-v", __file__]))

python/ray/tests/test_token_auth_integration.py

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,34 @@ def test_connect_without_token_raises_error(setup_cluster_with_token_auth):
176176
ray.init(address=cluster.address)
177177

178178

179+
@pytest.mark.parametrize(
180+
"token,expected_status",
181+
[
182+
(None, 401), # No token -> Unauthorized
183+
("wrong_token", 403), # Wrong token -> Forbidden
184+
],
185+
ids=["no_token", "wrong_token"],
186+
)
187+
def test_state_api_auth_failure(token, expected_status, setup_cluster_with_token_auth):
188+
"""Test that state API calls fail with missing or incorrect token."""
189+
import requests
190+
191+
cluster_info = setup_cluster_with_token_auth
192+
dashboard_url = cluster_info["dashboard_url"]
193+
194+
# Make direct HTTP request to state API endpoint
195+
headers = {}
196+
if token is not None:
197+
headers["Authorization"] = f"Bearer {token}"
198+
199+
response = requests.get(f"{dashboard_url}/api/v0/actors", headers=headers)
200+
201+
assert response.status_code == expected_status, (
202+
f"State API should return {expected_status}, got {response.status_code}: "
203+
f"{response.text}"
204+
)
205+
206+
179207
@pytest.mark.parametrize("tokens_match", [True, False])
180208
def test_cluster_token_authentication(tokens_match, setup_cluster_with_token_auth):
181209
"""Test cluster authentication with matching and non-matching tokens."""
@@ -363,9 +391,10 @@ def test_e2e_operations_with_token_auth(setup_cluster_with_token_auth):
363391
"""Test that e2e operations work with token authentication enabled.
364392
365393
This verifies that with token auth enabled:
366-
1. Job submission works
367-
2. Tasks execute successfully
368-
3. Actors can be created and called
394+
1. Tasks execute successfully
395+
2. Actors can be created and called
396+
3. State API works (list_nodes, list_actors, list_tasks)
397+
4. Job submission works
369398
"""
370399
cluster_info = setup_cluster_with_token_auth
371400

@@ -391,7 +420,26 @@ def increment(self):
391420
result = ray.get(actor.increment.remote())
392421
assert result == 1, f"Actor method should return 1, got {result}"
393422

394-
# Test 3: Submit a job and wait for completion
423+
# Test 3: State API operations (uses HTTP with auth headers)
424+
from ray.util.state import list_actors, list_nodes, list_tasks
425+
426+
# List nodes - should include at least the head node
427+
nodes = list_nodes()
428+
assert len(nodes) >= 1, f"Expected at least 1 node, got {len(nodes)}"
429+
430+
# List actors - should include our SimpleActor
431+
actors = list_actors()
432+
assert len(actors) >= 1, f"Expected at least 1 actor, got {len(actors)}"
433+
actor_classes = [a.class_name for a in actors]
434+
assert (
435+
"SimpleActor" in actor_classes[0]
436+
), f"SimpleActor not found in {actor_classes}"
437+
438+
# List tasks - should include completed tasks
439+
tasks = list_tasks()
440+
assert len(tasks) >= 1, f"Expected at least 1 task, got {len(tasks)}"
441+
442+
# Test 4: Submit a job and wait for completion
395443
from ray.job_submission import JobSubmissionClient
396444

397445
# Create job submission client (uses HTTP with auth headers)

src/ray/rpc/authentication/authentication_token_loader.cc

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,9 @@ std::optional<AuthenticationToken> AuthenticationTokenLoader::GetToken(
6363
RAY_LOG(FATAL)
6464
<< "Token authentication is enabled but Ray couldn't find an "
6565
"authentication token. "
66-
<< "Set the RAY_AUTH_TOKEN environment variable, or set RAY_AUTH_TOKEN_PATH to "
67-
"point to a file with the token, "
68-
"or create a token file at ~/.ray/auth_token.";
66+
<< "Create a token file at ~/.ray/auth_token, "
67+
"or store the token in any file and set RAY_AUTH_TOKEN_PATH to point to it, "
68+
"or set the RAY_AUTH_TOKEN environment variable.";
6969
}
7070

7171
// Cache and return the loaded token
@@ -119,8 +119,8 @@ AuthenticationToken AuthenticationTokenLoader::LoadTokenFromSources() {
119119
if (env_token != nullptr) {
120120
std::string token_str(env_token);
121121
if (!token_str.empty()) {
122-
RAY_LOG(DEBUG) << "Loaded authentication token from RAY_AUTH_TOKEN environment "
123-
"variable";
122+
RAY_LOG(INFO) << "Loaded authentication token from RAY_AUTH_TOKEN environment "
123+
"variable";
124124
return AuthenticationToken(TrimWhitespace(token_str));
125125
}
126126
}
@@ -136,7 +136,8 @@ AuthenticationToken AuthenticationTokenLoader::LoadTokenFromSources() {
136136
"but file cannot be opened or is empty: "
137137
<< path_str;
138138
}
139-
RAY_LOG(INFO) << "Loaded authentication token from file: " << path_str;
139+
RAY_LOG(INFO) << "Loaded authentication token from file (RAY_AUTH_TOKEN_PATH): "
140+
<< path_str;
140141
return AuthenticationToken(token_str);
141142
}
142143
}
@@ -145,7 +146,7 @@ AuthenticationToken AuthenticationTokenLoader::LoadTokenFromSources() {
145146
if (GetAuthenticationMode() == AuthenticationMode::K8S) {
146147
std::string token_str = TrimWhitespace(ReadTokenFromFile(k8s::kK8sSaTokenPath));
147148
if (!token_str.empty()) {
148-
RAY_LOG(DEBUG)
149+
RAY_LOG(INFO)
149150
<< "Loaded authentication token from Kubernetes service account path: "
150151
<< k8s::kK8sSaTokenPath;
151152
return AuthenticationToken(token_str);
@@ -158,7 +159,7 @@ AuthenticationToken AuthenticationTokenLoader::LoadTokenFromSources() {
158159
std::string default_path = GetDefaultTokenPath();
159160
std::string token_str = TrimWhitespace(ReadTokenFromFile(default_path));
160161
if (!token_str.empty()) {
161-
RAY_LOG(DEBUG) << "Loaded authentication token from default path: " << default_path;
162+
RAY_LOG(INFO) << "Loaded authentication token from default path: " << default_path;
162163
return AuthenticationToken(token_str);
163164
}
164165

0 commit comments

Comments
 (0)