package ldapsync import ( "context" "errors" "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "gitea.perlbach24.de/scripte/nexarch/internal/rbac" ) // RoleMappingStore verwaltet die Erlaubnis-Matrix LDAP-Gruppe -> Rolle // (Akzeptanzkriterium 3). Nur explizit gemappte Gruppen vergeben eine Rolle // — der Schutz gegen Privilege-Escalation ist strukturell: es gibt keinen // Code-Pfad, der eine unbekannte Gruppe irgendeiner Rolle zuordnet. type RoleMappingStore struct { pool *pgxpool.Pool } func NewRoleMappingStore(pool *pgxpool.Pool) *RoleMappingStore { return &RoleMappingStore{pool: pool} } // SetMapping ordnet eine LDAP-Gruppe genau einer NEXARCH-Rolle zu. Nur // 'user'/'tenant_admin' sind zuweisbar (dieselbe Matrix wie RBAC-01/03) — // 'superadmin' bleibt mandantenuebergreifend und nie ueber LDAP erreichbar. func (s *RoleMappingStore) SetMapping(ctx context.Context, ldapGroup string, role rbac.Role) error { if role != rbac.RoleUser && role != rbac.RoleTenantAdmin { return fmt.Errorf("ldapsync: rolle %q ist ueber ldap nicht zuweisbar", role) } _, err := s.pool.Exec(ctx, ` INSERT INTO ldap_group_role_mapping (ldap_group, role) VALUES ($1, $2) ON CONFLICT (ldap_group) DO UPDATE SET role = $2 `, ldapGroup, string(role)) if err != nil { return fmt.Errorf("gruppenzuordnung speichern: %w", err) } return nil } // roleFor liefert die einer LDAP-Gruppe zugeordnete Rolle, sofern konfiguriert. func (s *RoleMappingStore) roleFor(ctx context.Context, ldapGroup string) (rbac.Role, bool, error) { var role string err := s.pool.QueryRow(ctx, `SELECT role FROM ldap_group_role_mapping WHERE ldap_group = $1`, ldapGroup).Scan(&role) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return "", false, nil } return "", false, fmt.Errorf("gruppenzuordnung lesen: %w", err) } return rbac.Role(role), true, nil } // HighestRoleFor bestimmt aus einer Liste von LDAP-Gruppen die hoechste // zugeordnete Rolle (tenant_admin > user). Gruppen ohne Eintrag in der // Matrix tragen NICHTS bei — auch nicht, wenn ihr Name zufaellig wie eine // interne Rolle aussieht (Akzeptanzkriterium 3 / Pruefung 1: keine // Privilege-Escalation ueber Gruppenmitgliedschaft). func (s *RoleMappingStore) HighestRoleFor(ctx context.Context, ldapGroups []string) (rbac.Role, bool, error) { var best rbac.Role found := false for _, g := range ldapGroups { role, ok, err := s.roleFor(ctx, g) if err != nil { return "", false, err } if !ok { continue } if !found || role == rbac.RoleTenantAdmin { best = role found = true } } return best, found, nil }