This is an automated email from the ASF dual-hosted git repository.

yzheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git


The following commit(s) were added to refs/heads/main by this push:
     new 07647451f Client: add credential reset option (#2698)
07647451f is described below

commit 07647451f04d5ed4937b0128765f1fc9cf66a5ba
Author: Yong Zheng <[email protected]>
AuthorDate: Sun Sep 28 09:59:48 2025 -0500

    Client: add credential reset option (#2698)
    
    * Client: add credential reset option
    
    * Client: add credential reset option
    
    * Client: add credential reset option
    
    * Add integration testing
    
    * Fix lint
---
 client/python/cli/command/__init__.py              |   2 +
 client/python/cli/command/principals.py            |  19 ++++
 client/python/cli/constants.py                     |   7 ++
 client/python/cli/options/option_tree.py           |   4 +
 client/python/integration_tests/conftest.py        |   2 +
 .../python/integration_tests/test_catalog_apis.py  |   2 +
 .../integration_tests/test_management_apis.py      | 102 +++++++++++++++++++++
 client/python/test/test_cli_parsing.py             |  34 ++++++-
 .../in-dev/unreleased/command-line-interface.md    |  27 ++++++
 9 files changed, 197 insertions(+), 2 deletions(-)

diff --git a/client/python/cli/command/__init__.py 
b/client/python/cli/command/__init__.py
index 53216f316..1aa6f1d81 100644
--- a/client/python/cli/command/__init__.py
+++ b/client/python/cli/command/__init__.py
@@ -99,6 +99,8 @@ class Command(ABC):
                 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 58779f77d..c39c221c0 100644
--- a/client/python/cli/command/principals.py
+++ b/client/python/cli/command/principals.py
@@ -30,6 +30,7 @@ from polaris.management import (
     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:
@@ -171,5 +174,21 @@ class PrincipalsCommand(Command):
                         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 82a4f5d01..44715d1f7 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 Hints:
         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 5b95741f2..f71589d90 100644
--- a/client/python/cli/options/option_tree.py
+++ b/client/python/cli/options/option_tree.py
@@ -160,6 +160,10 @@ class OptionTree:
                     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/integration_tests/conftest.py 
b/client/python/integration_tests/conftest.py
index 5ad5165a0..eaa98e1e4 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 44bc16264..60109713e 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 ac74a290b..4256408e5 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 @@ from polaris.management import (
     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 = "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 = "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 = "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 = "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)
diff --git a/client/python/test/test_cli_parsing.py 
b/client/python/test/test_cli_parsing.py
index 916cfe3ee..1cca75e2f 100644
--- a/client/python/test/test_cli_parsing.py
+++ b/client/python/test/test_cli_parsing.py
@@ -141,8 +141,7 @@ class TestCliParsing(unittest.TestCase):
                 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 @@ class TestCliParsing(unittest.TestCase):
                 (0, 'catalog.connection_config_info.uri'): 'u',
             })
 
+        check_arguments(
+            mock_execute(['principals', 'reset', 'test', '--new-client-id', 
'e469c048cf866df1', '--new-client-secret', 'e469c048cf866dfae469c048cf866df1']),
+            'reset_credentials', {
+                (0, None): 'test',
+                (1, 'client_id'): 'e469c048cf866df1',
+                (1, 'client_secret'): 'e469c048cf866dfae469c048cf866df1',
+            })
+
+        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', 
'e469c048cf866df1']),
+            'reset_credentials', {
+                (0, None): 'test',
+                (1, 'client_id'): 'e469c048cf866df1',
+                (1, 'client_secret'): None,
+            })
+
+        check_arguments(
+            mock_execute(['principals', 'reset', 'test', 
'--new-client-secret', 'e469c048cf866dfae469c048cf866df1']),
+            'reset_credentials', {
+                (0, None): 'test',
+                (1, 'client_id'): None,
+                (1, 'client_secret'): 'e469c048cf866dfae469c048cf866df1',
+            })
+
 
 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 5f7ab65bd..c191fe960 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.

Reply via email to