Zum Inhalt

Granulares Permissions-System

Übersicht

Das System verwendet ein zweistufiges Berechtigungssystem:

  1. Rollen (UserRoleEnum) - Grundlegende Zugriffsebene
  2. Granulare Permissions - Individuelle Berechtigungen pro User

Rollen (UserRoleEnum)

1️⃣ User (0) - Vertriebsmitarbeiter

Basis-Zugriff: - ✅ Alle Daten lesen (Kunden, Artikel, Bestellungen, Statistiken) - ❌ Standardmäßig KEINE Schreibrechte

Individuelle Berechtigungen: - Schreibrechte werden über permissions-Objekt pro User konfiguriert - Kann in der Benutzerverwaltung individuell eingestellt werden

2️⃣ Admin (1) - Geschäftsführer/Manager

Vollzugriff auf: - ✅ Alle Kunden (erstellen, bearbeiten, löschen) - ✅ Alle Artikel (erstellen, bearbeiten, löschen) - ✅ Alle Bestellungen (erstellen, bearbeiten, löschen) - ✅ Kategorien (Artikel, Kunden, Package Sizes) - ✅ Statistiken aller Mitarbeiter - ✅ Push-Benachrichtigungen versenden - ✅ System-Einstellungen ändern

KANN NICHT: - ❌ Benutzer-Rollen ändern - ❌ User-Permissions ändern - ❌ Benutzer erstellen/löschen

3️⃣ SuperAdmin (2) - IT/Owner

Vollzugriff auf ALLES: - ✅ Alle Admin-Rechte - ✅ Benutzer erstellen, bearbeiten, löschen - ✅ Benutzer-Rollen vergeben - ✅ User-Permissions konfigurieren - ✅ Connector-Credentials verwalten


Granulare Permissions (UserPermissions)

Jeder User hat ein permissions-Objekt mit folgenden Flags:

Kunden-Berechtigungen

bool canCreateCustomers  // Kunden anlegen
bool canEditCustomers    // Kunden bearbeiten
bool canDeleteCustomers  // Kunden löschen

Artikel-Berechtigungen

bool canCreateArticles   // Artikel anlegen
bool canEditArticles     // Artikel bearbeiten
bool canDeleteArticles   // Artikel löschen

Bestellungen-Berechtigungen

bool canCreateOrders     // Bestellungen anlegen
bool canEditOrders       // Bestellungen bearbeiten
bool canDeleteOrders     // Bestellungen löschen

Standard-Permissions nach Rolle

User (Standard: Nur Lesen)

UserPermissions.readOnly()
// Alle Flags = false

Admin (Standard: Alles)

UserPermissions.admin()
canCreateCustomers: true
canEditCustomers: true
canDeleteCustomers: true
canCreateArticles: true
canEditArticles: true
canDeleteArticles: true
canCreateOrders: true
canEditOrders: true
canDeleteOrders: true

SuperAdmin (Standard: Alles)

UserPermissions.superAdmin()
// Gleich wie Admin, aber zusätzlich User-Verwaltung

Firestore Security Rules

System Settings

// Nur Admin und SuperAdmin können ändern
allow read: if isAuthenticated();
allow write: if isAdmin(); // userRole >= 1

Users Collection

// Lesen: Alle
allow read: if isAuthenticated();

// User kann eigene Daten ändern (NICHT userRole/permissions)
allow update: if isAuthenticated() && request.auth.uid == userId 
              && !('userRole' in request.resource.data.diff(resource.data))
              && !('permissions' in request.resource.data.diff(resource.data));

// Nur SuperAdmin kann User verwalten
allow create, delete: if isSuperAdmin();
allow update: if isSuperAdmin(); // Überschreibt obige Regel für SuperAdmin

Customers, Articles, Orders

// Lesen: Alle
allow read: if isAuthenticated();

// Schreiben: Admin ODER User mit entsprechender Permission
allow create: if isAdmin() || hasPermission('canCreateCustomers');
allow update: if isAdmin() || hasPermission('canEditCustomers');
allow delete: if isAdmin() || hasPermission('canDeleteCustomers');

Verwendung im Code

Permissions prüfen

// Im User-Objekt
if (user.permissions.canCreateCustomers) {
  // Zeige "Kunde anlegen" Button
}

if (user.permissions.hasAnyArticlePermission) {
  // User hat mindestens eine Artikel-Berechtigung
}

// Rolle prüfen
if (user.userRole == UserRoleEnum.admin || user.userRole == UserRoleEnum.superadmin) {
  // Zeige Admin-Bereich
}

User erstellen mit Custom Permissions

final newUser = User(
  firstName: 'Max',
  lastName: 'Mustermann',
  email: 'max@example.com',
  userRole: UserRoleEnum.user,
  language: LanguageEnum.de,
  permissions: UserPermissions(
    canCreateOrders: true,  // Darf Bestellungen erstellen
    canEditOrders: true,    // Darf Bestellungen bearbeiten
    canEditCustomers: true, // Darf Kunden bearbeiten
    // Rest bleibt false
  ),
);

Permissions aktualisieren

final updatedUser = currentUser.copyWith(
  permissions: currentUser.permissions.copyWith(
    canDeleteArticles: true, // Artikel-Löschrecht hinzufügen
  ),
);

Migration bestehender User

Bestehende User ohne permissions-Feld erhalten automatisch Standard-Permissions basierend auf ihrer Rolle:

// Im User.fromMap Constructor
permissions: json['permissions'] != null
    ? UserPermissions.fromMap(json['permissions'])
    : null, // null wird zu Standard-Permissions der Rolle

Benutzerverwaltung UI

In der Benutzerverwaltung sollte es pro User folgende Einstellungen geben:

Rolle auswählen: - [ ] User (Standard: Nur Lesen) - [ ] Admin (Vollzugriff außer User-Verwaltung) - [ ] SuperAdmin (Vollzugriff auf alles)

Für User-Rolle - Individuelle Berechtigungen:

Kunden: - [ ] Anlegen - [ ] Bearbeiten - [ ] Löschen

Artikel: - [ ] Anlegen - [ ] Bearbeiten - [ ] Löschen

Bestellungen: - [ ] Anlegen - [ ] Bearbeiten - [ ] Löschen

⚠️ Hinweis: Permissions werden bei Admin/SuperAdmin ignoriert, da diese Rollen bereits Vollzugriff haben.


Sicherheitshinweise

  1. Permissions nur durch SuperAdmin änderbar
  2. Firestore Rules blockieren Permission-Änderungen durch normale User

  3. Admin vs SuperAdmin Unterschied

  4. Admins können Geschäftsdaten UND System-Settings verwalten
  5. Nur SuperAdmins können User-Berechtigungen verwalten

  6. Cloud Functions umgehen Rules

  7. Backend-Funktionen haben Admin-Zugriff unabhängig von Rules
  8. Zusätzliche Prüfungen in Functions einbauen bei kritischen Operationen

  9. Default Permissions

  10. Neue User starten immer mit readOnly() Permissions
  11. Muss explizit durch SuperAdmin erweitert werden

  12. Source-System Integration

  13. Wenn ein Quellsystem (Customer/Article/Order) aktiviert wird, werden automatisch alle entsprechenden Permissions von ALLEN Usern entfernt
  14. Dies verhindert, dass User Daten ändern die aus einem externen System synchronisiert werden
  15. Im User-Editor werden die Checkboxen disabled und eine orange Warnung angezeigt

Source-System Automatisierung

Beispiel-Szenarien

Szenario 1: Vertriebsmitarbeiter (nur Bestellungen)

User(
  userRole: UserRoleEnum.user,
  permissions: UserPermissions(
    canCreateOrders: true,
    canEditOrders: true,
    // Kann keine Kunden/Artikel anlegen
  ),
)

Szenario 2: Junior Produktmanager

User(
  userRole: UserRoleEnum.user,
  permissions: UserPermissions(
    canCreateArticles: true,
    canEditArticles: true,
    // Kann Artikel nicht löschen
  ),
)

Szenario 3: Customer Service

User(
  userRole: UserRoleEnum.user,
  permissions: UserPermissions(
    canCreateCustomers: true,
    canEditCustomers: true,
    canEditOrders: true,
    // Kann nichts löschen
  ),
)

Szenario 4: Geschäftsführer

User(
  userRole: UserRoleEnum.admin,
  // Permissions werden ignoriert - hat alle Rechte
  // Kann System-Settings ändern
  // Kann KEINE User-Rollen vergeben
)

Szenario 5: IT-Administrator

User(
  userRole: UserRoleEnum.superadmin,
  // Permissions werden ignoriert - hat alle Rechte
  // Kann System-Settings ändern
  // Kann User-Rollen vergeben
  // Kann User-Permissions konfigurieren
)

Firestore Security Rules - Rollen-basierter Zugriff

User Rollen

Basierend auf UserRoleEnum in /lib/models/all_enums/user_role_enum.dart:

enum UserRoleEnum { 
  user,        // index 0
  admin,       // index 1
  superadmin   // index 2
}

Rollen in Firestore Rules

Die Rolle wird im users Document als Index gespeichert:

// Firestore Document: /users/{userId}
{
  "firstName": "Max",
  "lastName": "Mustermann",
  "email": "max@example.com",
  "userRole": 2,  // ← 0=user, 1=admin, 2=superadmin
  ...
}

Helper Functions

// Prüft ob User SuperAdmin ist (userRole == 2)
function isSuperAdmin() {
  return isAuthenticated() && getUserData().userRole == 2;
}

// Prüft ob User mindestens Admin ist (userRole >= 1)
function isAdmin() {
  return isAuthenticated() && getUserData().userRole >= 1;
}

// Holt die User-Daten aus der users Collection
function getUserData() {
  return get(/databases/$(database)/documents/users/$(request.auth.uid)).data;
}

Berechtigungen nach Rolle

🔵 User (index 0)

  • ✅ Lesen: Articles, Customers, Orders, eigene User-Daten
  • ✅ Schreiben: Articles, Customers, Orders erstellen/bearbeiten
  • ✅ Lesen: System Settings
  • ❌ Schreiben: System Settings
  • ❌ Schreiben: Andere User

🟡 Admin (index 1)

  • ✅ Alle Berechtigungen von User
  • ✅ Zusätzlich: (kann erweitert werden)

🔴 SuperAdmin (index 2)

  • ✅ Alle Berechtigungen von Admin
  • System Settings schreiben (einzige Rolle mit Zugriff!)
  • ✅ Alle User bearbeiten
  • ✅ Connector Credentials verwalten

Geschützte Collections

System Settings - NUR SuperAdmin

match /system_settings/{settingId} {
  allow read: if isAuthenticated();        // Alle können lesen
  allow write: if isSuperAdmin();          // Nur SuperAdmin schreiben

  match /push_notification_settings/{notificationId} {
    allow read: if isAuthenticated();
    allow write: if isSuperAdmin();
  }
}

Users Collection

match /users/{userId} {
  allow read: if isAuthenticated();        // Alle können alle User lesen
  allow write: if isOwner(userId) ||       // Eigene Daten
                  isSuperAdmin();          // Oder SuperAdmin
}

Warum können alle User lesen? - Die isSuperAdmin() Funktion muss die Rolle aus /users/{uid} lesen - Wenn User ihre eigene Rolle nicht lesen können, funktioniert die Funktion nicht - Nur userRole, email, name sind sichtbar (keine sensitiven Daten)

Testing

Test 1: Normal User versucht System Settings zu ändern

// User Document: userRole = 0
await db.collection('system_settings').doc('test').update({value: 'test'});
// ❌ FEHLER: Missing or insufficient permissions

Test 2: SuperAdmin ändert System Settings

// User Document: userRole = 2
await db.collection('system_settings').doc('test').update({value: 'test'});
// ✅ ERFOLG

Test 3: User ändert eigene Daten

// Eigene UID = 'abc123'
await db.collection('users').doc('abc123').update({firstName: 'Max'});
// ✅ ERFOLG

Test 4: User ändert fremde User-Daten

// Fremde UID = 'xyz789'
await db.collection('users').doc('xyz789').update({firstName: 'Max'});
// ❌ FEHLER: Missing or insufficient permissions

Ersten SuperAdmin erstellen

Option 1: Firebase Console (manuell)

  1. Gehe zu Firebase Console → Firestore
  2. Navigiere zu users Collection
  3. Finde deinen User (nach UID oder Email)
  4. Bearbeite das Feld userRole → setze auf 2

Option 2: Cloud Function (einmalig)

// functions/setup_first_admin.js
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();

exports.makeFirstSuperAdmin = functions.https.onRequest(async (req, res) => {
  const email = 'admin@example.com';

  // Finde User by Email
  const userSnapshot = await db.collection('users')
    .where('email', '==', email)
    .limit(1)
    .get();

  if (userSnapshot.empty) {
    return res.status(404).send('User not found');
  }

  const userId = userSnapshot.docs[0].id;

  // Setze SuperAdmin Rolle
  await db.collection('users').doc(userId).update({
    userRole: 2  // SuperAdmin
  });

  res.send(`User ${email} is now SuperAdmin`);
});

Deployment:

firebase deploy --only functions:makeFirstSuperAdmin

# Aufrufen (einmalig!)
curl https://YOUR-REGION-YOUR-PROJECT.cloudfunctions.net/makeFirstSuperAdmin

# Danach löschen:
# Lösche die Function aus functions/index.js
firebase deploy --only functions

Option 3: Admin SDK Script (lokal)

// scripts/make_superadmin.js
const admin = require('firebase-admin');
const serviceAccount = require('./serviceAccountKey.json');

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount)
});

const db = admin.firestore();

async function makeSuperAdmin(email) {
  const snapshot = await db.collection('users')
    .where('email', '==', email)
    .limit(1)
    .get();

  if (snapshot.empty) {
    console.log('User not found');
    return;
  }

  const userId = snapshot.docs[0].id;
  await db.collection('users').doc(userId).update({ userRole: 2 });

  console.log(`✅ ${email} is now SuperAdmin`);
}

makeSuperAdmin('admin@example.com');

Ausführen:

node scripts/make_superadmin.js

Performance-Hinweis

⚠️ Die getUserData() Funktion macht einen zusätzlichen Firestore-Read!

Bei jedem Schreibzugriff auf system_settings wird: 1. Der Request geprüft 2. Ein zusätzlicher Read auf /users/{uid} gemacht 3. Die userRole geprüft

Kosten: - Jeder Schreibversuch = 1 Write + 1 Read - Bei vielen System Settings Writes → höhere Kosten

Alternative: Custom Claims (komplexer, aber performanter) - User-Rolle wird im Auth Token gespeichert - Keine zusätzlichen Firestore Reads - Siehe: https://firebase.google.com/docs/auth/admin/custom-claims

Migration auf Production Rules

# 1. Backup aktuelle Rules
cp firestore.rules firestore.rules.backup

# 2. Production Rules aktivieren
cp firebase_config/firestore.rules.production firestore.rules

# 3. Ersten SuperAdmin erstellen (siehe oben)

# 4. Deployen
firebase deploy --only firestore:rules

# 5. Testen mit verschiedenen User-Rollen

Troubleshooting

"Missing or insufficient permissions" für normale Admins

→ Nur SuperAdmin (userRole=2) kann System Settings ändern → Prüfe in Firebase Console: users/{uid}/userRole sollte 2 sein

"getUserData() returns null"

→ Der User existiert nicht in der users Collection → Stelle sicher, dass bei der Registrierung ein User-Document erstellt wird

"isSuperAdmin() always returns false"

→ Prüfe ob request.auth.uid mit der Document-ID in users übereinstimmt → Prüfe ob das Feld userRole als Number (nicht String) gespeichert ist

Firebase Security Rules & Cloud Functions

Wichtige Unterscheidung

🔒 Security Rules gelten für:

  • Client Apps (Web, iOS, Android)
  • Direkte Firebase SDK Zugriffe
  • Firestore REST API (mit User-Authentifizierung)

🔓 Security Rules gelten NICHT für:

  • Cloud Functions (nutzen Admin SDK)
  • Firebase Admin SDK (Server-seitig)
  • Service Accounts

Wie Cloud Functions auf Firestore zugreifen

❌ FALSCH - Client SDK (unterliegt Security Rules)

// In Client-Apps - WIRD von Security Rules geprüft
import { getFirestore } from 'firebase/firestore';

const db = getFirestore();
await db.collection('orders').add({...}); // ← Unterliegt Security Rules

✅ RICHTIG - Admin SDK in Cloud Functions (umgeht Security Rules)

// In Cloud Functions - UMGEHT Security Rules
const admin = require('firebase-admin');
admin.initializeApp();

const db = admin.firestore();
await db.collection('orders').add({...}); // ← Voller Admin-Zugriff!

Deine Functions nutzen Admin SDK

Schaue in /functions/index.js:

const admin = require('firebase-admin');
admin.initializeApp();

// Diese Zeile gibt deinen Functions VOLLEN Zugriff
const db = admin.firestore();

Bedeutung: - Deine Cloud Functions können IMMER auf Firestore zugreifen - Auch wenn Security Rules allow read, write: if false; haben - Auch wenn keine Authentifizierung vorhanden ist

Beispiel: Statistik-Berechnung

// functions/internal_statistic_calculation.js
const admin = require('firebase-admin');
const db = admin.firestore();

// Cloud Function schreibt Statistiken
exports.calculateStatistics = functions.pubsub
  .schedule('every 24 hours')
  .onRun(async (context) => {
    // ✅ Funktioniert IMMER - auch wenn Security Rules
    //    für normale User "write: if false" haben
    await db.collection('articleSalesStatistics').doc('today').set({
      totalSales: 1000,
      calculatedAt: admin.firestore.FieldValue.serverTimestamp()
    });
  });

Security Rules:

match /articleSalesStatistics/{statId} {
  allow read: if isAuthenticated();
  allow write: if false;  // ← Normale User können NICHT schreiben
  // Aber Cloud Functions KÖNNEN trotzdem schreiben!
}

Best Practices für Production

1. Statistics Collections - Nur Functions schreiben

match /articleSalesStatistics/{statId} {
  allow read: if isAuthenticated();
  allow write: if false;  // Nur Cloud Functions (Admin SDK) können schreiben
}

2. Orders - User können erstellen, Functions können alles

match /orders/{orderId} {
  allow read: if isAuthenticated();
  allow create: if isAuthenticated();
  allow update: if isAuthenticated() && 
                (!resource.data.keys().hasAny(['sourceSystem']));
  allow delete: if false;  // Nur Cloud Functions können löschen
}

3. Audit Logs - Nur Functions schreiben

match /auditLogs/{logId} {
  allow read: if request.auth != null && 
              request.auth.token.admin == true;  // Nur Admins
  allow write: if false;  // Nur Cloud Functions
}

Wie testest du das?

1. Security Rules testen (Client-Perspektive)

# Emulator starten
firebase emulators:start --only firestore

# In deiner App testen
# Sollte FEHLSCHLAGEN wenn Rules es verbieten

2. Cloud Function testen

# Function lokal ausführen
firebase emulators:start --only functions,firestore

# Function aufrufen
# Sollte FUNKTIONIEREN auch wenn Rules es verbieten

Zusammenfassung

Zugriff über Security Rules Beispiel
Flutter App ✅ Ja FirebaseFirestore.instance.collection('orders')
Web App ✅ Ja getFirestore().collection('orders')
Cloud Function ❌ Nein admin.firestore().collection('orders')
Admin SDK (Server) ❌ Nein admin.firestore()
REST API (mit Auth) ✅ Ja https://firestore.googleapis.com/v1/...

Merke: - Wenn admin.firestore() → Voller Zugriff, Rules egal - Wenn Firebase SDK → Security Rules gelten - Cloud Functions = Vertrauenswürdiger Server-Code = Voller Zugriff