package ldapsync import ( "context" "errors" "fmt" "gitea.perlbach24.de/scripte/nexarch/internal/rbac" "gitea.perlbach24.de/scripte/nexarch/internal/user" ) // SyncResult fasst das Ergebnis eines Synchronisationslaufs zusammen. type SyncResult struct { Created int Updated int Deactivated int Failed []string // Email-Adressen, bei denen die Verarbeitung fehlschlug } // Syncer fuehrt die Benutzer-/Rollen-Synchronisation aus LDAP aus. type Syncer struct { client Client users *user.TenantUserStore roles *rbac.Store mappings *RoleMappingStore } func NewSyncer(client Client, users *user.TenantUserStore, roles *rbac.Store, mappings *RoleMappingStore) *Syncer { return &Syncer{client: client, users: users, roles: roles, mappings: mappings} } // Run synchronisiert Benutzer aus LDAP (Akzeptanzkriterium 1). Ist die // LDAP-Quelle nicht erreichbar/fehlerhaft, wird SOFORT und OHNE jede // Aenderung an bestehenden Konten abgebrochen (Akzeptanzkriterium 2 / // Pruefung 2) — Search() wird als allererstes aufgerufen, bevor irgendein // Schreibzugriff stattfindet. func (s *Syncer) Run(ctx context.Context) (SyncResult, error) { entries, err := s.client.Search() if err != nil { return SyncResult{}, fmt.Errorf("ldap-quelle nicht erreichbar, synchronisation abgebrochen: %w", err) } var result SyncResult for _, entry := range entries { if err := s.syncOne(ctx, entry, &result); err != nil { // Ein fehlerhafter Einzeleintrag isoliert sich selbst — er // beschaedigt weder bereits verarbeitete noch nachfolgende // Konten (Akzeptanzkriterium 2). result.Failed = append(result.Failed, entry.Email) } } return result, nil } func (s *Syncer) syncOne(ctx context.Context, entry Entry, result *SyncResult) error { existing, err := s.users.GetByEmailForAuth(ctx, entry.Email) isNew := errors.Is(err, user.ErrNotFound) if err != nil && !isNew { return err } var userID string if isNew { created, err := s.users.Create(ctx, entry.Email, entry.Name) if err != nil { return err } userID = created.ID result.Created++ } else { userID = existing.User.ID result.Updated++ } // Deaktivierung in LDAP wird uebernommen (Akzeptanzkriterium 3 / Pruefung 3). if entry.Disabled { if _, err := s.users.Deactivate(ctx, userID); err != nil { return err } result.Deactivated++ } role, ok, err := s.mappings.HighestRoleFor(ctx, entry.Groups) if err != nil { return err } if ok { if _, err := s.roles.Assign(ctx, userID, role, "ldap-sync"); err != nil { return err } } return nil }