Files
Pivot/services/gedeelde_drives_service.py
T
2026-10-14 13:34:37 +02:00

281 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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