Commit f6ceddc
Fixes #20515
Clause-②: yes (narrowing)
## What changed
`resolveUserAuthzGrants`
(`packages/core/src/security/resolve-authz-context.ts`) now states one
rule, once, as the module-private predicate `grantAppliesInTenant`: a
grant row with no organization is global and applies everywhere; a row
scoped to an organization applies only while that organization is the
active tenant. With no active organization, only the global grants
apply. There is no "every organization" option and no keep-all fallback.
Three sites ask the predicate:
- **§4** `sys_user_position` (the triage's `:802`).
- **§6** `sys_user_permission_set` (the triage's `:829`).
- **§6a** the `sys_position` rows whose bound permission sets the
resolver collects. This one is a **declared deviation** from the claim's
two sites; see H6 below for the measurement that put it here. With a
tenant it is a no-op, because the driver's tenant scope already returned
only that organization's rows and the organization-less ones.
The §4 and §6 comments, which already stated this rule, are now true. §3
(`sys_member`) is not edited.
`@objectstack/plugin-security`: `buildContextForUser(ql, userId, nowMs?,
tenantId?)` takes the organization to resolve in (H3):
- `resolveDelegatorContext` resolves the on-behalf-of delegator in the
live principal's organization. That is an **enforcement** input: the D10
intersection.
- `explainAccessForCaller` resolves an explained user in the caller's
organization.
No second check was added to `requireManageMetadata` or to any other
door. The `plugin-sharing` `adminOrgScope` guard is untouched.
**Not in this card, per triage:** revoking custom organization-scoped
grants when a member is removed. Once this rule holds, those grants no
longer apply.
## H0: the defect at the public door, before and after
These readings use the #20492 rig (`dispatch()` with real identity
resolution, `resolveRequestScope` into `resolveExecutionContext` into
`resolveAuthzContext`, under an `isolated` posture). The base is
unmodified `397572ed5`, which already includes PR #20514. The "after"
column is the committed pin file
`packages/runtime/src/domains/packages-orgless-grants-capability-gate.test.ts`:
every cell of it has an arm there, and the file is green at `4e3e4f5e4`.
Patch round 1 added the three arms that were probe-only readings at
`c9e6463ce`: the control at `PATCH disable`, the platform admin with
`org_alpha` active at `PATCH disable`, and the org-less no-grant row.
The gate is observable at two doors:
- **`PATCH /packages/:id/disable`** is the door where the capability
gate's effect is fully visible. It asks no organization, so a caller who
passes the gate switches the package off for the whole environment
(200). A caller refused by the gate gets 403.
- **`DELETE /packages/:id`**: since PR #20514, an org-less caller who
passes the gate reaches the door's own organization check (400
`TENANT_SCOPE_REQUIRED`). A caller refused by the gate gets 403.
| Arm (every session names `org_alpha`) | DELETE, base | DELETE, after |
PATCH disable, base | PATCH disable, after |
|---|---|---|---|---|
| removed from `org_alpha`, still in `org_beta`, holds an
`org_alpha`-scoped `manage_metadata` set (a2) | 400
`TENANT_SCOPE_REQUIRED` (gate passed) | **403 `PERMISSION_DENIED`** |
**200 (package disabled)** | **403** |
| same, no membership left anywhere (a2b) | 400 (gate passed) | **403**
| **200** | **403** |
| control: current `org_alpha` member, same grant, `org_alpha` active |
200 | 200 | 200 | 200 |
| removed member holding the same set **globally** | 400 | 400
(unchanged) | 200 | 200 (unchanged) |
| platform admin (unscoped `admin_full_access`), `org_alpha` active |
200 | 200 | 200 | 200 |
| platform admin, no active organization | 400 | 400 (unchanged) | 200 |
200 (unchanged) |
| org-less user with no grant | 403 | 403 | 403 | 403 |
The base readings come from a throwaway probe at `397572ed5`; the first
"after" reading was taken at `514e681cd`, and the committed file
reproduces every "after" cell. The committed file adds a third
removed-member arm: `org_alpha` bound the set to **its own copy of
`org_member`**, and the member is still an `org_member` in `org_beta`.
That arm is refused 403 on both doors. With the §6a predicate ablated it
passes the gate (see Verification).
## H1: census of the no-tenant callers (source, `packages/**`, at
`c9e6463ce`)
| Caller | Tenant it passes | Class | What it loses under the rule |
Right? |
|---|---|---|---|---|
| `resolveAuthzContext` first resolution
(`resolve-authz-context.ts:428`) | the API key's
`active_organization_id`, else the session's `activeOrganizationId` |
sometimes none | every organization-scoped §4, §6 and §6a grant, when
the key or session names no organization | Yes, per ruling. A request
with no active organization acts in none. Most exposed are
`group`-posture principals with no active organization: their data wall
still spans every member organization, but their organization-scoped
grants now need that organization active. |
| `resolveAuthzContext` dropped-claim re-resolution (`:548`) | none, by
construction | never | the left organization's grants: **the defect**.
It also loses every other organization's scoped grants. | Yes. This is
the fix. |
| `hasPlatformAdminStanding` (`:1184`, `{ nowMs }` only) | none | never
| nothing. `PLATFORM_ADMIN` derives only from the unscoped
`admin_full_access` user grant or the declared-administrator config
(H2). | Yes |
| plugin-auth `customSession` (`auth-manager.ts:3987`) |
`activeOrganizationId ?? undefined` | sometimes | `positions[]` loses
organization-scoped `sys_user_position` names when no organization is
active. `isPlatformAdmin` is unchanged. | Yes. Its docblock already
scopes the payload to the active organization. |
| plugin-auth `isPlatformAdminUserId` (`:7009`), through
`hasPlatformAdminStanding` | none | never | nothing | Yes |
| plugin-hono-server `makeExecutionContextResolver`
(`current-user-endpoints.ts:424`) | `activeOrganizationId ?? undefined`
| sometimes | same as `resolveAuthzContext` | Yes |
| explainer `buildContextForUser` (`explain-engine.ts:581`) | **was
none; now the caller's choice** | H3 | see H3 | changed |
| `resolveDelegatorContext` into `buildContextForUser`
(`explain-engine.ts:686`), an **enforcement** path | was none (the live
tenant was stamped on afterwards); now the live principal's tenant |
always, when the live principal has one | **Before:** every
organization's delegator grants. **Under the rule with no caller
change:** only global grants, a regression for an OAuth agent acting for
an organization admin. **Now:** the delegator's grants in the
organization the request runs in. | fixed here |
| `explainAccessForCaller` into `buildContextForUser`
(`security-plugin.ts:4690`) | was none; now the caller's tenant |
sometimes | grants the explained user holds in organizations other than
the caller's | changed (H3) |
| invitation placement `assertIssuable` (`invitation-placement.ts:153`)
| the invitation's `organizationId ?? undefined` | always in practice (a
better-auth invitation belongs to an organization) | nothing in practice
| Yes |
| automation `runAs:'user'` (`service-automation/src/plugin.ts:909`) |
the triggering run's `tenantId` | sometimes | organization-scoped grants
for a run triggered with no organization | Yes. The run matches the
user's own direct request in that state. |
| transports calling `resolveAuthzContext`: `rest-server.ts:2968`,
`runtime/src/security/resolve-execution-context.ts:217`,
`sharing-plugin.ts:945`, `marketplace-install-local-plugin.ts:1806`,
`service-datasource/admin-routes.ts:480`,
`service-settings/settings-service-plugin.ts:296`,
`service-storage/storage-service-plugin.ts:1152` | session or API key |
sometimes | same as the first row | Yes |
| MCP stdio (`mcp/src/plugin.ts:164`), API key only | the key's
organization | sometimes (an org-less key, allowed under `single` /
`group`) | organization-scoped grants for an org-less key | Yes, per
ruling |
**No caller needs every organization's grants**, so **no option was
added** to `ResolveUserAuthzGrantsOptions` and the grants-cache key is
unchanged (H4 moot). `tenantId` already keys cache entries; a new pin
checks, with the cache on, that an `org_a` entry and a no-tenant entry
never serve each other. `Clause-②` stays `yes (narrowing)`: the `yes`
arm is now carried by `buildContextForUser`'s new optional parameter, a
public widening of `@objectstack/plugin-security`.
## H2: platform-admin standing is global (measured)
This was measured on a real `SqlDriver` (better-sqlite3) with the
shipped `bootstrapPlatformAdmin`, under `single`, after the fix:
- the minted `admin_full_access` row reads `organization_id: null`;
- `hasPlatformAdminStanding` answers `true`;
- an org-less resolution answers posture `PLATFORM_ADMIN`, with
`manage_metadata` held.
`hasPlatformAdminStanding` loses nothing, so it needs no answer of its
own. Core pins cover both polarities: the unscoped grant is
`PLATFORM_ADMIN` with and without a tenant; an organization-scoped
`admin_full_access` confers no standing with or without that tenant.
## H3: the explainer takes a tenant, (b), not the triage's (a)
The explainer's own contract decided it. The module header says the
report "can never drift from enforcement". `buildContextForUser`'s
docblock says it is called "with the exact arguments" enforcement uses.
Its parity suite asserts, field by field, that it equals
`resolveUserAuthzGrants`.
Option (a), an explicit every-organization option, breaks that parity by
construction. It would also have kept every organization's grants in an
**enforcement** path, because `resolveDelegatorContext` builds the D10
delegator leg through `buildContextForUser`. So `buildContextForUser`
takes the organization to resolve in, and each caller names it.
Pinned in `explain-engine.test.ts`, `security-plugin.test.ts` and the
parity suite, which now runs two cases in `org1` on both sides.
**What the explainer shows for the removed member, against
enforcement:**
| Explained by | Explain shows | Enforcement (the member's own session:
claim dropped, no tenant) | Agree? |
|---|---|---|---|
| a caller with no active organization, or in `org_beta` | global grants
only; no `manage_metadata` | refused | yes |
| an admin in `org_alpha` (the left organization) | the
`org_alpha`-scoped `manage_metadata` set | refused | **no**: explain
overstates |
| (before this PR) anyone | every organization's grants | passed (the
defect) | yes, both wrong |
The disagreement in the second row is the #20431 class (explain ≠
enforce). It is reported, not fixed here. The explain API resolves the
explained user in the caller's organization, and it does not model the
session arm's membership check. Explain-of-another-user's record-level
Layer 0 still evaluates with no active organization; that is
pre-existing and unchanged.
## H5: section 3, measured
Measured on a real `SqlDriver` over the shipped per-organization
built-in catalog (`bootstrapBuiltinRoles` for `org_jia` and `org_yi`).
The user is a current member of `org_jia` (`admin`) and `org_yi`
(`member`), with no organization active.
- §3 projects **both** organizations' roles: `positions: [org_admin,
org_member, everyone]`.
- **The gate that read them was §6a.** At the pre-§6a state, those names
pulled every organization's copy of `org_admin`, `org_member` and
`everyone`, and their bindings. A member removed from `org_jia` and
still in `org_yi` kept `org_jia`'s `org_member`-bound `manage_metadata`
set with no tenant. A user with no membership at all picked up
`org_jia`'s `everyone` binding.
- **After the §6a predicate:** the same resolutions carry no
organization's bindings. With `org_jia` active, only `org_jia`'s apply.
The verdict on §3 itself: **not the same class once §6a holds**, and not
edited. Its rows are the user's own current memberships, not grant rows,
and every capability a role name can confer now arrives through
organization-scoped rows that answer the rule. What remains is display:
`positions[]` with no active organization names every membership's role.
## H6: the smallest fix that satisfies the stated rule
Sections 4 and 6 alone did not satisfy the rule. The real-driver
measurement in H5 shows the removed member keeping the left
organization's `manage_metadata` through §6a after the §4 and §6 fix, so
§6a asks the same predicate.
- It is written as the **grant rule**, not as a second tenant wall. The
driver's `applyTenantScope` stays the one spelling of the wall, as the
#10103 comment requires.
- With a tenant it filters nothing: the driver already returned only
that organization's rows and the organization-less ones.
## Verification (head `4e3e4f5e4`, after merging `origin/main`
`288611e3e` with a true merge; the first round's merge was `31d281d3b`)
- **Build:** `turbo run build --filter='./packages/*'
--filter='./packages/*/*' --concurrency=2`: 71/71.
- **Tests at `4e3e4f5e4`:**
- **`@objectstack/dogfood`, the whole suite:** `vitest run --shard=1/3`,
`2/3` and `3/3`, all exit 0. 47 files and 378 passed; 47 files and 321
passed, 1 skipped; 46 files and 451 passed, 1 file and 2 tests skipped.
That is 141 files and 1150 tests passed.
- `sharing-rule-org-less-caller.dogfood.test.ts` alone: 16 passed. That
is the 13 it had, plus 3 for the organization-scoped persona.
- `@objectstack/core` `vitest run --project local`: 56 files, 1518
passed.
- `@objectstack/plugin-security` `explain-engine`, `security-plugin` and
`per-organization-catalog`: 361 passed.
- `@objectstack/runtime` the door file (13 pins) plus
`packages-uninstall-refuse-before-mutate`: 21 passed.
- `@objectstack/plugin-sharing` `sharing-rule-positions-name-authority`:
7 passed.
- Typecheck: `@objectstack/runtime` and `@objectstack/dogfood` exit 0;
the runtime test-typecheck ledger is unchanged.
- **Full suites at `c9e6463ce`'s source, before the first merge** (the
two `origin/main` merges since then brought main's own `packages/rest`
changes, `rest-server.ts`, `meta-item-read-gate.ts` and four test files,
and main's `packages/runtime` test
`meta-list-projection-parity.test.ts`; this PR's patch round moved its
runtime door-pin file. For those suites, the verdict is the head's `Test
Core` runs):
| Package | Files | Tests |
|---|---|---|
| `plugin-security` | 143 | 3052 passed, 16 skipped |
| `runtime` local | 287 | 4181 passed |
| `rest` local | 221 | 4231 passed |
| `plugin-auth` | 115 | 2464 |
| `plugin-hono-server` | 27 | 324 |
| `service-automation` | 149 | 1837 |
| `plugin-sharing` | 37 | 913 |
| `plugin-approvals` | 51 | 791 |
| `organizations` | 8 | 108 |
| `mcp` | 32 | 344 |
| `cloud-connection` | 30 | 397 |
| `service-datasource` | 34 | 693 |
| `service-settings` | 33 | 584 |
| `service-storage` | 40 | 627 |
| `client` | 50 | 641 |
All green. The consumer direction is the downstream importers of
`@objectstack/core` named in the H1 census, plus their own consumers
`plugin-approvals` and `client`.
This table omitted `@objectstack/dogfood` in the first round, and its
shard 3/3 was red on `c9e6463ce`. The whole dogfood suite is the first
bullet above.
- **Typecheck:** `@objectstack/core`, `@objectstack/plugin-security` and
`@objectstack/runtime` `typecheck` all exit 0. Each
`check:test-typecheck` is OK with its debt ledger unchanged.
- **Fixture triage (dogfood, patch round 1):**
`sharing-rule-org-less-caller.dogfood.test.ts` (#8158's HTTP proof) gave
its exposed org-less persona `manage_sharing` through a grant scoped to
`org_8158_a`. That grant reached `adminOrgScope` only through the
defect, so shard 3/3 went red on "the refusal names the ORGANIZATION".
- The exposed persona now holds the set **globally**. It keeps pinning
`adminOrgScope` (#8158's defence in depth) with every assertion
unchanged: 403, the "active organization" message, by-name and by-id
refused, evaluate / delete / create refused, no cross-tenant read.
- A new persona holds the grant **as #8158 filed it**: scoped to
`org_8158_a`, no membership, no active organization. It is refused 403
`PERMISSION_DENIED` at the capability gate ("requires the manage_sharing
capability"), with no rows returned, and refused by name too. Its
session is pinned to carry no active organization.
- The control keeps the scoped grant with `org_8158_a` active and still
reads only its own tenant.
- The new persona's red direction without the fix is the old test's
green on `main`: that case measured exactly this persona reaching
`adminOrgScope`'s message.
- Census of the rest of `packages/qa/dogfood`:
- No other fixture writes an organization-scoped
`sys_user_permission_set` or `sys_user_position` row. The two other
hits, in `membership-actor-attribution`, are reads of the auto-grant
row.
- `test/armed.ts:215` resolves through the real `resolveAuthzContext`
(whatever the session carries), and its users are armed through
memberships.
- The `authz-conformance.matrix.ts` rows cite §3/§4/§6 as enforcement
sites, and none of them states the old no-tenant reading.
- The whole suite is green, as above.
- **Fixture triage (plugin-sharing, one file, two cases):**
`sharing-rule-positions-name-authority.test.ts` gave an org-less caller
`manage_sharing` through an organization-scoped grant, which is exactly
the defect's behaviour. The grant is re-spelled as **global**, the one
way an org-less caller still holds it; it is still `sharing_admin`,
never `admin_full_access`. Three explain fixtures were re-judged to
resolve in `org1`, where the scoped set applies.
- **Ablations**, each through `scripts/ablation-replace.mjs` in WRAP
mode, with a script-level `trap` restore on the absolute path. Core
resolves from `src` in both the core and runtime suites, and
`explain-engine` is imported relatively, so no `dist/` leg applies. Each
is labelled with the source state it was measured at.
1. **Re-run at `4e3e4f5e4`** (resolver blob `1f0d2889e626`, which
includes §6a). The predicate was put back to the old skip condition.
Anchor 1 to 0, blob `1f0d2889e626` to `a03a16f630a3`.
- **Red:** 5 core pins (§4/§6 with no tenant; §6a with no tenant;
`u_ex`; `u_gone`; cache on) and the 6 removed-member door pins (DELETE
and disable, for a2, a2b and the position-bound arm).
- **Green, 7 door pins:** both controls, the global grant, the platform
admin, the org-less no-grant caller, and the claim-drop proof.
- Restored: blob == HEAD, `git diff HEAD` empty.
- The first round's run of this ablation was taken before §6a landed
(blob `490bd8a377af`) and is superseded.
2. Measured before the first merge; `92716c91af53` is still
`explain-engine.ts`'s blob at `4e3e4f5e4`. `buildContextForUser` stopped
passing its tenant. Blob `92716c91af53` to `372ec2454029`. **Red:** 6
pins, which are the three re-judged fixtures,
explain-in-an-organization, delegator-in-`org_alpha` and the route
caller-in-`org_alpha`. Restored and proven the same way.
3. Measured before the first merge; `1f0d2889e626` is still the
resolver's blob at `4e3e4f5e4`. The §6a predicate was replaced by a
filter that keeps every row. Blob `1f0d2889e626` to `404c23da10ec`.
**Red:** both §6a core pins and 4 door pins: the position-bound arm,
plus the a2 arm, whose `org_beta` membership also reaches `org_alpha`'s
`org_member` binding. Restored and proven the same way.
- **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `4e3e4f5e4` derived 72 commands. That is
68, plus four `@objectstack/spec` families: `check:empty-state`,
`check:liveness`, `check:strictness-ledger` and `check:variant-docs`.
All 72 were run with exit codes recorded before any pipe, and all ended
0.
- `check:type-check-debt` first exited 3 (`PREREQUISITE NOT MET`):
ablation 1's restore left core's source newer than its `dist/`. Core was
rebuilt and the gate re-run: 0.
- `--ran`: `72 derived, 72 run, 0 NOT-MEASURED, 0 UNRUN`.
- **Lint, a declared narrowing:** `eslint --no-inline-config --format
json` over the 10 changed `.ts` files at `4e3e4f5e4` gave 10 files, 0
errors, 0 warnings.
- The population is `eslint.config.mjs`'s `**/*.{ts,...}` object minus
`NEVER_LINTED` and `packages/spec/**`, and all 10 files are in it.
- The config enables no type-aware linting (no `parserOptions.project`,
as the config itself states), so this diff cannot move any untouched
file's verdict. The full `pnpm lint` is CI's.
## Acceptance notes
- **Review nit ①5, not carried:** `explainAccessForCaller` reads
`tenantId` where `resolvePermissionSetsForContext` reads `organizationId
?? tenantId`. Patch round 1 does not otherwise touch
`security-plugin.ts`, so it is left as reviewed.
- **Explain ≠ enforce for a removed member explained from the left
organization** (H3, second row). This is the #20431 family, reported and
not fixed. The explained user is resolved in the caller's organization,
without the membership check the session arm applies.
- **§6a no-tenant page cap:** the organization-less `sys_position` read
is installation-wide and capped at 200 rows, so with many organizations
the organization-less rows can fall outside the page. That was already
true before this change; the predicate only decides which of the
returned rows apply.
- **The position-name fold with no tenant**
(`resolvePermissionSetsForContext` requesting position names as
permission-set names, loaded through `dbLoaderForContext`) is a separate
seam. NOT MEASURED here.
- **`group` posture:** a principal with no active organization keeps a
data wall spanning every member organization, but it now holds no
organization-scoped grant until one is active. That is the ruling; it is
named here because it is the most visible population.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 487a784 commit f6ceddc
11 files changed
Lines changed: 907 additions & 37 deletions
File tree
- .changeset
- packages
- core/src/security
- plugins
- plugin-security/src
- plugin-sharing/src
- qa/dogfood/test
- runtime/src/domains
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
1831 | 1831 | | |
1832 | 1832 | | |
1833 | 1833 | | |
| 1834 | + | |
| 1835 | + | |
| 1836 | + | |
| 1837 | + | |
| 1838 | + | |
| 1839 | + | |
| 1840 | + | |
| 1841 | + | |
| 1842 | + | |
| 1843 | + | |
| 1844 | + | |
| 1845 | + | |
| 1846 | + | |
| 1847 | + | |
| 1848 | + | |
| 1849 | + | |
| 1850 | + | |
| 1851 | + | |
| 1852 | + | |
| 1853 | + | |
| 1854 | + | |
| 1855 | + | |
| 1856 | + | |
| 1857 | + | |
| 1858 | + | |
| 1859 | + | |
| 1860 | + | |
| 1861 | + | |
| 1862 | + | |
| 1863 | + | |
| 1864 | + | |
| 1865 | + | |
| 1866 | + | |
| 1867 | + | |
| 1868 | + | |
| 1869 | + | |
| 1870 | + | |
| 1871 | + | |
| 1872 | + | |
| 1873 | + | |
| 1874 | + | |
| 1875 | + | |
| 1876 | + | |
| 1877 | + | |
| 1878 | + | |
| 1879 | + | |
| 1880 | + | |
| 1881 | + | |
| 1882 | + | |
| 1883 | + | |
| 1884 | + | |
| 1885 | + | |
| 1886 | + | |
| 1887 | + | |
| 1888 | + | |
| 1889 | + | |
| 1890 | + | |
| 1891 | + | |
| 1892 | + | |
| 1893 | + | |
| 1894 | + | |
| 1895 | + | |
| 1896 | + | |
| 1897 | + | |
| 1898 | + | |
| 1899 | + | |
| 1900 | + | |
| 1901 | + | |
| 1902 | + | |
| 1903 | + | |
| 1904 | + | |
| 1905 | + | |
| 1906 | + | |
| 1907 | + | |
| 1908 | + | |
| 1909 | + | |
| 1910 | + | |
| 1911 | + | |
| 1912 | + | |
| 1913 | + | |
| 1914 | + | |
| 1915 | + | |
| 1916 | + | |
| 1917 | + | |
| 1918 | + | |
| 1919 | + | |
| 1920 | + | |
| 1921 | + | |
| 1922 | + | |
| 1923 | + | |
| 1924 | + | |
| 1925 | + | |
| 1926 | + | |
| 1927 | + | |
| 1928 | + | |
| 1929 | + | |
| 1930 | + | |
| 1931 | + | |
| 1932 | + | |
| 1933 | + | |
| 1934 | + | |
| 1935 | + | |
| 1936 | + | |
| 1937 | + | |
| 1938 | + | |
| 1939 | + | |
| 1940 | + | |
| 1941 | + | |
| 1942 | + | |
| 1943 | + | |
| 1944 | + | |
| 1945 | + | |
| 1946 | + | |
| 1947 | + | |
| 1948 | + | |
| 1949 | + | |
| 1950 | + | |
| 1951 | + | |
| 1952 | + | |
| 1953 | + | |
| 1954 | + | |
| 1955 | + | |
| 1956 | + | |
| 1957 | + | |
| 1958 | + | |
| 1959 | + | |
| 1960 | + | |
| 1961 | + | |
| 1962 | + | |
| 1963 | + | |
| 1964 | + | |
| 1965 | + | |
| 1966 | + | |
| 1967 | + | |
| 1968 | + | |
| 1969 | + | |
| 1970 | + | |
| 1971 | + | |
| 1972 | + | |
| 1973 | + | |
| 1974 | + | |
| 1975 | + | |
| 1976 | + | |
| 1977 | + | |
| 1978 | + | |
| 1979 | + | |
| 1980 | + | |
| 1981 | + | |
| 1982 | + | |
| 1983 | + | |
| 1984 | + | |
| 1985 | + | |
| 1986 | + | |
| 1987 | + | |
| 1988 | + | |
| 1989 | + | |
| 1990 | + | |
| 1991 | + | |
| 1992 | + | |
| 1993 | + | |
| 1994 | + | |
| 1995 | + | |
| 1996 | + | |
| 1997 | + | |
| 1998 | + | |
| 1999 | + | |
| 2000 | + | |
| 2001 | + | |
| 2002 | + | |
| 2003 | + | |
| 2004 | + | |
| 2005 | + | |
| 2006 | + | |
| 2007 | + | |
| 2008 | + | |
| 2009 | + | |
| 2010 | + | |
| 2011 | + | |
| 2012 | + | |
| 2013 | + | |
| 2014 | + | |
| 2015 | + | |
| 2016 | + | |
| 2017 | + | |
| 2018 | + | |
| 2019 | + | |
| 2020 | + | |
| 2021 | + | |
| 2022 | + | |
| 2023 | + | |
| 2024 | + | |
| 2025 | + | |
| 2026 | + | |
| 2027 | + | |
| 2028 | + | |
| 2029 | + | |
| 2030 | + | |
| 2031 | + | |
| 2032 | + | |
| 2033 | + | |
| 2034 | + | |
| 2035 | + | |
| 2036 | + | |
| 2037 | + | |
| 2038 | + | |
| 2039 | + | |
| 2040 | + | |
| 2041 | + | |
| 2042 | + | |
| 2043 | + | |
| 2044 | + | |
| 2045 | + | |
| 2046 | + | |
| 2047 | + | |
| 2048 | + | |
| 2049 | + | |
| 2050 | + | |
| 2051 | + | |
| 2052 | + | |
| 2053 | + | |
| 2054 | + | |
| 2055 | + | |
| 2056 | + | |
| 2057 | + | |
| 2058 | + | |
| 2059 | + | |
| 2060 | + | |
| 2061 | + | |
| 2062 | + | |
| 2063 | + | |
| 2064 | + | |
| 2065 | + | |
| 2066 | + | |
| 2067 | + | |
0 commit comments