5920b690d1
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>
124 lines
5.7 KiB
Go
124 lines
5.7 KiB
Go
// Package api - rule editor metadata (vocabulary served to the UI)
|
|
package api
|
|
|
|
// OperatorMeta describes a condition operator for the editor.
|
|
type OperatorMeta struct {
|
|
Value string `json:"value"`
|
|
Label string `json:"label"`
|
|
NeedsValue bool `json:"needsValue"` // false for Exists/NotExists
|
|
Custom bool `json:"custom,omitempty"` // true for raw LDAP expression
|
|
Hint string `json:"hint,omitempty"`
|
|
}
|
|
|
|
// LabeledValue is a generic {value,label,description} tuple for dropdowns.
|
|
type LabeledValue struct {
|
|
Value string `json:"value"`
|
|
Label string `json:"label"`
|
|
Description string `json:"description,omitempty"`
|
|
}
|
|
|
|
// AttributeMeta is a suggested attribute for a given object type.
|
|
type AttributeMeta struct {
|
|
Name string `json:"name"`
|
|
Label string `json:"label"`
|
|
}
|
|
|
|
// RuleMetadata is the full vocabulary the rule editor renders from.
|
|
type RuleMetadata struct {
|
|
ObjectTypes []LabeledValue `json:"objectTypes"`
|
|
SearchScopes []LabeledValue `json:"searchScopes"`
|
|
JoinOperators []LabeledValue `json:"joinOperators"`
|
|
Operators []OperatorMeta `json:"operators"`
|
|
ActionTypes []LabeledValue `json:"actionTypes"`
|
|
SyncModes []LabeledValue `json:"syncModes"`
|
|
ScheduleUnits []LabeledValue `json:"scheduleUnits"`
|
|
CommonAttributes map[string][]AttributeMeta `json:"commonAttributes"`
|
|
}
|
|
|
|
func ruleMetadata() RuleMetadata {
|
|
return RuleMetadata{
|
|
ObjectTypes: []LabeledValue{
|
|
{Value: "User", Label: "Users"},
|
|
{Value: "Computer", Label: "Computers"},
|
|
{Value: "Group", Label: "Groups"},
|
|
},
|
|
SearchScopes: []LabeledValue{
|
|
{Value: "Subtree", Label: "This container and everything below"},
|
|
{Value: "OneLevel", Label: "Immediate children only"},
|
|
{Value: "Base", Label: "This object only"},
|
|
},
|
|
JoinOperators: []LabeledValue{
|
|
{Value: "AND", Label: "Match ALL of", Description: "Every condition must be true"},
|
|
{Value: "OR", Label: "Match ANY of", Description: "At least one condition must be true"},
|
|
},
|
|
Operators: []OperatorMeta{
|
|
{Value: "Equals", Label: "equals", NeedsValue: true},
|
|
{Value: "NotEquals", Label: "does not equal", NeedsValue: true},
|
|
{Value: "Contains", Label: "contains", NeedsValue: true},
|
|
{Value: "StartsWith", Label: "starts with", NeedsValue: true},
|
|
{Value: "EndsWith", Label: "ends with", NeedsValue: true},
|
|
{Value: "GreaterThan", Label: "is greater than or equal to", NeedsValue: true},
|
|
{Value: "LessThan", Label: "is less than or equal to", NeedsValue: true},
|
|
{Value: "Exists", Label: "is present", NeedsValue: false},
|
|
{Value: "NotExists", Label: "is not present", NeedsValue: false},
|
|
{Value: "MemberOf", Label: "is a member of (direct)", NeedsValue: true, Hint: "Group DN"},
|
|
{Value: "MemberOfRecursive", Label: "is a member of (incl. nested)", NeedsValue: true, Hint: "Group DN"},
|
|
{Value: "CustomLdap", Label: "raw LDAP filter", NeedsValue: true, Custom: true, Hint: "e.g. (department=Sales)"},
|
|
},
|
|
ActionTypes: []LabeledValue{
|
|
{Value: "SyncGroupMembership", Label: "Sync membership to group", Description: "Keep a target group's members in step with the matched set (add + remove)"},
|
|
{Value: "MoveToOu", Label: "Move to OU", Description: "Move each matched object into a target OU"},
|
|
{Value: "EnsureGroupExists", Label: "Ensure group exists", Description: "Create the target group if it is missing"},
|
|
{Value: "AddToGroup", Label: "Add to group (no removal)", Description: "Add matched objects to a group without ever removing anyone"},
|
|
},
|
|
SyncModes: []LabeledValue{
|
|
{Value: "FullSync", Label: "Full sync (add + remove)", Description: "Membership becomes exactly the matched set; members that no longer match are removed, including manual additions"},
|
|
{Value: "ManagedAdd", Label: "Managed (add + remove only what we added)", Description: "Add matches and remove only members this rule added; leaves manually-added members alone"},
|
|
{Value: "AddOnly", Label: "Add only", Description: "Only add matching members; never remove automatically"},
|
|
},
|
|
ScheduleUnits: []LabeledValue{
|
|
{Value: "Minutes", Label: "minutes"},
|
|
{Value: "Hours", Label: "hours"},
|
|
{Value: "Days", Label: "days"},
|
|
},
|
|
CommonAttributes: map[string][]AttributeMeta{
|
|
"User": {
|
|
{Name: "sAMAccountName", Label: "Logon name (sAMAccountName)"},
|
|
{Name: "userPrincipalName", Label: "User principal name (UPN)"},
|
|
{Name: "mail", Label: "Email"},
|
|
{Name: "displayName", Label: "Display name"},
|
|
{Name: "givenName", Label: "First name"},
|
|
{Name: "sn", Label: "Last name"},
|
|
{Name: "department", Label: "Department"},
|
|
{Name: "title", Label: "Job title"},
|
|
{Name: "company", Label: "Company"},
|
|
{Name: "physicalDeliveryOfficeName", Label: "Office"},
|
|
{Name: "l", Label: "City"},
|
|
{Name: "st", Label: "State/Province"},
|
|
{Name: "co", Label: "Country"},
|
|
{Name: "manager", Label: "Manager (DN)"},
|
|
{Name: "employeeType", Label: "Employee type"},
|
|
{Name: "description", Label: "Description"},
|
|
{Name: "memberOf", Label: "Member of (group DN)"},
|
|
{Name: "userAccountControl", Label: "Account control flags"},
|
|
},
|
|
"Computer": {
|
|
{Name: "cn", Label: "Name"},
|
|
{Name: "dNSHostName", Label: "DNS host name"},
|
|
{Name: "operatingSystem", Label: "Operating system"},
|
|
{Name: "operatingSystemVersion", Label: "OS version"},
|
|
{Name: "description", Label: "Description"},
|
|
{Name: "memberOf", Label: "Member of (group DN)"},
|
|
},
|
|
"Group": {
|
|
{Name: "cn", Label: "Name"},
|
|
{Name: "sAMAccountName", Label: "Group name (sAMAccountName)"},
|
|
{Name: "description", Label: "Description"},
|
|
{Name: "groupType", Label: "Group type flags"},
|
|
{Name: "mail", Label: "Email"},
|
|
{Name: "memberOf", Label: "Member of (group DN)"},
|
|
},
|
|
},
|
|
}
|
|
}
|