arize-phoenix 12.3.0__py3-none-any.whl → 12.5.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.3.0.dist-info → arize_phoenix-12.5.0.dist-info}/METADATA +2 -1
- {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.5.0.dist-info}/RECORD +73 -72
- phoenix/auth.py +27 -2
- phoenix/config.py +302 -53
- phoenix/db/README.md +546 -28
- phoenix/db/models.py +3 -3
- phoenix/server/api/auth.py +9 -0
- phoenix/server/api/context.py +2 -0
- phoenix/server/api/dataloaders/__init__.py +2 -0
- phoenix/server/api/dataloaders/dataset_dataset_splits.py +52 -0
- phoenix/server/api/input_types/ProjectSessionSort.py +158 -1
- phoenix/server/api/input_types/SpanSort.py +2 -1
- phoenix/server/api/input_types/UserRoleInput.py +1 -0
- phoenix/server/api/mutations/annotation_config_mutations.py +6 -6
- phoenix/server/api/mutations/api_key_mutations.py +13 -5
- phoenix/server/api/mutations/chat_mutations.py +3 -3
- phoenix/server/api/mutations/dataset_label_mutations.py +6 -6
- phoenix/server/api/mutations/dataset_mutations.py +8 -8
- phoenix/server/api/mutations/dataset_split_mutations.py +7 -7
- phoenix/server/api/mutations/experiment_mutations.py +2 -2
- phoenix/server/api/mutations/export_events_mutations.py +3 -3
- phoenix/server/api/mutations/model_mutations.py +4 -4
- phoenix/server/api/mutations/project_mutations.py +4 -4
- phoenix/server/api/mutations/project_session_annotations_mutations.py +4 -4
- phoenix/server/api/mutations/project_trace_retention_policy_mutations.py +8 -4
- phoenix/server/api/mutations/prompt_label_mutations.py +7 -7
- phoenix/server/api/mutations/prompt_mutations.py +7 -7
- phoenix/server/api/mutations/prompt_version_tag_mutations.py +3 -3
- phoenix/server/api/mutations/span_annotations_mutations.py +5 -5
- phoenix/server/api/mutations/trace_annotations_mutations.py +4 -4
- phoenix/server/api/mutations/trace_mutations.py +3 -3
- phoenix/server/api/mutations/user_mutations.py +8 -5
- phoenix/server/api/routers/auth.py +23 -32
- phoenix/server/api/routers/oauth2.py +213 -24
- phoenix/server/api/routers/v1/__init__.py +18 -4
- phoenix/server/api/routers/v1/annotation_configs.py +19 -30
- phoenix/server/api/routers/v1/annotations.py +21 -22
- phoenix/server/api/routers/v1/datasets.py +86 -64
- 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 +16 -11
- phoenix/server/api/routers/v1/experiments.py +57 -22
- phoenix/server/api/routers/v1/projects.py +16 -50
- 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 +13 -29
- phoenix/server/api/routers/v1/utils.py +3 -7
- phoenix/server/api/subscriptions.py +3 -3
- phoenix/server/api/types/Dataset.py +95 -6
- phoenix/server/api/types/Project.py +24 -68
- phoenix/server/app.py +3 -2
- phoenix/server/authorization.py +5 -4
- phoenix/server/bearer_auth.py +13 -5
- phoenix/server/jwt_store.py +8 -6
- phoenix/server/oauth2.py +172 -5
- phoenix/server/static/.vite/manifest.json +39 -39
- phoenix/server/static/assets/{components-Bs8eJEpU.js → components-cwdYEs7B.js} +501 -404
- phoenix/server/static/assets/{index-C6WEu5UP.js → index-Dc0vD1Rn.js} +1 -1
- phoenix/server/static/assets/{pages-D-n2pkoG.js → pages-BDkB3a_a.js} +577 -533
- phoenix/server/static/assets/{vendor-D2eEI-6h.js → vendor-Ce6GTAin.js} +1 -1
- phoenix/server/static/assets/{vendor-arizeai-kfOei7nf.js → vendor-arizeai-CSF-1Kc5.js} +1 -1
- phoenix/server/static/assets/{vendor-codemirror-1bq_t1Ec.js → vendor-codemirror-Bv8J_7an.js} +3 -3
- phoenix/server/static/assets/{vendor-recharts-DQ4xfrf4.js → vendor-recharts-DcLgzI7g.js} +1 -1
- phoenix/server/static/assets/{vendor-shiki-GGmcIQxA.js → vendor-shiki-BF8rh_7m.js} +1 -1
- phoenix/trace/attributes.py +80 -13
- phoenix/version.py +1 -1
- {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.5.0.dist-info}/WHEEL +0 -0
- {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.5.0.dist-info}/entry_points.txt +0 -0
- {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.5.0.dist-info}/licenses/IP_NOTICE +0 -0
- {arize_phoenix-12.3.0.dist-info → arize_phoenix-12.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -12,7 +12,7 @@ from strawberry.relay import GlobalID
|
|
|
12
12
|
from strawberry.types import Info
|
|
13
13
|
|
|
14
14
|
from phoenix.db import models
|
|
15
|
-
from phoenix.server.api.auth import IsNotReadOnly
|
|
15
|
+
from phoenix.server.api.auth import IsNotReadOnly, IsNotViewer
|
|
16
16
|
from phoenix.server.api.context import Context
|
|
17
17
|
from phoenix.server.api.exceptions import BadRequest, Conflict, NotFound
|
|
18
18
|
from phoenix.server.api.queries import Query
|
|
@@ -81,7 +81,7 @@ class DeleteModelMutationPayload:
|
|
|
81
81
|
|
|
82
82
|
@strawberry.type
|
|
83
83
|
class ModelMutationMixin:
|
|
84
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
|
|
84
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
85
85
|
async def create_model(
|
|
86
86
|
self,
|
|
87
87
|
info: Info[Context, None],
|
|
@@ -114,7 +114,7 @@ class ModelMutationMixin:
|
|
|
114
114
|
query=Query(),
|
|
115
115
|
)
|
|
116
116
|
|
|
117
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
|
|
117
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
118
118
|
async def update_model(
|
|
119
119
|
self,
|
|
120
120
|
info: Info[Context, None],
|
|
@@ -167,7 +167,7 @@ class ModelMutationMixin:
|
|
|
167
167
|
query=Query(),
|
|
168
168
|
)
|
|
169
169
|
|
|
170
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
|
|
170
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
171
171
|
async def delete_model(
|
|
172
172
|
self,
|
|
173
173
|
info: Info[Context, None],
|
|
@@ -8,7 +8,7 @@ from strawberry.types import Info
|
|
|
8
8
|
|
|
9
9
|
from phoenix.config import DEFAULT_PROJECT_NAME
|
|
10
10
|
from phoenix.db import models
|
|
11
|
-
from phoenix.server.api.auth import IsNotReadOnly
|
|
11
|
+
from phoenix.server.api.auth import IsNotReadOnly, IsNotViewer
|
|
12
12
|
from phoenix.server.api.context import Context
|
|
13
13
|
from phoenix.server.api.exceptions import BadRequest, Conflict
|
|
14
14
|
from phoenix.server.api.input_types.ClearProjectInput import ClearProjectInput
|
|
@@ -27,7 +27,7 @@ class ProjectMutationPayload:
|
|
|
27
27
|
|
|
28
28
|
@strawberry.type
|
|
29
29
|
class ProjectMutationMixin:
|
|
30
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
|
|
30
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
31
31
|
async def create_project(
|
|
32
32
|
self,
|
|
33
33
|
info: Info[Context, None],
|
|
@@ -52,7 +52,7 @@ class ProjectMutationMixin:
|
|
|
52
52
|
info.context.event_queue.put(ProjectInsertEvent((project.id,)))
|
|
53
53
|
return ProjectMutationPayload(project=to_gql_project(project), query=Query())
|
|
54
54
|
|
|
55
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
|
|
55
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
56
56
|
async def delete_project(self, info: Info[Context, None], id: GlobalID) -> Query:
|
|
57
57
|
project_id = from_global_id_with_expected_type(global_id=id, expected_type_name="Project")
|
|
58
58
|
async with info.context.db() as session:
|
|
@@ -69,7 +69,7 @@ class ProjectMutationMixin:
|
|
|
69
69
|
info.context.event_queue.put(ProjectDeleteEvent((project_id,)))
|
|
70
70
|
return Query()
|
|
71
71
|
|
|
72
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
|
|
72
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
73
73
|
async def clear_project(self, info: Info[Context, None], input: ClearProjectInput) -> Query:
|
|
74
74
|
project_id = from_global_id_with_expected_type(
|
|
75
75
|
global_id=input.id, expected_type_name="Project"
|
|
@@ -8,7 +8,7 @@ from strawberry import Info
|
|
|
8
8
|
from strawberry.relay import GlobalID
|
|
9
9
|
|
|
10
10
|
from phoenix.db import models
|
|
11
|
-
from phoenix.server.api.auth import IsLocked, IsNotReadOnly
|
|
11
|
+
from phoenix.server.api.auth import IsLocked, IsNotReadOnly, IsNotViewer
|
|
12
12
|
from phoenix.server.api.context import Context
|
|
13
13
|
from phoenix.server.api.exceptions import BadRequest, Conflict, NotFound, Unauthorized
|
|
14
14
|
from phoenix.server.api.helpers.annotations import get_user_identifier
|
|
@@ -38,7 +38,7 @@ class ProjectSessionAnnotationMutationPayload:
|
|
|
38
38
|
|
|
39
39
|
@strawberry.type
|
|
40
40
|
class ProjectSessionAnnotationMutationMixin:
|
|
41
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
41
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
42
42
|
async def create_project_session_annotations(
|
|
43
43
|
self, info: Info[Context, None], input: CreateProjectSessionAnnotationInput
|
|
44
44
|
) -> ProjectSessionAnnotationMutationPayload:
|
|
@@ -85,7 +85,7 @@ class ProjectSessionAnnotationMutationMixin:
|
|
|
85
85
|
query=Query(),
|
|
86
86
|
)
|
|
87
87
|
|
|
88
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
88
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
89
89
|
async def update_project_session_annotations(
|
|
90
90
|
self, info: Info[Context, None], input: UpdateAnnotationInput
|
|
91
91
|
) -> ProjectSessionAnnotationMutationPayload:
|
|
@@ -126,7 +126,7 @@ class ProjectSessionAnnotationMutationMixin:
|
|
|
126
126
|
query=Query(),
|
|
127
127
|
)
|
|
128
128
|
|
|
129
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
|
|
129
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
130
130
|
async def delete_project_session_annotation(
|
|
131
131
|
self, info: Info[Context, None], id: GlobalID
|
|
132
132
|
) -> ProjectSessionAnnotationMutationPayload:
|
|
@@ -16,7 +16,7 @@ from phoenix.db.types.trace_retention import (
|
|
|
16
16
|
TraceRetentionCronExpression,
|
|
17
17
|
TraceRetentionRule,
|
|
18
18
|
)
|
|
19
|
-
from phoenix.server.api.auth import IsAdminIfAuthEnabled, IsLocked, IsNotReadOnly
|
|
19
|
+
from phoenix.server.api.auth import IsAdminIfAuthEnabled, IsLocked, IsNotReadOnly, IsNotViewer
|
|
20
20
|
from phoenix.server.api.context import Context
|
|
21
21
|
from phoenix.server.api.exceptions import BadRequest, NotFound
|
|
22
22
|
from phoenix.server.api.queries import Query
|
|
@@ -113,7 +113,9 @@ class ProjectTraceRetentionPolicyMutationPayload:
|
|
|
113
113
|
|
|
114
114
|
@strawberry.type
|
|
115
115
|
class ProjectTraceRetentionPolicyMutationMixin:
|
|
116
|
-
@strawberry.mutation(
|
|
116
|
+
@strawberry.mutation(
|
|
117
|
+
permission_classes=[IsNotReadOnly, IsNotViewer, IsAdminIfAuthEnabled, IsLocked]
|
|
118
|
+
) # type: ignore
|
|
117
119
|
async def create_project_trace_retention_policy(
|
|
118
120
|
self,
|
|
119
121
|
info: Info[Context, None],
|
|
@@ -146,7 +148,9 @@ class ProjectTraceRetentionPolicyMutationMixin:
|
|
|
146
148
|
node=ProjectTraceRetentionPolicy(id=policy.id, db_policy=policy),
|
|
147
149
|
)
|
|
148
150
|
|
|
149
|
-
@strawberry.mutation(
|
|
151
|
+
@strawberry.mutation(
|
|
152
|
+
permission_classes=[IsNotReadOnly, IsNotViewer, IsAdminIfAuthEnabled, IsLocked]
|
|
153
|
+
) # type: ignore
|
|
150
154
|
async def patch_project_trace_retention_policy(
|
|
151
155
|
self,
|
|
152
156
|
info: Info[Context, None],
|
|
@@ -204,7 +208,7 @@ class ProjectTraceRetentionPolicyMutationMixin:
|
|
|
204
208
|
node=ProjectTraceRetentionPolicy(id=policy.id, db_policy=policy),
|
|
205
209
|
)
|
|
206
210
|
|
|
207
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsAdminIfAuthEnabled]) # type: ignore
|
|
211
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsAdminIfAuthEnabled]) # type: ignore
|
|
208
212
|
async def delete_project_trace_retention_policy(
|
|
209
213
|
self,
|
|
210
214
|
info: Info[Context, None],
|
|
@@ -10,7 +10,7 @@ from strawberry.relay import GlobalID
|
|
|
10
10
|
from strawberry.types import Info
|
|
11
11
|
|
|
12
12
|
from phoenix.db import models
|
|
13
|
-
from phoenix.server.api.auth import IsLocked, IsNotReadOnly
|
|
13
|
+
from phoenix.server.api.auth import IsLocked, IsNotReadOnly, IsNotViewer
|
|
14
14
|
from phoenix.server.api.context import Context
|
|
15
15
|
from phoenix.server.api.exceptions import Conflict, NotFound
|
|
16
16
|
from phoenix.server.api.queries import Query
|
|
@@ -69,7 +69,7 @@ class PromptLabelAssociationMutationPayload:
|
|
|
69
69
|
|
|
70
70
|
@strawberry.type
|
|
71
71
|
class PromptLabelMutationMixin:
|
|
72
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
72
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
73
73
|
async def create_prompt_label(
|
|
74
74
|
self, info: Info[Context, None], input: CreatePromptLabelInput
|
|
75
75
|
) -> PromptLabelMutationPayload:
|
|
@@ -89,7 +89,7 @@ class PromptLabelMutationMixin:
|
|
|
89
89
|
query=Query(),
|
|
90
90
|
)
|
|
91
91
|
|
|
92
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
92
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
93
93
|
async def patch_prompt_label(
|
|
94
94
|
self, info: Info[Context, None], input: PatchPromptLabelInput
|
|
95
95
|
) -> PromptLabelMutationPayload:
|
|
@@ -117,7 +117,7 @@ class PromptLabelMutationMixin:
|
|
|
117
117
|
query=Query(),
|
|
118
118
|
)
|
|
119
119
|
|
|
120
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
|
|
120
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
121
121
|
async def delete_prompt_labels(
|
|
122
122
|
self, info: Info[Context, None], input: DeletePromptLabelsInput
|
|
123
123
|
) -> PromptLabelDeleteMutationPayload:
|
|
@@ -139,7 +139,7 @@ class PromptLabelMutationMixin:
|
|
|
139
139
|
query=Query(),
|
|
140
140
|
)
|
|
141
141
|
|
|
142
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
142
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
143
143
|
async def set_prompt_labels(
|
|
144
144
|
self, info: Info[Context, None], input: SetPromptLabelsInput
|
|
145
145
|
) -> PromptLabelAssociationMutationPayload:
|
|
@@ -168,7 +168,7 @@ class PromptLabelMutationMixin:
|
|
|
168
168
|
query=Query(),
|
|
169
169
|
)
|
|
170
170
|
|
|
171
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
|
|
171
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
172
172
|
async def unset_prompt_labels(
|
|
173
173
|
self, info: Info[Context, None], input: UnsetPromptLabelsInput
|
|
174
174
|
) -> PromptLabelAssociationMutationPayload:
|
|
@@ -188,7 +188,7 @@ class PromptLabelMutationMixin:
|
|
|
188
188
|
)
|
|
189
189
|
result = await session.execute(stmt)
|
|
190
190
|
|
|
191
|
-
if result.rowcount != len(label_ids):
|
|
191
|
+
if result.rowcount != len(label_ids): # type: ignore[attr-defined]
|
|
192
192
|
label_ids_str = ", ".join(str(i) for i in label_ids)
|
|
193
193
|
raise NotFound(
|
|
194
194
|
f"No association between prompt={prompt_id} and labels={label_ids_str}."
|
|
@@ -13,7 +13,7 @@ from strawberry.types import Info
|
|
|
13
13
|
from phoenix.db import models
|
|
14
14
|
from phoenix.db.types.identifier import Identifier as IdentifierModel
|
|
15
15
|
from phoenix.db.types.model_provider import ModelProvider
|
|
16
|
-
from phoenix.server.api.auth import IsLocked, IsNotReadOnly
|
|
16
|
+
from phoenix.server.api.auth import IsLocked, IsNotReadOnly, IsNotViewer
|
|
17
17
|
from phoenix.server.api.context import Context
|
|
18
18
|
from phoenix.server.api.exceptions import BadRequest, Conflict, NotFound
|
|
19
19
|
from phoenix.server.api.helpers.prompts.models import (
|
|
@@ -75,7 +75,7 @@ class DeletePromptMutationPayload:
|
|
|
75
75
|
|
|
76
76
|
@strawberry.type
|
|
77
77
|
class PromptMutationMixin:
|
|
78
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
78
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
79
79
|
async def create_chat_prompt(
|
|
80
80
|
self, info: Info[Context, None], input: CreateChatPromptInput
|
|
81
81
|
) -> Prompt:
|
|
@@ -142,7 +142,7 @@ class PromptMutationMixin:
|
|
|
142
142
|
raise Conflict(f"A prompt named '{input.name}' already exists")
|
|
143
143
|
return to_gql_prompt_from_orm(prompt)
|
|
144
144
|
|
|
145
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
145
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
146
146
|
async def create_chat_prompt_version(
|
|
147
147
|
self,
|
|
148
148
|
info: Info[Context, None],
|
|
@@ -220,7 +220,7 @@ class PromptMutationMixin:
|
|
|
220
220
|
|
|
221
221
|
return to_gql_prompt_from_orm(prompt)
|
|
222
222
|
|
|
223
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
|
|
223
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
224
224
|
async def delete_prompt(
|
|
225
225
|
self, info: Info[Context, None], input: DeletePromptInput
|
|
226
226
|
) -> DeletePromptMutationPayload:
|
|
@@ -231,13 +231,13 @@ class PromptMutationMixin:
|
|
|
231
231
|
stmt = delete(models.Prompt).where(models.Prompt.id == prompt_id)
|
|
232
232
|
result = await session.execute(stmt)
|
|
233
233
|
|
|
234
|
-
if result.rowcount == 0:
|
|
234
|
+
if result.rowcount == 0: # type: ignore[attr-defined]
|
|
235
235
|
raise NotFound(f"Prompt with ID '{input.prompt_id}' not found")
|
|
236
236
|
|
|
237
237
|
await session.commit()
|
|
238
238
|
return DeletePromptMutationPayload(query=Query())
|
|
239
239
|
|
|
240
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
240
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
241
241
|
async def clone_prompt(self, info: Info[Context, None], input: ClonePromptInput) -> Prompt:
|
|
242
242
|
prompt_id = from_global_id_with_expected_type(
|
|
243
243
|
global_id=input.prompt_id, expected_type_name=Prompt.__name__
|
|
@@ -290,7 +290,7 @@ class PromptMutationMixin:
|
|
|
290
290
|
raise Conflict(f"A prompt named '{input.name}' already exists")
|
|
291
291
|
return to_gql_prompt_from_orm(new_prompt)
|
|
292
292
|
|
|
293
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
293
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
294
294
|
async def patch_prompt(self, info: Info[Context, None], input: PatchPromptInput) -> Prompt:
|
|
295
295
|
prompt_id = from_global_id_with_expected_type(
|
|
296
296
|
global_id=input.prompt_id, expected_type_name=Prompt.__name__
|
|
@@ -10,7 +10,7 @@ from strawberry.types import Info
|
|
|
10
10
|
|
|
11
11
|
from phoenix.db import models
|
|
12
12
|
from phoenix.db.types.identifier import Identifier as IdentifierModel
|
|
13
|
-
from phoenix.server.api.auth import IsLocked, IsNotReadOnly
|
|
13
|
+
from phoenix.server.api.auth import IsLocked, IsNotReadOnly, IsNotViewer
|
|
14
14
|
from phoenix.server.api.context import Context
|
|
15
15
|
from phoenix.server.api.exceptions import BadRequest, Conflict, NotFound
|
|
16
16
|
from phoenix.server.api.queries import Query
|
|
@@ -42,7 +42,7 @@ class PromptVersionTagMutationPayload:
|
|
|
42
42
|
|
|
43
43
|
@strawberry.type
|
|
44
44
|
class PromptVersionTagMutationMixin:
|
|
45
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
|
|
45
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
46
46
|
async def delete_prompt_version_tag(
|
|
47
47
|
self, info: Info[Context, None], input: DeletePromptVersionTagInput
|
|
48
48
|
) -> PromptVersionTagMutationPayload:
|
|
@@ -78,7 +78,7 @@ class PromptVersionTagMutationMixin:
|
|
|
78
78
|
prompt_version_tag=None, query=Query(), prompt=to_gql_prompt_from_orm(prompt)
|
|
79
79
|
)
|
|
80
80
|
|
|
81
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
81
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
82
82
|
async def set_prompt_version_tag(
|
|
83
83
|
self, info: Info[Context, None], input: SetPromptVersionTagInput
|
|
84
84
|
) -> PromptVersionTagMutationPayload:
|
|
@@ -7,7 +7,7 @@ from starlette.requests import Request
|
|
|
7
7
|
from strawberry import UNSET, Info
|
|
8
8
|
|
|
9
9
|
from phoenix.db import models
|
|
10
|
-
from phoenix.server.api.auth import IsLocked, IsNotReadOnly
|
|
10
|
+
from phoenix.server.api.auth import IsLocked, IsNotReadOnly, IsNotViewer
|
|
11
11
|
from phoenix.server.api.context import Context
|
|
12
12
|
from phoenix.server.api.exceptions import BadRequest, NotFound, Unauthorized
|
|
13
13
|
from phoenix.server.api.helpers.annotations import get_user_identifier
|
|
@@ -34,7 +34,7 @@ class SpanAnnotationMutationPayload:
|
|
|
34
34
|
|
|
35
35
|
@strawberry.type
|
|
36
36
|
class SpanAnnotationMutationMixin:
|
|
37
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
37
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
38
38
|
async def create_span_annotations(
|
|
39
39
|
self, info: Info[Context, None], input: list[CreateSpanAnnotationInput]
|
|
40
40
|
) -> SpanAnnotationMutationPayload:
|
|
@@ -148,7 +148,7 @@ class SpanAnnotationMutationMixin:
|
|
|
148
148
|
query=Query(),
|
|
149
149
|
)
|
|
150
150
|
|
|
151
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
151
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
152
152
|
async def create_span_note(
|
|
153
153
|
self, info: Info[Context, None], annotation_input: CreateSpanNoteInput
|
|
154
154
|
) -> SpanAnnotationMutationPayload:
|
|
@@ -191,7 +191,7 @@ class SpanAnnotationMutationMixin:
|
|
|
191
191
|
query=Query(),
|
|
192
192
|
)
|
|
193
193
|
|
|
194
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
194
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
195
195
|
async def patch_span_annotations(
|
|
196
196
|
self, info: Info[Context, None], input: list[PatchAnnotationInput]
|
|
197
197
|
) -> SpanAnnotationMutationPayload:
|
|
@@ -268,7 +268,7 @@ class SpanAnnotationMutationMixin:
|
|
|
268
268
|
query=Query(),
|
|
269
269
|
)
|
|
270
270
|
|
|
271
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
|
|
271
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
272
272
|
async def delete_span_annotations(
|
|
273
273
|
self, info: Info[Context, None], input: DeleteAnnotationsInput
|
|
274
274
|
) -> SpanAnnotationMutationPayload:
|
|
@@ -6,7 +6,7 @@ from starlette.requests import Request
|
|
|
6
6
|
from strawberry import UNSET, Info
|
|
7
7
|
|
|
8
8
|
from phoenix.db import models
|
|
9
|
-
from phoenix.server.api.auth import IsLocked, IsNotReadOnly
|
|
9
|
+
from phoenix.server.api.auth import IsLocked, IsNotReadOnly, IsNotViewer
|
|
10
10
|
from phoenix.server.api.context import Context
|
|
11
11
|
from phoenix.server.api.exceptions import BadRequest, NotFound, Unauthorized
|
|
12
12
|
from phoenix.server.api.helpers.annotations import get_user_identifier
|
|
@@ -29,7 +29,7 @@ class TraceAnnotationMutationPayload:
|
|
|
29
29
|
|
|
30
30
|
@strawberry.type
|
|
31
31
|
class TraceAnnotationMutationMixin:
|
|
32
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
32
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
33
33
|
async def create_trace_annotations(
|
|
34
34
|
self, info: Info[Context, None], input: list[CreateTraceAnnotationInput]
|
|
35
35
|
) -> TraceAnnotationMutationPayload:
|
|
@@ -120,7 +120,7 @@ class TraceAnnotationMutationMixin:
|
|
|
120
120
|
query=Query(),
|
|
121
121
|
)
|
|
122
122
|
|
|
123
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsLocked]) # type: ignore
|
|
123
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsLocked]) # type: ignore
|
|
124
124
|
async def patch_trace_annotations(
|
|
125
125
|
self, info: Info[Context, None], input: list[PatchAnnotationInput]
|
|
126
126
|
) -> TraceAnnotationMutationPayload:
|
|
@@ -195,7 +195,7 @@ class TraceAnnotationMutationMixin:
|
|
|
195
195
|
query=Query(),
|
|
196
196
|
)
|
|
197
197
|
|
|
198
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
|
|
198
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
199
199
|
async def delete_trace_annotations(
|
|
200
200
|
self, info: Info[Context, None], input: DeleteAnnotationsInput
|
|
201
201
|
) -> TraceAnnotationMutationPayload:
|
|
@@ -6,7 +6,7 @@ from strawberry.relay import GlobalID
|
|
|
6
6
|
from strawberry.types import Info
|
|
7
7
|
|
|
8
8
|
from phoenix.db import models
|
|
9
|
-
from phoenix.server.api.auth import IsNotReadOnly
|
|
9
|
+
from phoenix.server.api.auth import IsNotReadOnly, IsNotViewer
|
|
10
10
|
from phoenix.server.api.context import Context
|
|
11
11
|
from phoenix.server.api.exceptions import BadRequest
|
|
12
12
|
from phoenix.server.api.queries import Query
|
|
@@ -16,7 +16,7 @@ from phoenix.server.dml_event import SpanDeleteEvent
|
|
|
16
16
|
|
|
17
17
|
@strawberry.type
|
|
18
18
|
class TraceMutationMixin:
|
|
19
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
|
|
19
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
20
20
|
async def delete_traces(
|
|
21
21
|
self,
|
|
22
22
|
info: Info[Context, None],
|
|
@@ -73,7 +73,7 @@ class TraceMutationMixin:
|
|
|
73
73
|
info.context.event_queue.put(SpanDeleteEvent(project_ids))
|
|
74
74
|
return Query()
|
|
75
75
|
|
|
76
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly]) # type: ignore
|
|
76
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer]) # type: ignore
|
|
77
77
|
async def transfer_traces_to_project(
|
|
78
78
|
self,
|
|
79
79
|
info: Info[Context, None],
|
|
@@ -27,7 +27,7 @@ from phoenix.auth import (
|
|
|
27
27
|
)
|
|
28
28
|
from phoenix.config import get_env_disable_basic_auth
|
|
29
29
|
from phoenix.db import models
|
|
30
|
-
from phoenix.server.api.auth import IsAdmin, IsLocked, IsNotReadOnly
|
|
30
|
+
from phoenix.server.api.auth import IsAdmin, IsLocked, IsNotReadOnly, IsNotViewer
|
|
31
31
|
from phoenix.server.api.context import Context
|
|
32
32
|
from phoenix.server.api.exceptions import BadRequest, Conflict, NotFound, Unauthorized
|
|
33
33
|
from phoenix.server.api.input_types.UserRoleInput import UserRoleInput
|
|
@@ -110,7 +110,7 @@ class UserMutationPayload:
|
|
|
110
110
|
|
|
111
111
|
@strawberry.type
|
|
112
112
|
class UserMutationMixin:
|
|
113
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsAdmin, IsLocked]) # type: ignore
|
|
113
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsAdmin, IsLocked]) # type: ignore
|
|
114
114
|
async def create_user(
|
|
115
115
|
self,
|
|
116
116
|
info: Info[Context, None],
|
|
@@ -157,7 +157,7 @@ class UserMutationMixin:
|
|
|
157
157
|
logger.error(f"Failed to send welcome email: {error}")
|
|
158
158
|
return UserMutationPayload(user=to_gql_user(user))
|
|
159
159
|
|
|
160
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsAdmin]) # type: ignore
|
|
160
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsAdmin]) # type: ignore
|
|
161
161
|
async def patch_user(
|
|
162
162
|
self,
|
|
163
163
|
info: Info[Context, None],
|
|
@@ -174,6 +174,7 @@ class UserMutationMixin:
|
|
|
174
174
|
if not (user := await session.scalar(_select_user_by_id(user_id))):
|
|
175
175
|
raise NotFound("User not found")
|
|
176
176
|
stack.enter_context(session.no_autoflush)
|
|
177
|
+
should_log_out = False
|
|
177
178
|
if input.new_role:
|
|
178
179
|
if user.email == DEFAULT_ADMIN_EMAIL:
|
|
179
180
|
raise Unauthorized("Cannot modify role for the default admin user")
|
|
@@ -183,6 +184,7 @@ class UserMutationMixin:
|
|
|
183
184
|
if user_role_id is None:
|
|
184
185
|
raise NotFound(f"Role {input.new_role.value} not found")
|
|
185
186
|
user.user_role_id = user_role_id
|
|
187
|
+
should_log_out = True
|
|
186
188
|
if password := input.new_password:
|
|
187
189
|
if user.auth_method != "LOCAL":
|
|
188
190
|
raise Conflict("Cannot modify password for non-local user")
|
|
@@ -191,6 +193,7 @@ class UserMutationMixin:
|
|
|
191
193
|
user.password_salt = salt
|
|
192
194
|
user.password_hash = await info.context.hash_password(Secret(password), salt)
|
|
193
195
|
user.reset_password = True
|
|
196
|
+
should_log_out = True
|
|
194
197
|
if username := input.new_username:
|
|
195
198
|
user.username = username
|
|
196
199
|
assert user in session.dirty
|
|
@@ -199,7 +202,7 @@ class UserMutationMixin:
|
|
|
199
202
|
except (PostgreSQLIntegrityError, SQLiteIntegrityError) as error:
|
|
200
203
|
raise Conflict(_user_operation_error_message(error, "modify"))
|
|
201
204
|
assert user
|
|
202
|
-
if
|
|
205
|
+
if should_log_out:
|
|
203
206
|
await info.context.log_out(user.id)
|
|
204
207
|
return UserMutationPayload(user=to_gql_user(user))
|
|
205
208
|
|
|
@@ -245,7 +248,7 @@ class UserMutationMixin:
|
|
|
245
248
|
response.delete_cookie(PHOENIX_ACCESS_TOKEN_COOKIE_NAME)
|
|
246
249
|
return UserMutationPayload(user=to_gql_user(user))
|
|
247
250
|
|
|
248
|
-
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsAdmin, IsLocked]) # type: ignore
|
|
251
|
+
@strawberry.mutation(permission_classes=[IsNotReadOnly, IsNotViewer, IsAdmin, IsLocked]) # type: ignore
|
|
249
252
|
async def delete_users(
|
|
250
253
|
self,
|
|
251
254
|
info: Info[Context, None],
|
|
@@ -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=
|
|
173
|
-
if expiration_time.timestamp()
|
|
174
|
-
raise HTTPException(status_code=
|
|
163
|
+
raise HTTPException(status_code=401, detail="Invalid refresh token")
|
|
164
|
+
if expiration_time.timestamp() <= datetime.now(timezone.utc).timestamp():
|
|
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
|
|
@@ -262,7 +253,7 @@ async def reset_password(request: Request) -> Response:
|
|
|
262
253
|
not (token := data.get("token"))
|
|
263
254
|
or not isinstance((claims := await token_store.read(token)), PasswordResetTokenClaims)
|
|
264
255
|
or not claims.expiration_time
|
|
265
|
-
or claims.expiration_time
|
|
256
|
+
or claims.expiration_time <= datetime.now(timezone.utc)
|
|
266
257
|
):
|
|
267
258
|
raise INVALID_TOKEN
|
|
268
259
|
assert (user_id := claims.subject)
|
|
@@ -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
|
)
|