Files
Alphaeus Mote 5920b690d1 feat(rules): dynamic-group reconciliation + editor-facing APIs
Turns rules into Adaxes/Active-Roles style dynamic groups. The core new
capability is a set-level SyncGroupMembership action that reconciles a
target group's membership against the matched object set in one pass
instead of the old add-only, per-object behaviour.

Engine / reconciliation:
- New ActionSyncGroupMembership runs once per target group after the
  match: resolve (and optionally create) the group, read its current
  members, diff against the matched set, and apply the adds/removes.
- Three per-rule sync modes (types.SyncMode): FullSync (membership ==
  matched set; removes stale members incl. manual adds), ManagedAdd (adds
  matches, removes only members this rule added), AddOnly (never removes).
- Managed ownership tracked in a new managed_group_members table
  (migration 005) + repository, wired into the runner's engine so
  ManagedAdd removes only what it added.
- Adds/removes are recorded as AddToGroup / RemoveFromGroupIfNoLongerMatched
  run-actions so the activity feed categorises them as syncs/removals.
- Preview now computes an accurate, non-mutating diff for sync actions
  (+add / -remove / already-in-sync counts and per-member entries).
- memberOf and memberOf-recursive (LDAP_MATCHING_RULE_IN_CHAIN) operators;
  Regex no longer silently degrades to equals.
- Canonical group targets: CanonicalToLeafDN / NormalizeGroupTarget so a
  target group can be given as domain.com/OU/Group as well as a DN.

Editor-facing APIs (backend-first; UI comes next):
- Rule create/update now accept conditionGroups + actions and persist them
  via RuleRepository.ReplaceLogic (soft-delete + insert, preserving the
  rule_run_actions FK). Omitting them leaves existing logic untouched.
- POST /api/v1/rules/preview evaluates an unsaved draft (live match panel).
- GET /api/v1/rules/metadata serves the operator vocabulary (object types,
  operators, action types, sync modes, common attributes) so UI dropdowns
  stay in lock-step with the backend.
- GET /api/v1/ad-connections/{id}/directory searches groups/OUs for the
  target pickers.

Tests: reconciliation across all three modes + create-if-missing and the
missing-group error path (fake directory client); canonical leaf-DN
conversion; ReplaceLogic round-trip. Full suite green.

Note: RuleRepository.GetByID nests a query (getConditionGroups holds a
cursor while calling getConditions); safe under the production pool (25)
but a follow-up should flatten it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 14:57:30 -04:00

147 lines
4.0 KiB
Go

// Package models - Rule domain models
package models
import "time"
// Rule represents an automation rule
type Rule struct {
ID string
Name string
Description *string
IsEnabled bool
ADConnectionID string
ObjectType string
BaseDNOverride *string
SearchScopeOverride *string
ScheduleID *string
ExecutionMode string
GroupJoinOperator string
MaxParallelism *int
StopOnError bool
LastRunUTC *time.Time
LastRunResult *string
CreatedUTC time.Time
UpdatedUTC time.Time
DeletedUTC *time.Time
ConditionGroups []RuleConditionGroup
Actions []RuleAction
}
// RuleConditionGroup represents a group of conditions
type RuleConditionGroup struct {
ID string
RuleID string
Name *string
IsEnabled bool
JoinOperator string
SortOrder int
Negate bool
CreatedUTC time.Time
UpdatedUTC time.Time
DeletedUTC *time.Time
Conditions []RuleCondition
}
// RuleCondition represents a single condition in a group
type RuleCondition struct {
ID string
ConditionGroupID string
IsEnabled bool
AttributeName string
Operator string
ValueType string
ComparisonValue *string
CustomLdapExpression *string
Negate bool
SortOrder int
CaseSensitive bool
CreatedUTC time.Time
UpdatedUTC time.Time
DeletedUTC *time.Time
}
// RuleAction represents an action to execute when conditions match
type RuleAction struct {
ID string
RuleID string
ActionType string
IsEnabled bool
SortOrder int
ConfigurationJSON string
RollbackMode *string
CreatedUTC time.Time
UpdatedUTC time.Time
DeletedUTC *time.Time
}
// RuleRun represents an execution history record
type RuleRun struct {
ID string
RuleID string
Status string
StartedUTC time.Time
CompletedUTC *time.Time
DurationMS *int
ObjectsMatched int
ObjectsProcessed int
ActionsExecuted int
ActionsFailed int
ErrorMessage *string
ExecutionMode string
TriggeredBy *string
CreatedUTC time.Time
}
// RuleRunAction represents a single action execution detail
type RuleRunAction struct {
ID string
RuleRunID string
RuleActionID string
ObjectDN string
ActionType string
Status string
DetailsJSON *string
ErrorMessage *string
DurationMS *int
CreatedUTC time.Time
}
// ActionConfig represents the configuration for different action types
type ActionConfig struct {
// AddToGroup / AddGroupToGroup
TargetGroupDN string `json:"targetGroupDn,omitempty"`
CreateIfMissing bool `json:"createIfMissing,omitempty"`
GroupScope string `json:"groupScope,omitempty"` // Global, DomainLocal, Universal
GroupType string `json:"groupType,omitempty"` // Security, Distribution
GroupOU string `json:"groupOu,omitempty"`
// MoveToOu
TargetOU string `json:"targetOu,omitempty"`
CreateOUIfMissing bool `json:"createOuIfMissing,omitempty"`
// RemoveFromGroupIfNoMatch
RemoveIfNoMatch bool `json:"removeIfNoMatch,omitempty"`
// SyncGroupMembership: how to reconcile the target group's membership
// against the matched set. One of FullSync, ManagedAdd, AddOnly. Empty
// defaults to FullSync.
SyncMode string `json:"syncMode,omitempty"`
// Dynamic path variables
UseDynamicPath bool `json:"useDynamicPath,omitempty"`
PathTemplate string `json:"pathTemplate,omitempty"`
}
// SummaryRuleStats represents aggregated statistics for a rule
type SummaryRuleStats struct {
RuleID string
TotalRuns int
SuccessfulRuns int
FailedRuns int
LastRunUTC *time.Time
LastRunStatus *string
AvgDurationMS *int
TotalObjectsProcessed int
UpdatedUTC time.Time
}