From 335b7c4fa5ca89c531c9a58716ac415a43791c93 Mon Sep 17 00:00:00 2001 From: Yong Date: Sat, 27 Sep 2025 13:18:12 -0500 Subject: [PATCH 1/5] Client: add credential reset option --- client/python/cli/command/__init__.py | 2 ++ client/python/cli/command/principals.py | 20 ++++++++++- client/python/cli/constants.py | 7 ++++ client/python/cli/options/option_tree.py | 4 +++ client/python/test/test_cli_parsing.py | 34 +++++++++++++++++-- .../unreleased/command-line-interface.md | 27 +++++++++++++++ 6 files changed, 91 insertions(+), 3 deletions(-) diff --git a/client/python/cli/command/__init__.py b/client/python/cli/command/__init__.py index 53216f3162..1aa6f1d818 100644 --- a/client/python/cli/command/__init__.py +++ b/client/python/cli/command/__init__.py @@ -99,6 +99,8 @@ def options_get(key, f=lambda x: x): remove_properties=[] if remove_properties is None else remove_properties, + new_client_id=options_get(Arguments.NEW_CLIENT_ID), + new_client_secret=options_get(Arguments.NEW_CLIENT_SECRET), ) elif options.command == Commands.PRINCIPAL_ROLES: from cli.command.principal_roles import PrincipalRolesCommand diff --git a/client/python/cli/command/principals.py b/client/python/cli/command/principals.py index 58779f77d4..068545cae8 100644 --- a/client/python/cli/command/principals.py +++ b/client/python/cli/command/principals.py @@ -30,6 +30,7 @@ Principal, PrincipalWithCredentials, UpdatePrincipalRequest, + ResetPrincipalRequest ) @@ -55,6 +56,8 @@ class PrincipalsCommand(Command): properties: Optional[Dict[str, StrictStr]] set_properties: Dict[str, StrictStr] remove_properties: List[str] + new_client_id: Optional[str] = None + new_client_secret: Optional[str] = None def _get_catalogs(self, api: PolarisDefaultApi): for catalog in api.list_catalogs().catalogs: @@ -170,6 +173,21 @@ def execute(self, api: PolarisDefaultApi) -> None: ) role_data["catalog_roles"].append(catalog_data) result["principal_roles"].append(role_data) - print(json.dumps(result)) + elif self.principals_subcommand == Subcommands.RESET: + if self.new_client_id or self.new_client_secret: + request = ResetPrincipalRequest( + clientId=self.new_client_id, clientSecret=self.new_client_secret + ) + print( + self.build_credential_json( + api.reset_credentials(self.principal_name, request) + ) + ) + else: + print( + self.build_credential_json( + api.reset_credentials(self.principal_name, None) + ) + ) else: raise Exception(f"{self.principals_subcommand} is not supported in the CLI") diff --git a/client/python/cli/constants.py b/client/python/cli/constants.py index 82a4f5d01e..44715d1f74 100644 --- a/client/python/cli/constants.py +++ b/client/python/cli/constants.py @@ -109,6 +109,7 @@ class Subcommands: GRANT = "grant" REVOKE = "revoke" ACCESS = "access" + RESET = "reset" class Actions: @@ -155,6 +156,8 @@ class Arguments: VIEW = "view" CASCADE = "cascade" CLIENT_SECRET = "client_secret" + NEW_CLIENT_ID = "new_client_id" + NEW_CLIENT_SECRET = "new_client_secret" ACCESS_TOKEN = "access_token" HOST = "host" PORT = "port" @@ -317,6 +320,10 @@ class Create: class Revoke: PRINCIPAL_ROLE = "A principal role to revoke from this principal" + class Reset: + CLIENT_ID = "The new client ID for the principal" + CLIENT_SECRET = "The new client secret for the principal" + class PrincipalRoles: PRINCIPAL_ROLE = "The name of a principal role" LIST = ( diff --git a/client/python/cli/options/option_tree.py b/client/python/cli/options/option_tree.py index 5b95741f23..f71589d901 100644 --- a/client/python/cli/options/option_tree.py +++ b/client/python/cli/options/option_tree.py @@ -160,6 +160,10 @@ def get_tree() -> List[Option]: Argument(Arguments.REMOVE_PROPERTY, str, Hints.REMOVE_PROPERTY, allow_repeats=True), ], input_name=Arguments.PRINCIPAL), Option(Subcommands.ACCESS, input_name=Arguments.PRINCIPAL), + Option(Subcommands.RESET, args=[ + Argument(Arguments.NEW_CLIENT_ID, str, Hints.Principals.Reset.CLIENT_ID), + Argument(Arguments.NEW_CLIENT_SECRET, str, Hints.Principals.Reset.CLIENT_SECRET), + ], input_name=Arguments.PRINCIPAL), ]), Option(Commands.PRINCIPAL_ROLES, 'manage principal roles', children=[ Option(Subcommands.CREATE, args=[ diff --git a/client/python/test/test_cli_parsing.py b/client/python/test/test_cli_parsing.py index 916cfe3ee4..1c53f0a6c1 100644 --- a/client/python/test/test_cli_parsing.py +++ b/client/python/test/test_cli_parsing.py @@ -141,8 +141,7 @@ def capture_method(method_name): def _capture(*args, **kwargs): client.call_tracker['_method'] = method_name for i, arg in enumerate(args): - if arg is not None: - client.call_tracker[i] = arg + client.call_tracker[i] = arg return _capture @@ -588,6 +587,37 @@ def get(obj, arg_string): (0, 'catalog.connection_config_info.uri'): 'u', }) + check_arguments( + mock_execute(['principals', 'reset', 'test', '--new-client-id', 'id1', '--new-client-secret', 'secret1']), + 'reset_credentials', { + (0, None): 'test', + (1, 'client_id'): 'id1', + (1, 'client_secret'): 'secret1', + }) + + check_arguments( + mock_execute(['principals', 'reset', 'test']), + 'reset_credentials', { + (0, None): 'test', + (1, None): None, + }) + + check_arguments( + mock_execute(['principals', 'reset', 'test', '--new-client-id', 'id1']), + 'reset_credentials', { + (0, None): 'test', + (1, 'client_id'): 'id1', + (1, 'client_secret'): None, + }) + + check_arguments( + mock_execute(['principals', 'reset', 'test', '--new-client-secret', 'secret1']), + 'reset_credentials', { + (0, None): 'test', + (1, 'client_id'): None, + (1, 'client_secret'): 'secret1', + }) + if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/site/content/in-dev/unreleased/command-line-interface.md b/site/content/in-dev/unreleased/command-line-interface.md index 5f7ab65bde..c191fe960c 100644 --- a/site/content/in-dev/unreleased/command-line-interface.md +++ b/site/content/in-dev/unreleased/command-line-interface.md @@ -283,6 +283,7 @@ The `principals` command is used to manage principals within Polaris. 5. rotate-credentials 6. update 7. access +8. reset #### create @@ -418,6 +419,32 @@ options: polaris principals access quickstart_user ``` +#### reset + +The `reset` subcommand is used to reset principal credentials. + +``` +input: polaris principals reset --help +options: + reset + Named arguments: + --new-client-id The new client ID for the principal + --new-client-secret The new client secret for the principal + Positional arguments: + principal +``` + +##### Examples + +``` +polaris principals create some_user + +polaris principals reset some_user +polaris principals reset --new-client-id ${NEW_CLIENT_ID} some_user +polaris principals reset --new-client-secret ${NEW_CLIENT_SECRET} some_user +polaris principals reset --new-client-id ${NEW_CLIENT_ID} --new-client-secret ${NEW_CLIENT_SECRET} some_user +``` + ### Principal Roles The `principal-roles` command is used to create, discover, and manage principal roles within Polaris. Additionally, this command can identify principals or catalog roles associated with a principal role, and can be used to grant a principal role to a principal. From a200a285b63514f3d1423e72ea918e456e14a4a7 Mon Sep 17 00:00:00 2001 From: Yong Date: Sat, 27 Sep 2025 13:22:14 -0500 Subject: [PATCH 2/5] Client: add credential reset option --- client/python/test/test_cli_parsing.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client/python/test/test_cli_parsing.py b/client/python/test/test_cli_parsing.py index 1c53f0a6c1..1cca75e2fa 100644 --- a/client/python/test/test_cli_parsing.py +++ b/client/python/test/test_cli_parsing.py @@ -588,11 +588,11 @@ def get(obj, arg_string): }) check_arguments( - mock_execute(['principals', 'reset', 'test', '--new-client-id', 'id1', '--new-client-secret', 'secret1']), + mock_execute(['principals', 'reset', 'test', '--new-client-id', 'e469c048cf866df1', '--new-client-secret', 'e469c048cf866dfae469c048cf866df1']), 'reset_credentials', { (0, None): 'test', - (1, 'client_id'): 'id1', - (1, 'client_secret'): 'secret1', + (1, 'client_id'): 'e469c048cf866df1', + (1, 'client_secret'): 'e469c048cf866dfae469c048cf866df1', }) check_arguments( @@ -603,19 +603,19 @@ def get(obj, arg_string): }) check_arguments( - mock_execute(['principals', 'reset', 'test', '--new-client-id', 'id1']), + mock_execute(['principals', 'reset', 'test', '--new-client-id', 'e469c048cf866df1']), 'reset_credentials', { (0, None): 'test', - (1, 'client_id'): 'id1', + (1, 'client_id'): 'e469c048cf866df1', (1, 'client_secret'): None, }) check_arguments( - mock_execute(['principals', 'reset', 'test', '--new-client-secret', 'secret1']), + mock_execute(['principals', 'reset', 'test', '--new-client-secret', 'e469c048cf866dfae469c048cf866df1']), 'reset_credentials', { (0, None): 'test', (1, 'client_id'): None, - (1, 'client_secret'): 'secret1', + (1, 'client_secret'): 'e469c048cf866dfae469c048cf866df1', }) From e50d3114b91811dc55df975458df9783de13b50b Mon Sep 17 00:00:00 2001 From: Yong Date: Sat, 27 Sep 2025 13:33:03 -0500 Subject: [PATCH 3/5] Client: add credential reset option --- client/python/cli/command/principals.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/python/cli/command/principals.py b/client/python/cli/command/principals.py index 068545cae8..c39c221c08 100644 --- a/client/python/cli/command/principals.py +++ b/client/python/cli/command/principals.py @@ -173,6 +173,7 @@ def execute(self, api: PolarisDefaultApi) -> None: ) role_data["catalog_roles"].append(catalog_data) result["principal_roles"].append(role_data) + print(json.dumps(result)) elif self.principals_subcommand == Subcommands.RESET: if self.new_client_id or self.new_client_secret: request = ResetPrincipalRequest( From cae1688a157ad0496238ac93e57307db9f1336e9 Mon Sep 17 00:00:00 2001 From: Yong Date: Sat, 27 Sep 2025 15:41:23 -0500 Subject: [PATCH 4/5] Add integration testing --- client/python/integration_tests/conftest.py | 2 + .../integration_tests/test_catalog_apis.py | 2 + .../integration_tests/test_management_apis.py | 102 ++++++++++++++++++ 3 files changed, 106 insertions(+) diff --git a/client/python/integration_tests/conftest.py b/client/python/integration_tests/conftest.py index 5ad5165a00..eaa98e1e43 100644 --- a/client/python/integration_tests/conftest.py +++ b/client/python/integration_tests/conftest.py @@ -1,3 +1,4 @@ +# # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information @@ -14,6 +15,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. +# import codecs import os diff --git a/client/python/integration_tests/test_catalog_apis.py b/client/python/integration_tests/test_catalog_apis.py index 44bc162640..60109713e3 100644 --- a/client/python/integration_tests/test_catalog_apis.py +++ b/client/python/integration_tests/test_catalog_apis.py @@ -1,3 +1,4 @@ +# # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information @@ -14,6 +15,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. +# import os.path import time diff --git a/client/python/integration_tests/test_management_apis.py b/client/python/integration_tests/test_management_apis.py index ac74a290b6..2df8780b77 100644 --- a/client/python/integration_tests/test_management_apis.py +++ b/client/python/integration_tests/test_management_apis.py @@ -1,3 +1,4 @@ +# # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information @@ -14,6 +15,8 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. +# + from integration_tests.conftest import ( create_principal, create_principal_role, @@ -28,6 +31,7 @@ RevokeGrantRequest, PolarisDefaultApi, Catalog, + ResetPrincipalRequest, ) @@ -125,3 +129,101 @@ def test_grants(management_client: PolarisDefaultApi, test_catalog: Catalog) -> assert len(grants.grants) == 0 finally: management_client.delete_catalog_role(test_catalog.name, catalog_role.name) + + +def test_reset_principal_credentials_default( + management_client: PolarisDefaultApi, +) -> None: + principal_name = f"test_principal_for_reset_creds_default" + principal_with_creds = create_principal(management_client, principal_name) + initial_client_id = principal_with_creds.principal.client_id + initial_client_secret = ( + principal_with_creds.credentials.client_secret.get_secret_value() + ) + try: + reset_request = ResetPrincipalRequest() + new_principal_with_creds = management_client.reset_credentials( + principal_name=principal_name, reset_principal_request=reset_request + ) + current_client_id = new_principal_with_creds.principal.client_id + current_client_secret = ( + new_principal_with_creds.credentials.client_secret.get_secret_value() + ) + + assert initial_client_id == current_client_id + assert initial_client_secret != current_client_secret + finally: + management_client.delete_principal(principal_name=principal_name) + + +def test_reset_principal_credentials_custom( + management_client: PolarisDefaultApi, +) -> None: + principal_name = f"test_principal_for_reset_creds_custom" + create_principal(management_client, principal_name) + custom_client_id = "e469c048cf866df1" + custom_client_secret = "1f37adcd21bf1586ed090332eded9cd3" + try: + reset_request = ResetPrincipalRequest( + clientId=custom_client_id, clientSecret=custom_client_secret + ) + new_principal_with_creds = management_client.reset_credentials( + principal_name=principal_name, reset_principal_request=reset_request + ) + current_client_id = new_principal_with_creds.principal.client_id + current_client_secret = ( + new_principal_with_creds.credentials.client_secret.get_secret_value() + ) + + assert current_client_id == custom_client_id + assert current_client_secret == custom_client_secret + finally: + management_client.delete_principal(principal_name=principal_name) + + +def test_reset_principal_credentials_custom_client_id( + management_client: PolarisDefaultApi, +) -> None: + principal_name = f"test_principal_for_reset_creds_client_id" + principal_with_creds = create_principal(management_client, principal_name) + initial_client_secret = ( + principal_with_creds.credentials.client_secret.get_secret_value() + ) + custom_client_id = "e469c048cf866df1" + try: + reset_request = ResetPrincipalRequest(clientId=custom_client_id) + new_principal_with_creds = management_client.reset_credentials( + principal_name=principal_name, reset_principal_request=reset_request + ) + current_client_id = new_principal_with_creds.principal.client_id + current_client_secret = ( + new_principal_with_creds.credentials.client_secret.get_secret_value() + ) + + assert current_client_id == custom_client_id + assert initial_client_secret != current_client_secret + finally: + management_client.delete_principal(principal_name=principal_name) + + +def test_reset_principal_credentials_custom_client_secret( + management_client: PolarisDefaultApi, +) -> None: + principal_name = f"test_principal_for_reset_creds_client_secret" + principal_with_creds = create_principal(management_client, principal_name) + initial_client_id = principal_with_creds.principal.client_id + custom_client_secret = "1f37adcd21bf1586ed090332eded9cd3" + try: + reset_request = ResetPrincipalRequest(clientSecret=custom_client_secret) + new_principal_with_creds = management_client.reset_credentials( + principal_name=principal_name, reset_principal_request=reset_request + ) + current_client_id = new_principal_with_creds.principal.client_id + current_client_secret = ( + new_principal_with_creds.credentials.client_secret.get_secret_value() + ) + + assert initial_client_id == current_client_id + assert current_client_secret == custom_client_secret + finally: + management_client.delete_principal(principal_name=principal_name) \ No newline at end of file From e5c7a380719dbb780241b6a14a29f6e427751c28 Mon Sep 17 00:00:00 2001 From: Yong Date: Sat, 27 Sep 2025 15:47:05 -0500 Subject: [PATCH 5/5] Fix lint --- .../python/integration_tests/test_management_apis.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/python/integration_tests/test_management_apis.py b/client/python/integration_tests/test_management_apis.py index 2df8780b77..4256408e5a 100644 --- a/client/python/integration_tests/test_management_apis.py +++ b/client/python/integration_tests/test_management_apis.py @@ -134,7 +134,7 @@ def test_grants(management_client: PolarisDefaultApi, test_catalog: Catalog) -> def test_reset_principal_credentials_default( management_client: PolarisDefaultApi, ) -> None: - principal_name = f"test_principal_for_reset_creds_default" + principal_name = "test_principal_for_reset_creds_default" principal_with_creds = create_principal(management_client, principal_name) initial_client_id = principal_with_creds.principal.client_id initial_client_secret = ( @@ -159,7 +159,7 @@ def test_reset_principal_credentials_default( def test_reset_principal_credentials_custom( management_client: PolarisDefaultApi, ) -> None: - principal_name = f"test_principal_for_reset_creds_custom" + principal_name = "test_principal_for_reset_creds_custom" create_principal(management_client, principal_name) custom_client_id = "e469c048cf866df1" custom_client_secret = "1f37adcd21bf1586ed090332eded9cd3" @@ -184,7 +184,7 @@ def test_reset_principal_credentials_custom( def test_reset_principal_credentials_custom_client_id( management_client: PolarisDefaultApi, ) -> None: - principal_name = f"test_principal_for_reset_creds_client_id" + principal_name = "test_principal_for_reset_creds_client_id" principal_with_creds = create_principal(management_client, principal_name) initial_client_secret = ( principal_with_creds.credentials.client_secret.get_secret_value() @@ -209,7 +209,7 @@ def test_reset_principal_credentials_custom_client_id( def test_reset_principal_credentials_custom_client_secret( management_client: PolarisDefaultApi, ) -> None: - principal_name = f"test_principal_for_reset_creds_client_secret" + principal_name = "test_principal_for_reset_creds_client_secret" principal_with_creds = create_principal(management_client, principal_name) initial_client_id = principal_with_creds.principal.client_id custom_client_secret = "1f37adcd21bf1586ed090332eded9cd3" @@ -226,4 +226,4 @@ def test_reset_principal_credentials_custom_client_secret( assert initial_client_id == current_client_id assert current_client_secret == custom_client_secret finally: - management_client.delete_principal(principal_name=principal_name) \ No newline at end of file + management_client.delete_principal(principal_name=principal_name)