From 3e7155d9976f6d80d930b1ab047a5b43909c18a8 Mon Sep 17 00:00:00 2001 From: etaylormcgregor-stytch Date: Thu, 10 Oct 2024 13:58:54 -0400 Subject: [PATCH] Add external connection endpoints (#221) * add external connection endpoints * bump minor version --- stytch/b2b/api/organizations_members.py | 8 + stytch/b2b/api/sso.py | 10 +- stytch/b2b/api/sso_external.py | 239 ++++++++++++++++++++++++ stytch/b2b/models/sso.py | 33 +++- stytch/b2b/models/sso_external.py | 65 +++++++ stytch/consumer/api/sessions.py | 4 +- stytch/version.py | 2 +- 7 files changed, 354 insertions(+), 7 deletions(-) create mode 100644 stytch/b2b/api/sso_external.py create mode 100644 stytch/b2b/models/sso_external.py diff --git a/stytch/b2b/api/organizations_members.py b/stytch/b2b/api/organizations_members.py index 7fb76a9..cf4fd2b 100644 --- a/stytch/b2b/api/organizations_members.py +++ b/stytch/b2b/api/organizations_members.py @@ -604,16 +604,20 @@ async def delete_password_async( def dangerously_get( self, member_id: str, + include_deleted: Optional[bool] = None, ) -> GetResponse: """Get a Member by `member_id`. This endpoint does not require an `organization_id`, enabling you to get members across organizations. This is a dangerous operation. Incorrect use may open you up to indirect object reference (IDOR) attacks. We recommend using the [Get Member](https://stytch.com/docs/b2b/api/get-member) API instead. Fields: - member_id: Globally unique UUID that identifies a specific Member. The `member_id` is critical to perform operations on a Member, so be sure to preserve this value. + - include_deleted: Whether to include deleted Members in the response. Defaults to false. """ # noqa headers: Dict[str, str] = {} data: Dict[str, Any] = { "member_id": member_id, } + if include_deleted is not None: + data["include_deleted"] = include_deleted url = self.api_base.url_for( "/v1/b2b/organizations/members/dangerously_get/{member_id}", data @@ -624,16 +628,20 @@ def dangerously_get( async def dangerously_get_async( self, member_id: str, + include_deleted: Optional[bool] = None, ) -> GetResponse: """Get a Member by `member_id`. This endpoint does not require an `organization_id`, enabling you to get members across organizations. This is a dangerous operation. Incorrect use may open you up to indirect object reference (IDOR) attacks. We recommend using the [Get Member](https://stytch.com/docs/b2b/api/get-member) API instead. Fields: - member_id: Globally unique UUID that identifies a specific Member. The `member_id` is critical to perform operations on a Member, so be sure to preserve this value. + - include_deleted: Whether to include deleted Members in the response. Defaults to false. """ # noqa headers: Dict[str, str] = {} data: Dict[str, Any] = { "member_id": member_id, } + if include_deleted is not None: + data["include_deleted"] = include_deleted url = self.api_base.url_for( "/v1/b2b/organizations/members/dangerously_get/{member_id}", data diff --git a/stytch/b2b/api/sso.py b/stytch/b2b/api/sso.py index 2cedc23..484e35c 100644 --- a/stytch/b2b/api/sso.py +++ b/stytch/b2b/api/sso.py @@ -8,6 +8,7 @@ from typing import Any, Dict, Optional, Union +from stytch.b2b.api.sso_external import External from stytch.b2b.api.sso_oidc import OIDC from stytch.b2b.api.sso_saml import SAML from stytch.b2b.models.sso import ( @@ -39,6 +40,11 @@ def __init__( sync_client=self.sync_client, async_client=self.async_client, ) + self.external = External( + api_base=self.api_base, + sync_client=self.sync_client, + async_client=self.async_client, + ) def get_connections( self, @@ -92,7 +98,7 @@ def delete_connection( Fields: - organization_id: The organization ID that the SSO connection belongs to. - - connection_id: The ID of the SSO connection. Both SAML and OIDC connection IDs can be provided. + - connection_id: The ID of the SSO connection. SAML, OIDC, and External connection IDs can be provided. """ # noqa headers: Dict[str, str] = {} if method_options is not None: @@ -118,7 +124,7 @@ async def delete_connection_async( Fields: - organization_id: The organization ID that the SSO connection belongs to. - - connection_id: The ID of the SSO connection. Both SAML and OIDC connection IDs can be provided. + - connection_id: The ID of the SSO connection. SAML, OIDC, and External connection IDs can be provided. """ # noqa headers: Dict[str, str] = {} if method_options is not None: diff --git a/stytch/b2b/api/sso_external.py b/stytch/b2b/api/sso_external.py new file mode 100644 index 0000000..cfad035 --- /dev/null +++ b/stytch/b2b/api/sso_external.py @@ -0,0 +1,239 @@ +# !!! +# WARNING: This file is autogenerated +# Only modify code within MANUAL() sections +# or your changes may be overwritten later! +# !!! + +from __future__ import annotations + +from typing import Any, Dict, List, Optional, Union + +from stytch.b2b.models.sso import ( + ConnectionImplicitRoleAssignment, + GroupImplicitRoleAssignment, + SAMLConnectionImplicitRoleAssignment, + SAMLGroupImplicitRoleAssignment, +) +from stytch.b2b.models.sso_external import ( + CreateConnectionRequestOptions, + CreateConnectionResponse, + UpdateConnectionRequestOptions, + UpdateConnectionResponse, +) +from stytch.core.api_base import ApiBase +from stytch.core.http.client import AsyncClient, SyncClient + + +class External: + def __init__( + self, api_base: ApiBase, sync_client: SyncClient, async_client: AsyncClient + ) -> None: + self.api_base = api_base + self.sync_client = sync_client + self.async_client = async_client + + def create_connection( + self, + organization_id: str, + external_organization_id: str, + external_connection_id: str, + display_name: Optional[str] = None, + connection_implicit_role_assignments: Optional[ + List[Union[SAMLConnectionImplicitRoleAssignment, Dict[str, Any]]] + ] = None, + group_implicit_role_assignments: Optional[ + List[Union[SAMLGroupImplicitRoleAssignment, Dict[str, Any]]] + ] = None, + method_options: Optional[CreateConnectionRequestOptions] = None, + ) -> CreateConnectionResponse: + """Create a new External SSO Connection. + + Fields: + - organization_id: Globally unique UUID that identifies a specific Organization. The `organization_id` is critical to perform operations on an Organization, so be sure to preserve this value. + - external_organization_id: Globally unique UUID that identifies a different Organization within your Project. + - external_connection_id: Globally unique UUID that identifies a specific SSO connection configured for a different Organization in your Project. + - display_name: A human-readable display name for the connection. + - connection_implicit_role_assignments: (no documentation yet) + - group_implicit_role_assignments: (no documentation yet) + """ # noqa + headers: Dict[str, str] = {} + if method_options is not None: + headers = method_options.add_headers(headers) + data: Dict[str, Any] = { + "organization_id": organization_id, + "external_organization_id": external_organization_id, + "external_connection_id": external_connection_id, + } + if display_name is not None: + data["display_name"] = display_name + if connection_implicit_role_assignments is not None: + data["connection_implicit_role_assignments"] = [ + item if isinstance(item, dict) else item.dict() + for item in connection_implicit_role_assignments + ] + if group_implicit_role_assignments is not None: + data["group_implicit_role_assignments"] = [ + item if isinstance(item, dict) else item.dict() + for item in group_implicit_role_assignments + ] + + url = self.api_base.url_for("/v1/b2b/sso/external/{organization_id}", data) + res = self.sync_client.post(url, data, headers) + return CreateConnectionResponse.from_json(res.response.status_code, res.json) + + async def create_connection_async( + self, + organization_id: str, + external_organization_id: str, + external_connection_id: str, + display_name: Optional[str] = None, + connection_implicit_role_assignments: Optional[ + List[SAMLConnectionImplicitRoleAssignment] + ] = None, + group_implicit_role_assignments: Optional[ + List[SAMLGroupImplicitRoleAssignment] + ] = None, + method_options: Optional[CreateConnectionRequestOptions] = None, + ) -> CreateConnectionResponse: + """Create a new External SSO Connection. + + Fields: + - organization_id: Globally unique UUID that identifies a specific Organization. The `organization_id` is critical to perform operations on an Organization, so be sure to preserve this value. + - external_organization_id: Globally unique UUID that identifies a different Organization within your Project. + - external_connection_id: Globally unique UUID that identifies a specific SSO connection configured for a different Organization in your Project. + - display_name: A human-readable display name for the connection. + - connection_implicit_role_assignments: (no documentation yet) + - group_implicit_role_assignments: (no documentation yet) + """ # noqa + headers: Dict[str, str] = {} + if method_options is not None: + headers = method_options.add_headers(headers) + data: Dict[str, Any] = { + "organization_id": organization_id, + "external_organization_id": external_organization_id, + "external_connection_id": external_connection_id, + } + if display_name is not None: + data["display_name"] = display_name + if connection_implicit_role_assignments is not None: + data["connection_implicit_role_assignments"] = [ + item if isinstance(item, dict) else item.dict() + for item in connection_implicit_role_assignments + ] + if group_implicit_role_assignments is not None: + data["group_implicit_role_assignments"] = [ + item if isinstance(item, dict) else item.dict() + for item in group_implicit_role_assignments + ] + + url = self.api_base.url_for("/v1/b2b/sso/external/{organization_id}", data) + res = await self.async_client.post(url, data, headers) + return CreateConnectionResponse.from_json(res.response.status, res.json) + + def update_connection( + self, + organization_id: str, + connection_id: str, + display_name: Optional[str] = None, + external_connection_implicit_role_assignments: Optional[ + List[Union[ConnectionImplicitRoleAssignment, Dict[str, Any]]] + ] = None, + external_group_implicit_role_assignments: Optional[ + List[Union[GroupImplicitRoleAssignment, Dict[str, Any]]] + ] = None, + method_options: Optional[UpdateConnectionRequestOptions] = None, + ) -> UpdateConnectionResponse: + """Updates an existing External SSO connection. + + Fields: + - organization_id: Globally unique UUID that identifies a specific Organization. The `organization_id` is critical to perform operations on an Organization, so be sure to preserve this value. + - connection_id: Globally unique UUID that identifies a specific External SSO Connection. + - display_name: A human-readable display name for the connection. + - external_connection_implicit_role_assignments: All Members who log in with this External connection will implicitly receive the specified Roles. See the [RBAC guide](https://stytch.com/docs/b2b/guides/rbac/role-assignment) for more information about role assignment.Implicit role assignments are not supported for External connections if the underlying SSO connection is an OIDC connection. + - external_group_implicit_role_assignments: Defines the names of the groups + that grant specific role assignments. For each group-Role pair, if a Member logs in with this external connection and + belongs to the specified group, they will be granted the associated Role. See the + [RBAC guide](https://stytch.com/docs/b2b/guides/rbac/role-assignment) for more information about role assignment. + Before adding any group implicit role assignments to an external connection, you must add a "groups" key to the underlying SAML connection's + `attribute_mapping`. Make sure that the SAML connection IdP is configured to correctly send the group information. Implicit role assignments are not supported + for External connections if the underlying SSO connection is an OIDC connection. + """ # noqa + headers: Dict[str, str] = {} + if method_options is not None: + headers = method_options.add_headers(headers) + data: Dict[str, Any] = { + "organization_id": organization_id, + "connection_id": connection_id, + } + if display_name is not None: + data["display_name"] = display_name + if external_connection_implicit_role_assignments is not None: + data["external_connection_implicit_role_assignments"] = [ + item if isinstance(item, dict) else item.dict() + for item in external_connection_implicit_role_assignments + ] + if external_group_implicit_role_assignments is not None: + data["external_group_implicit_role_assignments"] = [ + item if isinstance(item, dict) else item.dict() + for item in external_group_implicit_role_assignments + ] + + url = self.api_base.url_for( + "/v1/b2b/sso/external/{organization_id}/connections/{connection_id}", data + ) + res = self.sync_client.put(url, data, headers) + return UpdateConnectionResponse.from_json(res.response.status_code, res.json) + + async def update_connection_async( + self, + organization_id: str, + connection_id: str, + display_name: Optional[str] = None, + external_connection_implicit_role_assignments: Optional[ + List[ConnectionImplicitRoleAssignment] + ] = None, + external_group_implicit_role_assignments: Optional[ + List[GroupImplicitRoleAssignment] + ] = None, + method_options: Optional[UpdateConnectionRequestOptions] = None, + ) -> UpdateConnectionResponse: + """Updates an existing External SSO connection. + + Fields: + - organization_id: Globally unique UUID that identifies a specific Organization. The `organization_id` is critical to perform operations on an Organization, so be sure to preserve this value. + - connection_id: Globally unique UUID that identifies a specific External SSO Connection. + - display_name: A human-readable display name for the connection. + - external_connection_implicit_role_assignments: All Members who log in with this External connection will implicitly receive the specified Roles. See the [RBAC guide](https://stytch.com/docs/b2b/guides/rbac/role-assignment) for more information about role assignment.Implicit role assignments are not supported for External connections if the underlying SSO connection is an OIDC connection. + - external_group_implicit_role_assignments: Defines the names of the groups + that grant specific role assignments. For each group-Role pair, if a Member logs in with this external connection and + belongs to the specified group, they will be granted the associated Role. See the + [RBAC guide](https://stytch.com/docs/b2b/guides/rbac/role-assignment) for more information about role assignment. + Before adding any group implicit role assignments to an external connection, you must add a "groups" key to the underlying SAML connection's + `attribute_mapping`. Make sure that the SAML connection IdP is configured to correctly send the group information. Implicit role assignments are not supported + for External connections if the underlying SSO connection is an OIDC connection. + """ # noqa + headers: Dict[str, str] = {} + if method_options is not None: + headers = method_options.add_headers(headers) + data: Dict[str, Any] = { + "organization_id": organization_id, + "connection_id": connection_id, + } + if display_name is not None: + data["display_name"] = display_name + if external_connection_implicit_role_assignments is not None: + data["external_connection_implicit_role_assignments"] = [ + item if isinstance(item, dict) else item.dict() + for item in external_connection_implicit_role_assignments + ] + if external_group_implicit_role_assignments is not None: + data["external_group_implicit_role_assignments"] = [ + item if isinstance(item, dict) else item.dict() + for item in external_group_implicit_role_assignments + ] + + url = self.api_base.url_for( + "/v1/b2b/sso/external/{organization_id}/connections/{connection_id}", data + ) + res = await self.async_client.put(url, data, headers) + return UpdateConnectionResponse.from_json(res.response.status, res.json) diff --git a/stytch/b2b/models/sso.py b/stytch/b2b/models/sso.py index 31807d9..7a4040b 100644 --- a/stytch/b2b/models/sso.py +++ b/stytch/b2b/models/sso.py @@ -26,6 +26,20 @@ class AuthenticateRequestLocale(str, enum.Enum): class ConnectionImplicitRoleAssignment(pydantic.BaseModel): + """ + Fields: + - role_id: The unique identifier of the RBAC Role, provided by the developer and intended to be human-readable. + + Reserved `role_id`s that are predefined by Stytch include: + + * `stytch_member` + * `stytch_admin` + + Check out the [guide on Stytch default Roles](https://stytch.com/docs/b2b/guides/rbac/stytch-default) for a more detailed explanation. + + + """ # noqa + role_id: str @@ -62,6 +76,21 @@ def add_headers(self, headers: Dict[str, str]) -> Dict[str, str]: class GroupImplicitRoleAssignment(pydantic.BaseModel): + """ + Fields: + - role_id: The unique identifier of the RBAC Role, provided by the developer and intended to be human-readable. + + Reserved `role_id`s that are predefined by Stytch include: + + * `stytch_member` + * `stytch_admin` + + Check out the [guide on Stytch default Roles](https://stytch.com/docs/b2b/guides/rbac/stytch-default) for a more detailed explanation. + + + - group: The name of the group that grants the specified role assignment. + """ # noqa + role_id: str group: str @@ -128,7 +157,7 @@ class SAMLGroupImplicitRoleAssignment(pydantic.BaseModel): Check out the [guide on Stytch default Roles](https://stytch.com/docs/b2b/guides/rbac/stytch-default) for a more detailed explanation. - - group: The name of the SAML group that grants the specified role assignment. + - group: The name of the group that grants the specified role assignment. """ # noqa role_id: str @@ -207,7 +236,7 @@ class GetConnectionsResponse(ResponseBase): Fields: - saml_connections: The list of [SAML Connections](https://stytch.com/docs/b2b/api/saml-connection-object) owned by this organization. - oidc_connections: The list of [OIDC Connections](https://stytch.com/docs/b2b/api/oidc-connection-object) owned by this organization. - - external_connections: (no documentation yet) + - external_connections: The list of [External Connections](https://stytch.com/docs/b2b/api/external-connection-object) owned by this organization. """ # noqa saml_connections: List[SAMLConnection] diff --git a/stytch/b2b/models/sso_external.py b/stytch/b2b/models/sso_external.py new file mode 100644 index 0000000..a1ea828 --- /dev/null +++ b/stytch/b2b/models/sso_external.py @@ -0,0 +1,65 @@ +# !!! +# WARNING: This file is autogenerated +# Only modify code within MANUAL() sections +# or your changes may be overwritten later! +# !!! + +from __future__ import annotations + +from typing import Dict, Optional + +import pydantic + +from stytch.b2b.models.sso import Connection +from stytch.core.response_base import ResponseBase +from stytch.shared.method_options import Authorization + + +class CreateConnectionRequestOptions(pydantic.BaseModel): + """ + Fields: + - authorization: Optional authorization object. + Pass in an active Stytch Member session token or session JWT and the request + will be run using that member's permissions. + """ # noqa + + authorization: Optional[Authorization] = None + + def add_headers(self, headers: Dict[str, str]) -> Dict[str, str]: + if self.authorization is not None: + headers = self.authorization.add_headers(headers) + return headers + + +class UpdateConnectionRequestOptions(pydantic.BaseModel): + """ + Fields: + - authorization: Optional authorization object. + Pass in an active Stytch Member session token or session JWT and the request + will be run using that member's permissions. + """ # noqa + + authorization: Optional[Authorization] = None + + def add_headers(self, headers: Dict[str, str]) -> Dict[str, str]: + if self.authorization is not None: + headers = self.authorization.add_headers(headers) + return headers + + +class CreateConnectionResponse(ResponseBase): + """Response type for `External.create_connection`. + Fields: + - connection: The `External Connection` object affected by this API call. See the [External Connection Object](https://stytch.com/docs/b2b/api/external-connection-object) for complete response field details. + """ # noqa + + connection: Optional[Connection] = None + + +class UpdateConnectionResponse(ResponseBase): + """Response type for `External.update_connection`. + Fields: + - connection: The `External Connection` object affected by this API call. See the [External Connection Object](https://stytch.com/docs/b2b/api/external-connection-object) for complete response field details. + """ # noqa + + connection: Optional[Connection] = None diff --git a/stytch/consumer/api/sessions.py b/stytch/consumer/api/sessions.py index 75b707d..786900e 100644 --- a/stytch/consumer/api/sessions.py +++ b/stytch/consumer/api/sessions.py @@ -204,7 +204,7 @@ def migrate( """Migrate a session from an external OIDC compliant endpoint. Stytch will call the external UserInfo endpoint defined in your Stytch Project settings in the [Dashboard](/dashboard), and then perform a lookup using the `session_token`. If the response contains a valid email address, Stytch will attempt to match that email address with an existing User and create a Stytch Session. You will need to create the user before using this endpoint. Fields: - - session_token: The `session_token` associated with a User's existing Session. + - session_token: The authorization token Stytch will pass in to the external userinfo endpoint. - session_duration_minutes: Set the session lifetime to be this many minutes from now. This will start a new session if one doesn't already exist, returning both an opaque `session_token` and `session_jwt` for this session. Remember that the `session_jwt` will have a fixed lifetime of five minutes regardless of the underlying session duration, and will need to be refreshed over time. @@ -240,7 +240,7 @@ async def migrate_async( """Migrate a session from an external OIDC compliant endpoint. Stytch will call the external UserInfo endpoint defined in your Stytch Project settings in the [Dashboard](/dashboard), and then perform a lookup using the `session_token`. If the response contains a valid email address, Stytch will attempt to match that email address with an existing User and create a Stytch Session. You will need to create the user before using this endpoint. Fields: - - session_token: The `session_token` associated with a User's existing Session. + - session_token: The authorization token Stytch will pass in to the external userinfo endpoint. - session_duration_minutes: Set the session lifetime to be this many minutes from now. This will start a new session if one doesn't already exist, returning both an opaque `session_token` and `session_jwt` for this session. Remember that the `session_jwt` will have a fixed lifetime of five minutes regardless of the underlying session duration, and will need to be refreshed over time. diff --git a/stytch/version.py b/stytch/version.py index b11143b..073f0d2 100644 --- a/stytch/version.py +++ b/stytch/version.py @@ -1 +1 @@ -__version__ = "11.6.0" +__version__ = "11.7.0"