Skip to content

fix(sso): tolerate missing active_organization in JWT response (sso_get_token)#1228

Open
lmeyerov wants to merge 1 commit intomasterfrom
fix/sso-site-level-active-organization-fallback
Open

fix(sso): tolerate missing active_organization in JWT response (sso_get_token)#1228
lmeyerov wants to merge 1 commit intomasterfrom
fix/sso-site-level-active-organization-fallback

Conversation

@lmeyerov
Copy link
Copy Markdown
Contributor

Summary

ArrowUploader.sso_get_token raises Exception("SSO response missing active organization; ...") when the server's JWT response payload omits (or nulls) the active_organization field. Site-wide-SSO server configurations (no per-org binding) legitimately omit this field, so login fails against any such server.

This PR restores the pre-0.45.10 behavior at this site: gracefully fall back to the existing self.org_name (set in __init__ from register(org_name=...) or client_session.org_name) and skip _switch_org. When active_organization IS present, behavior is unchanged.

Before / After

Before (graphistry/arrow_uploader.py:430-439):

active_org = data.get('active_organization')
if not active_org or not active_org.get('slug'):
    raise Exception(
        "SSO response missing active organization; see graphistry/graphistry#2933"
    )

slug = active_org['slug']
logger.debug("@ArrowUploader.sso_get_token, org_name: %s", slug)
self.org_name = slug
self._switch_org(slug, token_value or self.token)

After:

active_org = data.get('active_organization')
slug = active_org.get('slug') if isinstance(active_org, dict) else None

if slug:
    logger.debug("@ArrowUploader.sso_get_token, org_name: %s", slug)
    self.org_name = slug
    self._switch_org(slug, token_value or self.token)
else:
    # Site-wide SSO servers legitimately omit active_organization
    # when the user has no per-org binding. Preserve the caller-
    # supplied self.org_name (set in __init__) and skip _switch_org.
    if self.org_name:
        logger.info(
            "SSO response did not include active_organization; "
            "preserving caller-supplied org_name=%s", self.org_name
        )
    else:
        logger.info(
            "SSO response did not include active_organization; "
            "site-wide SSO login (no org binding)"
        )

Backwards-compatibility matrix

Server returns Before After
active_organization: {"slug": "x", ...} use x, switch org use x, switch org (identical)
active_organization: None raise preserve self.org_name, no switch
active_organization absent raise preserve self.org_name, no switch
active_organization: {"slug": ""} raise preserve self.org_name, no switch
active_organization: "non-dict" AttributeError preserve self.org_name, no switch (now defensive)

The graceful path does not manufacture org access: if the server doesn't bind an org to the session, the client doesn't either — the client merely preserves whatever org_name the caller passed to register() (or whatever the session already had). The next request that needs org context routes through existing _switch_org paths.

Other read-site audit

active_organization has only two production read sites in this repo:

  • arrow_uploader.py:307 (_finalize_login, used by login() / pkey_login()) — already tolerant via .get('active_organization', {}). Not changed.
  • arrow_uploader.py:430 (sso_get_token) — strict raise. This PR.

Test changes

The pre-existing test_sso_get_token_missing_org_raises was authored to assert the strict-raise behavior via pytest.raises(Exception); that test pinned the regression in place and is removed. Three replacement tests added in tests/test_arrow_uploader.py:

  • test_sso_get_token_missing_active_organization_no_caller_org — payload omits the field, caller passed no org_name → no crash, org_name stays None, _switch_org not called.
  • test_sso_get_token_missing_active_organization_preserves_caller_org — payload omits the field, ArrowUploader(org_name="caller-org") → caller value preserved, _switch_org not called.
  • test_sso_get_token_null_active_organization_falls_back — payload sends active_organization: None (vs absent) → same fallback path.

The existing happy-path test test_sso_login_get_sso_token_ok (payload contains active_organization with a valid slug) is unmodified and still asserts the unchanged "use slug + call _switch_org" path.

Companion server-side fix

graphistry/graphistry#3002 ensures the server emits active_organization for site-wide-SSO signups going forward (already merged). This PR is required regardless: older servers that predate that fix continue to omit the field, and defense-in-depth on newer servers is cheap.

Test plan

  • Replaced regression-pinning unit test
  • Added 3 new fallback tests (missing field × no-caller-org / caller-org / null-value)
  • Existing happy-path test still passes
  • pytest graphistry/tests/test_arrow_uploader.py graphistry/tests/test_pygraphistry.py graphistry/tests/test_client_session.py -q → 54 passed
  • ./bin/ruff.sh clean on changed files
  • ./bin/mypy.sh graphistry/arrow_uploader.py clean
  • CI green
  • Live SSO repro against a site-wide-SSO server (operator-side, requires SSO test environment)

🤖 Generated with Claude Code

…ization

Restore 0.45.8 behavior at arrow_uploader.py sso_get_token: when the
server's JWT response omits (or nulls) active_organization, preserve
self.org_name (already populated in __init__ from register(org_name=...)
or client_session.org_name) and skip _switch_org instead of raising.

This unblocks customers running against site-wide-SSO servers (no
per-org binding) — which legitimately omit the field — from upgrading
past 0.45.8. The strict raise was introduced in 0.45.10 (PR #832 /
#850 cluster).

Backwards compatible: when active_organization IS present, behavior is
identical to current master (use the supplied slug, call _switch_org).
Replaces test_sso_get_token_missing_org_raises (which pinned the bug)
with three fallback tests covering: missing field + no caller
org_name, missing field + caller org_name preserved, null
active_organization.

Companion server-side fix: graphistry/graphistry#3002 (already
merged); this client-side fix is required for older servers and for
defense-in-depth on newer ones.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant