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 """Exercise the authority paths a user actually hits.
0003 
0004 Run before deploying a change to sign-in membership resolution, after
0005 swf-monitor's authority endpoint changes, and once more against the deployed
0006 site. Failures here surface to a person mid-action, so the paths that must not
0007 break are checked against live GitHub rather than mocked.
0008 
0009 Scope is what swf-remote owns: observing membership and recording it. The
0010 rule that decides whether an account may act is enforced in swf-monitor and is
0011 checked there — reimplementing it here would be a second copy to drift.
0012 
0013 Needs a GitHub token with the read:org scope in GH_TOKEN, or the `gh` CLI
0014 logged in. The token stands in for a signed-in user's own token, which is what
0015 the sign-in hook uses.
0016 
0017     scripts/check_authority.py
0018 """
0019 from __future__ import annotations
0020 
0021 import os
0022 import subprocess
0023 from contextlib import contextmanager
0024 import sys
0025 from pathlib import Path
0026 
0027 SRC = Path(__file__).resolve().parent.parent / 'src'
0028 sys.path.insert(0, str(SRC))
0029 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'swf_remote_project.settings')
0030 
0031 import django  # noqa: E402
0032 django.setup()
0033 
0034 from django.conf import settings  # noqa: E402
0035 settings.ALLOWED_HOSTS = ['*']  # in-process only, for the test client
0036 
0037 from django.contrib.auth.models import User  # noqa: E402
0038 from django.test import Client  # noqa: E402
0039 from allauth.socialaccount.models import SocialAccount  # noqa: E402
0040 
0041 from remote_app import authority  # noqa: E402
0042 
0043 @contextmanager
0044 def preserved_last_login(user):
0045     """Sign a real account in without leaving a sign-in that never happened.
0046 
0047     The checks run against the live database, and Client.force_login fires the
0048     login signal, which stamps last_login. That column is shown on the User
0049     admin page and read when deciding a grant, so a test must not write it.
0050     Restored with queryset.update, which fires no signals.
0051     """
0052     from django.contrib.auth.models import User as U
0053     before = U.objects.filter(pk=user.pk).values_list('last_login', flat=True).first()
0054     try:
0055         yield
0056     finally:
0057         U.objects.filter(pk=user.pk).update(last_login=before)
0058 
0059 
0060 results: list[tuple[bool, str, str]] = []
0061 
0062 
0063 def check(name: str, condition: bool, detail: str = '') -> None:
0064     results.append((condition, name, detail))
0065     print(f'{"PASS" if condition else "FAIL"}  {name}' + (f'  — {detail}' if detail else ''))
0066 
0067 
0068 def token() -> str:
0069     tok = os.environ.get('GH_TOKEN')
0070     if tok:
0071         return tok
0072     return subprocess.run(['gh', 'auth', 'token'], capture_output=True,
0073                           text=True, check=True).stdout.strip()
0074 
0075 
0076 def main() -> int:
0077     org = settings.EIC_ORG
0078     tok = token()
0079 
0080     # ── Membership observation against live GitHub ─────────────────────────
0081     # A member must read as a member. A false negative here does not lock a
0082     # collaborator out by itself, but it writes eic=false over a true value.
0083     check(f'member of {org} resolves True',
0084           authority.resolve_membership(tok, org) is True)
0085 
0086     # A non-member must read as a non-member, not as indeterminate: an
0087     # indeterminate result writes nothing and leaves a stale value standing.
0088     absent = authority.resolve_membership(tok, 'python')
0089     check('non-member org resolves False', absent is False, f'got {absent!r}')
0090 
0091     # A bad credential must be indeterminate, never False. Writing False here
0092     # would revoke every account that signed in during a GitHub outage.
0093     bad = authority.resolve_membership('ghp_' + 'x' * 36, org)
0094     check('invalid token resolves None, not False', bad is None, f'got {bad!r}')
0095 
0096     check('unknown org resolves False',
0097           authority.resolve_membership(tok, 'org-that-does-not-exist-' + 'z' * 12) is False)
0098 
0099     # ── The sweep writes eic, never rights ─────────────────────────────────
0100     # This is the whole basis of the model: a person's grant must survive
0101     # every subsequent sign-in, which holds only if the sweep cannot write
0102     # the field that carries it.
0103     import inspect
0104     source = inspect.getsource(authority.refresh_for)
0105     check('sign-in path never calls the rights endpoint',
0106           'record_rights' not in source and 'RIGHTS_PATH' not in source)
0107 
0108     # An unknown rights value is refused rather than stored, so a typo cannot
0109     # create a level that enforcement does not recognise.
0110     check('record_rights() refuses an unknown level',
0111           authority.record_rights('swf-remote-sync', 'administrator') is False)
0112 
0113     # ── Degradation while the endpoint is absent ───────────────────────────
0114     # Until swf-monitor ships the endpoint this returns False and logs. It
0115     # must not raise, or every sign-in raises with it.
0116     try:
0117         # Clears rather than asserts: a service account has no GitHub
0118         # membership, so this exercises the call without writing a claim.
0119         wrote = authority.record_membership('swf-remote-sync', None)
0120         check('record_membership() returns a bool and never raises', isinstance(wrote, bool),
0121               f'returned {wrote!r}')
0122     except Exception as e:
0123         check('record() returns a bool and never raises', False, repr(e))
0124 
0125     # ── Sign-in must survive a failing membership check ────────────────────
0126     linked = SocialAccount.objects.filter(provider='github').first()
0127     if linked:
0128         client = Client()
0129         try:
0130             with preserved_last_login(linked.user):
0131                 client.force_login(linked.user)
0132                 landed = client.get('/prod/').status_code
0133             check('login with a GitHub-linked account still completes',
0134                   landed in (200, 302), f'/prod/ returned {landed}')
0135         except Exception as e:
0136             check('login with a GitHub-linked account still completes', False, repr(e))
0137     else:
0138         check('login with a GitHub-linked account still completes', False,
0139               'no GitHub-linked account to test with')
0140 
0141     # A local account has no GitHub token: the sweep must decline quietly and
0142     # leave the rights the BNL sync established.
0143     local = (User.objects
0144              .exclude(id__in=SocialAccount.objects.values('user_id'))
0145              .first())
0146     if local:
0147         check('local account: sweep writes nothing',
0148               authority.refresh_for(local) is None, local.username)
0149 
0150     failed = [n for ok, n, _ in results if not ok]
0151     print(f'\n{len(results) - len(failed)}/{len(results)} passed')
0152     if failed:
0153         print('failed: ' + ', '.join(failed))
0154     return 1 if failed else 0
0155 
0156 
0157 if __name__ == '__main__':
0158     sys.exit(main())