Skip to content

fix(auth): report rejected org switches and tolerate a null active_organization#1720

Draft
aucahuasi wants to merge 1 commit into
masterfrom
fix/switch-org-error-handling
Draft

fix(auth): report rejected org switches and tolerate a null active_organization#1720
aucahuasi wants to merge 1 commit into
masterfrom
fix/switch-org-error-handling

Conversation

@aucahuasi

Copy link
Copy Markdown
Contributor

Summary

Two defects on the login path. Both take a server response the client should act on and turn it into silence.

1. A rejected organization switch was recorded as a success

ArrowUploader._switch_org posts to /api/v2/o/<slug>/switch/, passes the response to log_requests_error, and then cached the (org, token) pair unconditionally.

log_requests_error only logs. It never raises. So a server that rejected the switch with a 403 was recorded as having accepted it, and the early return at the top of the method turned every later attempt into a silent no-op for the rest of the session. The client had no way to distinguish a 200 from a 403.

Now the pair is cached only on a 2xx response. The method returns a bool so callers can tell acceptance from rejection, and logs a warning naming the status code. A rejected switch is retried on the next call instead of being skipped forever.

Login still succeeds when a switch fails, because not every deployment exposes the endpoint. Callers must not treat a False return as fatal, and that contract is stated in the docstring.

2. Login crashed on a null active_organization

_finalize_login read:

org = json_response.get('active_organization', {})
logged_in_org_name = org.get('slug', None)

The dict default only covers a missing key. A server sending "active_organization": null, or any non-dict value, made the second line raise AttributeError rather than reading as "no organization bound".

The username/password and personal-key paths now apply the same isinstance guard that the SSO path in sso_get_token already used.

Tests

Nine new cases in graphistry/tests/test_arrow_uploader.py:

  • _switch_org: accepted, rejected, retried after rejection, transport failure, already cached, and the no-op when org or token is absent.
  • login: against a null, a missing, and a non-dict active_organization.

All nine fail on the prior implementation. The null case fails with exactly AttributeError: 'NoneType' object has no attribute 'get'.

Existing coverage mocked _switch_org itself (test_login_invokes_switch_org and friends), so it asserted that the client calls switch, never that the server accepted it. That is why the suite stayed green while the behavior was wrong. The new tests mock the HTTP layer instead.

Verified locally: test_arrow_uploader.py and test_client_session.py pass at 71 tests, bin/lint.sh passes, and mypy reports no errors in arrow_uploader.py.

Notes for review

_switch_org changes its return type from None to bool. Every current caller ignores the return value, so nothing breaks, but it is a signature change on a private method and callers may now want to branch on it.

Behavior on a failed switch is deliberately unchanged in one respect: login proceeds. The only difference is that the failure is now visible in the logs and not remembered as a success.

…ganization

Two defects on the login path, both of which turn a server response the
client should act on into silence.

1. _switch_org posted to /api/v2/o/<slug>/switch/, handed the response to
   log_requests_error (which only logs and never raises), then cached the
   (org, token) pair unconditionally. A server that rejected the switch was
   recorded as having accepted it, and the early return at the top of the
   method made every later attempt a silent no-op for the rest of the
   session. The client could not distinguish a 200 from a 403.

   The pair is now cached only on a 2xx response. The method returns a bool
   so callers can tell acceptance from rejection, and logs a warning naming
   the status code. A rejected switch is retried on the next call. Login
   still succeeds when the switch fails, because not every deployment
   exposes the endpoint, so callers must not treat False as fatal.

2. _finalize_login read json_response.get('active_organization', {}) and
   then called .get('slug') on the result. The dict default only covers a
   missing key, so a server sending "active_organization": null, or any
   non-dict value, raised AttributeError rather than reading as "no
   organization bound". The username/password and personal-key paths now
   apply the same isinstance guard the SSO path already used.

Tests: nine cases in graphistry/tests/test_arrow_uploader.py covering
accepted, rejected, retried, transport-failed, already-cached and no-op
switches, plus login against a null, missing and non-dict
active_organization. Existing coverage mocked _switch_org itself, so it
asserted only that the client calls switch, never that the server accepted
it. All nine fail on the prior implementation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants