arize-phoenix 12.2.0__py3-none-any.whl → 12.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of arize-phoenix might be problematic. Click here for more details.
- {arize_phoenix-12.2.0.dist-info → arize_phoenix-12.4.0.dist-info}/METADATA +2 -1
- {arize_phoenix-12.2.0.dist-info → arize_phoenix-12.4.0.dist-info}/RECORD +45 -44
- phoenix/auth.py +19 -0
- phoenix/config.py +302 -53
- phoenix/db/README.md +546 -28
- phoenix/server/api/auth_messages.py +46 -0
- phoenix/server/api/routers/auth.py +21 -30
- phoenix/server/api/routers/oauth2.py +255 -46
- phoenix/server/api/routers/v1/__init__.py +2 -3
- phoenix/server/api/routers/v1/annotation_configs.py +12 -29
- phoenix/server/api/routers/v1/annotations.py +21 -22
- phoenix/server/api/routers/v1/datasets.py +38 -56
- phoenix/server/api/routers/v1/documents.py +2 -3
- phoenix/server/api/routers/v1/evaluations.py +12 -24
- phoenix/server/api/routers/v1/experiment_evaluations.py +2 -3
- phoenix/server/api/routers/v1/experiment_runs.py +9 -10
- phoenix/server/api/routers/v1/experiments.py +16 -17
- phoenix/server/api/routers/v1/projects.py +15 -21
- phoenix/server/api/routers/v1/prompts.py +30 -31
- phoenix/server/api/routers/v1/sessions.py +2 -5
- phoenix/server/api/routers/v1/spans.py +35 -26
- phoenix/server/api/routers/v1/traces.py +11 -19
- phoenix/server/api/routers/v1/users.py +14 -23
- phoenix/server/api/routers/v1/utils.py +3 -7
- phoenix/server/app.py +6 -2
- phoenix/server/authorization.py +2 -3
- phoenix/server/bearer_auth.py +4 -5
- phoenix/server/cost_tracking/model_cost_manifest.json +54 -54
- phoenix/server/oauth2.py +174 -9
- phoenix/server/static/.vite/manifest.json +39 -39
- phoenix/server/static/assets/{components-BG6v0EM8.js → components-BvsExS75.js} +422 -387
- phoenix/server/static/assets/{index-CSVcULw1.js → index-iq8WDxat.js} +12 -12
- phoenix/server/static/assets/{pages-DgaM7kpM.js → pages-Ckg4SLQ9.js} +542 -488
- phoenix/server/static/assets/vendor-D2eEI-6h.js +914 -0
- phoenix/server/static/assets/{vendor-arizeai-DlOj0PQQ.js → vendor-arizeai-kfOei7nf.js} +2 -2
- phoenix/server/static/assets/{vendor-codemirror-B2PHH5yZ.js → vendor-codemirror-1bq_t1Ec.js} +3 -3
- phoenix/server/static/assets/{vendor-recharts-CKsi4IjN.js → vendor-recharts-DQ4xfrf4.js} +1 -1
- phoenix/server/static/assets/{vendor-shiki-DN26BkKE.js → vendor-shiki-GGmcIQxA.js} +1 -1
- phoenix/server/templates/index.html +1 -0
- phoenix/trace/attributes.py +80 -13
- phoenix/version.py +1 -1
- phoenix/server/static/assets/vendor-BqTEkGQU.js +0 -903
- {arize_phoenix-12.2.0.dist-info → arize_phoenix-12.4.0.dist-info}/WHEEL +0 -0
- {arize_phoenix-12.2.0.dist-info → arize_phoenix-12.4.0.dist-info}/entry_points.txt +0 -0
- {arize_phoenix-12.2.0.dist-info → arize_phoenix-12.4.0.dist-info}/licenses/IP_NOTICE +0 -0
- {arize_phoenix-12.2.0.dist-info → arize_phoenix-12.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# ruff: noqa: E501
|
|
2
|
+
"""
|
|
3
|
+
Authentication error and success message codes.
|
|
4
|
+
|
|
5
|
+
These codes are used in authentication flows to safely communicate status
|
|
6
|
+
to users via query parameters. Using codes instead of raw messages prevents
|
|
7
|
+
social engineering and phishing attacks.
|
|
8
|
+
|
|
9
|
+
The messages are passed to the frontend via window.Config to ensure a single
|
|
10
|
+
source of truth between backend and frontend.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from types import MappingProxyType
|
|
14
|
+
from typing import Literal, Mapping, get_args
|
|
15
|
+
|
|
16
|
+
# Error code type - used for type hints in redirect functions
|
|
17
|
+
AuthErrorCode = Literal[
|
|
18
|
+
"unknown_idp",
|
|
19
|
+
"auth_failed",
|
|
20
|
+
"invalid_state",
|
|
21
|
+
"unsafe_return_url",
|
|
22
|
+
"oauth_error",
|
|
23
|
+
"no_oidc_support",
|
|
24
|
+
"missing_email_scope",
|
|
25
|
+
"email_in_use",
|
|
26
|
+
"sign_in_not_allowed",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# Error messages - passed to frontend via window.Config.authErrorMessages
|
|
30
|
+
# Backend generates these codes when redirecting users after OAuth errors
|
|
31
|
+
AUTH_ERROR_MESSAGES: Mapping[AuthErrorCode, str] = MappingProxyType(
|
|
32
|
+
{
|
|
33
|
+
"unknown_idp": "Unknown identity provider.",
|
|
34
|
+
"auth_failed": "Authentication failed. Please contact your administrator.",
|
|
35
|
+
"invalid_state": "Invalid authentication state. Please try again.",
|
|
36
|
+
"unsafe_return_url": "Invalid return URL. Please try again.",
|
|
37
|
+
"oauth_error": "Authentication failed. Please try again.",
|
|
38
|
+
"no_oidc_support": "Your identity provider does not appear to support OpenID Connect. Please contact your administrator.",
|
|
39
|
+
"missing_email_scope": "Please ensure your identity provider is configured to use the 'email' scope.",
|
|
40
|
+
"email_in_use": "An account with this email already exists.",
|
|
41
|
+
"sign_in_not_allowed": "Sign in is not allowed. Please contact your administrator.",
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Runtime assertion to ensure AUTH_ERROR_MESSAGES keys match AuthErrorCode Literal values
|
|
46
|
+
assert set(AUTH_ERROR_MESSAGES.keys()) == set(get_args(AuthErrorCode))
|
|
@@ -7,15 +7,6 @@ from urllib.parse import urlencode, urlparse, urlunparse
|
|
|
7
7
|
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
|
8
8
|
from sqlalchemy import func, select
|
|
9
9
|
from sqlalchemy.orm import joinedload
|
|
10
|
-
from starlette.status import (
|
|
11
|
-
HTTP_204_NO_CONTENT,
|
|
12
|
-
HTTP_302_FOUND,
|
|
13
|
-
HTTP_401_UNAUTHORIZED,
|
|
14
|
-
HTTP_403_FORBIDDEN,
|
|
15
|
-
HTTP_404_NOT_FOUND,
|
|
16
|
-
HTTP_422_UNPROCESSABLE_ENTITY,
|
|
17
|
-
HTTP_503_SERVICE_UNAVAILABLE,
|
|
18
|
-
)
|
|
19
10
|
|
|
20
11
|
from phoenix.auth import (
|
|
21
12
|
DEFAULT_SECRET_LENGTH,
|
|
@@ -76,7 +67,7 @@ router = APIRouter(prefix="/auth", include_in_schema=False, dependencies=auth_de
|
|
|
76
67
|
@router.post("/login")
|
|
77
68
|
async def login(request: Request) -> Response:
|
|
78
69
|
if get_env_disable_basic_auth():
|
|
79
|
-
raise HTTPException(status_code=
|
|
70
|
+
raise HTTPException(status_code=403)
|
|
80
71
|
assert isinstance(access_token_expiry := request.app.state.access_token_expiry, timedelta)
|
|
81
72
|
assert isinstance(refresh_token_expiry := request.app.state.refresh_token_expiry, timedelta)
|
|
82
73
|
token_store: TokenStore = request.app.state.get_token_store()
|
|
@@ -85,7 +76,7 @@ async def login(request: Request) -> Response:
|
|
|
85
76
|
password = data.get("password")
|
|
86
77
|
|
|
87
78
|
if not email or not password:
|
|
88
|
-
raise HTTPException(status_code=
|
|
79
|
+
raise HTTPException(status_code=401, detail="Email and password required")
|
|
89
80
|
|
|
90
81
|
# Sanitize email by trimming and lowercasing
|
|
91
82
|
email = sanitize_email(email)
|
|
@@ -101,14 +92,14 @@ async def login(request: Request) -> Response:
|
|
|
101
92
|
or (password_hash := user.password_hash) is None
|
|
102
93
|
or (salt := user.password_salt) is None
|
|
103
94
|
):
|
|
104
|
-
raise HTTPException(status_code=
|
|
95
|
+
raise HTTPException(status_code=401, detail=LOGIN_FAILED_MESSAGE)
|
|
105
96
|
|
|
106
97
|
loop = asyncio.get_running_loop()
|
|
107
98
|
password_is_valid = partial(
|
|
108
99
|
is_valid_password, password=password, salt=salt, password_hash=password_hash
|
|
109
100
|
)
|
|
110
101
|
if not await loop.run_in_executor(None, password_is_valid):
|
|
111
|
-
raise HTTPException(status_code=
|
|
102
|
+
raise HTTPException(status_code=401, detail=LOGIN_FAILED_MESSAGE)
|
|
112
103
|
|
|
113
104
|
access_token, refresh_token = await create_access_and_refresh_tokens(
|
|
114
105
|
token_store=token_store,
|
|
@@ -116,7 +107,7 @@ async def login(request: Request) -> Response:
|
|
|
116
107
|
access_token_expiry=access_token_expiry,
|
|
117
108
|
refresh_token_expiry=refresh_token_expiry,
|
|
118
109
|
)
|
|
119
|
-
response = Response(status_code=
|
|
110
|
+
response = Response(status_code=204)
|
|
120
111
|
response = set_access_token_cookie(
|
|
121
112
|
response=response, access_token=access_token, max_age=access_token_expiry
|
|
122
113
|
)
|
|
@@ -146,7 +137,7 @@ async def logout(
|
|
|
146
137
|
await token_store.log_out(user_id)
|
|
147
138
|
redirect_path = "/logout" if get_env_disable_basic_auth() else "/login"
|
|
148
139
|
redirect_url = prepend_root_path(request.scope, redirect_path)
|
|
149
|
-
response = Response(status_code=
|
|
140
|
+
response = Response(status_code=302, headers={"Location": redirect_url})
|
|
150
141
|
response = delete_access_token_cookie(response)
|
|
151
142
|
response = delete_refresh_token_cookie(response)
|
|
152
143
|
response = delete_oauth2_state_cookie(response)
|
|
@@ -159,7 +150,7 @@ async def refresh_tokens(request: Request) -> Response:
|
|
|
159
150
|
assert isinstance(access_token_expiry := request.app.state.access_token_expiry, timedelta)
|
|
160
151
|
assert isinstance(refresh_token_expiry := request.app.state.refresh_token_expiry, timedelta)
|
|
161
152
|
if (refresh_token := request.cookies.get(PHOENIX_REFRESH_TOKEN_COOKIE_NAME)) is None:
|
|
162
|
-
raise HTTPException(status_code=
|
|
153
|
+
raise HTTPException(status_code=401, detail="Missing refresh token")
|
|
163
154
|
token_store: TokenStore = request.app.state.get_token_store()
|
|
164
155
|
refresh_token_claims = await token_store.read(Token(refresh_token))
|
|
165
156
|
if (
|
|
@@ -169,9 +160,9 @@ async def refresh_tokens(request: Request) -> Response:
|
|
|
169
160
|
or (user_id := int(refresh_token_claims.subject)) is None
|
|
170
161
|
or (expiration_time := refresh_token_claims.expiration_time) is None
|
|
171
162
|
):
|
|
172
|
-
raise HTTPException(status_code=
|
|
163
|
+
raise HTTPException(status_code=401, detail="Invalid refresh token")
|
|
173
164
|
if expiration_time.timestamp() < datetime.now().timestamp():
|
|
174
|
-
raise HTTPException(status_code=
|
|
165
|
+
raise HTTPException(status_code=401, detail="Expired refresh token")
|
|
175
166
|
await token_store.revoke(refresh_token_id)
|
|
176
167
|
|
|
177
168
|
if (
|
|
@@ -189,14 +180,14 @@ async def refresh_tokens(request: Request) -> Response:
|
|
|
189
180
|
select(models.User).filter_by(id=user_id).options(joinedload(models.User.role))
|
|
190
181
|
)
|
|
191
182
|
) is None:
|
|
192
|
-
raise HTTPException(status_code=
|
|
183
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
193
184
|
access_token, refresh_token = await create_access_and_refresh_tokens(
|
|
194
185
|
token_store=token_store,
|
|
195
186
|
user=user,
|
|
196
187
|
access_token_expiry=access_token_expiry,
|
|
197
188
|
refresh_token_expiry=refresh_token_expiry,
|
|
198
189
|
)
|
|
199
|
-
response = Response(status_code=
|
|
190
|
+
response = Response(status_code=204)
|
|
200
191
|
response = set_access_token_cookie(
|
|
201
192
|
response=response, access_token=access_token, max_age=access_token_expiry
|
|
202
193
|
)
|
|
@@ -209,7 +200,7 @@ async def refresh_tokens(request: Request) -> Response:
|
|
|
209
200
|
@router.post("/password-reset-email")
|
|
210
201
|
async def initiate_password_reset(request: Request) -> Response:
|
|
211
202
|
if get_env_disable_basic_auth():
|
|
212
|
-
raise HTTPException(status_code=
|
|
203
|
+
raise HTTPException(status_code=403)
|
|
213
204
|
data = await request.json()
|
|
214
205
|
if not (email := data.get("email")):
|
|
215
206
|
raise MISSING_EMAIL
|
|
@@ -231,7 +222,7 @@ async def initiate_password_reset(request: Request) -> Response:
|
|
|
231
222
|
)
|
|
232
223
|
if user is None or user.auth_method != "LOCAL":
|
|
233
224
|
# Withold privileged information
|
|
234
|
-
return Response(status_code=
|
|
225
|
+
return Response(status_code=204)
|
|
235
226
|
token_store: TokenStore = request.app.state.get_token_store()
|
|
236
227
|
if user.password_reset_token:
|
|
237
228
|
await token_store.revoke(PasswordResetTokenId(user.password_reset_token.id))
|
|
@@ -247,13 +238,13 @@ async def initiate_password_reset(request: Request) -> Response:
|
|
|
247
238
|
components = (url.scheme, url.netloc, path, "", query_string, "")
|
|
248
239
|
reset_url = urlunparse(components)
|
|
249
240
|
await sender.send_password_reset_email(email, reset_url)
|
|
250
|
-
return Response(status_code=
|
|
241
|
+
return Response(status_code=204)
|
|
251
242
|
|
|
252
243
|
|
|
253
244
|
@router.post("/password-reset")
|
|
254
245
|
async def reset_password(request: Request) -> Response:
|
|
255
246
|
if get_env_disable_basic_auth():
|
|
256
|
-
raise HTTPException(status_code=
|
|
247
|
+
raise HTTPException(status_code=403)
|
|
257
248
|
data = await request.json()
|
|
258
249
|
if not (password := data.get("password")):
|
|
259
250
|
raise MISSING_PASSWORD
|
|
@@ -270,7 +261,7 @@ async def reset_password(request: Request) -> Response:
|
|
|
270
261
|
user = await session.scalar(select(models.User).filter_by(id=int(user_id)))
|
|
271
262
|
if user is None or user.auth_method != "LOCAL":
|
|
272
263
|
# Withold privileged information
|
|
273
|
-
return Response(status_code=
|
|
264
|
+
return Response(status_code=204)
|
|
274
265
|
validate_password_format(password)
|
|
275
266
|
user.password_salt = secrets.token_bytes(DEFAULT_SECRET_LENGTH)
|
|
276
267
|
loop = asyncio.get_running_loop()
|
|
@@ -281,7 +272,7 @@ async def reset_password(request: Request) -> Response:
|
|
|
281
272
|
async with request.app.state.db() as session:
|
|
282
273
|
session.add(user)
|
|
283
274
|
await session.flush()
|
|
284
|
-
response = Response(status_code=
|
|
275
|
+
response = Response(status_code=204)
|
|
285
276
|
assert (token_id := claims.token_id)
|
|
286
277
|
await token_store.revoke(token_id)
|
|
287
278
|
await token_store.log_out(UserId(user.id))
|
|
@@ -291,18 +282,18 @@ async def reset_password(request: Request) -> Response:
|
|
|
291
282
|
LOGIN_FAILED_MESSAGE = "Invalid email and/or password"
|
|
292
283
|
|
|
293
284
|
MISSING_EMAIL = HTTPException(
|
|
294
|
-
status_code=
|
|
285
|
+
status_code=422,
|
|
295
286
|
detail="Email required",
|
|
296
287
|
)
|
|
297
288
|
MISSING_PASSWORD = HTTPException(
|
|
298
|
-
status_code=
|
|
289
|
+
status_code=422,
|
|
299
290
|
detail="Password required",
|
|
300
291
|
)
|
|
301
292
|
SMTP_UNAVAILABLE = HTTPException(
|
|
302
|
-
status_code=
|
|
293
|
+
status_code=503,
|
|
303
294
|
detail="SMTP server not configured",
|
|
304
295
|
)
|
|
305
296
|
INVALID_TOKEN = HTTPException(
|
|
306
|
-
status_code=
|
|
297
|
+
status_code=401,
|
|
307
298
|
detail="Invalid token",
|
|
308
299
|
)
|