arize-phoenix 4.25.0__py3-none-any.whl → 4.26.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-4.25.0.dist-info → arize_phoenix-4.26.0.dist-info}/METADATA +3 -2
- {arize_phoenix-4.25.0.dist-info → arize_phoenix-4.26.0.dist-info}/RECORD +13 -11
- phoenix/auth.py +45 -0
- phoenix/db/engines.py +1 -1
- phoenix/server/api/input_types/UserRoleInput.py +9 -0
- phoenix/server/api/mutations/__init__.py +2 -0
- phoenix/server/api/mutations/user_mutations.py +89 -0
- phoenix/server/api/routers/auth.py +52 -0
- phoenix/server/app.py +25 -2
- phoenix/version.py +1 -1
- phoenix/auth/__init__.py +0 -0
- phoenix/auth/utils.py +0 -15
- {arize_phoenix-4.25.0.dist-info → arize_phoenix-4.26.0.dist-info}/WHEEL +0 -0
- {arize_phoenix-4.25.0.dist-info → arize_phoenix-4.26.0.dist-info}/licenses/IP_NOTICE +0 -0
- {arize_phoenix-4.25.0.dist-info → arize_phoenix-4.26.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: arize-phoenix
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.26.0
|
|
4
4
|
Summary: AI Observability and Evaluation
|
|
5
5
|
Project-URL: Documentation, https://docs.arize.com/phoenix/
|
|
6
6
|
Project-URL: Issues, https://github.com/Arize-ai/phoenix/issues
|
|
@@ -90,7 +90,7 @@ Requires-Dist: pandas>=1.0; extra == 'dev'
|
|
|
90
90
|
Requires-Dist: portpicker; extra == 'dev'
|
|
91
91
|
Requires-Dist: pre-commit; extra == 'dev'
|
|
92
92
|
Requires-Dist: prometheus-client; extra == 'dev'
|
|
93
|
-
Requires-Dist: psycopg[binary]; extra == 'dev'
|
|
93
|
+
Requires-Dist: psycopg[binary,pool]; extra == 'dev'
|
|
94
94
|
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
95
95
|
Requires-Dist: pytest-cov; extra == 'dev'
|
|
96
96
|
Requires-Dist: pytest-postgresql; extra == 'dev'
|
|
@@ -111,6 +111,7 @@ Requires-Dist: llama-index-readers-file==0.1.25; extra == 'llama-index'
|
|
|
111
111
|
Requires-Dist: llama-index==0.10.51; extra == 'llama-index'
|
|
112
112
|
Provides-Extra: pg
|
|
113
113
|
Requires-Dist: asyncpg; extra == 'pg'
|
|
114
|
+
Requires-Dist: psycopg[binary,pool]; extra == 'pg'
|
|
114
115
|
Provides-Extra: test
|
|
115
116
|
Description-Content-Type: text/markdown
|
|
116
117
|
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
phoenix/__init__.py,sha256=TGNWqm2UW-l67yIRpOtmqGHVAmdoobSNqUsiTtip7uQ,1542
|
|
2
|
+
phoenix/auth.py,sha256=N8vTFmc5BEsdX4xr6Bmh6OwBrNUQykr74LuCIkC28jA,1455
|
|
2
3
|
phoenix/config.py,sha256=wYA_8GSSz5rnpfIWDjeBL9ehKuTy9jqXaMZnxUqRYEU,10131
|
|
3
4
|
phoenix/datetime_utils.py,sha256=yDKjwX2Vtqw9h5F_ProtP-TsXidM43uIvmJ_pOzYc9A,3405
|
|
4
5
|
phoenix/exceptions.py,sha256=n2L2KKuecrdflB9MsCdAYCiSEvGJptIsfRkXMoJle7A,169
|
|
5
6
|
phoenix/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
6
7
|
phoenix/services.py,sha256=OyML4t2XGnlqF0JXA9_uccL8HslTABxep9Ci7MViKEU,5216
|
|
7
8
|
phoenix/settings.py,sha256=cO-qgis_S27nHirTobYI9hHPfZH18R--WMmxNdsVUwc,273
|
|
8
|
-
phoenix/version.py,sha256=
|
|
9
|
-
phoenix/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
phoenix/auth/utils.py,sha256=g-97oiJR219P6JvDUU5likHN4CdWtWo7bDJRmfVb0qk,450
|
|
9
|
+
phoenix/version.py,sha256=4h0uTKk4f4HhC84z3Ggthi61qR_IkvFgq-cnwxm5tCU,23
|
|
11
10
|
phoenix/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
11
|
phoenix/core/embedding_dimension.py,sha256=zKGbcvwOXgLf-yrJBpQyKtd-LEOPRKHnUToyAU8Owis,87
|
|
13
12
|
phoenix/core/model.py,sha256=km_a--PBHOuA337ClRw9xqhOHhrUT6Rl9pz_zV0JYkQ,4843
|
|
@@ -17,7 +16,7 @@ phoenix/db/README.md,sha256=IvKaZyf9ECbGBYYePaRhBveKZwDbxAc-c7BMxJYZh6Q,595
|
|
|
17
16
|
phoenix/db/__init__.py,sha256=pDjEFXukHmJBM-1D8RjmXkvLsz85YWNxMQczt81ec3A,118
|
|
18
17
|
phoenix/db/alembic.ini,sha256=p8DjVqGUs_tTx8oU56JP7qj-rMUebNFizItUSv_hPhs,3763
|
|
19
18
|
phoenix/db/bulk_inserter.py,sha256=qgg8pt5k4VnHKOE0-KoReXVAfXRhLt-sMZihI-b4X9I,12761
|
|
20
|
-
phoenix/db/engines.py,sha256=
|
|
19
|
+
phoenix/db/engines.py,sha256=l9Zl7mPd1q4RTXpThzYzc4Lo7TuuBwaGrC-zK0SMnn4,5300
|
|
21
20
|
phoenix/db/helpers.py,sha256=2zSc4n5IJfu-CaOFoBfqTB35M1nTFcAc8tqLsNtF2Jw,3488
|
|
22
21
|
phoenix/db/migrate.py,sha256=NNcci4LHw0wFR7U6quWrA-sw_A4h2lAA1_LePMLkb4w,2629
|
|
23
22
|
phoenix/db/models.py,sha256=lYWJYtD2asQwKU1B8JKyteWpHVYjhr1j0tmZdf9CQ5Y,23686
|
|
@@ -70,7 +69,7 @@ phoenix/pointcloud/pointcloud.py,sha256=4zAIkKs2xOUbchpj4XDAV-iPMXrfAJ15TG6rlIYG
|
|
|
70
69
|
phoenix/pointcloud/projectors.py,sha256=zO_RrtDYSv2rqVOfIP2_9Cv11Dc8EmcZR94xhFcBYPU,1057
|
|
71
70
|
phoenix/pointcloud/umap_parameters.py,sha256=3UQSjrysVOvq2V4KNpTMqNqNiK0BsTZnPBHWZ4fyJtQ,1708
|
|
72
71
|
phoenix/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
73
|
-
phoenix/server/app.py,sha256=
|
|
72
|
+
phoenix/server/app.py,sha256=KcxSXDR_awZlIcJg4YkUTZIXlje85_dpdMB2OM-M7SQ,22059
|
|
74
73
|
phoenix/server/dml_event.py,sha256=MpjCFqljxvgb9OB5Cez9vJesb3oHb3XxXictynBfcis,2851
|
|
75
74
|
phoenix/server/dml_event_handler.py,sha256=6p-PucctivelVHfO-_9zNxWZYPr_eGjDF3bKjLtc5co,8251
|
|
76
75
|
phoenix/server/grpc_server.py,sha256=jllxDNkpLQxDkvej4RhTokobowbvydF-SU8gSw1MTCc,3378
|
|
@@ -138,8 +137,9 @@ phoenix/server/api/input_types/SpanAnnotationSort.py,sha256=T5pAGzmh4MiJp9JMAzND
|
|
|
138
137
|
phoenix/server/api/input_types/SpanSort.py,sha256=Dhvl8BIoV52yHoqntfOax_gUc15uH8ITI_00Ha7PvYc,5959
|
|
139
138
|
phoenix/server/api/input_types/TimeRange.py,sha256=yzx-gxj8mDeGLft1FzU_x1MVEgIG5Pt6-f8PUVDgipQ,522
|
|
140
139
|
phoenix/server/api/input_types/TraceAnnotationSort.py,sha256=BzwiUnMh2VsgQYnhDlbJ6ljHugqIS4YDUlYzvq_tl3o,365
|
|
140
|
+
phoenix/server/api/input_types/UserRoleInput.py,sha256=xxhFe0ITZOgRVEJbVem_W6F1Ip_H6xDENdQqMMx-kKE,129
|
|
141
141
|
phoenix/server/api/input_types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
142
|
-
phoenix/server/api/mutations/__init__.py,sha256=
|
|
142
|
+
phoenix/server/api/mutations/__init__.py,sha256=Cu4lPgUFRAGzKO528jKepwKtfre9lkLTN059S2Shmnw,977
|
|
143
143
|
phoenix/server/api/mutations/api_key_mutations.py,sha256=cv6AT6UAL55lTC_UqMdcN-1TjWAgqqZi__S9Tw12t6I,3688
|
|
144
144
|
phoenix/server/api/mutations/auth.py,sha256=8o4tTfGCPkpUauuB9ijPH84Od77UX_UrQWfmUsnujI4,524
|
|
145
145
|
phoenix/server/api/mutations/dataset_mutations.py,sha256=0feBUW_07FEIx6uzepjxfRVhk5lAck0AkrqS1GVdoF4,27041
|
|
@@ -148,10 +148,12 @@ phoenix/server/api/mutations/export_events_mutations.py,sha256=t_wYBxaqvBJYRoHsl
|
|
|
148
148
|
phoenix/server/api/mutations/project_mutations.py,sha256=MLm7I97lJ85hTuc1tq8sdYA8Ps5WKMV-bGqeeN-Ey90,2279
|
|
149
149
|
phoenix/server/api/mutations/span_annotations_mutations.py,sha256=DM9gzxrMSAcxwXQ6jNaNGDVgl8oP50LZsBWRYQwLaSo,5955
|
|
150
150
|
phoenix/server/api/mutations/trace_annotations_mutations.py,sha256=VDiNzX63Agci7WeMbiK-C770JedlC5R7TZVe1UaRhDE,5930
|
|
151
|
+
phoenix/server/api/mutations/user_mutations.py,sha256=uUZ9LEPQAWRxGA4CVHFClHSGpyMlFHwgi6blu3pkuVA,2998
|
|
151
152
|
phoenix/server/api/openapi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
152
153
|
phoenix/server/api/openapi/main.py,sha256=KNutA_7AvV_WlGX8cOkvvDujcJKQ7AD1HT6rTpCpR8A,616
|
|
153
154
|
phoenix/server/api/openapi/schema.py,sha256=oVZoflWMfzOrLKMIrjr3iLnJ13rmN-t_DOe9g6KoN5s,471
|
|
154
155
|
phoenix/server/api/routers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
156
|
+
phoenix/server/api/routers/auth.py,sha256=dGug0NjOjW1mmIghmheAgHutG7_0-RjL-FcEReWzTHc,1806
|
|
155
157
|
phoenix/server/api/routers/utils.py,sha256=M41BoH-fl37izhRuN2aX7lWm7jOC20A_3uClv9TVUUY,583
|
|
156
158
|
phoenix/server/api/routers/v1/__init__.py,sha256=nb49zcOdAi3DSGuC9gUubN9Yri-o7-WFdlGak4jGuFw,1462
|
|
157
159
|
phoenix/server/api/routers/v1/datasets.py,sha256=l3Hlc9AVyvX5GdT9iOXBsV-i4c_vtnCaXeSAWdNzcw8,37090
|
|
@@ -289,8 +291,8 @@ phoenix/utilities/logging.py,sha256=lDXd6EGaamBNcQxL4vP1au9-i_SXe0OraUDiJOcszSw,
|
|
|
289
291
|
phoenix/utilities/project.py,sha256=8IJuMM4yUMoooPi37sictGj8Etu9rGmq6RFtc9848cQ,436
|
|
290
292
|
phoenix/utilities/re.py,sha256=PDve_OLjRTM8yQQJHC8-n3HdIONi7aNils3ZKRZ5uBM,2045
|
|
291
293
|
phoenix/utilities/span_store.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
292
|
-
arize_phoenix-4.
|
|
293
|
-
arize_phoenix-4.
|
|
294
|
-
arize_phoenix-4.
|
|
295
|
-
arize_phoenix-4.
|
|
296
|
-
arize_phoenix-4.
|
|
294
|
+
arize_phoenix-4.26.0.dist-info/METADATA,sha256=tf_mydvCrJ0GBxa58iEnat5FdCDItFKPN88hnZj7Dqs,12023
|
|
295
|
+
arize_phoenix-4.26.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
296
|
+
arize_phoenix-4.26.0.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
|
|
297
|
+
arize_phoenix-4.26.0.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
|
|
298
|
+
arize_phoenix-4.26.0.dist-info/RECORD,,
|
phoenix/auth.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from hashlib import pbkdf2_hmac
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def compute_password_hash(*, password: str, salt: str) -> str:
|
|
6
|
+
"""
|
|
7
|
+
Salts and hashes a password using PBKDF2, HMAC, and SHA256.
|
|
8
|
+
"""
|
|
9
|
+
password_bytes = password.encode("utf-8")
|
|
10
|
+
salt_bytes = salt.encode("utf-8")
|
|
11
|
+
password_hash_bytes = pbkdf2_hmac("sha256", password_bytes, salt_bytes, NUM_ITERATIONS)
|
|
12
|
+
password_hash = password_hash_bytes.hex()
|
|
13
|
+
return password_hash
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def is_valid_password(*, password: str, salt: str, password_hash: str) -> bool:
|
|
17
|
+
"""
|
|
18
|
+
Determines whether the password is valid by salting and hashing the password
|
|
19
|
+
and comparing against the existing hash value.
|
|
20
|
+
"""
|
|
21
|
+
return password_hash == compute_password_hash(password=password, salt=salt)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def validate_email_format(email: str) -> None:
|
|
25
|
+
"""
|
|
26
|
+
Checks that the email has a valid format.
|
|
27
|
+
"""
|
|
28
|
+
if EMAIL_PATTERN.match(email) is None:
|
|
29
|
+
raise ValueError("Invalid email address")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def validate_password_format(password: str) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Checks that the password has a valid format.
|
|
35
|
+
"""
|
|
36
|
+
if not password:
|
|
37
|
+
raise ValueError("Password must be non-empty")
|
|
38
|
+
if any(char.isspace() for char in password):
|
|
39
|
+
raise ValueError("Password cannot contain whitespace characters")
|
|
40
|
+
if not password.isascii():
|
|
41
|
+
raise ValueError("Password can contain only ASCII characters")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
EMAIL_PATTERN = re.compile(r"^[^@\s]+@[^@\s]+[.][^@\s]+\Z")
|
|
45
|
+
NUM_ITERATIONS = 10_000
|
phoenix/db/engines.py
CHANGED
|
@@ -139,7 +139,7 @@ def aio_postgresql_engine(
|
|
|
139
139
|
if not migrate:
|
|
140
140
|
return engine
|
|
141
141
|
sync_engine = sqlalchemy.create_engine(
|
|
142
|
-
url=url.set(drivername="postgresql"),
|
|
142
|
+
url=url.set(drivername="postgresql+psycopg"),
|
|
143
143
|
echo=Settings.log_migrations,
|
|
144
144
|
json_serializer=_dumps,
|
|
145
145
|
)
|
|
@@ -7,6 +7,7 @@ from phoenix.server.api.mutations.export_events_mutations import ExportEventsMut
|
|
|
7
7
|
from phoenix.server.api.mutations.project_mutations import ProjectMutationMixin
|
|
8
8
|
from phoenix.server.api.mutations.span_annotations_mutations import SpanAnnotationMutationMixin
|
|
9
9
|
from phoenix.server.api.mutations.trace_annotations_mutations import TraceAnnotationMutationMixin
|
|
10
|
+
from phoenix.server.api.mutations.user_mutations import UserMutationMixin
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
@strawberry.type
|
|
@@ -18,5 +19,6 @@ class Mutation(
|
|
|
18
19
|
SpanAnnotationMutationMixin,
|
|
19
20
|
TraceAnnotationMutationMixin,
|
|
20
21
|
ApiKeyMutationMixin,
|
|
22
|
+
UserMutationMixin,
|
|
21
23
|
):
|
|
22
24
|
pass
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import strawberry
|
|
5
|
+
from sqlalchemy import insert, select
|
|
6
|
+
from sqlean.dbapi2 import IntegrityError # type: ignore[import-untyped]
|
|
7
|
+
from strawberry.types import Info
|
|
8
|
+
|
|
9
|
+
from phoenix.auth import compute_password_hash, validate_email_format, validate_password_format
|
|
10
|
+
from phoenix.db import models
|
|
11
|
+
from phoenix.server.api.context import Context
|
|
12
|
+
from phoenix.server.api.input_types.UserRoleInput import UserRoleInput
|
|
13
|
+
from phoenix.server.api.types.User import User
|
|
14
|
+
from phoenix.server.api.types.UserRole import UserRole
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@strawberry.input
|
|
18
|
+
class CreateUserInput:
|
|
19
|
+
email: str
|
|
20
|
+
username: Optional[str]
|
|
21
|
+
password: str
|
|
22
|
+
role: UserRoleInput
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@strawberry.type
|
|
26
|
+
class UserMutationPayload:
|
|
27
|
+
user: User
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@strawberry.type
|
|
31
|
+
class UserMutationMixin:
|
|
32
|
+
@strawberry.mutation
|
|
33
|
+
async def create_user(
|
|
34
|
+
self,
|
|
35
|
+
info: Info[Context, None],
|
|
36
|
+
input: CreateUserInput,
|
|
37
|
+
) -> UserMutationPayload:
|
|
38
|
+
validate_email_format(email := input.email)
|
|
39
|
+
validate_password_format(password := input.password)
|
|
40
|
+
role_name = input.role.value
|
|
41
|
+
user_role_id = (
|
|
42
|
+
select(models.UserRole.id).where(models.UserRole.name == role_name).scalar_subquery()
|
|
43
|
+
)
|
|
44
|
+
secret = info.context.get_secret()
|
|
45
|
+
loop = asyncio.get_running_loop()
|
|
46
|
+
password_hash = await loop.run_in_executor(
|
|
47
|
+
executor=None,
|
|
48
|
+
func=lambda: compute_password_hash(password=password, salt=secret),
|
|
49
|
+
)
|
|
50
|
+
try:
|
|
51
|
+
async with info.context.db() as session:
|
|
52
|
+
user = await session.scalar(
|
|
53
|
+
insert(models.User)
|
|
54
|
+
.values(
|
|
55
|
+
user_role_id=user_role_id,
|
|
56
|
+
username=input.username,
|
|
57
|
+
email=email,
|
|
58
|
+
auth_method="LOCAL",
|
|
59
|
+
password_hash=password_hash,
|
|
60
|
+
reset_password=True,
|
|
61
|
+
)
|
|
62
|
+
.returning(models.User)
|
|
63
|
+
)
|
|
64
|
+
assert user is not None
|
|
65
|
+
except IntegrityError as error:
|
|
66
|
+
raise ValueError(_get_user_create_error_message(error))
|
|
67
|
+
return UserMutationPayload(
|
|
68
|
+
user=User(
|
|
69
|
+
id_attr=user.id,
|
|
70
|
+
email=user.email,
|
|
71
|
+
username=user.username,
|
|
72
|
+
created_at=user.created_at,
|
|
73
|
+
role=UserRole(id_attr=user.user_role_id, name=role_name),
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _get_user_create_error_message(error: IntegrityError) -> str:
|
|
79
|
+
"""
|
|
80
|
+
Gets a user-facing error message to explain why user creation failed.
|
|
81
|
+
"""
|
|
82
|
+
original_error_message = str(error)
|
|
83
|
+
username_already_exists = "users.username" in original_error_message
|
|
84
|
+
email_already_exists = "users.email" in original_error_message
|
|
85
|
+
if username_already_exists:
|
|
86
|
+
return "Username already exists"
|
|
87
|
+
elif email_already_exists:
|
|
88
|
+
return "Email already exists"
|
|
89
|
+
return "Failed to create user"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from datetime import timedelta
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, Form, Request, Response
|
|
5
|
+
from sqlalchemy import select
|
|
6
|
+
from starlette.status import HTTP_204_NO_CONTENT, HTTP_401_UNAUTHORIZED
|
|
7
|
+
from typing_extensions import Annotated
|
|
8
|
+
|
|
9
|
+
from phoenix.auth import is_valid_password
|
|
10
|
+
from phoenix.db import models
|
|
11
|
+
|
|
12
|
+
router = APIRouter(include_in_schema=False)
|
|
13
|
+
|
|
14
|
+
PHOENIX_ACCESS_TOKEN_COOKIE_NAME = "phoenix-access-token"
|
|
15
|
+
PHOENIX_ACCESS_TOKEN_COOKIE_MAX_AGE_IN_SECONDS = int(timedelta(days=31).total_seconds())
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@router.post("/login")
|
|
19
|
+
async def login(
|
|
20
|
+
request: Request,
|
|
21
|
+
email: Annotated[str, Form()],
|
|
22
|
+
password: Annotated[str, Form()],
|
|
23
|
+
) -> Response:
|
|
24
|
+
async with request.app.state.db() as session:
|
|
25
|
+
if (
|
|
26
|
+
user := await session.scalar(select(models.User).where(models.User.email == email))
|
|
27
|
+
) is None or (password_hash := user.password_hash) is None:
|
|
28
|
+
return Response(status_code=HTTP_401_UNAUTHORIZED)
|
|
29
|
+
secret = request.app.state.get_secret()
|
|
30
|
+
loop = asyncio.get_running_loop()
|
|
31
|
+
if not await loop.run_in_executor(
|
|
32
|
+
executor=None,
|
|
33
|
+
func=lambda: is_valid_password(password=password, salt=secret, password_hash=password_hash),
|
|
34
|
+
):
|
|
35
|
+
return Response(status_code=HTTP_401_UNAUTHORIZED)
|
|
36
|
+
response = Response(status_code=HTTP_204_NO_CONTENT)
|
|
37
|
+
response.set_cookie(
|
|
38
|
+
key=PHOENIX_ACCESS_TOKEN_COOKIE_NAME,
|
|
39
|
+
value="token", # todo: compute access token
|
|
40
|
+
secure=True,
|
|
41
|
+
httponly=True,
|
|
42
|
+
samesite="strict",
|
|
43
|
+
max_age=PHOENIX_ACCESS_TOKEN_COOKIE_MAX_AGE_IN_SECONDS,
|
|
44
|
+
)
|
|
45
|
+
return response
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@router.post("/logout")
|
|
49
|
+
async def logout() -> Response:
|
|
50
|
+
response = Response(status_code=HTTP_204_NO_CONTENT)
|
|
51
|
+
response.delete_cookie(key=PHOENIX_ACCESS_TOKEN_COOKIE_NAME)
|
|
52
|
+
return response
|
phoenix/server/app.py
CHANGED
|
@@ -4,6 +4,7 @@ import json
|
|
|
4
4
|
import logging
|
|
5
5
|
from functools import cached_property
|
|
6
6
|
from pathlib import Path
|
|
7
|
+
from types import MethodType
|
|
7
8
|
from typing import (
|
|
8
9
|
TYPE_CHECKING,
|
|
9
10
|
Any,
|
|
@@ -30,6 +31,7 @@ from sqlalchemy.ext.asyncio import (
|
|
|
30
31
|
AsyncSession,
|
|
31
32
|
async_sessionmaker,
|
|
32
33
|
)
|
|
34
|
+
from starlette.datastructures import State as StarletteState
|
|
33
35
|
from starlette.exceptions import HTTPException
|
|
34
36
|
from starlette.middleware import Middleware
|
|
35
37
|
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
|
@@ -80,6 +82,7 @@ from phoenix.server.api.dataloaders import (
|
|
|
80
82
|
TokenCountDataLoader,
|
|
81
83
|
TraceRowIdsDataLoader,
|
|
82
84
|
)
|
|
85
|
+
from phoenix.server.api.routers.auth import router as auth_router
|
|
83
86
|
from phoenix.server.api.routers.v1 import REST_API_VERSION
|
|
84
87
|
from phoenix.server.api.routers.v1 import router as v1_router
|
|
85
88
|
from phoenix.server.api.schema import schema
|
|
@@ -536,6 +539,8 @@ def create_app(
|
|
|
536
539
|
app.include_router(router)
|
|
537
540
|
app.include_router(graphql_router)
|
|
538
541
|
app.add_middleware(GZipMiddleware)
|
|
542
|
+
if authentication_enabled:
|
|
543
|
+
app.include_router(auth_router)
|
|
539
544
|
if serve_ui:
|
|
540
545
|
app.mount(
|
|
541
546
|
"/",
|
|
@@ -554,8 +559,7 @@ def create_app(
|
|
|
554
559
|
),
|
|
555
560
|
name="static",
|
|
556
561
|
)
|
|
557
|
-
|
|
558
|
-
app.state.db = db
|
|
562
|
+
app = _update_app_state(app, db=db, secret=secret)
|
|
559
563
|
if tracer_provider:
|
|
560
564
|
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
|
561
565
|
|
|
@@ -563,3 +567,22 @@ def create_app(
|
|
|
563
567
|
FastAPIInstrumentor.instrument_app(app, tracer_provider=tracer_provider)
|
|
564
568
|
shutdown_callbacks_list.append(FastAPIInstrumentor().uninstrument)
|
|
565
569
|
return app
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _update_app_state(app: FastAPI, /, *, db: DbSessionFactory, secret: Optional[str]) -> FastAPI:
|
|
573
|
+
"""
|
|
574
|
+
Dynamically updates the app's `state` to include useful fields and methods
|
|
575
|
+
(at the time of this writing, FastAPI does not support setting this state
|
|
576
|
+
during the creation of the app).
|
|
577
|
+
"""
|
|
578
|
+
app.state.db = db
|
|
579
|
+
app.state._secret = secret
|
|
580
|
+
|
|
581
|
+
def get_secret(self: StarletteState) -> str:
|
|
582
|
+
if (secret := self._secret) is None:
|
|
583
|
+
raise ValueError("app secret is not set")
|
|
584
|
+
assert isinstance(secret, str)
|
|
585
|
+
return secret
|
|
586
|
+
|
|
587
|
+
app.state.get_secret = MethodType(get_secret, app.state)
|
|
588
|
+
return app
|
phoenix/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "4.
|
|
1
|
+
__version__ = "4.26.0"
|
phoenix/auth/__init__.py
DELETED
|
File without changes
|
phoenix/auth/utils.py
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
from hashlib import pbkdf2_hmac
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def compute_password_hash(password: str, salt: str) -> str:
|
|
5
|
-
"""
|
|
6
|
-
Salts and hashes a password using PBKDF2, HMAC, and SHA256.
|
|
7
|
-
"""
|
|
8
|
-
password_bytes = password.encode("utf-8")
|
|
9
|
-
salt_bytes = salt.encode("utf-8")
|
|
10
|
-
password_hash_bytes = pbkdf2_hmac("sha256", password_bytes, salt_bytes, NUM_ITERATIONS)
|
|
11
|
-
password_hash = password_hash_bytes.hex()
|
|
12
|
-
return password_hash
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
NUM_ITERATIONS = 1_000_000
|
|
File without changes
|
|
File without changes
|
|
File without changes
|