""" 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