File indexing completed on 2026-09-28 09:37:30
0001
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
0032 django.setup()
0033
0034 from django.conf import settings
0035 settings.ALLOWED_HOSTS = ['*']
0036
0037 from django.contrib.auth.models import User
0038 from django.test import Client
0039 from allauth.socialaccount.models import SocialAccount
0040
0041 from remote_app import authority
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
0081
0082
0083 check(f'member of {org} resolves True',
0084 authority.resolve_membership(tok, org) is True)
0085
0086
0087
0088 absent = authority.resolve_membership(tok, 'python')
0089 check('non-member org resolves False', absent is False, f'got {absent!r}')
0090
0091
0092
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
0100
0101
0102
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
0109
0110 check('record_rights() refuses an unknown level',
0111 authority.record_rights('swf-remote-sync', 'administrator') is False)
0112
0113
0114
0115
0116 try:
0117
0118
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
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
0142
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())