|
| 1 | +# User Item Sync |
| 2 | + |
| 3 | +Create or update OSW `User` items from the MediaWiki accounts of an OSL |
| 4 | +instance, enriching ORCID users from the public ORCID API. The run is |
| 5 | +idempotent and previews every change before writing. |
| 6 | + |
| 7 | +This is a Python script built on the osw library. It is not a command of the |
| 8 | +`osw` CLI and not an MCP tool, see [Tools](index.md). It runs from a source |
| 9 | +checkout, because `examples/` is not part of the installed package. |
| 10 | + |
| 11 | +## When to use it |
| 12 | + |
| 13 | +OSL instances gain accounts from manual creation and from whitelisted ORCID |
| 14 | +single sign-on (whose username is the ORCID iD). Those accounts have no |
| 15 | +semantic `User` item until this tool reconciles them. |
| 16 | + |
| 17 | +## Run it |
| 18 | + |
| 19 | +```bash |
| 20 | +uv run python examples/user_sync.py --domain your-instance.example.org --dry-run |
| 21 | +``` |
| 22 | + |
| 23 | +The script reads the credential file given by `--cred-filepath`, otherwise |
| 24 | +`accounts.pwd.yaml` in the working directory, otherwise the file named by the |
| 25 | +`OSW_CRED_FILEPATH` environment variable. It does not read a `.env` file. |
| 26 | +`--domain` falls back to the `OSW_DOMAIN` environment variable. See |
| 27 | +[Credentials](configuration.md#credentials) for the file format. Start with |
| 28 | +`--dry-run` to preview, then run without it to write after confirming at the |
| 29 | +prompt. |
| 30 | + |
| 31 | +To also clean up existing items, add `--prune`. Without it, the sync never |
| 32 | +writes `employment_contract_status` and never removes anything. With it, |
| 33 | +`employment_contract_status` is removed from the stored jsondata of existing |
| 34 | +items. `website` and `organization` are overwritten with an empty list, unless |
| 35 | +`--with-websites` or `--with-organizations` enables them. This happens in a |
| 36 | +second edit of the page's jsondata after the store. The sync decides from the |
| 37 | +stored jsondata, not from the loaded item, which shows the model default for |
| 38 | +`employment_contract_status`. A second `--prune` run therefore changes nothing: |
| 39 | + |
| 40 | +```bash |
| 41 | +uv run python examples/user_sync.py --domain your-instance.example.org --prune --dry-run |
| 42 | +``` |
| 43 | + |
| 44 | +Users whose ORCID record exposes no email are listed in a warning at the end of |
| 45 | +the run; a missing email never fails the sync. |
| 46 | + |
| 47 | +### Options |
| 48 | + |
| 49 | +By default the tool syncs both ORCID and MediaWiki-native accounts and writes |
| 50 | +only core identity plus email. |
| 51 | + |
| 52 | +| Flag | Effect | |
| 53 | +| --- | --- | |
| 54 | +| `--domain` | Target OSL domain. | |
| 55 | +| `--cred-filepath` | Path to `accounts.pwd.yaml`. | |
| 56 | +| `--dry-run` | Preview only; never writes. | |
| 57 | +| `--auto-apply` | Non-interactive: apply creates, gap-fills and removals; keep existing on conflicts. | |
| 58 | +| `--limit N` | Process at most N accounts (testing). | |
| 59 | +| `--orcid-only` | Only ORCID-username accounts (excludes `--mw-only`). | |
| 60 | +| `--mw-only` | Only non-ORCID (MediaWiki-native) accounts (excludes `--orcid-only`). | |
| 61 | +| `--include-system` | Do not skip MediaWiki reserved system accounts. | |
| 62 | +| `--no-redirects` | Do not create `User:` redirect pages. | |
| 63 | +| `--with-websites` | Also store ORCID researcher URLs (opt-in). | |
| 64 | +| `--with-organizations` | Also create and link Organization items from ORCID affiliations (opt-in). | |
| 65 | +| `--with-extras` | Enable both websites and organizations. | |
| 66 | +| `--include-blocked` | Do not skip blocked MediaWiki accounts. | |
| 67 | +| `--prune` | Remove `employment_contract_status` from the stored jsondata of existing items, and overwrite `website` or `organization` with an empty list when their opt-in flag is not given, in a second edit of the page's jsondata after the store (default off; a normal run never writes `employment_contract_status` and never removes anything). | |
| 68 | + |
| 69 | +## What it does |
| 70 | + |
| 71 | +1. Enumerate MediaWiki accounts, skipping blocked accounts (unless |
| 72 | + `--include-blocked` is set; the User item of a skipped blocked account is |
| 73 | + left unchanged), bots and privileged groups |
| 74 | + (`bot`, `sysop`, `bureaucrat`, `interface-admin`) and MediaWiki reserved |
| 75 | + system usernames. Accounts whose username is an exact ORCID iD are exempt |
| 76 | + from the group filter (so a person's own admin account is synced); other |
| 77 | + accounts, such as `<orcid>@bot`, are still excluded by their groups. |
| 78 | +2. Enrich ORCID users from `https://pub.orcid.org/v3.0/<id>`: names and email |
| 79 | + always; researcher URLs and affiliations only when their flag is enabled. |
| 80 | +3. Map each account to a proposed `User` item with a deterministic id |
| 81 | + (uuid5 on the ORCID iD, else the username), so re-runs are idempotent. |
| 82 | +4. Reconcile against existing items. Existing items are found by `username` |
| 83 | + across all `User` items. An item whose deterministic page already exists is |
| 84 | + updated and not created again. Each user gets one of four categories: NEW, |
| 85 | + GAP_FILL, CONFLICT or UNCHANGED. Each changed field has one of three |
| 86 | + statuses: GAP_FILL, CONFLICT or REMOVE. The preview shows a removed field as |
| 87 | + `-field`. A user with only gap-fills or removals is GAP_FILL. Gap-fills apply |
| 88 | + automatically and conflicts are resolved interactively. Removals appear only |
| 89 | + with `--prune`; a normal run never removes anything. |
| 90 | +5. Store items, create `User:<username>` redirects to each item, ensure any |
| 91 | + linked Organization items, verify by reloading, and warn about users with no |
| 92 | + email. |
| 93 | + |
| 94 | +## Data written |
| 95 | + |
| 96 | +| Field | Policy | |
| 97 | +| --- | --- | |
| 98 | +| `username`, `first_name`, `surname`, `label`, `orcid` | Always (core identity). | |
| 99 | +| `email` | Standard: always attempted; missing email is warned, not fatal; never removed. | |
| 100 | +| `website` | Opt-in (`--with-websites`); without that flag, on existing items overwritten with an empty list only with `--prune`, in a second jsondata edit after the store. | |
| 101 | +| `organization` | Opt-in (`--with-organizations`); without that flag, on existing items overwritten with an empty list only with `--prune`, in a second jsondata edit after the store. | |
| 102 | +| `employment_contract_status` | Never written without `--prune`; with `--prune`, removed from the stored jsondata of existing items in a second jsondata edit after the store. | |
| 103 | + |
| 104 | +## Notes |
| 105 | + |
| 106 | +- MediaWiki does not expose other users' email or real name, so ORCID is the |
| 107 | + only source of rich data. Non-ORCID accounts get username-based placeholder |
| 108 | + names, flagged in the preview. Placeholder names are written only for new |
| 109 | + items and are never proposed for an existing item. |
| 110 | +- In a source checkout (for developers), the first accepted run autofetches |
| 111 | + schemas and may regenerate `src/osw/model/entity.py`. Restore it if you do not |
| 112 | + want that change. |
0 commit comments