Warning, file /swf-monitor/src/monitor_app/viewdir/user_admin.py was not indexed
or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
0001 """User admin — the page where rights are granted, and the account's own view.
0002
0003 Two surfaces over `monitor_app.authority`:
0004
0005 - ``user_admin_page``, under the System menu, open to staff and to accounts
0006 holding ``rights == 'ops'``. It lists accounts with the GitHub login their
0007 membership was observed for, that membership, their rights, and whether
0008 they may act. ``eic`` is display-only: it is observed from GitHub and a
0009 hand edit would be overwritten at the next sign-in.
0010 - ``user_rights_set``, the JSON endpoint the page writes through, and the
0011 only path by which ``rights`` is written. Keeping it apart from
0012 ``/api/user-authority/`` is what makes the split structural: the endpoint
0013 every sign-in goes through cannot touch rights at all.
0014
0015 The write is a JSON POST rather than a page-view POST because the page is
0016 served to external users through the swf-remote proxy, which carries no
0017 session or CSRF and cannot relay a redirect (docs/EXTERNAL_ACCESS.md).
0018
0019 Model: docs/AUTHORITY.md.
0020 """
0021
0022 import json
0023
0024 from django.contrib.auth import get_user_model
0025 from django.http import JsonResponse
0026 from django.shortcuts import render
0027 from django.views.decorators.http import require_http_methods
0028
0029 from monitor_app.authority import (OPS_MEANING, PAC_MEANING, RIGHTS_BASIC,
0030 RIGHTS_MEANING, RIGHTS_READ,
0031 AuthorityError, all_authority,
0032 get_authority, is_ops, may_act,
0033 may_set_priority, set_ops, set_pac,
0034 set_rights)
0035
0036
0037 ACCESS_VALUES = (RIGHTS_READ, RIGHTS_BASIC)
0038
0039
0040 def may_administer(user):
0041 """Staff on this side, or an account holding operations rights."""
0042 if not user or not user.is_authenticated:
0043 return False
0044 return bool(user.is_staff) or is_ops(user.username)
0045
0046
0047 def _origin(record):
0048 """Where the account's identity comes from, as far as the record says.
0049
0050 A GitHub identity is one the sweep has observed membership for. Rights
0051 without one is an account established inside the BNL perimeter through
0052 the account sync. Neither is an account nothing has been written for
0053 yet, and the column says nothing rather than guessing.
0054 """
0055 if record['github'] or record['eic'] is not None:
0056 return 'GitHub'
0057 if record['rights']:
0058 return 'account sync'
0059 return ''
0060
0061
0062 def _rows():
0063 """Every account known on this side, with its authority."""
0064 User = get_user_model()
0065 records = all_authority()
0066 logins = dict(User.objects.values_list('username', 'last_login'))
0067 staff = set(User.objects.filter(is_staff=True)
0068 .values_list('username', flat=True))
0069 rows = []
0070 for username in sorted(set(records) | set(logins), key=str.lower):
0071 record = records.get(username) or get_authority(username)
0072 rows.append({
0073 'username': username,
0074 'github': record['github'],
0075 'origin': _origin(record),
0076 'eic': record['eic'],
0077 'rights': (RIGHTS_BASIC if record['rights'] == 'ops'
0078 else record['rights']),
0079 'pac': record['pac'],
0080 'ops': record['ops'],
0081 'may_act': may_act(record),
0082 'may_set_priority': may_set_priority(record),
0083 'last_seen': record['eic_at'] or '',
0084 'check_failed': record['check_failed'],
0085 'check_failed_at': record['check_failed_at'],
0086 'last_login': logins.get(username),
0087 'is_staff': username in staff,
0088 })
0089 return rows
0090
0091
0092 @require_http_methods(['GET'])
0093 def user_admin_page(request):
0094 """GET users/admin/ — the account list, and where rights are set."""
0095 if not may_administer(request.user):
0096 return render(request, 'monitor_app/user_admin.html',
0097 {'forbidden': True}, status=403)
0098 rows = _rows()
0099 return render(request, 'monitor_app/user_admin.html', {
0100 'rows': rows,
0101 'rights_values': ACCESS_VALUES,
0102 'rights_levels': [{'value': v, 'meaning': RIGHTS_MEANING[v]}
0103 for v in ACCESS_VALUES],
0104 'pac_meaning': PAC_MEANING,
0105 'ops_meaning': OPS_MEANING,
0106 'acting_count': sum(1 for r in rows if r['may_act']),
0107 'pac_count': sum(1 for r in rows if r['pac']),
0108 'ops_count': sum(1 for r in rows if r['ops']),
0109 })
0110
0111
0112 @require_http_methods(['POST'])
0113 def user_rights_set(request):
0114 """POST /api/user-rights/ ``{"username": "...", "rights": "basic"}``,
0115 ``{"username": "...", "pac": true}``, ``{"username": "...", "ops": true}``,
0116 or any of them together.
0117
0118 ``rights: null`` clears the grant, returning the account to drawing
0119 authority from ``eic`` alone; ``pac: false`` and ``ops: false`` clear
0120 the roles. All are a person's fields, written through this one
0121 endpoint and never by the sign-in sweep. Returns the account's full
0122 record.
0123 """
0124 from monitor_app.authority import AUTHORITY_WRITER
0125 from monitor_app.middleware import is_tunnel_request
0126
0127 user = request.user
0128 writer = (user.is_authenticated and is_tunnel_request(request)
0129 and user.username == AUTHORITY_WRITER)
0130 if not (may_administer(user) or writer):
0131 return JsonResponse(
0132 {'error': 'not authorized to set rights'}, status=403)
0133
0134 try:
0135 body = json.loads(request.body or b'{}')
0136 except ValueError as e:
0137 return JsonResponse({'error': f'malformed JSON: {e}'}, status=400)
0138 if not isinstance(body, dict):
0139 return JsonResponse({'error': 'body must be an object'}, status=400)
0140
0141 username = (body.get('username') or '').strip()
0142 if not username:
0143 return JsonResponse({'error': 'username is required'}, status=400)
0144 if 'eic' in body:
0145 return JsonResponse(
0146 {'error': 'eic is observed at sign-in and cannot be set here'},
0147 status=400)
0148
0149 if not any(k in body for k in ('rights', 'pac', 'ops')):
0150 return JsonResponse({'error': 'rights, pac or ops is required'},
0151 status=400)
0152 try:
0153 record = None
0154 if 'rights' in body:
0155 rights = body.get('rights')
0156 if rights == '':
0157 rights = None
0158 record = set_rights(username, rights)
0159 if 'pac' in body:
0160 record = set_pac(username, bool(body.get('pac')))
0161 if 'ops' in body:
0162 record = set_ops(username, bool(body.get('ops')))
0163 except AuthorityError as e:
0164 return JsonResponse({'error': str(e)}, status=400)
0165 return JsonResponse({'username': username, 'authority': record,
0166 'may_act': may_act(record),
0167 'may_set_priority': may_set_priority(record)})