From a2a620a59cab37e8322361473fc9b8b48a739b46 Mon Sep 17 00:00:00 2001 From: njbrake <33383515+njbrake@users.noreply.github.com> Date: Fri, 21 Aug 2026 00:45:18 +0000 Subject: [PATCH] chore: regenerate SDK client core from Otari OpenAPI spec --- sdk-endpoints.txt | 4 + src/otari/_client/__init__.py | 4 + src/otari/_client/api/auth_api.py | 286 +++++++++++++++++- src/otari/_client/api/bootstrap_api.py | 6 +- src/otari/_client/models/__init__.py | 2 + .../_client/models/create_session_request.py | 32 +- .../_client/models/deployment_bootstrap.py | 14 +- src/otari/_client/models/password_response.py | 90 ++++++ .../_client/models/set_password_request.py | 103 +++++++ 9 files changed, 527 insertions(+), 14 deletions(-) create mode 100644 src/otari/_client/models/password_response.py create mode 100644 src/otari/_client/models/set_password_request.py diff --git a/sdk-endpoints.txt b/sdk-endpoints.txt index 7fabe80..d7d85f1 100644 --- a/sdk-endpoints.txt +++ b/sdk-endpoints.txt @@ -114,6 +114,10 @@ POST /v1/auth/session # dashboard-only # it exists for a browser deciding what to render. An SDK already knows. GET /v1/bootstrap # dashboard-only DELETE /v1/auth/session # dashboard-only +# Setting the dashboard sign-in password: the other half of the same browser +# flow. An SDK authenticates with the master key or an API key and never holds +# a password, so there is nothing here for one to wrap. +PUT /v1/auth/password # dashboard-only # OTLP ingest: OpenTelemetry collector receivers, not an SDK surface. POST /v1/logs # otel ingest POST /v1/traces # otel ingest diff --git a/src/otari/_client/__init__.py b/src/otari/_client/__init__.py index 826af6f..4b26419 100644 --- a/src/otari/_client/__init__.py +++ b/src/otari/_client/__init__.py @@ -347,6 +347,7 @@ "OrganizationModelPricingUpdate", "OrganizationModelPricingsPublic", "OrganizationPublic", + "PasswordResponse", "PolicyRequest", "PolicyResponse", "PoolStatus", @@ -385,6 +386,7 @@ "SendTestMailRequest", "SendTestMailResponse", "SessionResponse", + "SetPasswordRequest", "SetPricingRequest", "Source", "Source1", @@ -790,6 +792,7 @@ from otari._client.models.organization_model_pricing_update import OrganizationModelPricingUpdate as OrganizationModelPricingUpdate from otari._client.models.organization_model_pricings_public import OrganizationModelPricingsPublic as OrganizationModelPricingsPublic from otari._client.models.organization_public import OrganizationPublic as OrganizationPublic +from otari._client.models.password_response import PasswordResponse as PasswordResponse from otari._client.models.policy_request import PolicyRequest as PolicyRequest from otari._client.models.policy_response import PolicyResponse as PolicyResponse from otari._client.models.pool_status import PoolStatus as PoolStatus @@ -828,6 +831,7 @@ from otari._client.models.send_test_mail_request import SendTestMailRequest as SendTestMailRequest from otari._client.models.send_test_mail_response import SendTestMailResponse as SendTestMailResponse from otari._client.models.session_response import SessionResponse as SessionResponse +from otari._client.models.set_password_request import SetPasswordRequest as SetPasswordRequest from otari._client.models.set_pricing_request import SetPricingRequest as SetPricingRequest from otari._client.models.source import Source as Source from otari._client.models.source1 import Source1 as Source1 diff --git a/src/otari/_client/api/auth_api.py b/src/otari/_client/api/auth_api.py index 5d72b9f..f60ad0d 100644 --- a/src/otari/_client/api/auth_api.py +++ b/src/otari/_client/api/auth_api.py @@ -16,7 +16,9 @@ from typing_extensions import Annotated from otari._client.models.create_session_request import CreateSessionRequest +from otari._client.models.password_response import PasswordResponse from otari._client.models.session_response import SessionResponse +from otari._client.models.set_password_request import SetPasswordRequest from otari._client.api_client import ApiClient, RequestSerialized from otari._client.api_response import ApiResponse @@ -55,7 +57,7 @@ def create_session_v1_auth_session_post( ) -> SessionResponse: """Create Session - Verify the master key and set the HttpOnly session cookie. The session is bound to the bootstrap operator identity, so every request it later authenticates resolves a user and that user's active organization rather than only \"the master key was presented once\". The response names both, so a client knows who it is signed in as without a second call. The rate-limit check deliberately runs only after a failed verification, not before it: a pre-verification gate can't know whether *this* attempt would have succeeded, so once an IP has used up its failure quota it would end up blocking that IP's legitimate owner too, not just further attackers. The issue this implements explicitly rules that out. The DB/hash lookup this exposes to repeated attempts only runs when no fixed master_key is configured (the auto-generated bootstrap-key path); with a configured master_key, verification is a constant-time string compare, not a DB round trip. + Verify a sign-in credential and set the HttpOnly session cookie. The session is bound to the identity that authenticated, so every request it later authenticates resolves a user and that user's active organization rather than only \"a credential was presented once\". The response names both, so a client knows who it is signed in as without a second call. The rate-limit check deliberately runs only after a failed verification, not before it: a pre-verification gate can't know whether *this* attempt would have succeeded, so once an IP has used up its failure quota it would end up blocking that IP's legitimate owner too, not just further attackers. Running after verification also means the throttle bounds how many verdicts an IP gets, not how much work it can cause: a password attempt pays for a bcrypt verification (cost 12, on the order of 200ms of CPU, and one is burned against a stand-in hash even for an address nobody holds) before the limit is consulted, so a 429 costs the same as a 401. A gateway exposed to the internet should rate-limit this path at the proxy as well. :param create_session_request: (required) :type create_session_request: CreateSessionRequest @@ -123,7 +125,7 @@ def create_session_v1_auth_session_post_with_http_info( ) -> ApiResponse[SessionResponse]: """Create Session - Verify the master key and set the HttpOnly session cookie. The session is bound to the bootstrap operator identity, so every request it later authenticates resolves a user and that user's active organization rather than only \"the master key was presented once\". The response names both, so a client knows who it is signed in as without a second call. The rate-limit check deliberately runs only after a failed verification, not before it: a pre-verification gate can't know whether *this* attempt would have succeeded, so once an IP has used up its failure quota it would end up blocking that IP's legitimate owner too, not just further attackers. The issue this implements explicitly rules that out. The DB/hash lookup this exposes to repeated attempts only runs when no fixed master_key is configured (the auto-generated bootstrap-key path); with a configured master_key, verification is a constant-time string compare, not a DB round trip. + Verify a sign-in credential and set the HttpOnly session cookie. The session is bound to the identity that authenticated, so every request it later authenticates resolves a user and that user's active organization rather than only \"a credential was presented once\". The response names both, so a client knows who it is signed in as without a second call. The rate-limit check deliberately runs only after a failed verification, not before it: a pre-verification gate can't know whether *this* attempt would have succeeded, so once an IP has used up its failure quota it would end up blocking that IP's legitimate owner too, not just further attackers. Running after verification also means the throttle bounds how many verdicts an IP gets, not how much work it can cause: a password attempt pays for a bcrypt verification (cost 12, on the order of 200ms of CPU, and one is burned against a stand-in hash even for an address nobody holds) before the limit is consulted, so a 429 costs the same as a 401. A gateway exposed to the internet should rate-limit this path at the proxy as well. :param create_session_request: (required) :type create_session_request: CreateSessionRequest @@ -191,7 +193,7 @@ def create_session_v1_auth_session_post_without_preload_content( ) -> RESTResponseType: """Create Session - Verify the master key and set the HttpOnly session cookie. The session is bound to the bootstrap operator identity, so every request it later authenticates resolves a user and that user's active organization rather than only \"the master key was presented once\". The response names both, so a client knows who it is signed in as without a second call. The rate-limit check deliberately runs only after a failed verification, not before it: a pre-verification gate can't know whether *this* attempt would have succeeded, so once an IP has used up its failure quota it would end up blocking that IP's legitimate owner too, not just further attackers. The issue this implements explicitly rules that out. The DB/hash lookup this exposes to repeated attempts only runs when no fixed master_key is configured (the auto-generated bootstrap-key path); with a configured master_key, verification is a constant-time string compare, not a DB round trip. + Verify a sign-in credential and set the HttpOnly session cookie. The session is bound to the identity that authenticated, so every request it later authenticates resolves a user and that user's active organization rather than only \"a credential was presented once\". The response names both, so a client knows who it is signed in as without a second call. The rate-limit check deliberately runs only after a failed verification, not before it: a pre-verification gate can't know whether *this* attempt would have succeeded, so once an IP has used up its failure quota it would end up blocking that IP's legitimate owner too, not just further attackers. Running after verification also means the throttle bounds how many verdicts an IP gets, not how much work it can cause: a password attempt pays for a bcrypt verification (cost 12, on the order of 200ms of CPU, and one is burned against a stand-in hash even for an address nobody holds) before the limit is consulted, so a 429 costs the same as a 401. A gateway exposed to the internet should rate-limit this path at the proxy as well. :param create_session_request: (required) :type create_session_request: CreateSessionRequest @@ -548,3 +550,281 @@ def _delete_session_v1_auth_session_delete_serialize( ) + + + @validate_call + def set_dashboard_password_v1_auth_password_put( + self, + set_password_request: SetPasswordRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> PasswordResponse: + """Set Dashboard Password + + Set or change the password the caller signs in to the dashboard with. Always the caller's own identity. Supply ``email`` when it has no sign-in address yet, which is the state first boot leaves the operator in, and ``current_password`` when it already has a password and the request is authenticated by the session cookie. The master key in a header is what excuses ``current_password``, which is how a forgotten password is recovered; it does not excuse ``email``, because an identity with no address has nothing to sign in with whoever is asking. Setting a password for the first time retires master-key sign-in on this deployment. Every other session this identity holds ends, the caller's own excepted, so a cookie stolen before the change does not outlive it. + + :param set_password_request: (required) + :type set_password_request: SetPasswordRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._set_dashboard_password_v1_auth_password_put_serialize( + set_password_request=set_password_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PasswordResponse", + '422': "HTTPValidationError", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + def set_dashboard_password_v1_auth_password_put_with_http_info( + self, + set_password_request: SetPasswordRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[PasswordResponse]: + """Set Dashboard Password + + Set or change the password the caller signs in to the dashboard with. Always the caller's own identity. Supply ``email`` when it has no sign-in address yet, which is the state first boot leaves the operator in, and ``current_password`` when it already has a password and the request is authenticated by the session cookie. The master key in a header is what excuses ``current_password``, which is how a forgotten password is recovered; it does not excuse ``email``, because an identity with no address has nothing to sign in with whoever is asking. Setting a password for the first time retires master-key sign-in on this deployment. Every other session this identity holds ends, the caller's own excepted, so a cookie stolen before the change does not outlive it. + + :param set_password_request: (required) + :type set_password_request: SetPasswordRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._set_dashboard_password_v1_auth_password_put_serialize( + set_password_request=set_password_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PasswordResponse", + '422': "HTTPValidationError", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + def set_dashboard_password_v1_auth_password_put_without_preload_content( + self, + set_password_request: SetPasswordRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Set Dashboard Password + + Set or change the password the caller signs in to the dashboard with. Always the caller's own identity. Supply ``email`` when it has no sign-in address yet, which is the state first boot leaves the operator in, and ``current_password`` when it already has a password and the request is authenticated by the session cookie. The master key in a header is what excuses ``current_password``, which is how a forgotten password is recovered; it does not excuse ``email``, because an identity with no address has nothing to sign in with whoever is asking. Setting a password for the first time retires master-key sign-in on this deployment. Every other session this identity holds ends, the caller's own excepted, so a cookie stolen before the change does not outlive it. + + :param set_password_request: (required) + :type set_password_request: SetPasswordRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._set_dashboard_password_v1_auth_password_put_serialize( + set_password_request=set_password_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "PasswordResponse", + '422': "HTTPValidationError", + } + response_data = self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _set_dashboard_password_v1_auth_password_put_serialize( + self, + set_password_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if set_password_request is not None: + _body_params = set_password_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'XApiKeyAuth', + 'ApiKeyAuth' + ] + + return self.api_client.param_serialize( + method='PUT', + resource_path='/v1/auth/password', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + diff --git a/src/otari/_client/api/bootstrap_api.py b/src/otari/_client/api/bootstrap_api.py index c9de74d..16e3087 100644 --- a/src/otari/_client/api/bootstrap_api.py +++ b/src/otari/_client/api/bootstrap_api.py @@ -53,7 +53,7 @@ def get_bootstrap_v1_bootstrap_get( ) -> DeploymentBootstrap: """Get Bootstrap - Return the deployment context the dashboard shell renders from. Public: the shell fetches this before it knows whether it can authenticate. + Return the deployment context the dashboard shell renders from. Public: the shell fetches this before it knows whether it can authenticate. That is also why ``sign_in_methods`` is answered here rather than behind a credential, and it publishes nothing an unauthenticated caller could not already learn by trying both credentials against the sign-in endpoint. The one database read is a ``LIMIT 1`` probe for any identity holding a password, over a table a standalone deployment keeps one row per person in. It runs only in standalone mode: a hybrid gateway has no session to describe, and ``get_db_if_needed`` hands it no session to read one from. :param _request_timeout: timeout setting for this request. If one number provided, it will be total request @@ -116,7 +116,7 @@ def get_bootstrap_v1_bootstrap_get_with_http_info( ) -> ApiResponse[DeploymentBootstrap]: """Get Bootstrap - Return the deployment context the dashboard shell renders from. Public: the shell fetches this before it knows whether it can authenticate. + Return the deployment context the dashboard shell renders from. Public: the shell fetches this before it knows whether it can authenticate. That is also why ``sign_in_methods`` is answered here rather than behind a credential, and it publishes nothing an unauthenticated caller could not already learn by trying both credentials against the sign-in endpoint. The one database read is a ``LIMIT 1`` probe for any identity holding a password, over a table a standalone deployment keeps one row per person in. It runs only in standalone mode: a hybrid gateway has no session to describe, and ``get_db_if_needed`` hands it no session to read one from. :param _request_timeout: timeout setting for this request. If one number provided, it will be total request @@ -179,7 +179,7 @@ def get_bootstrap_v1_bootstrap_get_without_preload_content( ) -> RESTResponseType: """Get Bootstrap - Return the deployment context the dashboard shell renders from. Public: the shell fetches this before it knows whether it can authenticate. + Return the deployment context the dashboard shell renders from. Public: the shell fetches this before it knows whether it can authenticate. That is also why ``sign_in_methods`` is answered here rather than behind a credential, and it publishes nothing an unauthenticated caller could not already learn by trying both credentials against the sign-in endpoint. The one database read is a ``LIMIT 1`` probe for any identity holding a password, over a table a standalone deployment keeps one row per person in. It runs only in standalone mode: a hybrid gateway has no session to describe, and ``get_db_if_needed`` hands it no session to read one from. :param _request_timeout: timeout setting for this request. If one number provided, it will be total request diff --git a/src/otari/_client/models/__init__.py b/src/otari/_client/models/__init__.py index 89c19d5..705c04c 100644 --- a/src/otari/_client/models/__init__.py +++ b/src/otari/_client/models/__init__.py @@ -297,6 +297,7 @@ from otari._client.models.organization_model_pricing_update import OrganizationModelPricingUpdate from otari._client.models.organization_model_pricings_public import OrganizationModelPricingsPublic from otari._client.models.organization_public import OrganizationPublic +from otari._client.models.password_response import PasswordResponse from otari._client.models.policy_request import PolicyRequest from otari._client.models.policy_response import PolicyResponse from otari._client.models.pool_status import PoolStatus @@ -335,6 +336,7 @@ from otari._client.models.send_test_mail_request import SendTestMailRequest from otari._client.models.send_test_mail_response import SendTestMailResponse from otari._client.models.session_response import SessionResponse +from otari._client.models.set_password_request import SetPasswordRequest from otari._client.models.set_pricing_request import SetPricingRequest from otari._client.models.source import Source from otari._client.models.source1 import Source1 diff --git a/src/otari/_client/models/create_session_request.py b/src/otari/_client/models/create_session_request.py index ecee07a..ae8eae7 100644 --- a/src/otari/_client/models/create_session_request.py +++ b/src/otari/_client/models/create_session_request.py @@ -17,18 +17,21 @@ import re # noqa: F401 import json -from pydantic import BaseModel, ConfigDict, Field, StrictStr -from typing import Any, ClassVar, Dict, List +from pydantic import BaseModel, ConfigDict, Field +from typing import Any, ClassVar, Dict, List, Optional +from typing_extensions import Annotated from typing import Optional, Set from typing_extensions import Self from pydantic_core import to_jsonable_python class CreateSessionRequest(BaseModel): """ - Sign in to the dashboard by proving possession of the master key. + Sign in to the dashboard with exactly one credential. A flat body with an optional field per credential, rather than a tagged union: it is one extra key on the wire, it generates a client type a hand-written form can fill in, and the validator below makes the two forms exclusive anyway. The example carries one credential, because a generated example is a body somebody will post: the schema alone would produce every field at once, which is the one shape the validator below refuses. """ # noqa: E501 - master_key: StrictStr = Field(description="The gateway master key; verified once and never stored by the browser.") - __properties: ClassVar[List[str]] = ["master_key"] + email: Optional[Annotated[str, Field(strict=True, max_length=255)]] = Field(default=None, description="The identity's sign-in address.") + master_key: Optional[Annotated[str, Field(strict=True, max_length=512)]] = Field(default=None, description="The gateway master key; verified once and never stored by the browser. Accepted only while no identity on this deployment has a password (see GET /v1/bootstrap).") + password: Optional[Annotated[str, Field(strict=True, max_length=72)]] = Field(default=None, description="The identity's password.") + __properties: ClassVar[List[str]] = ["email", "master_key", "password"] model_config = ConfigDict( validate_by_name=True, @@ -69,6 +72,21 @@ def to_dict(self) -> Dict[str, Any]: exclude=excluded_fields, exclude_none=True, ) + # set to None if email (nullable) is None + # and model_fields_set contains the field + if self.email is None and "email" in self.model_fields_set: + _dict['email'] = None + + # set to None if master_key (nullable) is None + # and model_fields_set contains the field + if self.master_key is None and "master_key" in self.model_fields_set: + _dict['master_key'] = None + + # set to None if password (nullable) is None + # and model_fields_set contains the field + if self.password is None and "password" in self.model_fields_set: + _dict['password'] = None + return _dict @classmethod @@ -81,7 +99,9 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: return cls.model_validate(obj) _obj = cls.model_validate({ - "master_key": obj.get("master_key") + "email": obj.get("email"), + "master_key": obj.get("master_key"), + "password": obj.get("password") }) return _obj diff --git a/src/otari/_client/models/deployment_bootstrap.py b/src/otari/_client/models/deployment_bootstrap.py index bacd002..2beb791 100644 --- a/src/otari/_client/models/deployment_bootstrap.py +++ b/src/otari/_client/models/deployment_bootstrap.py @@ -30,9 +30,10 @@ class DeploymentBootstrap(BaseModel): deployment_type: StrictStr = Field(description="Which deployment serves this URL. 'standalone' owns its own data; 'hosted' is otari.ai; 'hybrid' is a gateway attached to otari.ai, which is data-plane only and holds no management surface of its own.") mail_ready: StrictBool = Field(description="Whether this deployment can deliver a message carrying a link back to itself (an invitation's accept link, and the verification and reset links to come), not merely whether a transport is configured: it also needs to know its own public URL to put in one. Lets the dashboard disable or hide a mail-dependent affordance instead of offering one that would fail at send time. Every message this control plane sends carries such a link, which is why this is one flag and not one per feature. False for a hybrid gateway, whose control plane is otari.ai and which sends no mail of its own.") management_url: Optional[StrictStr] = Field(description="Where the authoritative control plane lives when it is not this deployment. Set for a hybrid gateway so its landing page can link to otari.ai; null otherwise.") - session_type: StrictStr = Field(description="The kind of session this deployment issues, not whether the caller holds one. 'local_operator' is the standalone master-key sign-in, 'hosted_user' an otari.ai account, and 'none' a deployment that issues no management session at all.") + session_type: StrictStr = Field(description="The kind of session this deployment issues, not whether the caller holds one. 'local_operator' is the standalone operator sign-in (see sign_in_methods for which credential it currently accepts), 'hosted_user' an otari.ai account, and 'none' a deployment that issues no management session at all.") + sign_in_methods: List[StrictStr] = Field(description="How POST /v1/auth/session may be authenticated right now, sorted. 'master_key' is the first-boot credential and is offered until some identity on this deployment has a password; 'password' replaces it from then on, and the master key stays the credential for the management API. Empty for a hybrid gateway, which issues no session. The login page renders from this rather than trying a credential to find out.") surfaces: List[StrictStr] = Field(description="Management API groups this deployment serves, sorted, which is what its dashboard pages gate on. Named surfaces, not capabilities: capability is otari.ai's word for the entitlement (licensing) axis, and this is the deployment (topology) axis. Empty for a hybrid gateway.") - __properties: ClassVar[List[str]] = ["deployment_type", "mail_ready", "management_url", "session_type", "surfaces"] + __properties: ClassVar[List[str]] = ["deployment_type", "mail_ready", "management_url", "session_type", "sign_in_methods", "surfaces"] @field_validator('deployment_type') def deployment_type_validate_enum(cls, value): @@ -48,6 +49,14 @@ def session_type_validate_enum(cls, value): raise ValueError("must be one of enum values ('local_operator', 'hosted_user', 'none')") return value + @field_validator('sign_in_methods') + def sign_in_methods_validate_enum(cls, value): + """Validates the enum""" + for i in value: + if i not in set(['master_key', 'password']): + raise ValueError("each list item must be one of ('master_key', 'password')") + return value + model_config = ConfigDict( validate_by_name=True, validate_by_alias=True, @@ -108,6 +117,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "mail_ready": obj.get("mail_ready"), "management_url": obj.get("management_url"), "session_type": obj.get("session_type"), + "sign_in_methods": obj.get("sign_in_methods"), "surfaces": obj.get("surfaces") }) return _obj diff --git a/src/otari/_client/models/password_response.py b/src/otari/_client/models/password_response.py new file mode 100644 index 0000000..79faca9 --- /dev/null +++ b/src/otari/_client/models/password_response.py @@ -0,0 +1,90 @@ +# coding: utf-8 + +""" + otari + + Otari, an OpenAI-compatible LLM gateway with API key management + + The version of the OpenAPI document: 0.0.0-dev + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class PasswordResponse(BaseModel): + """ + What the identity signs in with now. + """ # noqa: E501 + email: StrictStr = Field(description="The address this identity signs in with.") + master_key_sign_in_retired: StrictBool = Field(description="Always true once this succeeds: some identity on this deployment now has a password, so POST /v1/auth/session no longer accepts the master key. It stays the credential for the management API.") + __properties: ClassVar[List[str]] = ["email", "master_key_sign_in_retired"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of PasswordResponse from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of PasswordResponse from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "email": obj.get("email"), + "master_key_sign_in_retired": obj.get("master_key_sign_in_retired") + }) + return _obj + + diff --git a/src/otari/_client/models/set_password_request.py b/src/otari/_client/models/set_password_request.py new file mode 100644 index 0000000..f4376a1 --- /dev/null +++ b/src/otari/_client/models/set_password_request.py @@ -0,0 +1,103 @@ +# coding: utf-8 + +""" + otari + + Otari, an OpenAI-compatible LLM gateway with API key management + + The version of the OpenAPI document: 0.0.0-dev + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field +from typing import Any, ClassVar, Dict, List, Optional +from typing_extensions import Annotated +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class SetPasswordRequest(BaseModel): + """ + Set or change the signed-in identity's password. The example is the first-boot claim, because that is the call an operator makes first and the one whose required fields are not obvious from the schema: ``email`` is optional here in general and *required* when the identity has no address yet. Without it the generated Postman body carries only ``new_password``, which is the one shape that cannot complete the flow the docs walk through. + """ # noqa: E501 + current_password: Optional[Annotated[str, Field(strict=True, max_length=1024)]] = Field(default=None, description="The password being replaced. Required when the identity already has one and the request is authenticated by the session cookie; ignored when the master key is sent in a header, which needs no proof of the old password (it still needs `email` when the identity has no sign-in address yet).") + email: Optional[Annotated[str, Field(strict=True, max_length=255)]] = Field(default=None, description="The address to sign in with. Required when the identity has none, which is the state first boot leaves the operator in, including when the master key is what authenticates the call. Resubmitting the address the identity already holds is accepted and ignored; only a *different* address is refused, because changing one is not supported yet.") + new_password: Annotated[str, Field(min_length=8, strict=True, max_length=1024)] = Field(description="The password to sign in with from now on. At least 8 characters, and at most 72 bytes, which is bcrypt's ceiling.") + __properties: ClassVar[List[str]] = ["current_password", "email", "new_password"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of SetPasswordRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # set to None if current_password (nullable) is None + # and model_fields_set contains the field + if self.current_password is None and "current_password" in self.model_fields_set: + _dict['current_password'] = None + + # set to None if email (nullable) is None + # and model_fields_set contains the field + if self.email is None and "email" in self.model_fields_set: + _dict['email'] = None + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of SetPasswordRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "current_password": obj.get("current_password"), + "email": obj.get("email"), + "new_password": obj.get("new_password") + }) + return _obj + +