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.