From 1404403bfbaca08b958cdf0b4a07cfdf3e2d7b66 Mon Sep 17 00:00:00 2001 From: Adam Mcgrath Date: Wed, 8 Nov 2023 17:32:28 +0000 Subject: [PATCH] Add orgs in client credentials support --- auth0/authentication/get_token.py | 5 ++ auth0/management/client_grants.py | 44 +++++++++++++++ auth0/management/organizations.py | 62 +++++++++++++++++++++ auth0/test/authentication/test_get_token.py | 22 ++++++++ auth0/test/management/test_client_grants.py | 45 +++++++++++++++ auth0/test/management/test_organizations.py | 41 ++++++++++++++ 6 files changed, 219 insertions(+) diff --git a/auth0/authentication/get_token.py b/auth0/authentication/get_token.py index a7321b88..8fea32ca 100644 --- a/auth0/authentication/get_token.py +++ b/auth0/authentication/get_token.py @@ -91,6 +91,7 @@ def client_credentials( self, audience: str, grant_type: str = "client_credentials", + organization: str | None = None, ) -> Any: """Client credentials grant @@ -104,6 +105,9 @@ def client_credentials( grant_type (str, optional): Denotes the flow you're using. For client credentials use "client_credentials" + organization (str, optional): Optional Organization name or ID. When included, the access token returned + will include the org_id and org_name claims + Returns: access_token """ @@ -114,6 +118,7 @@ def client_credentials( "client_id": self.client_id, "audience": audience, "grant_type": grant_type, + "organization": organization, }, ) diff --git a/auth0/management/client_grants.py b/auth0/management/client_grants.py index 88cbcf05..46b2d9d9 100644 --- a/auth0/management/client_grants.py +++ b/auth0/management/client_grants.py @@ -59,6 +59,7 @@ def all( per_page: int | None = None, include_totals: bool = False, client_id: str | None = None, + allow_any_organization: bool | None = None, ): """Retrieves all client grants. @@ -77,6 +78,8 @@ def all( client_id (string, optional): The id of a client to filter. + allow_any_organization (bool, optional): Optional filter on allow_any_organization. + See: https://auth0.com/docs/api/management/v2#!/Client_Grants/get_client_grants """ @@ -86,6 +89,7 @@ def all( "per_page": per_page, "include_totals": str(include_totals).lower(), "client_id": client_id, + "allow_any_organization": allow_any_organization, } return self.client.get(self._url(), params=params) @@ -124,3 +128,43 @@ def update(self, id: str, body: dict[str, Any]) -> dict[str, Any]: """ return self.client.patch(self._url(id), data=body) + + def get_organizations( + self, + id: str, + page: int | None = None, + per_page: int | None = None, + include_totals: bool = False, + from_param: str | None = None, + take: int | None = None, + ): + """Get the organizations associated to a client grant. + + Args: + id (str): Id of client grant. + + page (int, optional): The result's page number (zero based). When not set, + the default value is up to the server. + + per_page (int, optional): The amount of entries per page. When not set, + the default value is up to the server. + + include_totals (bool, optional): True if the query summary is + to be included in the result, False otherwise. Defaults to False. + + from_param (str, optional): Id to start retrieving entries. You can + limit the amount of entries using the take parameter. + + take (int, optional): The total amount of entries to retrieve when + using the from parameter. When not set, the default value is up to the server. + """ + + params = { + "per_page": per_page, + "page": page, + "include_totals": str(include_totals).lower(), + "from": from_param, + "take": take, + } + + return self.client.get(self._url(f"{id}/organizations"), params=params) diff --git a/auth0/management/organizations.py b/auth0/management/organizations.py index dabaf6c6..8e0473a3 100644 --- a/auth0/management/organizations.py +++ b/auth0/management/organizations.py @@ -460,3 +460,65 @@ def delete_organization_invitation(self, id: str, invitation_id: str) -> Any: """ return self.client.delete(self._url(id, "invitations", invitation_id)) + + def get_client_grants( + self, + id: str, + audience: str | None = None, + client_id: str | None = None, + page: int | None = None, + per_page: int | None = None, + include_totals: bool = False, + ): + """Get client grants associated to an organization. + + Args: + id (str): Id of organization. + + audience (str, optional): URL encoded audience of a Resource Server + to filter. + + client_id (string, optional): The id of a client to filter. + + page (int, optional): The result's page number (zero based). When not set, + the default value is up to the server. + + per_page (int, optional): The amount of entries per page. When not set, + the default value is up to the server. + + include_totals (bool, optional): True if the query summary is + to be included in the result, False otherwise. Defaults to False. + """ + params = { + "audience": audience, + "client_id": client_id, + "page": page, + "per_page": per_page, + "include_totals": str(include_totals).lower(), + } + + return self.client.get(self._url(id, "client-grants"), params=params) + + def add_client_grant(self, id: str, grant_id: str) -> dict[str, Any]: + """Associate a client grant with an organization. + + Args: + id (str): the ID of the organization. + + grant_id (string) A Client Grant ID to add to the organization. + """ + + return self.client.post( + self._url(id, "client-grants"), data={"grant_id": grant_id} + ) + + def delete_client_grant(self, id: str, grant_id: str) -> dict[str, Any]: + """Remove a client grant from an organization. + + Args: + id (str): the ID of the organization. + + grant_id (string) A Client Grant ID to remove from the organization. + """ + + return self.client.delete(self._url(id, "client-grants", grant_id)) diff --git a/auth0/test/authentication/test_get_token.py b/auth0/test/authentication/test_get_token.py index 5a8e3e06..21dfc949 100644 --- a/auth0/test/authentication/test_get_token.py +++ b/auth0/test/authentication/test_get_token.py @@ -111,6 +111,7 @@ def test_client_credentials(self, mock_post): "client_secret": "clsec", "audience": "aud", "grant_type": "gt", + "organization": None, }, ) @@ -133,11 +134,32 @@ def test_client_credentials_with_client_assertion(self, mock_post): "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "audience": "aud", "grant_type": "gt", + "organization": None, }, ) self.assertTrue(fnmatch(kwargs["data"]["client_assertion"], "*.*.*")) + @mock.patch("auth0.rest.RestClient.post") + def test_client_credentials_with_organization(self, mock_post): + g = GetToken("my.domain.com", "cid", client_secret="clsec") + + g.client_credentials("aud", organization="my-org") + + args, kwargs = mock_post.call_args + + self.assertEqual(args[0], "https://my.domain.com/oauth/token") + self.assertEqual( + kwargs["data"], + { + "client_id": "cid", + "grant_type": "client_credentials", + "client_secret": "clsec", + "audience": "aud", + "organization": "my-org", + }, + ) + @mock.patch("auth0.rest.RestClient.post") def test_login(self, mock_post): g = GetToken("my.domain.com", "cid", client_secret="clsec") diff --git a/auth0/test/management/test_client_grants.py b/auth0/test/management/test_client_grants.py index 583c90e2..eeef4f7b 100644 --- a/auth0/test/management/test_client_grants.py +++ b/auth0/test/management/test_client_grants.py @@ -33,6 +33,7 @@ def test_all(self, mock_rc): "per_page": None, "include_totals": "false", "client_id": None, + "allow_any_organization": None, }, ) @@ -50,6 +51,7 @@ def test_all(self, mock_rc): "per_page": None, "include_totals": "false", "client_id": None, + "allow_any_organization": None, }, ) @@ -67,6 +69,7 @@ def test_all(self, mock_rc): "per_page": 23, "include_totals": "true", "client_id": None, + "allow_any_organization": None, }, ) @@ -84,6 +87,25 @@ def test_all(self, mock_rc): "per_page": None, "include_totals": "false", "client_id": "exampleid", + "allow_any_organization": None, + }, + ) + + # With allow any organization + c.all(allow_any_organization=True) + + args, kwargs = mock_instance.get.call_args + + self.assertEqual("https://domain/api/v2/client-grants", args[0]) + self.assertEqual( + kwargs["params"], + { + "audience": None, + "page": None, + "per_page": None, + "include_totals": "false", + "client_id": None, + "allow_any_organization": True, }, ) @@ -120,3 +142,26 @@ def test_update(self, mock_rc): self.assertEqual("https://domain/api/v2/client-grants/this-id", args[0]) self.assertEqual(kwargs["data"], {"a": "b", "c": "d"}) + + @mock.patch("auth0.management.client_grants.RestClient") + def test_get_organizations(self, mock_rc): + mock_instance = mock_rc.return_value + + c = ClientGrants(domain="domain", token="jwttoken") + c.get_organizations("cgid") + + args, kwargs = mock_instance.get.call_args + + self.assertEqual( + "https://domain/api/v2/client-grants/cgid/organizations", args[0] + ) + self.assertEqual( + kwargs["params"], + { + "page": None, + "per_page": None, + "include_totals": "false", + "from": None, + "take": None, + }, + ) diff --git a/auth0/test/management/test_organizations.py b/auth0/test/management/test_organizations.py index ec1fc84b..c4b9235a 100644 --- a/auth0/test/management/test_organizations.py +++ b/auth0/test/management/test_organizations.py @@ -479,3 +479,44 @@ def test_delete_organization_invitation(self, mock_rc): mock_instance.delete.assert_called_with( "https://domain/api/v2/organizations/test-org/invitations/test-inv" ) + + @mock.patch("auth0.management.organizations.RestClient") + def test_get_client_grants(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + c.get_client_grants("test-org") + + mock_instance.get.assert_called_with( + "https://domain/api/v2/organizations/test-org/client-grants", + params={ + "audience": None, + "client_id": None, + "page": None, + "per_page": None, + "include_totals": "false", + }, + ) + + @mock.patch("auth0.management.organizations.RestClient") + def test_add_client_grant(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + c.add_client_grant("test-org", "test-cg") + + mock_instance.post.assert_called_with( + "https://domain/api/v2/organizations/test-org/client-grants", + data={"grant_id": "test-cg"}, + ) + + @mock.patch("auth0.management.organizations.RestClient") + def test_delete_client_grant(self, mock_rc): + mock_instance = mock_rc.return_value + + c = Organizations(domain="domain", token="jwttoken") + c.delete_client_grant("test-org", "test-cg") + + mock_instance.delete.assert_called_with( + "https://domain/api/v2/organizations/test-org/client-grants/test-cg", + )