diff --git a/main.go b/main.go index 38fd3c8..72a93dc 100644 --- a/main.go +++ b/main.go @@ -31,6 +31,7 @@ import ( "github.com/go-core-stack/auth-gateway/pkg/apidocs" "github.com/go-core-stack/auth-gateway/pkg/auth" "github.com/go-core-stack/auth-gateway/pkg/config" + "github.com/go-core-stack/auth-gateway/pkg/controller/orgunit" "github.com/go-core-stack/auth-gateway/pkg/controller/request" "github.com/go-core-stack/auth-gateway/pkg/controller/roledef" "github.com/go-core-stack/auth-gateway/pkg/controller/tenant" @@ -544,6 +545,16 @@ func main() { log.Panicf("failed to create email verification cleanup controller: %s", err) } + // Start org-unit cleanup reconciler only when soft-delete is enabled. + // The reconciler handles hold-period expiry and hard-delete of + // soft-deleted org-units. + if conf.GetExperimental().AllowOUDelete { + _, err = orgunit.NewOrgUnitCleanupController(conf.GetExperimental()) + if err != nil { + log.Panicf("failed to create org-unit cleanup controller: %s", err) + } + } + // role definition manager resourceMgr := roledef.NewResourceManager() diff --git a/pkg/controller/orgunit/reconciler.go b/pkg/controller/orgunit/reconciler.go new file mode 100644 index 0000000..41d829f --- /dev/null +++ b/pkg/controller/orgunit/reconciler.go @@ -0,0 +1,92 @@ +// Copyright © 2025-2026 Prabhjot Singh Sethi, All Rights reserved +// Author: Prabhjot Singh Sethi + +package orgunit + +import ( + "context" + "log" + "time" + + "github.com/go-core-stack/auth-gateway/pkg/config" + "github.com/go-core-stack/auth-gateway/pkg/table" + "github.com/go-core-stack/core/errors" + "github.com/go-core-stack/core/reconciler" +) + +// OrgUnitCleanupController manages the lifecycle of soft-deleted org-units, +// hard-deleting them after the configured hold period expires. +type OrgUnitCleanupController struct { + tbl *table.OrgUnitTable + holdDuration int +} + +type orgUnitReconciler struct { + reconciler.Controller + ctrl *OrgUnitCleanupController +} + +func (r *orgUnitReconciler) Reconcile(k any) (*reconciler.Result, error) { + ctx := context.Background() + key := k.(*table.OrgUnitKey) + + entry, err := r.ctrl.tbl.Find(ctx, key) + if err != nil { + if !errors.IsNotFound(err) { + // transient error — requeue with backoff + return &reconciler.Result{RequeueAfter: 5 * time.Second}, nil + } + // entry already gone — nothing to do + return &reconciler.Result{}, nil + } + + // only process soft-deleted entries + if entry.Deleted == 0 { + return &reconciler.Result{}, nil + } + + holdExpiry := entry.Deleted + int64(r.ctrl.holdDuration) + now := time.Now().Unix() + + if holdExpiry > now { + // hold period has not expired — requeue after remaining time (+1s buffer) + remaining := holdExpiry - now + return &reconciler.Result{RequeueAfter: time.Duration(remaining+1) * time.Second}, nil + } + + // hold period expired — hard-delete the entry + err = r.ctrl.tbl.DeleteKey(ctx, key) + if err != nil && !errors.IsNotFound(err) { + // transient error — requeue with backoff + log.Printf("orgunit reconciler: failed to hard-delete org-unit %s: %s", key.ID, err) + return &reconciler.Result{RequeueAfter: 5 * time.Second}, nil + } + + log.Printf("orgunit reconciler: hard-deleted org-unit %s after hold period", key.ID) + return &reconciler.Result{}, nil +} + +// NewOrgUnitCleanupController creates and registers the org-unit reconciler. +// It should only be called when experimental.allow_ou_delete is enabled. +func NewOrgUnitCleanupController(experimental config.ExperimentalConfig) (*OrgUnitCleanupController, error) { + tbl, err := table.GetOrgUnitTable() + if err != nil { + return nil, err + } + + ctrl := &OrgUnitCleanupController{ + tbl: tbl, + holdDuration: experimental.HoldDeletedOU, + } + + r := &orgUnitReconciler{ + ctrl: ctrl, + } + + err = tbl.Register("OrgUnitCleanupController", r) + if err != nil { + return nil, err + } + + return ctrl, nil +} diff --git a/pkg/table/org-unit.go b/pkg/table/org-unit.go index e19ed0d..c528cf2 100644 --- a/pkg/table/org-unit.go +++ b/pkg/table/org-unit.go @@ -5,6 +5,9 @@ package table import ( "context" + "log" + + "go.mongodb.org/mongo-driver/v2/bson" "github.com/go-core-stack/core/db" "github.com/go-core-stack/core/errors" @@ -65,6 +68,29 @@ func (t *OrgUnitTable) FindByTenant(ctx context.Context, tenant, ouId string) ([ return t.FindMany(ctx, filter, 0, 0) } +// ReconcilerGetAllKeys returns keys for all soft-deleted org-unit entries +// (deleted > 0). This overrides the generic Table.ReconcilerGetAllKeys to +// ensure the reconciler only bootstraps entries that are pending hard-delete. +func (t *OrgUnitTable) ReconcilerGetAllKeys() []any { + type keyOnly struct { + Key OrgUnitKey `bson:"_id,omitempty"` + } + + filter := bson.M{"deleted": bson.M{"$gt": 0}} + + list := []keyOnly{} + err := t.col.FindMany(context.Background(), filter, &list) + if err != nil { + log.Panicf("orgunit: failed to fetch deleted keys: %s", err) + } + + keys := make([]any, 0, len(list)) + for _, k := range list { + keys = append(keys, &k.Key) + } + return keys +} + func (t *OrgUnitTable) StartEventLogger() error { logger := db.NewEventLogger[OrgUnitKey, OrgUnitEntry](t.col, nil) return logger.Start(context.Background())