Initial commit

This commit is contained in:
Kristof committed 2026-10-14 12:29:29 +02:00
commit 536ff43ce4
158 files changed
+30355

No files matched your search

+280
View File
@@ -0,0 +1,280 @@
"""
Google › Gedeelde drives: alle gedeelde drives van het domein, wie er rechten op heeft, en leden beheren.
Google Drive API v3 namens de beheerder (domeinbrede delegatie), met useDomainAdminAccess=True zodat ook drives
zichtbaar zijn waar de beheerder zelf geen lid van is:
drives.list / drives.create
permissions.list / create / update / delete (supportsAllDrives=True)
Vereist: https://www.googleapis.com/auth/drive (en de Google Drive API aan in het Cloud-project).
"""
import threading
import time
import uuid
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
from config import Config
from tenancy import google_credentials, huidige_school, in_school
SCOPE = 'https://www.googleapis.com/auth/drive'
CACHE_SECONDEN = 10 * 60
ROLLEN = [('organizer', 'Beheerder'), ('fileOrganizer', 'Inhoudsbeheerder'), ('writer', 'Bijdrager'),
('commenter', 'Reageerder'), ('reader', 'Lezer')]
ROL_NAAM = dict(ROLLEN)
PERM_VELDEN = 'nextPageToken,permissions(id,type,role,emailAddress,domain,displayName,deleted)'
class DriveFout(Exception):
pass
def vriendelijk(e):
t = str(e)
laag = t.lower()
if 'unauthorized_client' in laag or 'access_denied' in laag:
return ("Google weigert de toegang: voeg in de Admin-console › Beveiliging › API-beheer › Domeinbrede delegatie "
f"het bereik {SCOPE} toe bij het serviceaccount van PIVOT.")
if 'has not been used' in laag or 'is disabled' in laag or 'accessnotconfigured' in laag:
return "De Google Drive API staat niet aan in het Google Cloud-project van het serviceaccount."
if 'insufficientadministratorprivileges' in laag or 'domain administrator' in laag:
return "De beheerder (Koppelingen › Algemeen) heeft geen beheerdersrechten voor Drive in het Admin-console."
if 'cannotremovelastorganizer' in laag or 'last organizer' in laag:
return "Dit is de laatste beheerder van de drive; voeg eerst een andere beheerder toe."
if 'invalidsharingrequest' in laag or 'shareoutnotpermitted' in laag or 'sharingrateLimitExceeded'.lower() in laag:
return "Google laat dit delen niet toe (bv. delen buiten het domein staat uit voor deze drive of organisatie)."
if 'notfound' in laag.replace(' ', '') or ' 404' in t:
return "Niet gevonden (bestaat de drive, de gebruiker of de groep nog?)."
return t[:300]
def _drive():
from googleapiclient.discovery import build
creds = google_credentials([SCOPE], subject=Config.GOOGLE_ADMIN_EMAIL)
try:
return build('drive', 'v3', credentials=creds, static_discovery=True, cache_discovery=False)
except Exception:
return build('drive', 'v3', credentials=creds, static_discovery=False, cache_discovery=False)
def _eigen_domeinen():
s = huidige_school()
d = {x.lower() for x in (getattr(s, 'domeinen', None) or [s.domein]) if x}
if Config.GOOGLE_ADMIN_EMAIL:
d.add(Config.GOOGLE_ADMIN_EMAIL.lower().partition('@')[2])
return d
def _is_extern(p, domeinen):
if p.get('type') == 'anyone':
return True
if p.get('type') == 'domain':
return (p.get('domain') or '').lower() not in domeinen
mail = (p.get('emailAddress') or '').lower()
return bool(mail) and mail.partition('@')[2] not in domeinen
def _leden(svc, drive_id):
uit, token = [], None
while True:
resp = svc.permissions().list(fileId=drive_id, supportsAllDrives=True, useDomainAdminAccess=True,
pageSize=100, pageToken=token, fields=PERM_VELDEN).execute()
uit += resp.get('permissions', [])
token = resp.get('nextPageToken')
if not token:
return uit
def _samenvatting(d, leden, domeinen):
return {'id': d['id'], 'naam': d.get('name', ''), 'aangemaakt': (d.get('createdTime') or '')[:10],
'verborgen': bool(d.get('hidden')), 'leden': len(leden),
'beheerders': sum(1 for p in leden if p.get('role') == 'organizer'),
'extern': sum(1 for p in leden if _is_extern(p, domeinen)),
'groepen': sum(1 for p in leden if p.get('type') == 'group'),
'fout': None}
def overzicht(vernieuw=False):
"""[{id, naam, aangemaakt, leden, beheerders, extern, groepen, verborgen, fout}] gesorteerd op naam."""
school = huidige_school()
bewaard = school.cache.get('gedeelde_drives')
if bewaard and not vernieuw and time.time() - bewaard[0] < CACHE_SECONDEN:
return bewaard[1], bewaard[0]
try:
svc = _drive()
drives, token = [], None
while True:
resp = svc.drives().list(useDomainAdminAccess=True, pageSize=100, pageToken=token,
fields='nextPageToken,drives(id,name,createdTime,hidden)').execute()
drives += resp.get('drives', [])
token = resp.get('nextPageToken')
if not token:
break
except Exception as e:
raise DriveFout(vriendelijk(e))
domeinen = _eigen_domeinen()
lokaal = threading.local()
alle_leden = {}
def info(d):
with in_school(school):
if not hasattr(lokaal, 'svc'):
lokaal.svc = _drive()
try:
leden = _leden(lokaal.svc, d['id'])
alle_leden[d['id']] = leden
return _samenvatting(d, leden, domeinen)
except Exception as e:
r = _samenvatting(d, [], domeinen)
r['fout'] = vriendelijk(e)
return r
with ThreadPoolExecutor(max_workers=6) as pool:
uit = list(pool.map(info, drives))
uit.sort(key=lambda d: d['naam'].lower())
nu = time.time()
school.cache['gedeelde_drives'] = (nu, uit)
school.cache['gedeelde_drives_leden'] = alle_leden
return uit, nu
def _vergeet():
huidige_school().cache.pop('gedeelde_drives', None)
def drive(drive_id):
"""(drive-info, leden) van één drive. Leden gesorteerd: beheerders eerst, dan op naam."""
svc = _drive()
try:
d = svc.drives().get(driveId=drive_id, useDomainAdminAccess=True, fields='id,name,createdTime,hidden').execute()
leden = _leden(svc, drive_id)
except Exception as e:
raise DriveFout(vriendelijk(e))
domeinen = _eigen_domeinen()
volgorde = {r: i for i, (r, _) in enumerate(ROLLEN)}
rijen = [{'id': p['id'], 'type': p.get('type'), 'rol': p.get('role'), 'rol_naam': ROL_NAAM.get(p.get('role'), p.get('role')),
'email': p.get('emailAddress') or (p.get('domain') if p.get('type') == 'domain' else ''),
'naam': p.get('displayName') or '', 'extern': _is_extern(p, domeinen), 'verwijderd': bool(p.get('deleted'))}
for p in leden]
rijen.sort(key=lambda r: (volgorde.get(r['rol'], 9), (r['naam'] or r['email']).lower()))
return _samenvatting(d, leden, domeinen), rijen
def _soort(email):
"""'group' als het adres een Google-groep is, anders 'user'."""
try:
from googleapiclient.discovery import build
creds = google_credentials(Config.GOOGLE_API_SCOPES, subject=Config.GOOGLE_ADMIN_EMAIL)
build('admin', 'directory_v1', credentials=creds, static_discovery=False, cache_discovery=False) \
.groups().get(groupKey=email, fields='id').execute()
return 'group'
except Exception:
return 'user'
def lid_toevoegen(drive_id, email, rol, melding=False):
email = (email or '').strip().lower()
if '@' not in email:
raise DriveFout("Geef een geldig e-mailadres (persoon of groep).")
if rol not in ROL_NAAM:
raise DriveFout("Onbekende rol.")
try:
_drive().permissions().create(fileId=drive_id, supportsAllDrives=True, useDomainAdminAccess=True,
sendNotificationEmail=bool(melding), fields='id',
body={'type': _soort(email), 'role': rol, 'emailAddress': email}).execute()
except Exception as e:
raise DriveFout(vriendelijk(e))
_vergeet()
def lid_wijzigen(drive_id, perm_id, rol):
if rol not in ROL_NAAM:
raise DriveFout("Onbekende rol.")
try:
_drive().permissions().update(fileId=drive_id, permissionId=perm_id, supportsAllDrives=True,
useDomainAdminAccess=True, body={'role': rol}, fields='id').execute()
except Exception as e:
raise DriveFout(vriendelijk(e))
_vergeet()
def lid_verwijderen(drive_id, perm_id):
try:
_drive().permissions().delete(fileId=drive_id, permissionId=perm_id, supportsAllDrives=True,
useDomainAdminAccess=True).execute()
except Exception as e:
raise DriveFout(vriendelijk(e))
_vergeet()
def drive_maken(naam, beheerder=None):
"""Nieuwe gedeelde drive (de PIVOT-beheerder wordt eerst beheerder); optioneel meteen een andere beheerder."""
naam = (naam or '').strip()
if not naam:
raise DriveFout("Geef de drive een naam.")
try:
d = _drive().drives().create(requestId=str(uuid.uuid4()), body={'name': naam[:100]}, fields='id,name').execute()
except Exception as e:
raise DriveFout(vriendelijk(e))
_vergeet()
if beheerder:
lid_toevoegen(d['id'], beheerder, 'organizer')
return d['id']
# ---------------------------------------------------------------------
# Een gebruiker op alle gedeelde drives vervangen door een andere
# ---------------------------------------------------------------------
def drives_van(email, vernieuw=True):
"""Alle gedeelde drives waar `email` lid van is: [{drive, naam, perm, rol, rol_naam}] (vers opgehaald)."""
email = (email or '').strip().lower()
drives, _ = overzicht(vernieuw=vernieuw)
leden = huidige_school().cache.get('gedeelde_drives_leden') or {}
uit = []
for d in drives:
for p in leden.get(d['id'], []):
adres = (p.get('emailAddress') or '').lower()
# Met een @: exact dat adres; zonder @: een deel van het adres of de naam (bv. 'peeters')
if adres == email or ('@' not in email and email and
(email in adres or email in (p.get('displayName') or '').lower())):
uit.append({'drive': d['id'], 'naam': d['naam'], 'perm': p['id'], 'rol': p.get('role'),
'rol_naam': ROL_NAAM.get(p.get('role'), p.get('role')), 'email': adres,
'wie': p.get('displayName') or adres})
return uit
def vervang(oud, nieuw, drive_ids, oud_verwijderen=True, melding=False):
"""Op elke gekozen drive: `nieuw` krijgt exact de rol van `oud` (ook als hij al lid was), daarna wordt `oud`
verwijderd (optioneel). Geeft [{naam, ok, melding}]."""
oud, nieuw = (oud or '').strip().lower(), (nieuw or '').strip().lower()
if '@' not in nieuw:
raise DriveFout("Geef een geldig e-mailadres voor de nieuwe gebruiker.")
if oud == nieuw:
raise DriveFout("De oude en de nieuwe gebruiker zijn dezelfde.")
svc, soort, uit = _drive(), _soort(nieuw), []
kies = set(drive_ids)
for t in drives_van(oud, vernieuw=False):
if t['drive'] not in kies:
continue
try:
leden = _leden(svc, t['drive'])
bestaand = next((p for p in leden if (p.get('emailAddress') or '').lower() == nieuw), None)
if bestaand is None:
svc.permissions().create(fileId=t['drive'], supportsAllDrives=True, useDomainAdminAccess=True,
sendNotificationEmail=bool(melding), fields='id',
body={'type': soort, 'role': t['rol'], 'emailAddress': nieuw}).execute()
tekst = f"{nieuw} toegevoegd als {t['rol_naam']}"
elif bestaand.get('role') != t['rol']: # altijd exact dezelfde rechten als de oude gebruiker
svc.permissions().update(fileId=t['drive'], permissionId=bestaand['id'], supportsAllDrives=True,
useDomainAdminAccess=True, body={'role': t['rol']}, fields='id').execute()
tekst = f"{nieuw} was al lid, nu {t['rol_naam']}"
else:
tekst = f"{nieuw} was al lid ({ROL_NAAM.get(bestaand.get('role'))})"
if oud_verwijderen:
svc.permissions().delete(fileId=t['drive'], permissionId=t['perm'], supportsAllDrives=True,
useDomainAdminAccess=True).execute()
tekst += f", {oud} verwijderd"
uit.append({'naam': t['naam'], 'ok': True, 'melding': tekst})
except Exception as e:
uit.append({'naam': t['naam'], 'ok': False, 'melding': vriendelijk(e)})
_vergeet()
return uit