Skip to content
3 changes: 3 additions & 0 deletions api/v1alpha1/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ package v1alpha1
const (
SuccessfullyReconciled = "SuccessfullyReconciled"
ReconcileFailed = "ReconcileFailed"

// MaxLDAPQueryDepth is the maximum nesting depth allowed for ldap_query filters.
MaxLDAPQueryDepth = 4
)
21 changes: 15 additions & 6 deletions api/v1alpha1/group_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,27 @@ type Backend struct {
}

type LDAPFilter struct {
// +optional
// +kubebuilder:validation:Enum=givenName;displayName;rhatJobTitle;title;employeeType;manager;rhatCostCenter;rhatCostCenterDesc;rhatGeo;co;st;rhatLocation;rhatOfficeLocation;rhatOfficeFloor;roomNumber
Key string `json:"key"`
Key string `json:"key,omitempty"`
// +optional
// +kubebuilder:validation:Enum=equals;contains;not
Criteria string `json:"criteria"`
Value string `json:"value"`
Criteria string `json:"criteria,omitempty"`
// +optional
Value string `json:"value,omitempty"`
// +optional
// +kubebuilder:validation:Schemaless
// +kubebuilder:pruning:PreserveUnknownFields
LDAPQuery *LDAPQuery `json:"ldap_query,omitempty"`
Comment thread
Abhidas747 marked this conversation as resolved.
}

type LDAPQuery struct {
// +kubebuilder:validation:Enum=and;or
Operator string `json:"operator"`
Filters []LDAPFilter `json:"filters"`
Options *LDAPOptions `json:"options,omitempty"`
Operator string `json:"operator"`
// +kubebuilder:validation:MinItems=1
Filters []LDAPFilter `json:"filters"`
// +optional
Options *LDAPOptions `json:"options,omitempty"`
}

type LDAPOptions struct {
Expand Down
9 changes: 8 additions & 1 deletion api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 3 additions & 4 deletions config/crd/bases/operator.dataverse.redhat.com_groups.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,12 @@ spec:
- rhatOfficeFloor
- roomNumber
type: string
ldap_query:
x-kubernetes-preserve-unknown-fields: true
value:
type: string
required:
- criteria
- key
- value
type: object
minItems: 1
type: array
operator:
enum:
Expand Down
54 changes: 54 additions & 0 deletions config/samples/v1alpha1_group_querybased_nested.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
apiVersion: operator.dataverse.redhat.com/v1alpha1
kind: Group
metadata:
labels:
app.kubernetes.io/name: usernaut
app.kubernetes.io/managed-by: kustomize
name: test-nested-query
namespace: usernaut
spec:
group_name: dataverse-consumer-test-nested
members:
ldap_query:
options:
include_indirect_reports: true
include_manager: true
operator: and
filters:
- key: employeeType
criteria: not
value: "external employee"
- ldap_query:
operator: or
filters:
- key: title
criteria: contains
value: engineer
- key: title
criteria: contains
value: developer
- ldap_query:
operator: or
filters:
- key: manager
criteria: equals
value: mgrAlpha
- key: manager
criteria: equals
value: mgrBeta
- ldap_query:
operator: and
filters:
- key: manager
criteria: equals
value: mgrGamma
- ldap_query:
operator: or
filters:
- key: rhatGeo
criteria: equals
value: na
- key: rhatGeo
criteria: equals
value: apac
backends: []
113 changes: 85 additions & 28 deletions internal/controller/group_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,13 +264,7 @@ func (r *GroupReconciler) fetchQueryMembers(ctx context.Context, query *usernaut

log.WithField("query_members_count", len(queryMembers)).Info("query members fetched successfully")

hasManagerFilter := false
for _, filter := range query.Filters {
if strings.EqualFold(strings.TrimSpace(filter.Key), "manager") {
hasManagerFilter = true
break
}
}
hasManagerFilter := queryHasManagerFilter(query)

// Manager filter present but indirect reports disabled: return only direct reports of the manager in the query (no recursion).
if hasManagerFilter && !includeIndirectReports {
Expand All @@ -279,9 +273,6 @@ func (r *GroupReconciler) fetchQueryMembers(ctx context.Context, query *usernaut

if hasManagerFilter && includeIndirectReports {
log.Info("has manager filter, fetching indirect reports")
nestedQuery := usernautdevv1alpha1.LDAPQuery{
Operator: query.Operator,
}

queue := make([]string, 0, len(queryMembers))
queue = append(queue, queryMembers...)
Expand All @@ -304,17 +295,10 @@ func (r *GroupReconciler) fetchQueryMembers(ctx context.Context, query *usernaut
}
visited[member] = struct{}{}

nestedQuery.Filters = make([]usernautdevv1alpha1.LDAPFilter, 0, len(query.Filters))
for _, filter := range query.Filters {
value := filter.Value
if strings.EqualFold(strings.TrimSpace(filter.Key), "manager") {
value = member
}
nestedQuery.Filters = append(nestedQuery.Filters, usernautdevv1alpha1.LDAPFilter{
Key: filter.Key,
Criteria: filter.Criteria,
Value: value,
})
nestedQuery := usernautdevv1alpha1.LDAPQuery{
Operator: query.Operator,
Filters: replaceManagerInFilters(query.Filters, member),
Options: query.Options,
}
nestedQueryMembers, err := r.fetchQueryMembers(ctx, &nestedQuery, includeIndirectReports, visited)
if err != nil {
Expand All @@ -332,18 +316,93 @@ func (r *GroupReconciler) fetchQueryMembers(ctx context.Context, query *usernaut
return r.deduplicateMembers(queryMembers), nil
}

func extractManagerUIDsFromQuery(query *usernautdevv1alpha1.LDAPQuery) []string {
// replaceManagerInFilters returns a copy of filters where every manager filter value
// is replaced with managerUID. Duplicate manager filters with the same criteria and
// value after replacement are collapsed to a single entry.
func replaceManagerInFilters(filters []usernautdevv1alpha1.LDAPFilter, managerUID string) []usernautdevv1alpha1.LDAPFilter {
if len(filters) == 0 {
return []usernautdevv1alpha1.LDAPFilter{}
}
Comment thread
Abhidas747 marked this conversation as resolved.
result := make([]usernautdevv1alpha1.LDAPFilter, 0, len(filters))
Comment thread
Abhidas747 marked this conversation as resolved.
seenManagers := make(map[string]struct{})
for _, filter := range filters {
if filter.LDAPQuery != nil {
nested := replaceManagerInQuery(filter.LDAPQuery, managerUID)
result = append(result, usernautdevv1alpha1.LDAPFilter{
LDAPQuery: nested,
})
continue
}

value := filter.Value
if strings.EqualFold(strings.TrimSpace(filter.Key), "manager") {
value = managerUID
dedupeKey := strings.ToLower(strings.TrimSpace(filter.Criteria)) + "|" + value
if _, ok := seenManagers[dedupeKey]; ok {
continue
}
seenManagers[dedupeKey] = struct{}{}
}
result = append(result, usernautdevv1alpha1.LDAPFilter{
Key: filter.Key,
Criteria: filter.Criteria,
Value: value,
})
}
return result
}

func replaceManagerInQuery(query *usernautdevv1alpha1.LDAPQuery, managerUID string) *usernautdevv1alpha1.LDAPQuery {
if query == nil {
return nil
}
return &usernautdevv1alpha1.LDAPQuery{
Operator: query.Operator,
Filters: replaceManagerInFilters(query.Filters, managerUID),
Options: query.Options,
}
}

func filtersHaveManager(filters []usernautdevv1alpha1.LDAPFilter) bool {
for _, filter := range filters {
if strings.EqualFold(strings.TrimSpace(filter.Key), "manager") {
return true
}
if filter.LDAPQuery != nil && queryHasManagerFilter(filter.LDAPQuery) {
return true
}
}
return false
}

managerUIDs := make([]string, 0)
// queryHasManagerFilter reports whether a manager filter exists anywhere in the query tree.
func queryHasManagerFilter(query *usernautdevv1alpha1.LDAPQuery) bool {
if query == nil {
return false
}
return filtersHaveManager(query.Filters)
}
Comment thread
piyush-garg marked this conversation as resolved.

// extractManagerUIDsFromQuery returns unique manager UIDs referenced anywhere in the query tree.
func extractManagerUIDsFromQuery(query *usernautdevv1alpha1.LDAPQuery) []string {
if query == nil {
return []string{}
}
seen := make(map[string]struct{})
for _, filter := range query.Filters {
result := []string{}
collectManagerUIDsFromFilters(query.Filters, seen, &result)
return result
}

func collectManagerUIDsFromFilters(filters []usernautdevv1alpha1.LDAPFilter, seen map[string]struct{}, result *[]string) {
for _, filter := range filters {
if filter.LDAPQuery != nil {
collectManagerUIDsFromFilters(filter.LDAPQuery.Filters, seen, result)
continue
}
if !strings.EqualFold(strings.TrimSpace(filter.Key), "manager") {
continue
}

uid := filter.Value
if uid == "" {
continue
Expand All @@ -352,10 +411,8 @@ func extractManagerUIDsFromQuery(query *usernautdevv1alpha1.LDAPQuery) []string
continue
}
seen[uid] = struct{}{}
managerUIDs = append(managerUIDs, uid)
*result = append(*result, uid)
}

return managerUIDs
}

// fetchLDAPData fetches LDAP data for all unique members and populates allLdapUserData.
Expand Down
Loading
Loading