fix(ldap): do not create interfaces from interface_filter entries (#746)
Docker / Build and Push (push) Canceled after 0s
github-pages / deploy (push) Canceled after 0s
Test / make test (push) Canceled after 0s
Docker / release (push) Canceled after 0s

updateInterfaceLdapFilters saved the matched users with SaveInterface,
which creates the interface when it is missing.

On first start that panics. The sync runs immediately (main.go:85) and
the importer later (main.go:116), so the sync creates a stub row for
every interface_filter key, and the importer, which snapshotted the
interface list before its device round-trips, then fails with "interface
already exists". The window is GetInterfaces plus GetPeers, so a
directory on localhost loses the race and a slower one hides it.

The stub rows are wrong anyway. They have no backend, so a typo in an
interface_filter key quietly created an interface attached to no
controller.

Look the interface up and skip with a warning when it is absent. The
filter is applied on the next sync once the importer has created it. A
lookup error that is not ErrNotFound also skips.

Signed-off-by: clark-ja <37738506+clark-ja@users.noreply.github.com>
This commit is contained in:
Jacopo Clark
2026-09-01 22:47:46 +02:00
committed by GitHub
parent 64195a25c1
commit a2ee8d1e46
3 changed files with 171 additions and 19 deletions
+44 -19
View File
@@ -311,30 +311,55 @@ func (m Manager) updateInterfaceLdapFilters(
}
}
// Save the interface
err = m.interfaces.SaveInterface(ctx, ifaceId, func(i *domain.Interface) (*domain.Interface, error) {
if i.LdapAllowedUsers == nil {
i.LdapAllowedUsers = make(map[string][]domain.UserIdentifier)
}
i.LdapAllowedUsers[provider.ProviderName] = matchedUserIds
return i, nil
})
if err != nil {
slog.Error("failed to save interface ldap allowed users",
"interface", ifaceId,
"provider", provider.ProviderName,
"error", err)
} else {
slog.Debug("updated interface ldap allowed users",
"interface", ifaceId,
"provider", provider.ProviderName,
"matched_count", len(matchedUserIds))
}
m.applyInterfaceLdapFilter(ctx, ifaceId, provider.ProviderName, matchedUserIds)
}
return nil
}
// applyInterfaceLdapFilter stores the users an LDAP filter matched onto an
// existing interface.
//
// It deliberately does not create the interface. SaveInterface would, which is
// wrong twice over: an interface_filter entry is a statement about who may use
// an interface, not a reason to bring one into existence, and the row it
// creates has no backend. It also races the startup importer, which snapshots
// the interface list before its device round-trips and then fails with
// "interface already exists" once it finds the row.
func (m Manager) applyInterfaceLdapFilter(
ctx context.Context,
ifaceId domain.InterfaceIdentifier,
providerName string,
matchedUserIds []domain.UserIdentifier,
) {
if _, err := m.interfaces.GetInterface(ctx, ifaceId); err != nil {
if errors.Is(err, domain.ErrNotFound) {
slog.Warn("skipping interface filter for unknown interface",
"interface", ifaceId, "provider", providerName)
} else {
slog.Error("failed to look up interface for ldap filter",
"interface", ifaceId, "provider", providerName, "error", err)
}
return
}
err := m.interfaces.SaveInterface(ctx, ifaceId, func(i *domain.Interface) (*domain.Interface, error) {
if i.LdapAllowedUsers == nil {
i.LdapAllowedUsers = make(map[string][]domain.UserIdentifier)
}
i.LdapAllowedUsers[providerName] = matchedUserIds
return i, nil
})
if err != nil {
slog.Error("failed to save interface ldap allowed users",
"interface", ifaceId, "provider", providerName, "error", err)
return
}
slog.Debug("updated interface ldap allowed users",
"interface", ifaceId, "provider", providerName, "matched_count", len(matchedUserIds))
}
func ldapUserIdentifier(rawUser map[string]any, field string) domain.UserIdentifier {
identifier := internal.MapDefaultString(rawUser, field, "")
identifier = domain.SanitizeIdentifier(identifier, 256)