281 lines
13 KiB
Python
281 lines
13 KiB
Python
"""
|
||
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
|