feat(ldap): perform user/group writes via the FreeIPA API

Raw LDAP adds/modifies bypassed FreeIPA's framework, so group/user creates
failed with Object Class Violation (no gidNumber/uidNumber/ipaUniqueID) and
deletes/mods needed ACIs the bind account couldn't exercise as a plain LDAP
write. Route all MUTATIONS through the FreeIPA JSON-RPC API instead; reads
stay on direct LDAP.

- internal/client/freeipa.go: new JSON-RPC client (form login_password →
  ipa_session cookie, re-auth on 401, multi-server failover, TLS via the
  configured CA). Derives the API host + login uid from the LDAP config.
- internal/service/ldap.go: CreateGroup/UpdateGroup/DeleteGroup/AddGroupMembers/
  RemoveGroupMember → group_add/_mod/_del/_add_member/_remove_member;
  CreateUser/UpdateUser/DisableUser/ResetPassword → user_add/_mod/_disable
  (+_enable)/passwd. Services map → addattr(objectclass)/setattr. Writes error
  cleanly when the IPA client is unconfigured.
- cmd/server/main.go: build the FreeIPA client from the LDAP config and inject
  it into the LDAP service.

Verified live: group create (IPA-assigned gidNumber), get, add/remove member,
delete all succeed; reads unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Claude (gsc-ops-api init)
2026-06-01 14:05:19 +02:00
parent f6a9d5e312
commit 90f98671fc
3 changed files with 399 additions and 172 deletions

View File

@@ -129,6 +129,19 @@ func main() {
logger.Info().Int("poolSize", cfg.LDAP.PoolSize).Msg("Connected to LDAP")
}
// Initialize FreeIPA management-API client (for user/group MUTATIONS).
// Reads stay on direct LDAP; writes go through the IPA API so gid/uid
// Numbers, ipaUniqueID and objectClasses are assigned by the framework.
var ipaClient *client.FreeIPAClient
if len(cfg.LDAP.Servers) > 0 {
ipaClient, err = client.NewFreeIPAClient(cfg.LDAP.Servers, cfg.LDAP.BindDN, cfg.LDAP.BindPass, cfg.LDAP.CAFile, logger)
if err != nil {
logger.Warn().Err(err).Msg("Failed to create FreeIPA API client (LDAP writes disabled)")
} else {
logger.Info().Int("servers", len(cfg.LDAP.Servers)).Msg("FreeIPA management-API client initialized")
}
}
// Initialize PowerDNS client
var pdnsClient *client.PowerDNSClient
if cfg.PowerDNS.BaseURL != "" {
@@ -193,7 +206,7 @@ func main() {
logger.Info().Int("attrs", len(registry.AllUserAttrs())).Int("entities", len(registry.AllEntityTypes())).Msg("Schema registry initialized")
// Create services
ldapSvc := service.NewLDAPService(ldapClient, cfg.LDAP.BaseDN, logger, registry)
ldapSvc := service.NewLDAPService(ldapClient, ipaClient, cfg.LDAP.BaseDN, logger, registry)
ldapEntitySvc := service.NewLDAPEntityService(ldapClient, cfg.LDAP.BaseDN, registry, logger)
dnsSvc := service.NewDNSService(pdnsClient, logger)
dbSvc := service.NewDatabaseService(coreDB.Pool(), logger)