Skip to content

Link unlinked organizations to their Keycloak org by alias - #3993

Open
blarghmatey wants to merge 2 commits into
mainfrom
b2b-link-keycloak-orgs-by-alias
Open

blarghmatey wants to merge 2 commits into
mainfrom
b2b-link-keycloak-orgs-by-alias

Conversation

@blarghmatey

Copy link
Copy Markdown
Member

What are the relevant tickets?

Part of https://github.com/mitodl/hq/discussions/12784. Follows #3928 to #3932.

Description (What does it do?)

  • The reconciler links a page with no sso_organization_id to the Keycloak org whose alias equals its org_key (ignoring case), instead of building a duplicate page that fails on the unique org_key.
  • New backfill_keycloak_orgs command reports what linking would do. It writes nothing without --apply. --create-missing creates Keycloak orgs for pages with no match, and --org-key limits the run.

As of 2026-09-18, production has 71 organizations and 45 have no sso_organization_id. I have not checked how many of the 45 have a Keycloak org, so the dry run in prod is the first step after merge.

Implementation details
  • reconcile_single_keycloak_org used to match on UUID only. test_reconcile_bad_keycloak_org shows the old behavior: the org_key collision surfaces as a ValidationError from save(), which reconcile_keycloak_orgs logs and skips.
  • find_unlinked_page_for_alias raises ValidationError when more than one unlinked page matches (e.g. UTK and utk). A page already linked to a different Keycloak org is not a candidate. Aliases longer than ORG_KEY_MAX_LENGTH (30) and missing aliases match nothing, so a long alias cannot match a page by its first 30 characters.
  • Keycloak is the source of truth for names. Once linked, the page goes through the existing update branch, so its name, title and description are overwritten from Keycloak, as they are for every linked page. The dry run lists each name that would change; descriptions are not listed.
  • Backfill links with a queryset update, not save(), to skip full_clean and a Wagtail revision. It also creates the OrganizationOnboarding record.
  • The backfill reports conflict for a Keycloak org whose UUID is already on another page, and for pages that differ only in case (neither is linked).
  • --create-missing writes the Keycloak org first and the UUID second, with no compensating delete. If the second step fails, the reconciler links the org by alias on its next run. The Keycloak create body is now shared with create_organization (_create_keycloak_organization).
  • Created orgs get no domains and no redirect URL. Legacy pages don't carry them.

How can this be tested?

Ran pytest b2b/provisioning_test.py b2b/api_test.py b2b/commands_test.py in the dev image against a throwaway Postgres and Redis: 266 passed, 1 skipped. The Keycloak side is mocked. Nothing has run against a real realm.

After merge, run manage.py backfill_keycloak_orgs (dry run) in QA, then in production, and check the link/create/no_keycloak_org split before using --apply.

Additional Context

  • The match ignores case because the provisioning collision checks lowercase aliases. I did not confirm that Keycloak itself treats aliases case-insensitively.
  • The daily sync (KEYCLOAK_ORG_SYNC_FREQUENCY, default 86400 s) will start linking matching pages on its next run after deploy. I did not check the production override for that setting.
  • Membership reconciliation for newly linked orgs is not in this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WZoM2TYuY8g9C15R2dJNKy

In production 45 of 71 organizations have no sso_organization_id. The
reconciler only matches a Keycloak org to a page by UUID, so for a page that
predates provisioning and shares its org_key with a realm alias it builds a
second page, fails on the unique org_key, logs the error, and leaves the page
unlinked on every run.

This makes the reconciler link such a page to the Keycloak org whose alias is
its org_key (ignoring case). More than one candidate page raises a
ValidationError rather than guessing. A page already linked to a different
Keycloak org is left alone.

It also adds backfill_keycloak_orgs, which reports what the link would do and
writes nothing without --apply. Pages with no Keycloak org are only reported
unless --create-missing is passed, since some are demo or test orgs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WZoM2TYuY8g9C15R2dJNKy
@github-actions

Copy link
Copy Markdown

OpenAPI Changes

Show/hide changes
## Changes for v0.yaml:
No changes detected

## Changes for v1.yaml:
No changes detected

## Changes for v2.yaml:
No changes detected

Unexpected changes? Ensure your branch is up-to-date with main (consider rebasing).

Comment thread b2b/provisioning.py
The rest of backfill_keycloak_organizations compares org_keys ignoring case,
but the --org-key filter used an exact match. `--org-key utk` against a page
keyed `UTK` matched nothing and reported nothing, which reads as success.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WZoM2TYuY8g9C15R2dJNKy
Comment thread b2b/provisioning.py
Comment on lines +865 to +866
_link_organization(organization, keycloak_org.id)
claimed_ids.add(keycloak_org.id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The backfill_keycloak_organizations function does not handle cases where keycloak_org.id is None, leading to a silent failure to link the organization.
Severity: MEDIUM

Suggested Fix

Add a check in backfill_keycloak_organizations to validate that keycloak_org.id is not None before attempting to use it. If the ID is None, the iteration should be skipped or an error should be logged to prevent silent failures and potential data corruption.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: b2b/provisioning.py#L865-L866

Potential issue: In the `backfill_keycloak_organizations` function, the
`keycloak_org.id` can be `None` as per its type definition `str | None`. However, there
is no validation to handle this case. If `keycloak_org.id` is `None`, the code will
proceed to call `_link_organization(organization, None)`, which will silently fail to
link the organization by setting its `sso_organization_id` to `None`. The function would
then incorrectly report that the operation was applied successfully, leading to data
inconsistencies where an organization is believed to be linked in Keycloak but is not.

This branch has not been deployed

No deployments
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