From 3fda02e5f833ceba04427b9533345b9792fdcbad Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Wed, 2 Oct 2024 07:03:23 -0400 Subject: [PATCH] Work on not auto-filling username (#792) * Work on not auto-filling username * Update changelog * Use clear_default_identity correctly --- .gitignore | 1 + CHANGELOG.md | 5 ++ tiled/_tests/conftest.py | 6 +- tiled/_tests/test_access_control.py | 36 ++++----- tiled/_tests/test_authentication.py | 112 ++++++++++++++-------------- tiled/_tests/test_catalog.py | 6 +- tiled/_tests/utils.py | 5 +- tiled/client/base.py | 4 +- tiled/client/context.py | 24 ++++-- 9 files changed, 113 insertions(+), 86 deletions(-) diff --git a/.gitignore b/.gitignore index b10648632..557b271b8 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,4 @@ tiled/_version.py .env .asv/ +venv/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 27de16a50..1eb9ec1b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ Write the date in place of the "Unreleased" in the case a new version is release # Changelog +## Unreleased + +- Add kwarg to client logout to auto-clear default identity. +- Do not automatically enter username if default identity is used. + ## v0.1.0b9 (2024-09-19) ### Added diff --git a/tiled/_tests/conftest.py b/tiled/_tests/conftest.py index 8ef04a21c..434741217 100644 --- a/tiled/_tests/conftest.py +++ b/tiled/_tests/conftest.py @@ -13,7 +13,7 @@ from ..catalog import from_uri, in_memory from ..client.base import BaseClient from ..server.settings import get_settings -from .utils import enter_password as utils_enter_password +from .utils import enter_username_password as utils_enter_uname_passwd from .utils import temp_postgres @@ -75,11 +75,11 @@ def tmp_profiles_dir(): @pytest.fixture -def enter_password(): +def enter_username_password(): """ DEPRECATED: Use the normal (non-fixture) context manager in .utils. """ - return utils_enter_password + return utils_enter_uname_passwd @pytest.fixture(scope="module") diff --git a/tiled/_tests/test_access_control.py b/tiled/_tests/test_access_control.py index 708ba8f4d..abb0c234b 100644 --- a/tiled/_tests/test_access_control.py +++ b/tiled/_tests/test_access_control.py @@ -8,7 +8,7 @@ from ..adapters.mapping import MapAdapter from ..client import Context, from_context from ..server.app import build_app_from_config -from .utils import enter_password, fail_with_status_code +from .utils import enter_username_password, fail_with_status_code arr = numpy.ones((5, 5)) arr_ad = ArrayAdapter.from_array(arr) @@ -132,7 +132,7 @@ def context(tmpdir_module): } app = build_app_from_config(config) with Context.from_app(app) as context: - with enter_password("admin"): + with enter_username_password("admin", "admin"): admin_client = from_context(context, username="admin") for k in ["c", "d", "e"]: admin_client[k].write_array(arr, key="A1") @@ -141,8 +141,8 @@ def context(tmpdir_module): yield context -def test_top_level_access_control(context, enter_password): - with enter_password("secret1"): +def test_top_level_access_control(context, enter_username_password): + with enter_username_password("alice", "secret1"): alice_client = from_context(context, username="alice") assert "a" in alice_client assert "A2" in alice_client["a"] @@ -152,7 +152,7 @@ def test_top_level_access_control(context, enter_password): with pytest.raises(KeyError): alice_client["b"] - with enter_password("secret2"): + with enter_username_password("bob", "secret2"): bob_client = from_context(context, username="bob") assert not list(bob_client) with pytest.raises(KeyError): @@ -160,12 +160,14 @@ def test_top_level_access_control(context, enter_password): with pytest.raises(KeyError): bob_client["b"] alice_client.logout() - bob_client.logout() + # Make sure clearing default identity works without raising an error. + bob_client.logout(clear_default=True) -def test_access_control_with_api_key_auth(context, enter_password): + +def test_access_control_with_api_key_auth(context, enter_username_password): # Log in, create an API key, log out. - with enter_password("secret1"): + with enter_username_password("alice", "secret1"): context.authenticate(username="alice") key_info = context.create_api_key() context.logout() @@ -180,9 +182,9 @@ def test_access_control_with_api_key_auth(context, enter_password): context.api_key = None -def test_node_export(enter_password, context, buffer): +def test_node_export(enter_username_password, context, buffer): "Exporting a node should include only the children we can see." - with enter_password("secret1"): + with enter_username_password("alice", "secret1"): alice_client = from_context(context, username="alice") alice_client.export(buffer, format="application/json") alice_client.logout() @@ -195,8 +197,8 @@ def test_node_export(enter_password, context, buffer): exported_dict["contents"]["a"]["contents"]["A2"] -def test_create_and_update_allowed(enter_password, context): - with enter_password("secret1"): +def test_create_and_update_allowed(enter_username_password, context): + with enter_username_password("alice", "secret1"): alice_client = from_context(context, username="alice") # Update @@ -209,8 +211,8 @@ def test_create_and_update_allowed(enter_password, context): alice_client.logout() -def test_writing_blocked_by_access_policy(enter_password, context): - with enter_password("secret1"): +def test_writing_blocked_by_access_policy(enter_username_password, context): + with enter_username_password("alice", "secret1"): alice_client = from_context(context, username="alice") alice_client["d"]["x"].metadata with fail_with_status_code(HTTP_403_FORBIDDEN): @@ -218,8 +220,8 @@ def test_writing_blocked_by_access_policy(enter_password, context): alice_client.logout() -def test_create_blocked_by_access_policy(enter_password, context): - with enter_password("secret1"): +def test_create_blocked_by_access_policy(enter_username_password, context): + with enter_username_password("alice", "secret1"): alice_client = from_context(context, username="alice") with fail_with_status_code(HTTP_403_FORBIDDEN): alice_client["e"].write_array([1, 2, 3]) @@ -278,7 +280,7 @@ def test_service_principal_access(tmpdir): ], } with Context.from_app(build_app_from_config(config)) as context: - with enter_password("admin"): + with enter_username_password("admin", "admin"): admin_client = from_context(context, username="admin") sp = admin_client.context.admin.create_service_principal("user") key_info = admin_client.context.admin.create_api_key(sp["uuid"]) diff --git a/tiled/_tests/test_authentication.py b/tiled/_tests/test_authentication.py index 1bc2cfb6c..7deaf1a39 100644 --- a/tiled/_tests/test_authentication.py +++ b/tiled/_tests/test_authentication.py @@ -63,13 +63,13 @@ def config(tmpdir): } -def test_password_auth(enter_password, config): +def test_password_auth(enter_username_password, config): """ A password that is wrong, empty, or belonging to a different user fails. """ with Context.from_app(build_app_from_config(config)) as context: # Log in as Alice. - with enter_password("secret1"): + with enter_username_password("alice", "secret1"): from_context(context, username="alice") # Reuse token from cache. client = from_context(context, username="alice") @@ -78,29 +78,29 @@ def test_password_auth(enter_password, config): assert "unauthenticated" in repr(client.context) # Log in as Bob. - with enter_password("secret2"): + with enter_username_password("bob", "secret2"): client = from_context(context, username="bob") assert "authenticated as 'bob'" in repr(client.context) client.logout() # Bob's password should not work for Alice. with fail_with_status_code(HTTP_401_UNAUTHORIZED): - with enter_password("secret2"): + with enter_username_password("alice", "secret2"): from_context(context, username="alice") # Empty password should not work. with fail_with_status_code(HTTP_401_UNAUTHORIZED): - with enter_password(""): + with enter_username_password("alice", ""): from_context(context, username="alice") -def test_logout(enter_password, config, tmpdir): +def test_logout(enter_username_password, config, tmpdir): """ Logging out revokes the session, such that it cannot be refreshed. """ with Context.from_app(build_app_from_config(config)) as context: # Log in as Alice. - with enter_password("secret1"): + with enter_username_password("alice", "secret1"): from_context(context, username="alice") # Reuse token from cache. client = from_context(context, username="alice") @@ -128,7 +128,7 @@ def test_logout(enter_password, config, tmpdir): client.context.force_auth_refresh() -def test_key_rotation(enter_password, config): +def test_key_rotation(enter_username_password, config): """ Rotate in a new secret used to sign keys. Confirm that clients experience a smooth transition. @@ -136,7 +136,7 @@ def test_key_rotation(enter_password, config): with Context.from_app(build_app_from_config(config)) as context: # Obtain refresh token. - with enter_password("secret1"): + with enter_username_password("alice", "secret1"): from_context(context, username="alice") # Use refresh token (no prompt to reauthenticate). client = from_context(context, username="alice") @@ -162,11 +162,11 @@ def test_key_rotation(enter_password, config): from_context(context, username="alice") -def test_refresh_forced(enter_password, config): +def test_refresh_forced(enter_username_password, config): "Forcing refresh obtains new token." with Context.from_app(build_app_from_config(config)) as context: # Normal default configuration: a refresh is not immediately required. - with enter_password("secret1"): + with enter_username_password("alice", "secret1"): client = from_context(context, username="alice") tokens1 = dict(client.context.tokens) # Wait for a moment or we will get a new token that happens to be identical @@ -178,12 +178,12 @@ def test_refresh_forced(enter_password, config): assert tokens1 != tokens2 -def test_refresh_transparent(enter_password, config): +def test_refresh_transparent(enter_username_password, config): "When access token expired, refresh happens transparently." # Pathological configuration: a refresh is almost immediately required config["authentication"]["access_token_max_age"] = 1 with Context.from_app(build_app_from_config(config)) as context: - with enter_password("secret1"): + with enter_username_password("alice", "secret1"): client = from_context(context, username="alice") tokens1 = dict(client.context.tokens) time.sleep(2) @@ -193,11 +193,11 @@ def test_refresh_transparent(enter_password, config): assert tokens2 != tokens1 -def test_expired_session(enter_password, config): +def test_expired_session(enter_username_password, config): # Pathological configuration: sessions do not last config["authentication"]["session_max_age"] = 1 with Context.from_app(build_app_from_config(config)) as context: - with enter_password("secret1"): + with enter_username_password("alice", "secret1"): client = from_context(context, username="alice") time.sleep(2) # Refresh should fail because the session is too old. @@ -205,9 +205,9 @@ def test_expired_session(enter_password, config): client.context.force_auth_refresh() -def test_revoke_session(enter_password, config): +def test_revoke_session(enter_username_password, config): with Context.from_app(build_app_from_config(config)) as context: - with enter_password("secret1"): + with enter_username_password("alice", "secret1"): client = from_context(context, username="alice") # Get the current session ID. info = client.context.whoami() @@ -224,7 +224,7 @@ def test_revoke_session(enter_password, config): client.context.force_auth_refresh() -def test_multiple_providers(enter_password, config, monkeypatch): +def test_multiple_providers(enter_username_password, config, monkeypatch): """ Test a configuration with multiple identity providers. @@ -249,13 +249,13 @@ def test_multiple_providers(enter_password, config, monkeypatch): ) with Context.from_app(build_app_from_config(config)) as context: monkeypatch.setattr("sys.stdin", io.StringIO("1\n")) - with enter_password("secret1"): + with enter_username_password("alice", "secret1"): from_context(context, username="alice") monkeypatch.setattr("sys.stdin", io.StringIO("2\n")) - with enter_password("secret3"): + with enter_username_password("cara", "secret3"): from_context(context, username="cara") monkeypatch.setattr("sys.stdin", io.StringIO("3\n")) - with enter_password("secret5"): + with enter_username_password("cara", "secret5"): from_context(context, username="cara") @@ -282,7 +282,7 @@ def test_multiple_providers_name_collision(config): build_app_from_config(config) -def test_admin(enter_password, config): +def test_admin(enter_username_password, config): """ Test that the 'tiled_admin' config confers the 'admin' Role on a Principal. """ @@ -290,7 +290,7 @@ def test_admin(enter_password, config): config["authentication"]["tiled_admins"] = [{"provider": "toy", "id": "alice"}] with Context.from_app(build_app_from_config(config)) as context: - with enter_password("secret1"): + with enter_username_password("alice", "secret1"): context.authenticate(username="alice") admin_roles = context.whoami()["roles"] assert "admin" in [role["name"] for role in admin_roles] @@ -300,7 +300,7 @@ def test_admin(enter_password, config): some_principal_uuid = principals[0]["uuid"] context.admin.show_principal(some_principal_uuid) - with enter_password("secret2"): + with enter_username_password("bob", "secret2"): context.authenticate(username="bob") user_roles = context.whoami()["roles"] assert [role["name"] for role in user_roles] == ["user"] @@ -313,19 +313,19 @@ def test_admin(enter_password, config): # Start the server a second time. Now alice is already an admin. with Context.from_app(build_app_from_config(config)) as context: - with enter_password("secret1"): + with enter_username_password("alice", "secret1"): context.authenticate(username="alice") admin_roles = context.whoami()["roles"] assert "admin" in [role["name"] for role in admin_roles] -def test_api_key_activity(enter_password, config): +def test_api_key_activity(enter_username_password, config): """ Create and use an API. Verify that latest_activity updates. """ with Context.from_app(build_app_from_config(config)) as context: # Log in as user. - with enter_password("secret1"): + with enter_username_password("alice", "secret1"): context.authenticate(username="alice") # Make and use an API key. Check that latest_activity is not set. key_info = context.create_api_key() @@ -362,12 +362,12 @@ def test_api_key_activity(enter_password, config): context.which_api_key() -def test_api_key_scopes(enter_password, config): +def test_api_key_scopes(enter_username_password, config): # Make alice an admin. Leave bob as a user. config["authentication"]["tiled_admins"] = [{"provider": "toy", "id": "alice"}] with Context.from_app(build_app_from_config(config)) as context: # Log in as admin. - with enter_password("secret1"): + with enter_username_password("alice", "secret1"): context.authenticate(username="alice") # Request a key with reduced scope that cannot read metadata. metrics_key_info = context.create_api_key(scopes=["metrics"]) @@ -378,7 +378,7 @@ def test_api_key_scopes(enter_password, config): context.api_key = None # Log in as ordinary user. - with enter_password("secret2"): + with enter_username_password("bob", "secret2"): context.authenticate(username="bob") # Try to request a key with more scopes that the user has. with fail_with_status_code(HTTP_400_BAD_REQUEST): @@ -394,9 +394,9 @@ def test_api_key_scopes(enter_password, config): context.api_key = None -def test_api_key_revoked(enter_password, config): +def test_api_key_revoked(enter_username_password, config): with Context.from_app(build_app_from_config(config)) as context: - with enter_password("secret1"): + with enter_username_password("alice", "secret1"): context.authenticate(username="alice") # Create a key with a note. @@ -414,7 +414,7 @@ def test_api_key_revoked(enter_password, config): context.api_key = None # Revoke the new key. - with enter_password("secret1"): + with enter_username_password("alice", "secret1"): context.authenticate(username="alice") context.revoke_api_key(key_info["first_eight"]) assert len(context.whoami()["api_keys"]) == 0 @@ -424,9 +424,9 @@ def test_api_key_revoked(enter_password, config): from_context(context) -def test_api_key_expiration(enter_password, config): +def test_api_key_expiration(enter_username_password, config): with Context.from_app(build_app_from_config(config)) as context: - with enter_password("secret1"): + with enter_username_password("alice", "secret1"): context.authenticate(username="alice") # Create a key with a very short lifetime. key_info = context.create_api_key( @@ -439,13 +439,13 @@ def test_api_key_expiration(enter_password, config): from_context(context) -def test_api_key_limit(enter_password, config): +def test_api_key_limit(enter_username_password, config): # Decrease the limit so this test runs faster. original_limit = authentication.API_KEY_LIMIT authentication.API_KEY_LIMIT = 3 try: with Context.from_app(build_app_from_config(config)) as context: - with enter_password("secret2"): + with enter_username_password("bob", "secret2"): context.authenticate(username="bob") for i in range(authentication.API_KEY_LIMIT): context.create_api_key(note=f"key {i}") @@ -456,14 +456,14 @@ def test_api_key_limit(enter_password, config): authentication.API_KEY_LIMIT = original_limit -def test_session_limit(enter_password, config): +def test_session_limit(enter_username_password, config): # Decrease the limit so this test runs faster. original_limit = authentication.SESSION_LIMIT authentication.SESSION_LIMIT = 3 # Use separate token caches to de-couple login attempts into separate sessions. try: with Context.from_app(build_app_from_config(config)) as context: - with enter_password("secret1"): + with enter_username_password("alice", "secret1"): for i in range(authentication.SESSION_LIMIT): context.authenticate(username="alice") context.logout() @@ -474,11 +474,11 @@ def test_session_limit(enter_password, config): authentication.SESSION_LIMIT = original_limit -def test_sticky_identity(enter_password, config): +def test_sticky_identity(enter_username_password, config): # Log in as Alice. with Context.from_app(build_app_from_config(config)) as context: assert get_default_identity(context.api_uri) is None - with enter_password("secret1"): + with enter_username_password("alice", "secret1"): context.authenticate(username="alice") assert context.whoami()["identities"][0]["id"] == "alice" # The default identity is now set. The login was "sticky". @@ -489,7 +489,7 @@ def test_sticky_identity(enter_password, config): # Opt out of the stickiness (set_default=False). with Context.from_app(build_app_from_config(config)) as context: assert get_default_identity(context.api_uri) is not None - with enter_password("secret2"): + with enter_username_password("bob", "secret2"): context.authenticate(username="bob", set_default=False) assert context.whoami()["identities"][0]["id"] == "bob" # The default is still Alice. @@ -503,7 +503,7 @@ def test_sticky_identity(enter_password, config): @pytest.fixture -def principals_context(enter_password, config): +def principals_context(enter_username_password, config): """ Fetch UUID for an admin and an ordinary user; include the client context. """ @@ -512,7 +512,7 @@ def principals_context(enter_password, config): with Context.from_app(build_app_from_config(config)) as context: # Log in as Alice and retrieve admin UUID for later use - with enter_password("secret1"): + with enter_username_password("alice", "secret1"): context.authenticate(username="alice") principal = context.whoami() @@ -521,7 +521,7 @@ def principals_context(enter_password, config): context.logout() # Log in as Bob and retrieve Bob's UUID for later use - with enter_password("secret2"): + with enter_username_password("bob", "secret2"): context.authenticate(username="bob") principal = context.whoami() @@ -543,14 +543,14 @@ def principals_context(enter_password, config): ), ) def test_admin_api_key_any_principal( - enter_password, principals_context, username, scopes, resource + enter_username_password, principals_context, username, scopes, resource ): """ Admin can create usable API keys for any prinicipal, within that principal's scopes. """ with principals_context["context"] as context: # Log in as Alice, create and use API key after logout - with enter_password("secret1"): + with enter_username_password("alice", "secret1"): context.authenticate(username="alice") principal_uuid = principals_context["uuid"][username] @@ -567,13 +567,13 @@ def test_admin_api_key_any_principal( context.http_client.get(resource).raise_for_status() -def test_admin_create_service_principal(enter_password, principals_context): +def test_admin_create_service_principal(enter_username_password, principals_context): """ Admin can create service accounts with API keys. """ with principals_context["context"] as context: # Log in as Alice, create and use API key after logout - with enter_password("secret1"): + with enter_username_password("alice", "secret1"): context.authenticate(username="alice") assert context.whoami()["type"] == "user" @@ -591,13 +591,15 @@ def test_admin_create_service_principal(enter_password, principals_context): assert f"authenticated as service '{principal_uuid}'" in repr(context) -def test_admin_api_key_any_principal_exceeds_scopes(enter_password, principals_context): +def test_admin_api_key_any_principal_exceeds_scopes( + enter_username_password, principals_context +): """ Admin cannot create API key that exceeds scopes for another principal. """ with principals_context["context"] as context: # Log in as Alice, create and use API key after logout - with enter_password("secret1"): + with enter_username_password("alice", "secret1"): context.authenticate(username="alice") principal_uuid = principals_context["uuid"]["bob"] @@ -609,13 +611,13 @@ def test_admin_api_key_any_principal_exceeds_scopes(enter_password, principals_c @pytest.mark.parametrize("username", ("alice", "bob")) -def test_api_key_any_principal(enter_password, principals_context, username): +def test_api_key_any_principal(enter_username_password, principals_context, username): """ Ordinary user cannot create API key for another principal. """ with principals_context["context"] as context: # Log in as Bob, this API endpoint is unauthorized - with enter_password("secret2"): + with enter_username_password("bob", "secret2"): context.authenticate(username="bob") principal_uuid = principals_context["uuid"][username] @@ -623,13 +625,13 @@ def test_api_key_any_principal(enter_password, principals_context, username): context.admin.create_api_key(principal_uuid, scopes=["read:metadata"]) -def test_api_key_bypass_scopes(enter_password, principals_context): +def test_api_key_bypass_scopes(enter_username_password, principals_context): """ Ordinary user cannot create API key that bypasses a scopes restriction. """ with principals_context["context"] as context: # Log in as Bob, create API key with empty scopes - with enter_password("secret2"): + with enter_username_password("bob", "secret2"): context.authenticate(username="bob") response = context.http_client.post( diff --git a/tiled/_tests/test_catalog.py b/tiled/_tests/test_catalog.py index 51c8df69b..61b24e98f 100644 --- a/tiled/_tests/test_catalog.py +++ b/tiled/_tests/test_catalog.py @@ -26,7 +26,7 @@ from ..server.schemas import Asset, DataSource, Management from ..structures.core import StructureFamily from ..utils import ensure_uri -from .utils import enter_password +from .utils import enter_username_password @pytest_asyncio.fixture @@ -417,13 +417,13 @@ async def test_access_control(tmpdir): app = build_app_from_config(config) with Context.from_app(app) as context: - with enter_password("admin"): + with enter_username_password("admin", "admin"): admin_client = from_context(context, username="admin") for key in ["outer_x", "outer_y", "outer_z"]: container = admin_client.create_container(key) container.write_array([1, 2, 3], key="inner") admin_client.logout() - with enter_password("secret1"): + with enter_username_password("alice", "secret1"): alice_client = from_context(context, username="alice") alice_client["outer_x"]["inner"].read() with pytest.raises(KeyError): diff --git a/tiled/_tests/utils.py b/tiled/_tests/utils.py index 538397da3..b3a4bc9df 100644 --- a/tiled/_tests/utils.py +++ b/tiled/_tests/utils.py @@ -52,7 +52,7 @@ async def temp_postgres(uri): @contextlib.contextmanager -def enter_password(password): +def enter_username_password(username, password): """ Override getpass, used like: @@ -61,12 +61,15 @@ def enter_password(password): """ original_prompt = context.PROMPT_FOR_REAUTHENTICATION + original_getusername = context.prompt_for_username original_getpass = getpass.getpass context.PROMPT_FOR_REAUTHENTICATION = True + context.prompt_for_username = lambda u: username setattr(getpass, "getpass", lambda: password) yield setattr(getpass, "getpass", original_getpass) context.PROMPT_FOR_REAUTHENTICATION = original_prompt + context.prompt_for_username = original_getusername class URL_LIMITS(IntEnum): diff --git a/tiled/client/base.py b/tiled/client/base.py index 479c3bd64..1f7b8b6b5 100644 --- a/tiled/client/base.py +++ b/tiled/client/base.py @@ -160,13 +160,13 @@ def login(self, username=None, provider=None): """ self.context.login(username=username, provider=provider) - def logout(self): + def logout(self, clear_default=False): """ Log out. This method is idempotent: if you are already logged out, it will do nothing. """ - self.context.logout() + self.context.logout(clear_default=clear_default) def __repr__(self): return f"<{type(self).__name__}>" diff --git a/tiled/client/context.py b/tiled/client/context.py index 46b9d8271..0bacf8cca 100644 --- a/tiled/client/context.py +++ b/tiled/client/context.py @@ -23,6 +23,19 @@ PROMPT_FOR_REAUTHENTICATION = None +def prompt_for_username(username): + """ + Utility function that displays a username prompt. + """ + if username: + username_reprompt = input(f"Username [{username}]: ") + if len(username_reprompt.strip()) != 0: + username = username_reprompt + else: + username = input("Username: ") + return username + + class Context: """ Wrap an httpx.Client with an optional cache and authentication functionality. @@ -526,10 +539,7 @@ def authenticate( mode = spec["mode"] auth_endpoint = spec["links"]["auth_endpoint"] if mode == "password": - if username: - print(f"Username {username}") - else: - username = input("Username: ") + username = prompt_for_username(username) password = getpass.getpass() form_data = { "grant_type": "password", @@ -685,7 +695,7 @@ def whoami(self): ) ).json() - def logout(self): + def logout(self, clear_default=False): """ Log out of the current session (if any). @@ -715,6 +725,10 @@ def logout(self): self.http_client.headers.pop("Authorization", None) self.http_client.auth = None + # If requested, automatically clear the default identity + if clear_default: + clear_default_identity(self.api_uri) + def revoke_session(self, session_id): """ Revoke a Session so it cannot be refreshed.