Initial commit
This commit is contained in:
commit
536ff43ce4
158 files changed
+30355
No files matched your search
@@ -0,0 +1,515 @@
|
||||
"""
|
||||
2FA-hulp: back-upcodes, tijdelijke uitzonderingen en overzicht van wie 2FA nog niet heeft ingesteld.
|
||||
|
||||
Vereist (eenmalig, Google Admin-console):
|
||||
1. Beveiliging › API-beheer › Domeinbrede delegatie: aan het service-account toevoegen
|
||||
https://www.googleapis.com/auth/admin.directory.user.security (back-upcodes)
|
||||
https://www.googleapis.com/auth/admin.directory.group.member (uitzonderingsgroep)
|
||||
2. Een groep aanmaken, bv. 2fa-uitzondering@school.be, en bij
|
||||
Beveiliging › Authenticatie › Verificatie in 2 stappen voor DIE GROEP
|
||||
'Afdwingen' op UIT zetten (groepsinstellingen gaan boven de OU-instelling).
|
||||
3. Het adres van die groep invullen in PIVOT › 🔌 Koppelingen van deze school › 🛡️ 2FA-hulp › Uitzonderingsgroep
|
||||
"""
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from googleapiclient.discovery import build
|
||||
from googleapiclient.errors import HttpError
|
||||
from config import Config
|
||||
from database import execute_query
|
||||
from tenancy import google_credentials, school_singleton, alle_scholen, in_school
|
||||
|
||||
SECURITY_SCOPE = 'https://www.googleapis.com/auth/admin.directory.user.security'
|
||||
GROUP_MEMBER_SCOPE = 'https://www.googleapis.com/auth/admin.directory.group.member'
|
||||
RESET_INTERVAL = 60 # wachtende 2FA-reset elke minuut opnieuw proberen
|
||||
CHECK_INTERVAL = 5 * 60 # elke 5 minuten verlopen uitzonderingen opruimen
|
||||
LIJST_CACHE = 10 * 60
|
||||
|
||||
|
||||
|
||||
def _is_afgedwongen_fout(e):
|
||||
"""Google weigert turnOff zolang 2FA nog afgedwongen wordt (uitzonderingsgroep nog niet toegepast)."""
|
||||
msg = str(e).lower()
|
||||
return 'cannot be turned off' in msg or ('enforced' in msg and 'admin policy' in msg)
|
||||
|
||||
def _is_netwerkfout(e):
|
||||
"""Tijdelijke verbindingsfout (SSL, time-out, Google 5xx): later opnieuw proberen."""
|
||||
import socket, ssl
|
||||
if isinstance(e, (ssl.SSLError, socket.timeout, ConnectionError, TimeoutError, OSError)):
|
||||
return True
|
||||
status = getattr(getattr(e, 'resp', None), 'status', None)
|
||||
try:
|
||||
return int(status) >= 500
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
class TweestapsError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TweestapsService:
|
||||
_scheduler = None # één achtergrondtaak voor alle scholen
|
||||
|
||||
def __new__(cls):
|
||||
# één instantie per school
|
||||
def init(inst):
|
||||
inst._clients = threading.local() # Google-clients (httplib2) zijn niet thread-safe: één per thread
|
||||
inst._directory = None
|
||||
inst._lock = threading.Lock()
|
||||
inst._lijst_cache = None
|
||||
return school_singleton(cls, init)
|
||||
|
||||
# ---------------- clients ----------------
|
||||
def _build(self, scope):
|
||||
creds = google_credentials([scope], subject=Config.GOOGLE_ADMIN_EMAIL)
|
||||
return build('admin', 'directory_v1', credentials=creds, static_discovery=False, cache_discovery=False)
|
||||
|
||||
def _client(self, naam, scope):
|
||||
client = getattr(self._clients, naam, None)
|
||||
if client is None:
|
||||
client = self._build(scope)
|
||||
setattr(self._clients, naam, client)
|
||||
return client
|
||||
|
||||
def _security_api(self):
|
||||
return self._client('security', SECURITY_SCOPE)
|
||||
|
||||
def _group_api(self):
|
||||
return self._client('groups', GROUP_MEMBER_SCOPE)
|
||||
|
||||
def _directory_api(self):
|
||||
if self._directory is None:
|
||||
from services.google_service import GoogleAdminService
|
||||
self._directory = GoogleAdminService().service
|
||||
return self._directory
|
||||
|
||||
@staticmethod
|
||||
def _friendly(e, scope=None):
|
||||
msg = str(e)
|
||||
status = getattr(getattr(e, 'resp', None), 'status', None)
|
||||
if 'unauthorized_client' in msg or 'access_denied' in msg:
|
||||
return (f"Google weigert de toegang. Voeg de scope {scope} toe aan de domeinbrede delegatie "
|
||||
"van het service-account." if scope else "Google weigert de toegang (scope ontbreekt).")
|
||||
if _is_afgedwongen_fout(e):
|
||||
return ("Google dwingt 2FA nog af voor deze gebruiker. De uitzonderingsgroep is nog niet "
|
||||
"toegepast (dat kan enkele minuten duren).")
|
||||
if 'INVALID_OU_ID' in msg:
|
||||
return ("Een organisatie-eenheid bestaat niet in Google. Controleer de OU's bij 🔌 Koppelingen van "
|
||||
"deze school › 🛡️ 2FA-hulp.")
|
||||
if status == 404:
|
||||
return "Gebruiker of groep niet gevonden."
|
||||
if status == 403:
|
||||
return "Geen toestemming voor deze actie bij Google."
|
||||
return msg
|
||||
|
||||
ontbrekende_ous = []
|
||||
|
||||
def ou_melding(self):
|
||||
if not self.ontbrekende_ous:
|
||||
return None
|
||||
return (f"Deze organisatie-eenheid bestaat niet in Google: {', '.join(self.ontbrekende_ous)}. "
|
||||
"Pas ze aan bij 🔌 Koppelingen van deze school › 🛡️ 2FA-hulp › \"OU's voor 'nog geen 2FA'\" "
|
||||
"(het volledige pad zoals in admin.google.com, bv. /Personeel/Leerkrachten; meerdere met komma's).")
|
||||
|
||||
@property
|
||||
def exception_group(self):
|
||||
return (Config.TWEESTAPS_UITZONDERING_GROEP or '').strip()
|
||||
|
||||
# ---------------- gebruiker opzoeken ----------------
|
||||
def get_user(self, email):
|
||||
email = (email or '').strip().lower()
|
||||
if '@' not in email:
|
||||
raise TweestapsError("Geef een volledig e-mailadres op.")
|
||||
try:
|
||||
u = self._directory_api().users().get(userKey=email).execute()
|
||||
except HttpError as e:
|
||||
if getattr(e.resp, 'status', None) == 404:
|
||||
raise TweestapsError(f"{email} bestaat niet in jullie Google Workspace.")
|
||||
raise TweestapsError(self._friendly(e))
|
||||
return {
|
||||
'email': u['primaryEmail'].lower(),
|
||||
'naam': (u.get('name') or {}).get('fullName', ''),
|
||||
'ou': u.get('orgUnitPath', ''),
|
||||
'ingesteld': bool(u.get('isEnrolledIn2Sv')),
|
||||
'afgedwongen': bool(u.get('isEnforcedIn2Sv')),
|
||||
'geschorst': bool(u.get('suspended')),
|
||||
'laatste_login': (u.get('lastLoginTime') or '')[:16].replace('T', ' ').replace('1970-01-01 00:00', ''),
|
||||
}
|
||||
|
||||
def search_users(self, query):
|
||||
try:
|
||||
resp = self._directory_api().users().list(
|
||||
customer='my_customer', query=query.strip(), maxResults=15, orderBy='email').execute()
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"2FA zoeken: {e}\n")
|
||||
return []
|
||||
return [{'email': u['primaryEmail'], 'naam': (u.get('name') or {}).get('fullName', ''),
|
||||
'ou': u.get('orgUnitPath', '')} for u in resp.get('users', [])]
|
||||
|
||||
# ---------------- back-upcodes ----------------
|
||||
def list_codes(self, email):
|
||||
try:
|
||||
resp = self._security_api().verificationCodes().list(userKey=email).execute()
|
||||
except Exception as e:
|
||||
raise TweestapsError(self._friendly(e, SECURITY_SCOPE))
|
||||
return [i['verificationCode'] for i in resp.get('items', []) if i.get('verificationCode')]
|
||||
|
||||
def generate_codes(self, email):
|
||||
"""Maakt een nieuwe set (de oude set wordt daardoor ongeldig) en geeft de codes terug."""
|
||||
try:
|
||||
self._security_api().verificationCodes().generate(userKey=email).execute()
|
||||
except Exception as e:
|
||||
raise TweestapsError(self._friendly(e, SECURITY_SCOPE))
|
||||
return self.list_codes(email)
|
||||
|
||||
def invalidate_codes(self, email):
|
||||
try:
|
||||
self._security_api().verificationCodes().invalidate(userKey=email).execute()
|
||||
except Exception as e:
|
||||
raise TweestapsError(self._friendly(e, SECURITY_SCOPE))
|
||||
|
||||
# ---------------- uitzonderingsgroep ----------------
|
||||
def _add_to_group(self, email):
|
||||
if not self.exception_group:
|
||||
raise TweestapsError("Er is nog geen uitzonderingsgroep ingesteld "
|
||||
"(🔌 Koppelingen van deze school › 🛡️ 2FA-hulp › Uitzonderingsgroep).")
|
||||
try:
|
||||
self._group_api().members().insert(
|
||||
groupKey=self.exception_group, body={'email': email, 'role': 'MEMBER'}).execute()
|
||||
except HttpError as e:
|
||||
if getattr(e.resp, 'status', None) == 409: # zit er al in → prima
|
||||
return
|
||||
raise TweestapsError(self._friendly(e, GROUP_MEMBER_SCOPE))
|
||||
except Exception as e:
|
||||
raise TweestapsError(self._friendly(e, GROUP_MEMBER_SCOPE))
|
||||
|
||||
def _remove_from_group(self, email):
|
||||
try:
|
||||
self._group_api().members().delete(groupKey=self.exception_group, memberKey=email).execute()
|
||||
except HttpError as e:
|
||||
if getattr(e.resp, 'status', None) == 404: # zit er al niet meer in → prima
|
||||
return
|
||||
raise TweestapsError(self._friendly(e, GROUP_MEMBER_SCOPE))
|
||||
except Exception as e:
|
||||
raise TweestapsError(self._friendly(e, GROUP_MEMBER_SCOPE))
|
||||
|
||||
# ---------------- database ----------------
|
||||
@staticmethod
|
||||
def ensure_tables():
|
||||
execute_query("""
|
||||
CREATE TABLE IF NOT EXISTS tweestaps_uitzonderingen (
|
||||
ID INT AUTO_INCREMENT PRIMARY KEY,
|
||||
Email VARCHAR(255) NOT NULL,
|
||||
Naam VARCHAR(255) NULL,
|
||||
Reden VARCHAR(255) NULL,
|
||||
Van DATETIME NOT NULL,
|
||||
Tot DATETIME NOT NULL,
|
||||
Door VARCHAR(100) NULL,
|
||||
Actief TINYINT(1) NOT NULL DEFAULT 1,
|
||||
Beeindigd_op DATETIME NULL,
|
||||
Beeindigd_door VARCHAR(100) NULL,
|
||||
INDEX idx_actief (Actief, Tot),
|
||||
INDEX idx_email (Email)
|
||||
)""", commit=True)
|
||||
execute_query("""
|
||||
CREATE TABLE IF NOT EXISTS tweestaps_log (
|
||||
ID INT AUTO_INCREMENT PRIMARY KEY,
|
||||
Tijdstip DATETIME NOT NULL,
|
||||
Door VARCHAR(100) NULL,
|
||||
Actie VARCHAR(40) NOT NULL,
|
||||
Email VARCHAR(255) NOT NULL,
|
||||
Details VARCHAR(255) NULL,
|
||||
INDEX idx_tijdstip (Tijdstip)
|
||||
)""", commit=True)
|
||||
execute_query("""
|
||||
CREATE TABLE IF NOT EXISTS tweestaps_resets (
|
||||
ID INT AUTO_INCREMENT PRIMARY KEY,
|
||||
Email VARCHAR(255) NOT NULL,
|
||||
Gevraagd_op DATETIME NOT NULL,
|
||||
Door VARCHAR(100) NULL,
|
||||
Reden VARCHAR(255) NULL,
|
||||
Tot DATETIME NOT NULL,
|
||||
Status VARCHAR(10) NOT NULL DEFAULT 'bezig',
|
||||
Pogingen INT NOT NULL DEFAULT 0,
|
||||
Laatste_fout VARCHAR(255) NULL,
|
||||
Klaar_op DATETIME NULL,
|
||||
INDEX idx_status (Status),
|
||||
INDEX idx_email (Email)
|
||||
)""", commit=True)
|
||||
|
||||
@staticmethod
|
||||
def log(door, actie, email, details=None):
|
||||
try:
|
||||
execute_query("INSERT INTO tweestaps_log (Tijdstip, Door, Actie, Email, Details) VALUES (NOW(), %s, %s, %s, %s)",
|
||||
(door, actie, email, details), commit=True)
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"2FA log: {e}\n")
|
||||
|
||||
@staticmethod
|
||||
def recent_log(pagina=1, per_pagina=10):
|
||||
"""Eén pagina van het logboek (nieuwste eerst). Geeft (rijen, totaal aantal)."""
|
||||
totaal = execute_query("SELECT COUNT(*) AS n FROM tweestaps_log")[0]['n']
|
||||
pagina = max(1, int(pagina or 1))
|
||||
rijen = execute_query("SELECT Tijdstip, Door, Actie, Email, Details FROM tweestaps_log "
|
||||
"ORDER BY Tijdstip DESC, ID DESC LIMIT %s OFFSET %s",
|
||||
(int(per_pagina), (pagina - 1) * int(per_pagina)))
|
||||
return rijen, totaal
|
||||
|
||||
@staticmethod
|
||||
def active_exceptions():
|
||||
return execute_query("SELECT ID, Email, Naam, Reden, Van, Tot, Door FROM tweestaps_uitzonderingen "
|
||||
"WHERE Actief = 1 ORDER BY Tot")
|
||||
|
||||
@staticmethod
|
||||
def active_for(email):
|
||||
rows = execute_query("SELECT ID, Tot FROM tweestaps_uitzonderingen WHERE Actief = 1 AND Email = %s "
|
||||
"ORDER BY Tot DESC LIMIT 1", (email,))
|
||||
return rows[0] if rows else None
|
||||
|
||||
# ---------------- uitzonderingen ----------------
|
||||
@staticmethod
|
||||
def einde_vandaag(nu=None):
|
||||
"""Een uitzondering (of 2FA-reset) loopt altijd tot 23:59 dezelfde dag."""
|
||||
return (nu or datetime.now()).replace(hour=23, minute=59, second=0, microsecond=0)
|
||||
|
||||
def grant_exception(self, email, eindtijd, door, reden=None):
|
||||
user = self.get_user(email)
|
||||
if user['geschorst']:
|
||||
raise TweestapsError(f"{user['email']} is geschorst.")
|
||||
bestaand = self.active_for(user['email'])
|
||||
self._add_to_group(user['email'])
|
||||
if bestaand:
|
||||
execute_query("UPDATE tweestaps_uitzonderingen SET Tot = %s WHERE ID = %s",
|
||||
(eindtijd, bestaand['ID']), commit=True)
|
||||
else:
|
||||
execute_query(
|
||||
"INSERT INTO tweestaps_uitzonderingen (Email, Naam, Reden, Van, Tot, Door, Actief) "
|
||||
"VALUES (%s, %s, %s, NOW(), %s, %s, 1)",
|
||||
(user['email'], user['naam'], (reden or '')[:255], eindtijd, door), commit=True)
|
||||
self.log(door, 'uitzondering', user['email'], f"tot {eindtijd:%d-%m %H:%M}" + (f" – {reden}" if reden else ''))
|
||||
self._lijst_cache = None
|
||||
return user
|
||||
|
||||
def reset_2fa(self, email, eindtijd, door, reden=None):
|
||||
"""
|
||||
2FA resetten (nieuwe telefoon, toestel kwijt): 2FA uitzetten voor de gebruiker zodat hij het opnieuw
|
||||
kan instellen. Wordt 2FA afgedwongen, dan krijgt hij eerst een tijdelijke uitzondering; anders zou hij
|
||||
na het uitzetten niet meer kunnen inloggen om 2FA opnieuw in te stellen.
|
||||
Google: users.twoStepVerification.turnOff (scope admin.directory.user.security).
|
||||
"""
|
||||
user = self.get_user(email)
|
||||
if user['geschorst']:
|
||||
raise TweestapsError(f"{user['email']} is geschorst.")
|
||||
if not user['ingesteld']:
|
||||
raise TweestapsError(f"{user['email']} heeft nog geen 2FA ingesteld; er valt niets te resetten. "
|
||||
"Moet hij 2FA nog instellen maar kan hij niet inloggen, geef dan een tijdelijke uitzondering.")
|
||||
uitzondering = False
|
||||
if user['afgedwongen']:
|
||||
if not self.exception_group:
|
||||
raise TweestapsError("2FA wordt afgedwongen voor deze gebruiker. Stel eerst een uitzonderingsgroep in "
|
||||
"(🔌 Koppelingen van deze school › 🛡️ 2FA-hulp), anders kan hij na de reset niet "
|
||||
"meer inloggen om 2FA opnieuw in te stellen.")
|
||||
self.grant_exception(user['email'], eindtijd, door, reden or '2FA-reset')
|
||||
uitzondering = True
|
||||
try:
|
||||
self._turn_off(user['email'])
|
||||
except Exception as e:
|
||||
if uitzondering and _is_afgedwongen_fout(e):
|
||||
# Google past de uitzonderingsgroep pas na enkele minuten toe → op de achtergrond opnieuw proberen.
|
||||
self._plan_reset(user['email'], eindtijd, door, reden)
|
||||
user['uitzondering'] = True
|
||||
user['wacht'] = True
|
||||
return user
|
||||
raise TweestapsError(f"2FA uitzetten mislukt: {self._friendly(e, SECURITY_SCOPE)}"
|
||||
+ (" De tijdelijke uitzondering blijft lopen tot ze afloopt." if uitzondering else ""))
|
||||
self._lijst_cache = None
|
||||
self.log(door, '2FA gereset', user['email'], reden or None)
|
||||
user['uitzondering'] = uitzondering
|
||||
user['wacht'] = False
|
||||
return user
|
||||
|
||||
def _turn_off(self, email):
|
||||
self._security_api().twoStepVerification().turnOff(userKey=email).execute()
|
||||
|
||||
# ---------------- 2FA-reset die op Google wacht ----------------
|
||||
def _plan_reset(self, email, tot, door, reden):
|
||||
bestaand = self.reset_bezig(email)
|
||||
if bestaand:
|
||||
execute_query("UPDATE tweestaps_resets SET Tot = %s WHERE ID = %s", (tot, bestaand['ID']), commit=True)
|
||||
rid = bestaand['ID']
|
||||
else:
|
||||
execute_query(
|
||||
"INSERT INTO tweestaps_resets (Email, Gevraagd_op, Door, Reden, Tot, Status, Pogingen) "
|
||||
"VALUES (%s, NOW(), %s, %s, %s, 'bezig', 1)", (email, door, (reden or '')[:255], tot), commit=True)
|
||||
rid = execute_query("SELECT ID FROM tweestaps_resets WHERE Email = %s AND Status = 'bezig' "
|
||||
"ORDER BY ID DESC LIMIT 1", (email,))[0]['ID']
|
||||
self.log(door, '2FA reset wacht', email, 'Google past de uitzondering nog toe; PIVOT probeert opnieuw')
|
||||
self._volg_reset_op(rid)
|
||||
|
||||
@staticmethod
|
||||
def reset_bezig(email=None):
|
||||
if email:
|
||||
rows = execute_query("SELECT * FROM tweestaps_resets WHERE Status = 'bezig' AND Email = %s "
|
||||
"ORDER BY ID DESC LIMIT 1", ((email or '').strip().lower(),))
|
||||
return rows[0] if rows else None
|
||||
return execute_query("SELECT * FROM tweestaps_resets WHERE Status = 'bezig' ORDER BY Gevraagd_op")
|
||||
|
||||
def probeer_reset(self, rij):
|
||||
"""Eén poging voor een wachtende reset. Geeft 'klaar', 'mislukt' of 'bezig' terug."""
|
||||
email = rij['Email']
|
||||
try:
|
||||
self._turn_off(email)
|
||||
except Exception as e:
|
||||
nog_tijd = rij['Tot'] > datetime.now()
|
||||
tijdelijk = _is_afgedwongen_fout(e) or _is_netwerkfout(e)
|
||||
if _is_netwerkfout(e):
|
||||
self._clients.security = None # verbinding kapot → volgende poging met een nieuwe client
|
||||
if tijdelijk and nog_tijd:
|
||||
execute_query("UPDATE tweestaps_resets SET Pogingen = Pogingen + 1, Laatste_fout = %s WHERE ID = %s",
|
||||
(self._friendly(e)[:255], rij['ID']), commit=True)
|
||||
return 'bezig'
|
||||
fout = (self._friendly(e, SECURITY_SCOPE) if nog_tijd else
|
||||
"Google heeft de uitzondering niet toegepast voor ze afliep. Controleer of 'Afdwingen' op 'Uit' "
|
||||
"staat voor de uitzonderingsgroep in admin.google.com en probeer opnieuw.")
|
||||
execute_query("UPDATE tweestaps_resets SET Status = 'mislukt', Pogingen = Pogingen + 1, Laatste_fout = %s, "
|
||||
"Klaar_op = NOW() WHERE ID = %s", (fout[:255], rij['ID']), commit=True)
|
||||
self.log('automatisch', '2FA reset mislukt', email, fout[:255])
|
||||
return 'mislukt'
|
||||
execute_query("UPDATE tweestaps_resets SET Status = 'klaar', Pogingen = Pogingen + 1, Laatste_fout = NULL, "
|
||||
"Klaar_op = NOW() WHERE ID = %s", (rij['ID'],), commit=True)
|
||||
self.log(rij['Door'] or 'automatisch', '2FA gereset', email,
|
||||
(rij['Reden'] or '') + f" (na {rij['Pogingen'] + 1} pogingen)")
|
||||
self._lijst_cache = None
|
||||
return 'klaar'
|
||||
|
||||
def verwerk_resets(self):
|
||||
"""Alle wachtende resets van deze school één keer proberen (vangnet van de 5-minutenplanner)."""
|
||||
n = 0
|
||||
for rij in self.reset_bezig():
|
||||
if self.probeer_reset(rij) == 'klaar':
|
||||
n += 1
|
||||
return n
|
||||
|
||||
def _volg_reset_op(self, rid):
|
||||
from tenancy import met_school
|
||||
svc = self
|
||||
|
||||
def loop():
|
||||
while True:
|
||||
time.sleep(RESET_INTERVAL)
|
||||
try:
|
||||
rows = execute_query("SELECT * FROM tweestaps_resets WHERE ID = %s", (rid,))
|
||||
if not rows or rows[0]['Status'] != 'bezig':
|
||||
return
|
||||
if svc.probeer_reset(rows[0]) != 'bezig':
|
||||
return
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"2FA-reset {rid}: {e}\n")
|
||||
threading.Thread(target=met_school(loop), daemon=True).start()
|
||||
|
||||
def end_exception(self, uitz_id, door):
|
||||
rows = execute_query("SELECT Email FROM tweestaps_uitzonderingen WHERE ID = %s AND Actief = 1", (uitz_id,))
|
||||
if not rows:
|
||||
raise TweestapsError("Deze uitzondering is al beëindigd.")
|
||||
email = rows[0]['Email']
|
||||
self._remove_from_group(email)
|
||||
execute_query("UPDATE tweestaps_uitzonderingen SET Actief = 0, Beeindigd_op = NOW(), Beeindigd_door = %s "
|
||||
"WHERE ID = %s", (door, uitz_id), commit=True)
|
||||
self.log(door, 'uitzondering beëindigd', email)
|
||||
self._lijst_cache = None
|
||||
return email
|
||||
|
||||
def expire_due(self):
|
||||
"""Haalt iedereen met een verlopen uitzondering terug uit de groep. Geeft het aantal terug."""
|
||||
if not self._lock.acquire(blocking=False):
|
||||
return 0
|
||||
try:
|
||||
rows = execute_query("SELECT ID, Email FROM tweestaps_uitzonderingen WHERE Actief = 1 AND Tot <= NOW()")
|
||||
klaar = 0
|
||||
for r in rows:
|
||||
try:
|
||||
self._remove_from_group(r['Email'])
|
||||
execute_query("UPDATE tweestaps_uitzonderingen SET Actief = 0, Beeindigd_op = NOW(), "
|
||||
"Beeindigd_door = 'automatisch' WHERE ID = %s", (r['ID'],), commit=True)
|
||||
self.log('automatisch', 'uitzondering verlopen', r['Email'])
|
||||
klaar += 1
|
||||
except Exception as e:
|
||||
# blijft actief → volgende ronde opnieuw proberen
|
||||
sys.stderr.write(f"2FA: kon {r['Email']} niet uit de groep halen: {e}\n")
|
||||
if klaar:
|
||||
self._lijst_cache = None
|
||||
return klaar
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
@classmethod
|
||||
def start_scheduler(cls):
|
||||
"""Ruimt elke 5 minuten verlopen uitzonderingen op, voor elke school met een Google-sleutel."""
|
||||
if TweestapsService._scheduler:
|
||||
return
|
||||
|
||||
def loop():
|
||||
time.sleep(20)
|
||||
while True:
|
||||
for school in alle_scholen():
|
||||
if not school.google_sleutel:
|
||||
continue
|
||||
try:
|
||||
with in_school(school):
|
||||
svc = cls()
|
||||
svc.ensure_tables()
|
||||
svc.verwerk_resets()
|
||||
n = svc.expire_due()
|
||||
if n:
|
||||
print(f"[{school.code}] 2FA: {n} verlopen uitzondering(en) opgeruimd.", flush=True)
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"[{school.code}] 2FA scheduler: {e}\n")
|
||||
time.sleep(CHECK_INTERVAL)
|
||||
|
||||
TweestapsService._scheduler = threading.Thread(target=loop, daemon=True)
|
||||
TweestapsService._scheduler.start()
|
||||
|
||||
# ---------------- wie heeft 2FA nog niet ingesteld ----------------
|
||||
def niet_ingesteld(self, force=False):
|
||||
if not force and self._lijst_cache and time.time() - self._lijst_cache[0] < LIJST_CACHE:
|
||||
return self._lijst_cache[1]
|
||||
result = []
|
||||
ous = [o.strip() for o in Config.TWEESTAPS_OUS.split(',') if o.strip()]
|
||||
self.ontbrekende_ous = []
|
||||
for ou in ous:
|
||||
token = None
|
||||
while True:
|
||||
try:
|
||||
resp = self._directory_api().users().list(
|
||||
customer='my_customer', query=f"orgUnitPath='{ou}' isSuspended=false",
|
||||
maxResults=500, pageToken=token, projection='basic',
|
||||
fields='nextPageToken,users(primaryEmail,name/fullName,orgUnitPath,isEnrolledIn2Sv,'
|
||||
'isEnforcedIn2Sv,archived,lastLoginTime,creationTime)').execute()
|
||||
except Exception as e:
|
||||
if 'INVALID_OU_ID' in str(e):
|
||||
self.ontbrekende_ous.append(ou) # deze OU bestaat niet in Google: overslaan
|
||||
break
|
||||
raise
|
||||
for u in resp.get('users', []):
|
||||
if u.get('archived') or u.get('isEnrolledIn2Sv'):
|
||||
continue
|
||||
result.append({
|
||||
'email': u['primaryEmail'].lower(),
|
||||
'naam': (u.get('name') or {}).get('fullName', ''),
|
||||
'ou': u.get('orgUnitPath', ''),
|
||||
'afgedwongen': bool(u.get('isEnforcedIn2Sv')),
|
||||
'laatste_login': (u.get('lastLoginTime') or '')[:10].replace('1970-01-01', ''),
|
||||
'aangemaakt': (u.get('creationTime') or '')[:10],
|
||||
})
|
||||
token = resp.get('nextPageToken')
|
||||
if not token:
|
||||
break
|
||||
if ous and len(self.ontbrekende_ous) == len(ous):
|
||||
raise TweestapsError(self.ou_melding())
|
||||
uniek = {r['email']: r for r in result}
|
||||
lijst = sorted(uniek.values(), key=lambda r: (r['ou'], r['naam']))
|
||||
self._lijst_cache = (time.time(), lijst)
|
||||
return lijst
|
||||
Reference in new issue
Block a user