Skip to content

Commit 9527a55

Browse files
edoakessampan-s-nayaksampan
authored
[core] Cherry pick token authentication UX improvements (#58831)
Cherry pick: #58737 Signed-off-by: sampan <[email protected]> Signed-off-by: Edward Oakes <[email protected]> Co-authored-by: Sampan S Nayak <[email protected]> Co-authored-by: sampan <[email protected]>
1 parent d7f8055 commit 9527a55

File tree

18 files changed

+156
-115
lines changed

18 files changed

+156
-115
lines changed

doc/source/ray-core/api/exceptions.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,4 @@ Exceptions
4040
ray.exceptions.RaySystemError
4141
ray.exceptions.NodeDiedError
4242
ray.exceptions.UnserializableException
43+
ray.exceptions.AuthenticationError

python/ray/_private/authentication/authentication_constants.py

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,11 @@
1-
# Token setup instructions (used in multiple contexts)
2-
TOKEN_SETUP_INSTRUCTIONS = """Please provide an authentication token using one of these methods:
3-
1. Set the RAY_AUTH_TOKEN environment variable
4-
2. Set the RAY_AUTH_TOKEN_PATH environment variable (pointing to a token file)
5-
3. Create a token file at the default location: ~/.ray/auth_token"""
6-
7-
# When token auth is enabled but no token is found anywhere
1+
# Authentication error messages
82
TOKEN_AUTH_ENABLED_BUT_NO_TOKEN_FOUND_ERROR_MESSAGE = (
9-
"Token authentication is enabled but no authentication token was found. "
10-
+ TOKEN_SETUP_INSTRUCTIONS
11-
)
12-
13-
# When HTTP request fails with 401 (Unauthorized - missing token)
14-
HTTP_REQUEST_MISSING_TOKEN_ERROR_MESSAGE = (
15-
"The Ray cluster requires authentication, but no token was provided.\n\n"
16-
+ TOKEN_SETUP_INSTRUCTIONS
3+
"Token authentication is enabled but no authentication token was found."
174
)
185

19-
# When HTTP request fails with 403 (Forbidden - invalid token)
20-
HTTP_REQUEST_INVALID_TOKEN_ERROR_MESSAGE = (
21-
"The authentication token you provided is invalid or incorrect.\n\n"
22-
+ TOKEN_SETUP_INSTRUCTIONS
23-
)
6+
TOKEN_INVALID_ERROR_MESSAGE = "Token authentication is enabled but the authentication token is invalid or incorrect." # noqa: E501
247

8+
# HTTP header and cookie constants
259
AUTHORIZATION_HEADER_NAME = "authorization"
2610
AUTHORIZATION_BEARER_PREFIX = "Bearer "
2711
RAY_AUTHORIZATION_HEADER_NAME = "x-ray-authorization"

python/ray/_private/authentication/authentication_token_setup.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
AuthenticationTokenLoader,
2121
get_authentication_mode,
2222
)
23+
from ray.exceptions import AuthenticationError
2324

2425
logger = logging.getLogger(__name__)
2526

@@ -97,4 +98,6 @@ def ensure_token_if_auth_enabled(
9798
# Reload the cache so subsequent calls to token_loader read the new token.
9899
token_loader.reset_cache()
99100
else:
100-
raise RuntimeError(TOKEN_AUTH_ENABLED_BUT_NO_TOKEN_FOUND_ERROR_MESSAGE)
101+
raise AuthenticationError(
102+
TOKEN_AUTH_ENABLED_BUT_NO_TOKEN_FOUND_ERROR_MESSAGE
103+
)

python/ray/_private/authentication/http_token_authentication.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,13 @@ def format_authentication_http_error(status: int, body: str) -> Optional[str]:
117117
if status == 401:
118118
return "Authentication required: {body}\n\n{details}".format(
119119
body=body,
120-
details=authentication_constants.HTTP_REQUEST_MISSING_TOKEN_ERROR_MESSAGE,
120+
details=authentication_constants.TOKEN_AUTH_ENABLED_BUT_NO_TOKEN_FOUND_ERROR_MESSAGE,
121121
)
122122

123123
if status == 403:
124124
return "Authentication failed: {body}\n\n{details}".format(
125125
body=body,
126-
details=authentication_constants.HTTP_REQUEST_INVALID_TOKEN_ERROR_MESSAGE,
126+
details=authentication_constants.TOKEN_INVALID_ERROR_MESSAGE,
127127
)
128128

129129
return None

python/ray/dashboard/client/src/authentication/TokenAuthenticationDialog.component.test.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ describe("TokenAuthenticationDialog", () => {
2121
);
2222

2323
expect(
24-
screen.getByText("Token Authentication Required"),
24+
screen.getByText("Authentication Token Required"),
2525
).toBeInTheDocument();
2626
expect(
27-
screen.getByText(/token authentication is enabled for this cluster/i),
27+
screen.getByText(/Token authentication is enabled/),
2828
).toBeInTheDocument();
2929
});
3030

@@ -38,10 +38,10 @@ describe("TokenAuthenticationDialog", () => {
3838
);
3939

4040
expect(
41-
screen.getByText("Token Authentication Required"),
41+
screen.getByText("Authentication Token Required"),
4242
).toBeInTheDocument();
4343
expect(
44-
screen.getByText(/authentication token is invalid or has expired/i),
44+
screen.getByText(/The existing authentication token is invalid/),
4545
).toBeInTheDocument();
4646
});
4747

@@ -71,7 +71,7 @@ describe("TokenAuthenticationDialog", () => {
7171
/>,
7272
);
7373

74-
const input = screen.getByLabelText(/authentication token/i);
74+
const input = screen.getByLabelText("Authentication Token");
7575
await user.type(input, "test-token-123");
7676

7777
const submitButton = screen.getByRole("button", { name: /submit/i });
@@ -94,7 +94,7 @@ describe("TokenAuthenticationDialog", () => {
9494
/>,
9595
);
9696

97-
const input = screen.getByLabelText(/authentication token/i);
97+
const input = screen.getByLabelText("Authentication Token");
9898
await user.type(input, "test-token-123{Enter}");
9999

100100
await waitFor(() => {
@@ -128,7 +128,7 @@ describe("TokenAuthenticationDialog", () => {
128128
const submitButton = screen.getByRole("button", { name: /submit/i });
129129
expect(submitButton).toBeDisabled();
130130

131-
const input = screen.getByLabelText(/authentication token/i);
131+
const input = screen.getByLabelText("Authentication Token");
132132
await user.type(input, "test-token");
133133

134134
expect(submitButton).not.toBeDisabled();
@@ -144,7 +144,7 @@ describe("TokenAuthenticationDialog", () => {
144144
/>,
145145
);
146146

147-
const input = screen.getByLabelText(/authentication token/i);
147+
const input = screen.getByLabelText("Authentication Token");
148148
await user.type(input, "secret-token");
149149

150150
// Initially should be password type (hidden)
@@ -177,7 +177,7 @@ describe("TokenAuthenticationDialog", () => {
177177
/>,
178178
);
179179

180-
const input = screen.getByLabelText(/authentication token/i);
180+
const input = screen.getByLabelText("Authentication Token");
181181
await user.type(input, "test-token");
182182

183183
const submitButton = screen.getByRole("button", { name: /submit/i });
@@ -200,7 +200,7 @@ describe("TokenAuthenticationDialog", () => {
200200

201201
// Dialog should not be visible
202202
expect(
203-
screen.queryByText("Token Authentication Required"),
203+
screen.queryByText("Authentication Token Required"),
204204
).not.toBeInTheDocument();
205205
});
206206
});

python/ray/dashboard/client/src/authentication/TokenAuthenticationDialog.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,13 @@ export const TokenAuthenticationDialog: React.FC<TokenAuthenticationDialogProps>
6868
setShowToken(!showToken);
6969
};
7070

71-
// Different messages based on whether this is initial auth or re-auth
72-
const title = "Token Authentication Required";
73-
const message = hasExistingToken
74-
? "The authentication token is invalid or has expired. Please provide a valid authentication token."
75-
: "Token authentication is enabled for this cluster. Please provide a valid authentication token.";
71+
// Different messages based on whether this is initial auth or re-auth.
72+
const title = "Authentication Token Required";
73+
const message =
74+
(hasExistingToken
75+
? "The existing authentication token is invalid."
76+
: "Token authentication is enabled.") +
77+
" Provide the matching authentication token for this cluster.\n- Local clusters: use `ray get-auth-token` to retrieve it.\n- Remote clusters: you must retrieve the token that was used when creating the cluster.\n\nSee: https://docs.ray.io/en/latest/ray-security/auth.html";
7678

7779
return (
7880
<Dialog
@@ -88,7 +90,7 @@ export const TokenAuthenticationDialog: React.FC<TokenAuthenticationDialogProps>
8890
<DialogContent>
8991
<DialogContentText
9092
id="token-auth-dialog-description"
91-
sx={{ marginBottom: 2 }}
93+
sx={{ marginBottom: 2, whiteSpace: "pre-line" }}
9294
>
9395
{message}
9496
</DialogContentText>

python/ray/dashboard/modules/dashboard_sdk.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from ray._private.utils import split_address
2727
from ray.autoscaler._private.cli_logger import cli_logger
2828
from ray.dashboard.modules.job.common import uri_to_http_components
29+
from ray.exceptions import AuthenticationError
2930
from ray.util.annotations import DeveloperAPI, PublicAPI
3031

3132
try:
@@ -323,7 +324,7 @@ def _do_request(
323324
response.status_code, response.text
324325
)
325326
if formatted_error:
326-
raise RuntimeError(formatted_error)
327+
raise AuthenticationError(formatted_error)
327328

328329
return response
329330

python/ray/exceptions.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,30 @@ def __str__(self):
477477
return error_msg
478478

479479

480+
@PublicAPI
481+
class AuthenticationError(RayError):
482+
"""Indicates that an authentication error occurred.
483+
484+
Most commonly, this is caused by a missing or mismatching token set on the client
485+
(e.g., a Ray CLI command interacting with a remote cluster).
486+
487+
Only applicable when `RAY_AUTH_MODE` is not set to `disabled`.
488+
"""
489+
490+
def __init__(self, message: str):
491+
self.message = message
492+
493+
# Always hide traceback for cleaner output
494+
self.__suppress_context__ = True
495+
super().__init__(message)
496+
497+
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. "
500+
"For more information, see: https://docs.ray.io/en/latest/ray-security/auth.html"
501+
)
502+
503+
480504
@DeveloperAPI
481505
class UserCodeException(RayError):
482506
"""Indicates that an exception occurred while executing user code.
@@ -963,4 +987,5 @@ def __str__(self):
963987
OufOfBandObjectRefSerializationException,
964988
RayCgraphCapacityExceeded,
965989
UnserializableException,
990+
AuthenticationError,
966991
]

python/ray/includes/common.pxd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ cdef extern from "ray/common/status.h" namespace "ray" nogil:
133133
c_bool IsUnexpectedSystemExit()
134134
c_bool IsChannelError()
135135
c_bool IsChannelTimeoutError()
136+
c_bool IsUnauthenticated()
136137

137138
c_string ToString()
138139
c_string CodeAsString()

python/ray/includes/common.pxi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ from ray.exceptions import (
3535
ActorDiedError,
3636
RayError,
3737
RaySystemError,
38+
AuthenticationError,
3839
RayTaskError,
3940
ObjectStoreFullError,
4041
OutOfDiskError,
@@ -105,6 +106,8 @@ cdef int check_status(const CRayStatus& status) except -1 nogil:
105106
raise ValueError(message)
106107
elif status.IsIOError():
107108
raise IOError(message)
109+
elif status.IsUnauthenticated():
110+
raise AuthenticationError(message)
108111
elif status.IsRpcError():
109112
raise RpcError(message, rpc_code=status.rpc_code())
110113
elif status.IsIntentionalSystemExit():

0 commit comments

Comments
 (0)