feat: Implement LDAP interface-specific provisioning filters (#642)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled

* Implement LDAP filter-based access control for interface provisioning

* test: add unit tests for LDAP interface filtering logic

* smaller improvements / cleanup

---------

Co-authored-by: jc <37738506+theguy147@users.noreply.github.com>
Co-authored-by: Christoph Haas <christoph.h@sprinternet.at>
This commit is contained in:
Jacopo Clark
2026-03-19 23:13:19 +01:00
committed by GitHub
parent f70f60a3f5
commit 402cc1b5f3
16 changed files with 339 additions and 18 deletions

View File

@@ -90,6 +90,12 @@ func (m Manager) synchronizeLdapUsers(ctx context.Context, provider *config.Ldap
}
}
// Update interface allowed users based on LDAP filters
err = m.updateInterfaceLdapFilters(ctx, conn, provider)
if err != nil {
return err
}
return nil
}
@@ -237,3 +243,59 @@ func (m Manager) disableMissingLdapUsers(
return nil
}
func (m Manager) updateInterfaceLdapFilters(
ctx context.Context,
conn *ldap.Conn,
provider *config.LdapProvider,
) error {
if len(provider.InterfaceFilter) == 0 {
return nil // nothing to do if no interfaces are configured for this provider
}
for ifaceName, groupFilter := range provider.InterfaceFilter {
ifaceId := domain.InterfaceIdentifier(ifaceName)
// Combined filter: user must match the provider's base SyncFilter AND the interface's LdapGroupFilter
combinedFilter := fmt.Sprintf("(&(%s)(%s))", provider.SyncFilter, groupFilter)
rawUsers, err := internal.LdapFindAllUsers(conn, provider.BaseDN, combinedFilter, &provider.FieldMap)
if err != nil {
slog.Error("failed to find users for interface filter",
"interface", ifaceId,
"provider", provider.ProviderName,
"error", err)
continue
}
matchedUserIds := make([]domain.UserIdentifier, 0, len(rawUsers))
for _, rawUser := range rawUsers {
userId := domain.UserIdentifier(internal.MapDefaultString(rawUser, provider.FieldMap.UserIdentifier, ""))
if userId != "" {
matchedUserIds = append(matchedUserIds, userId)
}
}
// 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))
}
}
return nil
}

View File

@@ -39,6 +39,11 @@ type PeerDatabaseRepo interface {
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error)
}
type InterfaceDatabaseRepo interface {
// SaveInterface saves the interface with the given identifier.
SaveInterface(ctx context.Context, id domain.InterfaceIdentifier, updateFunc func(i *domain.Interface) (*domain.Interface, error)) error
}
type EventBus interface {
// Publish sends a message to the message bus.
Publish(topic string, args ...any)
@@ -50,22 +55,27 @@ type EventBus interface {
type Manager struct {
cfg *config.Config
bus EventBus
users UserDatabaseRepo
peers PeerDatabaseRepo
bus EventBus
users UserDatabaseRepo
peers PeerDatabaseRepo
interfaces InterfaceDatabaseRepo
}
// NewUserManager creates a new user manager instance.
func NewUserManager(cfg *config.Config, bus EventBus, users UserDatabaseRepo, peers PeerDatabaseRepo) (
*Manager,
error,
) {
func NewUserManager(
cfg *config.Config,
bus EventBus,
users UserDatabaseRepo,
peers PeerDatabaseRepo,
interfaces InterfaceDatabaseRepo,
) (*Manager, error) {
m := &Manager{
cfg: cfg,
bus: bus,
users: users,
peers: peers,
users: users,
peers: peers,
interfaces: interfaces,
}
return m, nil
}