@@ -82,6 +82,90 @@ const (
8282 unknownType = "unknown"
8383)
8484
85+ // callTypedGetter invokes a no-argument getter method on obj by reflection.
86+ // Returns (value, found, hasGetter):
87+ // - hasGetter=false: no method with the expected name/signature exists.
88+ // - hasGetter=true, found=false: method exists but returned nil/zero (field not set).
89+ // - hasGetter=true, found=true: method returned a non-nil/non-zero value.
90+ func callTypedGetter (obj interface {}, methodName string ) (interface {}, bool , bool ) {
91+ m := reflect .ValueOf (obj ).MethodByName (methodName )
92+ if ! m .IsValid () || m .Type ().NumIn () != 0 || m .Type ().NumOut () != 1 {
93+ return nil , false , false
94+ }
95+ rv := m .Call (nil )[0 ]
96+ if rv .Kind () == reflect .Pointer && ! rv .IsNil () {
97+ return rv .Elem ().Interface (), true , true
98+ }
99+ if rv .IsValid () && ! rv .IsZero () {
100+ return rv .Interface (), true , true
101+ }
102+ return nil , false , true
103+ }
104+
105+ // lookupAdditionalData checks whether field is present in the object's OData
106+ // additional data bag (used for extension attributes not modelled as typed fields).
107+ func lookupAdditionalData (obj interface {}, field string ) (interface {}, bool ) {
108+ type additionalDataProvider interface {
109+ GetAdditionalData () map [string ]interface {}
110+ }
111+ if adder , ok := obj .(additionalDataProvider ); ok {
112+ val , exists := adder .GetAdditionalData ()[field ]
113+ return val , exists
114+ }
115+ return nil , false
116+ }
117+
118+ // applyAdditionalFields extracts each requested field from obj and stores
119+ // found values in m. It is a convenience wrapper around extractTypedOrAdditionalField.
120+ func (g * GraphQuery ) applyAdditionalFields (m map [string ]interface {}, obj interface {}, objectID string , fields []string ) {
121+ for _ , field := range fields {
122+ if val , ok := g .extractTypedOrAdditionalField (obj , field , objectID ); ok {
123+ m [field ] = val
124+ }
125+ }
126+ }
127+
128+ // extractTypedOrAdditionalField resolves a field value from a Microsoft Graph
129+ // SDK model object. It first attempts to call the typed getter derived from
130+ // the field name (e.g. "city" → GetCity(), "jobTitle" → GetJobTitle()) using
131+ // reflection. This is necessary because the kiota-generated SDK deserializes
132+ // known properties into typed struct fields, not into GetAdditionalData().
133+ // If no typed getter exists (e.g. for OData extension attributes), the
134+ // method falls back to GetAdditionalData().
135+ //
136+ // Logging behaviour:
137+ // - Debug: field is a known SDK property but has no value set for this object
138+ // (e.g. user.city is empty in Entra ID) — expected, no action needed.
139+ // - Info: field is not found via typed getter OR additionalData — likely a
140+ // typo or unsupported field name in additionalFields config.
141+ func (g * GraphQuery ) extractTypedOrAdditionalField (obj interface {}, field , objectID string ) (interface {}, bool ) {
142+ if len (field ) == 0 {
143+ return nil , false
144+ }
145+ // Build the expected getter name: "city" → "GetCity", "jobTitle" → "GetJobTitle"
146+ methodName := "Get" + strings .ToUpper (field [:1 ]) + field [1 :]
147+ if val , found , hasGetter := callTypedGetter (obj , methodName ); hasGetter {
148+ if found {
149+ return val , true
150+ }
151+ // Typed getter exists but returned nil/zero — field is known but not set.
152+ if g .log != nil {
153+ g .log .Debug ("additionalFields: field is a valid Graph property but has no value for this object" ,
154+ "field" , field , "objectID" , objectID )
155+ }
156+ return nil , false
157+ }
158+ if val , found := lookupAdditionalData (obj , field ); found {
159+ return val , true
160+ }
161+ // Field not found anywhere — likely a typo or unsupported field name.
162+ if g .log != nil {
163+ g .log .Info ("additionalFields: field not found in Graph SDK model or additionalData — verify the field name in additionalFields config" ,
164+ "field" , field , "objectID" , objectID )
165+ }
166+ return nil , false
167+ }
168+
85169// GraphQueryInterface defines the methods required for querying Microsoft Graph API.
86170type GraphQueryInterface interface {
87171 graphQuery (ctx context.Context , azureCreds map [string ]string , in * v1beta1.Input ) (interface {}, error )
@@ -545,9 +629,12 @@ func (g *GraphQuery) validateUsers(ctx context.Context, client *msgraphsdk.Graph
545629 filterValue := fmt .Sprintf ("userPrincipalName eq '%s'" , * userPrincipalName )
546630 requestConfig .QueryParameters .Filter = & filterValue
547631
548- // Use standard fields for user validation. accountEnabled is returned by
549- // Microsoft Graph only when it is explicitly selected.
550- requestConfig .QueryParameters .Select = []string {"id" , fieldDisplayName , fieldUserPrincipalName , fieldMail , fieldAccountEnabled }
632+ // Use standard fields for user validation, appending any extra fields requested.
633+ // accountEnabled is returned by Microsoft Graph only when explicitly selected.
634+ selectFields := make ([]string , 0 , 5 + len (in .AdditionalFields ))
635+ selectFields = append (selectFields , "id" , fieldDisplayName , fieldUserPrincipalName , fieldMail , fieldAccountEnabled )
636+ selectFields = append (selectFields , in .AdditionalFields ... )
637+ requestConfig .QueryParameters .Select = selectFields
551638
552639 // Execute the query
553640 result , err := client .Users ().Get (ctx , requestConfig )
@@ -556,7 +643,7 @@ func (g *GraphQuery) validateUsers(ctx context.Context, client *msgraphsdk.Graph
556643 }
557644
558645 // Process results
559- results = append (results , g .buildUserResults (result .GetValue (), requireActiveAccount )... )
646+ results = append (results , g .buildUserResults (result .GetValue (), requireActiveAccount , in . AdditionalFields )... )
560647 }
561648
562649 return results , nil
@@ -567,7 +654,7 @@ func (g *GraphQuery) validateUsers(ctx context.Context, client *msgraphsdk.Graph
567654// attribute is not true are omitted. A nil attribute is treated as disabled,
568655// because Microsoft Graph returns accountEnabled only when it is explicitly
569656// selected, so an absent value means the account state is unconfirmed.
570- func (g * GraphQuery ) buildUserResults (graphUsers []models.Userable , requireActiveAccount bool ) []interface {} {
657+ func (g * GraphQuery ) buildUserResults (graphUsers []models.Userable , requireActiveAccount bool , additionalFields [] string ) []interface {} {
571658 results := make ([]interface {}, 0 , len (graphUsers ))
572659
573660 for _ , user := range graphUsers {
@@ -582,13 +669,15 @@ func (g *GraphQuery) buildUserResults(graphUsers []models.Userable, requireActiv
582669 continue
583670 }
584671
585- results = append ( results , map [string ]interface {}{
672+ userMap := map [string ]interface {}{
586673 "id" : ptr .Deref (user .GetId (), "" ),
587674 fieldDisplayName : ptr .Deref (user .GetDisplayName (), "" ),
588675 fieldUserPrincipalName : ptr .Deref (user .GetUserPrincipalName (), "" ),
589676 fieldMail : ptr .Deref (user .GetMail (), "" ),
590677 fieldAccountEnabled : accountEnabled ,
591- })
678+ }
679+ g .applyAdditionalFields (userMap , user , ptr .Deref (user .GetId (), "unknown" ), additionalFields )
680+ results = append (results , userMap )
592681 }
593682
594683 return results
@@ -619,18 +708,23 @@ func (g *GraphQuery) findGroupByName(ctx context.Context, client *msgraphsdk.Gra
619708 return groupResult .GetValue ()[0 ].GetId (), nil
620709}
621710
622- // fetchGroupMembers fetches all members of a group by group ID
623- func (g * GraphQuery ) fetchGroupMembers (ctx context.Context , client * msgraphsdk.GraphServiceClient , groupID string , groupName string ) ([]models.DirectoryObjectable , error ) {
624- // Create a request configuration that expands members
625- // This is the workaround for the known issue where service principals
626- // are not listed as group members in v1.0
711+ // fetchGroupMembers fetches all members of a group by group ID.
712+ // additionalFields extends the nested $select inside the $expand expression.
713+ func (g * GraphQuery ) fetchGroupMembers (ctx context.Context , client * msgraphsdk.GraphServiceClient , groupID string , groupName string , additionalFields []string ) ([]models.DirectoryObjectable , error ) {
714+ // Build the nested $select list for the $expand workaround.
715+ // The workaround is required because service principals are not listed as
716+ // group members via the standard /members endpoint in v1.0.
627717 // See: https://developer.microsoft.com/en-us/graph/known-issues/?search=25984
718+ memberSelectFields := append (
719+ []string {"id" , "displayName" , "mail" , "userPrincipalName" , "appId" },
720+ additionalFields ... ,
721+ )
628722 requestConfig := & groups.GroupItemRequestBuilderGetRequestConfiguration {
629723 QueryParameters : & groups.GroupItemRequestBuilderGetQueryParameters {
630- // Explicitly select the standard member fields via a nested $select so
631- // that user properties such as mail and userPrincipalName are returned
632- // for the expanded members (see issue #115).
633- Expand : []string {"members($select=id,displayName,mail,userPrincipalName,appId)" },
724+ // Explicitly select member fields via a nested $select so that user
725+ // properties such as mail and userPrincipalName are returned for the
726+ // expanded members (see issue #115).
727+ Expand : []string {fmt . Sprintf ( "members($select=%s)" , strings . Join ( memberSelectFields , "," )) },
634728 },
635729 }
636730
@@ -806,16 +900,17 @@ func (g *GraphQuery) getGroupMembers(ctx context.Context, client *msgraphsdk.Gra
806900 return nil , err
807901 }
808902
809- // Fetch the members
810- memberObjects , err := g .fetchGroupMembers (ctx , client , * groupID , groupName )
903+ // Fetch the members, forwarding any extra fields for the nested $select
904+ memberObjects , err := g .fetchGroupMembers (ctx , client , * groupID , groupName , in . AdditionalFields )
811905 if err != nil {
812906 return nil , err
813907 }
814908
815- // Process the members
909+ // Process the members and attach any additional fields from additionalData
816910 members := make ([]interface {}, 0 , len (memberObjects ))
817911 for _ , member := range memberObjects {
818912 memberMap := g .processMember (member )
913+ g .applyAdditionalFields (memberMap , member , ptr .Deref (member .GetId (), "unknown" ), in .AdditionalFields )
819914 members = append (members , memberMap )
820915 }
821916
@@ -844,8 +939,11 @@ func (g *GraphQuery) getGroupObjectIDs(ctx context.Context, client *msgraphsdk.G
844939 filterValue := fmt .Sprintf ("displayName eq '%s'" , * groupName )
845940 requestConfig .QueryParameters .Filter = & filterValue
846941
847- // Use standard fields for group object IDs
848- requestConfig .QueryParameters .Select = []string {"id" , fieldDisplayName , fieldDescription }
942+ // Use standard fields for group object IDs, appending any extra fields requested
943+ selectFields := make ([]string , 0 , 3 + len (in .AdditionalFields ))
944+ selectFields = append (selectFields , "id" , fieldDisplayName , fieldDescription )
945+ selectFields = append (selectFields , in .AdditionalFields ... )
946+ requestConfig .QueryParameters .Select = selectFields
849947
850948 groupResult , err := client .Groups ().Get (ctx , requestConfig )
851949 if err != nil {
@@ -859,6 +957,7 @@ func (g *GraphQuery) getGroupObjectIDs(ctx context.Context, client *msgraphsdk.G
859957 fieldDisplayName : ptr .Deref (group .GetDisplayName (), "" ),
860958 fieldDescription : ptr .Deref (group .GetDescription (), "" ),
861959 }
960+ g .applyAdditionalFields (groupMap , group , ptr .Deref (group .GetId (), "unknown" ), in .AdditionalFields )
862961 results = append (results , groupMap )
863962 }
864963 }
@@ -889,8 +988,11 @@ func (g *GraphQuery) getServicePrincipalDetails(ctx context.Context, client *msg
889988 filterValue := fmt .Sprintf ("displayName eq '%s'" , * spName )
890989 requestConfig .QueryParameters .Filter = & filterValue
891990
892- // Use standard fields for service principals
893- requestConfig .QueryParameters .Select = []string {"id" , fieldAppID , fieldDisplayName , fieldDescription }
991+ // Use standard fields for service principals, appending any extra fields requested
992+ selectFields := make ([]string , 0 , 4 + len (in .AdditionalFields ))
993+ selectFields = append (selectFields , "id" , fieldAppID , fieldDisplayName , fieldDescription )
994+ selectFields = append (selectFields , in .AdditionalFields ... )
995+ requestConfig .QueryParameters .Select = selectFields
894996
895997 spResult , err := client .ServicePrincipals ().Get (ctx , requestConfig )
896998 if err != nil {
@@ -905,6 +1007,7 @@ func (g *GraphQuery) getServicePrincipalDetails(ctx context.Context, client *msg
9051007 fieldDisplayName : ptr .Deref (sp .GetDisplayName (), "" ),
9061008 fieldDescription : ptr .Deref (sp .GetDescription (), "" ),
9071009 }
1010+ g .applyAdditionalFields (spMap , sp , ptr .Deref (sp .GetId (), "unknown" ), in .AdditionalFields )
9081011 results = append (results , spMap )
9091012 }
9101013 }
0 commit comments