Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-09-28 09:37:30

0001 #!/usr/bin/env python3
0002 """Populate the account authority attributes for accounts that predate them.
0003 
0004 An account may act when `rights` is not `read` and either `eic` is true or
0005 `rights` grants it. Accounts created before either field existed carry
0006 neither, so they can read but not act. This writes what each account is
0007 entitled to, once.
0008 
0009 What gets written, and why each is the honest value:
0010 
0011   GitHub-linked accounts get `eic` from the organization's member listing,
0012   together with the GitHub login the check was made against. Membership is
0013   read through the `gh` CLI rather than from each account's own OAuth token:
0014   every existing token was issued before the read:org scope was added and
0015   cannot answer the question. The listing is authoritative for private
0016   membership provided the token's owner is an organization member.
0017 
0018   Accounts with no GitHub identity get `rights: basic` and no `eic` at all.
0019   Their access was established inside the BNL authentication perimeter,
0020   through the swf-monitor account sync from pandaserver02. Writing `eic:
0021   false` for them would be recording a GitHub fact about an account that has
0022   no GitHub identity, and writing `eic: true` would be a fabrication.
0023 
0024   GitHub accounts that are not organization members get `eic: false` — the
0025   observation — and also `rights: basic`, grandfathering the people already
0026   working on the system while they go through the joining procedure. That
0027   grant is a person's decision, not an observation, which is why it lands in
0028   `rights`; a later sign-in re-observes `eic` and leaves `rights` untouched.
0029 
0030 Dry run by default. Pass --apply to write.
0031 
0032     scripts/backfill_authority.py
0033     scripts/backfill_authority.py --apply
0034 
0035 --eic-only re-observes membership for every GitHub-linked account and writes
0036 `eic` alone, as a sign-in would; it never writes `rights`, so no grant or veto
0037 a person has made since the first backfill is touched.
0038 
0039     scripts/backfill_authority.py --eic-only --apply
0040 """
0041 from __future__ import annotations
0042 
0043 import argparse
0044 import os
0045 import subprocess
0046 import sys
0047 from pathlib import Path
0048 
0049 SRC = Path(__file__).resolve().parent.parent / 'src'
0050 sys.path.insert(0, str(SRC))
0051 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'swf_remote_project.settings')
0052 
0053 import django  # noqa: E402
0054 django.setup()
0055 
0056 from django.conf import settings  # noqa: E402
0057 from django.contrib.auth.models import User  # noqa: E402
0058 from allauth.socialaccount.models import SocialAccount  # noqa: E402
0059 
0060 from remote_app import authority  # noqa: E402
0061 
0062 
0063 def org_members(org: str) -> set[str]:
0064     """Lowercased logins of every visible member of the organization."""
0065     out = subprocess.run(
0066         ['gh', 'api', '--paginate', f'/orgs/{org}/members?per_page=100',
0067          '--jq', '.[].login'],
0068         capture_output=True, text=True, check=True,
0069     )
0070     return {line.strip().lower() for line in out.stdout.splitlines() if line.strip()}
0071 
0072 
0073 def main() -> int:
0074     ap = argparse.ArgumentParser(description=__doc__)
0075     ap.add_argument('--apply', action='store_true',
0076                     help='write the attributes; otherwise report only')
0077     ap.add_argument('--eic-only', action='store_true',
0078                     help='re-observe membership of GitHub accounts; never write rights')
0079     args = ap.parse_args()
0080 
0081     org = settings.EIC_ORG
0082     members = org_members(org)
0083     print(f'{len(members)} members visible in the {org} organization\n')
0084 
0085     links = {
0086         s.user_id: (s.extra_data or {}).get('login')
0087         for s in SocialAccount.objects.filter(provider='github')
0088     }
0089 
0090     # (username, eic, github, rights, basis). eic and rights go to separate
0091     # endpoints upstream, so an account needing both takes two calls.
0092     plan: list[tuple[str, bool | None, str, str | None, str]] = []
0093     for user in User.objects.order_by('username'):
0094         login = links.get(user.id)
0095         if login:
0096             member = login.lower() in members
0097             if member:
0098                 plan.append((user.username, True, login, None,
0099                              f'{login} is a member; access follows eic'))
0100             else:
0101                 plan.append((user.username, False, login, 'basic',
0102                              f'{login} is not a member; granted basic'))
0103         else:
0104             plan.append((user.username, None, '', 'basic',
0105                          'no GitHub identity; established through the BNL account sync'))
0106 
0107     if args.eic_only:
0108         plan = [(name, eic, github, None, basis.split(';')[0])
0109                 for name, eic, github, _, basis in plan if eic is not None]
0110 
0111     width = max(len(name) for name, _, _, _, _ in plan)
0112     for name, eic, _, rights, basis in plan:
0113         shown = ', '.join(
0114             p for p in (f'eic={eic}' if eic is not None else '',
0115                         f'rights={rights}' if rights else '') if p)
0116         print(f'{name:{width}s}  {shown:24s}  {basis}')
0117 
0118     members_n = sum(1 for _, e, _, _, _ in plan if e is True)
0119     granted = sum(1 for _, _, _, r, _ in plan if r == 'basic')
0120     print(f'\n{members_n} recorded as members, {granted} granted basic, '
0121           f'{len(plan)} accounts')
0122 
0123     if not args.apply:
0124         print('\nDry run. Pass --apply to write.')
0125         return 0
0126 
0127     failures = 0
0128     for name, eic, github, rights, _ in plan:
0129         if eic is not None and not authority.record_membership(name, eic, github):
0130             failures += 1
0131             print(f'FAILED (membership): {name}')
0132         if rights and not authority.record_rights(name, rights):
0133             failures += 1
0134             print(f'FAILED (rights): {name}')
0135     print(f'\nfailures: {failures}')
0136     return 1 if failures else 0
0137 
0138 
0139 if __name__ == '__main__':
0140     sys.exit(main())