Summary
UserItem.CSVImport was added to consolidate CSV user-import logic, but several bugs currently prevent it from being used as a reliable foundation. tabcmd duplicates the entire CSV stack independently (user_data.py) and cannot yet delegate to TSC without introducing regressions. This issue documents what needs to be fixed so that tabcmd can remove its duplicate code.
Bugs
1. Off-by-one on MAX makes the AUTH column unusable
ColumnType.AUTH = 7 and ColumnType.MAX = 7 have the same value. Both create_user_from_line and _validate_import_line_or_throw guard with len > MAX, which fires for any 8-element line — i.e. any line that includes the auth type column. The auth column defined in the spec is silently unreachable.
# user_item.py line ~446
if len(values) > UserItem.CSVImport.ColumnType.MAX: # MAX=7, so 8-column lines always raise
raise ValueError("Too many attributes for user import")
Fix: MAX should equal the number of columns (8), not the last index (7). Change to MAX = 8 and update bounds checks to >= MAX.
2. create_user_from_line lowercases the whole line including usernames
line = line.strip().lower() # line ~442
This lowercases the username before assignment, which is destructive for case-sensitive authentication systems (e.g. LDAP) or mixed-case email-format usernames. Only the comparison-relevant fields (license, admin, publisher, auth) should be normalized.
Fix: lowercase per-field only for the fields that need it (license, admin, publisher, auth), not the entire line.
3. _valid_attributes auth column is missing TableauIDWithMFA
[UserItem.Auth.SAML, UserItem.Auth.OpenID, UserItem.Auth.ServerDefault], # auth — line ~504
UserItem.Auth.TableauIDWithMFA is defined in the Auth class and is a valid value (documented in Tableau Online). It should be included in the validation list.
4. validate_file_for_import doesn't normalize case before validation
validate_file_for_import passes the raw (mixed-case) line directly to _validate_import_line_or_throw, whose _valid_attributes lists are all lowercase. So Viewer fails validation but viewer passes — but _evaluate_site_role handles both correctly. This means validate_file_for_import rejects CSVs that create_user_from_line would parse successfully.
Fix: lowercase the line (or individual non-username fields) in _validate_import_line_or_throw before comparison, matching the behaviour of create_user_from_line.
5. _set_values bypasses the @property_is_enum guard on auth_setting
create_user_from_line calls _set_values(...) which writes directly to self._auth_setting, skipping the @property_is_enum(Auth) setter. A CSV-parsed UserItem can therefore carry an invalid auth_setting that only fails at the API call, not at construction.
if auth_setting:
self._auth_setting = auth_setting # bypasses @property_is_enum
Fix: use self.auth_setting = auth_setting in _set_values, or validate explicitly before assignment. (Note: this requires fixing bug #3 first so TableauIDWithMFA is accepted.)
Context: tabcmd duplication
tabcmd (tabcmd/commands/user/user_data.py) contains a complete independent re-implementation of CSVImport: _parse_line, get_users_from_file, validate_file_for_import, _validate_user_or_throw, _validate_item, and evaluate_site_role. The role derivation function is copied character-for-character (including a # is this the expected outcome? comment on the Unlicensed fallback).
Once the above bugs are fixed, tabcmd can delete ~100 lines of duplicate code and delegate to UserItem.CSVImport. A follow-up tabcmd PR will do this once this issue is resolved.
Affected code
tableauserverclient/models/user_item.py — UserItem.CSVImport class (lines ~418–566)
Summary
UserItem.CSVImportwas added to consolidate CSV user-import logic, but several bugs currently prevent it from being used as a reliable foundation. tabcmd duplicates the entire CSV stack independently (user_data.py) and cannot yet delegate to TSC without introducing regressions. This issue documents what needs to be fixed so that tabcmd can remove its duplicate code.Bugs
1. Off-by-one on
MAXmakes the AUTH column unusableColumnType.AUTH = 7andColumnType.MAX = 7have the same value. Bothcreate_user_from_lineand_validate_import_line_or_throwguard withlen > MAX, which fires for any 8-element line — i.e. any line that includes the auth type column. The auth column defined in the spec is silently unreachable.Fix:
MAXshould equal the number of columns (8), not the last index (7). Change toMAX = 8and update bounds checks to>= MAX.2.
create_user_from_linelowercases the whole line including usernamesThis lowercases the username before assignment, which is destructive for case-sensitive authentication systems (e.g. LDAP) or mixed-case email-format usernames. Only the comparison-relevant fields (license, admin, publisher, auth) should be normalized.
Fix: lowercase per-field only for the fields that need it (license, admin, publisher, auth), not the entire line.
3.
_valid_attributesauth column is missingTableauIDWithMFAUserItem.Auth.TableauIDWithMFAis defined in theAuthclass and is a valid value (documented in Tableau Online). It should be included in the validation list.4.
validate_file_for_importdoesn't normalize case before validationvalidate_file_for_importpasses the raw (mixed-case) line directly to_validate_import_line_or_throw, whose_valid_attributeslists are all lowercase. SoViewerfails validation butviewerpasses — but_evaluate_site_rolehandles both correctly. This meansvalidate_file_for_importrejects CSVs thatcreate_user_from_linewould parse successfully.Fix: lowercase the line (or individual non-username fields) in
_validate_import_line_or_throwbefore comparison, matching the behaviour ofcreate_user_from_line.5.
_set_valuesbypasses the@property_is_enumguard onauth_settingcreate_user_from_linecalls_set_values(...)which writes directly toself._auth_setting, skipping the@property_is_enum(Auth)setter. A CSV-parsedUserItemcan therefore carry an invalidauth_settingthat only fails at the API call, not at construction.Fix: use
self.auth_setting = auth_settingin_set_values, or validate explicitly before assignment. (Note: this requires fixing bug #3 first soTableauIDWithMFAis accepted.)Context: tabcmd duplication
tabcmd (
tabcmd/commands/user/user_data.py) contains a complete independent re-implementation ofCSVImport:_parse_line,get_users_from_file,validate_file_for_import,_validate_user_or_throw,_validate_item, andevaluate_site_role. The role derivation function is copied character-for-character (including a# is this the expected outcome?comment on theUnlicensedfallback).Once the above bugs are fixed, tabcmd can delete ~100 lines of duplicate code and delegate to
UserItem.CSVImport. A follow-up tabcmd PR will do this once this issue is resolved.Affected code
tableauserverclient/models/user_item.py—UserItem.CSVImportclass (lines ~418–566)