LDAP-Sync ergaenzt eine fehlende ldap_uid bei einem Treffer nachtraeglich, ohne den Lauf bei fehlender UID zu blockieren. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
103 lines
2.8 KiB
Go
103 lines
2.8 KiB
Go
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++
|
|
}
|
|
|
|
// COR-04 Pruefung 2: ein fehlender ldap_uid blockiert den Sync-Lauf
|
|
// nicht, ein vorhandener ergaenzt ihn auch bei bereits migrierten Konten
|
|
// ohne Fehler nach.
|
|
if entry.UID != "" {
|
|
if err := s.users.SetLDAPUID(ctx, userID, entry.UID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|