From 588395aacc76dbe31797735a8edc0af6018c135e Mon Sep 17 00:00:00 2001 From: Stytch Codegen Bot Date: Thu, 29 Jan 2026 19:48:57 +0000 Subject: [PATCH] Custom Org Roles: Policy Caching --- stytch/b2b/api/rbac_organizations.py | 55 +++++++++++----- stytch/b2b/api/sessions.py | 4 -- stytch/consumer/api/sessions.py | 4 -- stytch/shared/tests/test_rbac_local.py | 90 +++++++++++++++++++------- stytch/version.py | 2 +- 5 files changed, 106 insertions(+), 49 deletions(-) diff --git a/stytch/b2b/api/rbac_organizations.py b/stytch/b2b/api/rbac_organizations.py index 797425c1..1af3fc2b 100644 --- a/stytch/b2b/api/rbac_organizations.py +++ b/stytch/b2b/api/rbac_organizations.py @@ -8,11 +8,8 @@ from typing import Any, Dict, Set, Union -from stytch.b2b.models.rbac import ( - OrgPolicy, - Policy as B2BPolicy, - PolicyResource, -) +from stytch.b2b.models.rbac import OrgPolicy +from stytch.b2b.models.rbac import Policy as B2BPolicy from stytch.b2b.models.rbac_organizations import ( GetOrgPolicyResponse, SetOrgPolicyResponse, @@ -33,7 +30,10 @@ def get_org_policy( self, organization_id: str, ) -> GetOrgPolicyResponse: - """Get the active RBAC Policy for a specific Organization within your Stytch Project. An Organization RBAC Policy contains the roles that have been defined specifically for that organization, allowing for organization-specific permissioning models. + """ + The organization RBAC policy feature is currently in private beta and must be enabled for your Workspace. Please contact Stytch support at support@stytch.com to request access. + + Get the active RBAC Policy for a specific Organization within your Stytch Project. An Organization RBAC Policy contains the roles that have been defined specifically for that organization, allowing for organization-specific permissioning models. This endpoint returns the organization-scoped roles that supplement the project-level RBAC policy. Organization policies allow you to define custom roles that are specific to individual organizations within your project. @@ -61,7 +61,10 @@ async def get_org_policy_async( self, organization_id: str, ) -> GetOrgPolicyResponse: - """Get the active RBAC Policy for a specific Organization within your Stytch Project. An Organization RBAC Policy contains the roles that have been defined specifically for that organization, allowing for organization-specific permissioning models. + """ + The organization RBAC policy feature is currently in private beta and must be enabled for your Workspace. Please contact Stytch support at support@stytch.com to request access. + + Get the active RBAC Policy for a specific Organization within your Stytch Project. An Organization RBAC Policy contains the roles that have been defined specifically for that organization, allowing for organization-specific permissioning models. This endpoint returns the organization-scoped roles that supplement the project-level RBAC policy. Organization policies allow you to define custom roles that are specific to individual organizations within your project. @@ -90,7 +93,10 @@ def set_org_policy( organization_id: str, org_policy: Union[OrgPolicy, Dict[str, Any]], ) -> SetOrgPolicyResponse: - """Set the RBAC Policy for a specific Organization within your Stytch Project. An Organization RBAC Policy allows you to define roles that are specific to that organization, providing fine-grained control over permissions at the organization level. + """ + The organization RBAC policy feature is currently in private beta and must be enabled for your Workspace. Please contact Stytch support at support@stytch.com to request access. + + Set the RBAC Policy for a specific Organization within your Stytch Project. An Organization RBAC Policy allows you to define roles that are specific to that organization, providing fine-grained control over permissions at the organization level. This endpoint allows you to create, update, or replace the organization-scoped roles for a given organization. Organization policies supplement the project-level RBAC policy with additional roles that are only applicable within the context of that specific organization. @@ -131,7 +137,10 @@ async def set_org_policy_async( organization_id: str, org_policy: OrgPolicy, ) -> SetOrgPolicyResponse: - """Set the RBAC Policy for a specific Organization within your Stytch Project. An Organization RBAC Policy allows you to define roles that are specific to that organization, providing fine-grained control over permissions at the organization level. + """ + The organization RBAC policy feature is currently in private beta and must be enabled for your Workspace. Please contact Stytch support at support@stytch.com to request access. + + Set the RBAC Policy for a specific Organization within your Stytch Project. An Organization RBAC Policy allows you to define roles that are specific to that organization, providing fine-grained control over permissions at the organization level. This endpoint allows you to create, update, or replace the organization-scoped roles for a given organization. Organization policies supplement the project-level RBAC policy with additional roles that are only applicable within the context of that specific organization. @@ -179,31 +188,45 @@ def validate_org_policy(project_policy: B2BPolicy, org_policy: OrgPolicy) -> Non for role in org_policy.roles: org_role_id = role.role_id if org_role_id in org_roles: - raise Exception(f"Duplicate role {org_role_id} in Organization RBAC policy") + raise Exception( + f"Duplicate role {org_role_id} in Organization RBAC policy" + ) org_roles.add(org_role_id) if org_role_id in project_roles: - raise Exception(f"Role {org_role_id} already defined in Project RBAC policy") + raise Exception( + f"Role {org_role_id} already defined in Project RBAC policy" + ) for permission in role.permissions: resource_id = permission.resource_id if not resource_id in project_resources: - raise Exception(f"Resource {resource_id} not defined in Project RBAC policy") + raise Exception( + f"Resource {resource_id} not defined in Project RBAC policy" + ) if len(permission.actions) == 0: - raise Exception(f"No actions defined for role {org_role_id}, resource {resource_id}") + raise Exception( + f"No actions defined for role {org_role_id}, resource {resource_id}" + ) if len(permission.actions) == 1 and "*" == permission.actions[0]: continue if len(permission.actions) > 1 and "*" in permission.actions: - raise Exception("Wildcard actions must be the only action defined for a role and resource") + raise Exception( + "Wildcard actions must be the only action defined for a role and resource" + ) project_resource = project_resources[resource_id] for action in permission.actions: if action.strip() == "": - raise Exception(f"Empty action on resource {resource_id} is not permitted") + raise Exception( + f"Empty action on resource {resource_id} is not permitted" + ) if not action in project_resource.actions: - raise Exception(f"Unknown action {action} defined on resource {resource_id}") + raise Exception( + f"Unknown action {action} defined on resource {resource_id}" + ) return diff --git a/stytch/b2b/api/sessions.py b/stytch/b2b/api/sessions.py index 1c2a8649..fbf31773 100644 --- a/stytch/b2b/api/sessions.py +++ b/stytch/b2b/api/sessions.py @@ -463,8 +463,6 @@ def exchange_access_token( The Access Token must contain the `full_access` scope (only available to First Party clients) and must not be more than 5 minutes old. Access Tokens may only be exchanged a single time. - The Member Session returned will be the same Member Session that was active in your application (the authorizing party) during the initial authorization flow. - Because the Member previously completed MFA and satisfied all Organization authentication requirements at the time of the original Access Token issuance, this endpoint will never return an `intermediate_session_token` or require MFA. Fields: @@ -512,8 +510,6 @@ async def exchange_access_token_async( The Access Token must contain the `full_access` scope (only available to First Party clients) and must not be more than 5 minutes old. Access Tokens may only be exchanged a single time. - The Member Session returned will be the same Member Session that was active in your application (the authorizing party) during the initial authorization flow. - Because the Member previously completed MFA and satisfied all Organization authentication requirements at the time of the original Access Token issuance, this endpoint will never return an `intermediate_session_token` or require MFA. Fields: diff --git a/stytch/consumer/api/sessions.py b/stytch/consumer/api/sessions.py index 6a791595..2078d3c4 100644 --- a/stytch/consumer/api/sessions.py +++ b/stytch/consumer/api/sessions.py @@ -315,8 +315,6 @@ def exchange_access_token( """Use this endpoint to exchange a Connected Apps Access Token back into a Stytch Session for the underlying User. This session can be used with the Stytch SDKs and APIs. - The Session returned will be the same Session that was active in your application (the authorizing party) during the initial authorization flow. - The Access Token must contain the `full_access` scope (only available to First Party clients) and must not be more than 5 minutes old. Access Tokens may only be exchanged a single time. Fields: @@ -360,8 +358,6 @@ async def exchange_access_token_async( """Use this endpoint to exchange a Connected Apps Access Token back into a Stytch Session for the underlying User. This session can be used with the Stytch SDKs and APIs. - The Session returned will be the same Session that was active in your application (the authorizing party) during the initial authorization flow. - The Access Token must contain the `full_access` scope (only available to First Party clients) and must not be more than 5 minutes old. Access Tokens may only be exchanged a single time. Fields: diff --git a/stytch/shared/tests/test_rbac_local.py b/stytch/shared/tests/test_rbac_local.py index 2c28509c..b836ac24 100644 --- a/stytch/shared/tests/test_rbac_local.py +++ b/stytch/shared/tests/test_rbac_local.py @@ -445,7 +445,9 @@ def setUp(self) -> None: def test_validate_org_rbac_policies(self) -> None: with self.subTest("exception if a role is already defined in Project policy"): - with self.assertRaisesRegex(Exception, r"Role \w+ already defined in Project RBAC policy"): + with self.assertRaisesRegex( + Exception, r"Role \w+ already defined in Project RBAC policy" + ): Organizations.validate_org_policy( project_policy=self.sample_project_policy, org_policy=OrgPolicy( @@ -454,7 +456,9 @@ def test_validate_org_rbac_policies(self) -> None: role_id="stytch_editor", description="", permissions=[ - PolicyRolePermission(actions=["*"], resource_id="resource") + PolicyRolePermission( + actions=["*"], resource_id="resource" + ) ], ) ] @@ -462,7 +466,9 @@ def test_validate_org_rbac_policies(self) -> None: ) with self.subTest("exception if a role is already defined in Org policy"): - with self.assertRaisesRegex(Exception, r"Duplicate role \w+ in Organization RBAC policy"): + with self.assertRaisesRegex( + Exception, r"Duplicate role \w+ in Organization RBAC policy" + ): Organizations.validate_org_policy( project_policy=self.sample_project_policy, org_policy=OrgPolicy( @@ -471,22 +477,28 @@ def test_validate_org_rbac_policies(self) -> None: role_id="researcher", description="", permissions=[ - PolicyRolePermission(resource_id="document", actions=["*"]) + PolicyRolePermission( + resource_id="document", actions=["*"] + ) ], ), PolicyRole( role_id="researcher", description="", permissions=[ - PolicyRolePermission(resource_id="document", actions=["*"]) + PolicyRolePermission( + resource_id="document", actions=["*"] + ) ], - ) + ), ] ), ) with self.subTest("exception if a role uses an undefined resource"): - with self.assertRaisesRegex(Exception, r"Resource \w+ not defined in Project RBAC policy"): + with self.assertRaisesRegex( + Exception, r"Resource \w+ not defined in Project RBAC policy" + ): Organizations.validate_org_policy( project_policy=self.sample_project_policy, org_policy=OrgPolicy( @@ -495,22 +507,30 @@ def test_validate_org_rbac_policies(self) -> None: role_id="researcher", description="", permissions=[ - PolicyRolePermission(resource_id="computer", actions=["boot"]) + PolicyRolePermission( + resource_id="computer", actions=["boot"] + ) ], ), PolicyRole( role_id="teacher", description="", permissions=[ - PolicyRolePermission(resource_id="document", actions=["*"]) + PolicyRolePermission( + resource_id="document", actions=["*"] + ) ], - ) + ), ] ), ) - with self.subTest("exception if a role does not define actions for a permission"): - with self.assertRaisesRegex(Exception, r"No actions defined for role \w+, resource \w+"): + with self.subTest( + "exception if a role does not define actions for a permission" + ): + with self.assertRaisesRegex( + Exception, r"No actions defined for role \w+, resource \w+" + ): Organizations.validate_org_policy( project_policy=self.sample_project_policy, org_policy=OrgPolicy( @@ -519,7 +539,9 @@ def test_validate_org_rbac_policies(self) -> None: role_id="teacher", description="", permissions=[ - PolicyRolePermission(resource_id="document", actions=[]) + PolicyRolePermission( + resource_id="document", actions=[] + ) ], ) ] @@ -527,8 +549,10 @@ def test_validate_org_rbac_policies(self) -> None: ) with self.subTest("exception if a role uses a wildcard with other actions"): - with self.assertRaisesRegex(Exception, - r"Wildcard actions must be the only action defined for a role and resource"): + with self.assertRaisesRegex( + Exception, + r"Wildcard actions must be the only action defined for a role and resource", + ): Organizations.validate_org_policy( project_policy=self.sample_project_policy, org_policy=OrgPolicy( @@ -537,7 +561,9 @@ def test_validate_org_rbac_policies(self) -> None: role_id="teacher", description="", permissions=[ - PolicyRolePermission(resource_id="document", actions=["*", "read"]) + PolicyRolePermission( + resource_id="document", actions=["*", "read"] + ) ], ) ] @@ -545,7 +571,9 @@ def test_validate_org_rbac_policies(self) -> None: ) with self.subTest("exception an action is left empty"): - with self.assertRaisesRegex(Exception, r"Empty action on resource \w+ is not permitted"): + with self.assertRaisesRegex( + Exception, r"Empty action on resource \w+ is not permitted" + ): Organizations.validate_org_policy( project_policy=self.sample_project_policy, org_policy=OrgPolicy( @@ -554,7 +582,9 @@ def test_validate_org_rbac_policies(self) -> None: role_id="teacher", description="", permissions=[ - PolicyRolePermission(resource_id="document", actions=["", "read"]) + PolicyRolePermission( + resource_id="document", actions=["", "read"] + ) ], ) ] @@ -562,7 +592,9 @@ def test_validate_org_rbac_policies(self) -> None: ) with self.subTest("exception if an unknown action is defined on a resource"): - with self.assertRaisesRegex(Exception, r"Unknown action \w+ defined on resource \w+"): + with self.assertRaisesRegex( + Exception, r"Unknown action \w+ defined on resource \w+" + ): Organizations.validate_org_policy( project_policy=self.sample_project_policy, org_policy=OrgPolicy( @@ -571,7 +603,10 @@ def test_validate_org_rbac_policies(self) -> None: role_id="teacher", description="", permissions=[ - PolicyRolePermission(resource_id="document", actions=["read", "shred"]) + PolicyRolePermission( + resource_id="document", + actions=["read", "shred"], + ) ], ) ] @@ -588,23 +623,30 @@ def test_validate_org_rbac_policies(self) -> None: role_id="teacher", description="High school teacher", permissions=[ - PolicyRolePermission(resource_id="document", actions=["*"]) + PolicyRolePermission( + resource_id="document", actions=["*"] + ) ], ), PolicyRole( role_id="student", description="High school student", permissions=[ - PolicyRolePermission(resource_id="document", actions=["read"]) + PolicyRolePermission( + resource_id="document", actions=["read"] + ) ], ), PolicyRole( role_id="sys_admin", description="Network administrator", permissions=[ - PolicyRolePermission(resource_id="program", actions=["read", "write", "execute"]) + PolicyRolePermission( + resource_id="program", + actions=["read", "write", "execute"], + ) ], - ) + ), ] ), ) diff --git a/stytch/version.py b/stytch/version.py index 331dde6f..4a77120b 100644 --- a/stytch/version.py +++ b/stytch/version.py @@ -1 +1 @@ -__version__ = "14.2.0" +__version__ = "14.2.1"