From 5311253d644b9f101a9d427f2a9219ae24065512 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Mon, 3 Aug 2026 18:04:32 -0700 Subject: [PATCH 1/2] Fix #434: map "Local" auth_setting to ServerDefault tabcmd Classic accepts "Local" as an auth type on the CLI and CSV column 7. tabcmd 2 kept "Local" in the accepted list but TSC's UserItem.Auth enum has no Local value, so when the user reached server.users.add() the wire representation raised ValueError. Map Classic's "Local" -> TSC.UserItem.Auth.ServerDefault in Userdata.to_tsc_user() so CSVs authored for Classic import cleanly. Case-insensitive to match Classic behavior. Fixes #434. --- tabcmd/commands/user/user_data.py | 8 +++++++- tests/commands/test_user_utils.py | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/tabcmd/commands/user/user_data.py b/tabcmd/commands/user/user_data.py index 272a5375..5c5d10d5 100644 --- a/tabcmd/commands/user/user_data.py +++ b/tabcmd/commands/user/user_data.py @@ -45,7 +45,13 @@ def to_tsc_user(self) -> TSC.UserItem: site_role = UserCommand.evaluate_site_role(self.license_level, self.admin_level, self.publisher) if not site_role: raise AttributeError(_("tabcmd.user.error.site_role_required")) - user = TSC.UserItem(self.name, site_role, self.auth) + # tabcmd Classic accepts "Local" as an auth type; TSC's UserItem.Auth enum + # has no Local value and rejects it. Map Classic's "Local" to ServerDefault + # so CSVs authored for Classic import without crashing. + auth = self.auth + if isinstance(auth, str) and auth.lower() == "local": + auth = TSC.UserItem.Auth.ServerDefault + user = TSC.UserItem(self.name, site_role, auth) user.email = self.email user.fullname = self.fullname return user diff --git a/tests/commands/test_user_utils.py b/tests/commands/test_user_utils.py index 67de8d56..87512a19 100644 --- a/tests/commands/test_user_utils.py +++ b/tests/commands/test_user_utils.py @@ -150,3 +150,25 @@ def test_parse_line_preserves_role(self): user = UserCommand._parse_line("username, pword, fname, creator, none, yes, email") assert user is not None assert user.site_role == "Creator", f"Expected Creator, got {user.site_role}" + + def test_local_auth_maps_to_server_default(self): + # tabcmd Classic accepts "Local" as an auth type; TSC's Auth enum has no + # Local value, so passing it through raises ValueError on server.users.add. + # to_tsc_user should map Classic's "Local" -> ServerDefault for parity. + data = Userdata() + data.populate(["username", "pword", "fname", "creator", "none", "yes", "email", "Local"]) + user = data.to_tsc_user() + assert user.auth_setting == TSC.UserItem.Auth.ServerDefault, user.auth_setting + + def test_local_auth_case_insensitive(self): + data = Userdata() + data.populate(["username", "pword", "fname", "creator", "none", "yes", "email", "local"]) + user = data.to_tsc_user() + assert user.auth_setting == TSC.UserItem.Auth.ServerDefault, user.auth_setting + + def test_non_local_auth_passes_through(self): + # SAML/OpenID/etc. are not remapped. + data = Userdata() + data.populate(["username", "pword", "fname", "creator", "none", "yes", "email", TSC.UserItem.Auth.SAML]) + user = data.to_tsc_user() + assert user.auth_setting == TSC.UserItem.Auth.SAML, user.auth_setting From 83ed29c2a1a13694b8d0d845ce7ce8279e81da2c Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Sat, 8 Aug 2026 01:13:44 -0700 Subject: [PATCH 2/2] Log a warning when Local auth is remapped to ServerDefault A silent remap is surprising: an operator who typed Local (or imported a Classic CSV that uses it) previously got a user with ServerDefault auth and no signal that anything changed. Emit a WARNING that names the user and states both the input and remapped auth so the operator can spot unexpected remaps in a large batch. Adds a matching assertion in the existing test. Co-Authored-By: Claude Opus 4.7 (1M context) --- tabcmd/commands/user/user_data.py | 3 +++ tabcmd/locales/en/tabcmd_messages_en.properties | 1 + tests/commands/test_user_utils.py | 12 +++++++++--- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/tabcmd/commands/user/user_data.py b/tabcmd/commands/user/user_data.py index 5c5d10d5..b7e88656 100644 --- a/tabcmd/commands/user/user_data.py +++ b/tabcmd/commands/user/user_data.py @@ -50,6 +50,9 @@ def to_tsc_user(self) -> TSC.UserItem: # so CSVs authored for Classic import without crashing. auth = self.auth if isinstance(auth, str) and auth.lower() == "local": + logging.getLogger(__name__).warning( + _("tabcmd.user.warning.local_auth_remapped").format(self.name) + ) auth = TSC.UserItem.Auth.ServerDefault user = TSC.UserItem(self.name, site_role, auth) user.email = self.email diff --git a/tabcmd/locales/en/tabcmd_messages_en.properties b/tabcmd/locales/en/tabcmd_messages_en.properties index 7eda4bff..296bdd05 100644 --- a/tabcmd/locales/en/tabcmd_messages_en.properties +++ b/tabcmd/locales/en/tabcmd_messages_en.properties @@ -214,6 +214,7 @@ tabcmd.status.job_completed=Job completed tabcmd.status.waiting_for_refresh_job=Waiting for refresh job to begin tabcmd.user.error.site_role_required=Site role is required tabcmd.user.help.auth_type=Assigns the authentication type for all users in the CSV file. Possible values: +tabcmd.user.warning.local_auth_remapped=User "{0}": auth_setting "Local" is not a Tableau Server auth type; mapped to ServerDefault. The site's default auth method will be used. tabcmd.user.help.site_role=Specifies a site role for all users in the .csv file. Possible roles: tabcmd.warning.calculations_not_supported=Adding or removing Calculations tasks are not supported tabcmdparser.global.behaviors=Global behaviors: diff --git a/tests/commands/test_user_utils.py b/tests/commands/test_user_utils.py index 87512a19..9c584c60 100644 --- a/tests/commands/test_user_utils.py +++ b/tests/commands/test_user_utils.py @@ -154,16 +154,22 @@ def test_parse_line_preserves_role(self): def test_local_auth_maps_to_server_default(self): # tabcmd Classic accepts "Local" as an auth type; TSC's Auth enum has no # Local value, so passing it through raises ValueError on server.users.add. - # to_tsc_user should map Classic's "Local" -> ServerDefault for parity. + # to_tsc_user should map Classic's "Local" -> ServerDefault for parity + # and log a warning so the operator sees the remap happened. data = Userdata() data.populate(["username", "pword", "fname", "creator", "none", "yes", "email", "Local"]) - user = data.to_tsc_user() + with self.assertLogs("tabcmd.commands.user.user_data", level="WARNING") as logs: + user = data.to_tsc_user() assert user.auth_setting == TSC.UserItem.Auth.ServerDefault, user.auth_setting + # In tests the gettext catalog isn't loaded, so `_()` returns the raw key; + # we just assert the localize key made it to the log record. + assert any("local_auth_remapped" in msg for msg in logs.output), logs.output def test_local_auth_case_insensitive(self): data = Userdata() data.populate(["username", "pword", "fname", "creator", "none", "yes", "email", "local"]) - user = data.to_tsc_user() + with self.assertLogs("tabcmd.commands.user.user_data", level="WARNING"): + user = data.to_tsc_user() assert user.auth_setting == TSC.UserItem.Auth.ServerDefault, user.auth_setting def test_non_local_auth_passes_through(self):