Skip to content

Conversation

@KaiHongTan
Copy link

Problem

All ConnId search and query operations for return only 2 attributes (__NAME__ and __UID__) when using Microsoft Graph SDK v6.0.0+, instead of extracting all requested attributes.

Affected Operations

  • User search/query (executeQuery for ObjectClass.ACCOUNT)
  • Group search/query (executeQuery for ObjectClass.GROUP)
  • License search/query (executeQuery for AZURE_LICENSE)
  • Get by UID operations (single object retrieval)
  • Filtered searches (query-based attribute filtering)
  • Paginated results (bulk operations)

Example Impact

Before Fix:

// User query via ConnId framework calls executeQuery internally
connector.executeQuery(ObjectClass.ACCOUNT, filter, handler, options);
// Results passed to handler contain incomplete data:
//   __NAME__ = [email protected]
//   __UID__ = 12345678-1234-1234-1234-123456789012
// Only 2 attributes returned, even though more requested

After Fix:

// User query via ConnId framework calls executeQuery internally
connector.executeQuery(ObjectClass.ACCOUNT, filter, handler, options);
// Results passed to handler contain complete data:
//   displayName = John Doe
//   accountEnabled = true
//   surname = Doe
//   mail = [email protected]
//   department = Engineering
//   givenName = John
//   userPrincipalName = [email protected]
//   __NAME__ = [email protected]
//   __UID__ = 12345678-1234-1234-1234-123456789012
// All requested attributes returned

Root Cause

Microsoft Graph SDK v6.x+ migrated to the Kiota code generator, which fundamentally changed how model classes store properties.

Architecture Change

Older SDK (v5.x and earlier)

public class User {
    private String displayName;  // Accessible via reflection
    private String mail;          // Accessible via reflection
    // ...
}

Field[] fields = User.class.getDeclaredFields();
// Result: [displayName, mail, ...] - Works correctly

Current SDK (v6.1.0+)

public class User {
    private BackingStore backingStore;  // Only this field exists
    
    public String getDisplayName() {
        return this.backingStore.get("displayName");  // Data accessed via backingStore
    }
}

Field[] fields = User.class.getDeclaredFields();
// Result: [] - Empty array! Breaks field-based reflection

Why Only 2 Attributes Were Returned

The connector's fromUser(), fromGroup(), and fromLicense() methods rely on field-based reflection:

Field[] fields = User.class.getDeclaredFields();
for (Field field : fields) {  // Never executes because fields.length == 0
    switch (field.getName()) {
        case "displayName": /* extract attribute */ break;
        // ...
    }
}
// Result: attrs Set remains empty

Only __NAME__ and __UID__ are set outside the try block, so they're the only attributes returned.

Evidence

Breaking Change Reference:

Properties Now Accessed via Getters and Setters

In v5 properties were accessed directly via the property name. In v6 properties are accessed via getters and setters.
For example, in v5 a user would access the displayName property of a User object as follows:

User me = graphClient.me().buildRequest().get();
String displayName = me.displayName;

In v6 the same call would look like the following:

User me = graphClient.me().get();
String displayName = me.getDisplayName();

Solution

Implemented a backward-compatible fallback that detects when field-based reflection fails and switches to getter-based reflection.

Implementation Strategy

Field[] fields = User.class.getDeclaredFields();

if (fields.length == 0) {
    // SDK v6.x+: Use getter-based reflection
    for (Method method : User.class.getMethods()) {
        if (method.getName().startsWith("get")) {
            Object value = method.invoke(user);  // Extract via getter
            attrs.add(buildAttribute(value, attributeName));
        }
    }
} else {
    // SDK v5.x and earlier: Use original field-based reflection
    for (Field field : fields) {
        field.setAccessible(true);
        attrs.add(buildAttribute(field.get(user), field.getName()));
    }
}

How It Works

  1. Detection: Check if getDeclaredFields() returns empty array (fields.length == 0)
  2. Fallback: Build method map from Class.getMethods() reflection
  3. Extraction: Invoke getter methods (e.g., getDisplayName()) to extract values
  4. Compatibility: Original field-based code path remains for SDK v5.x and earlier

Changes

Modified File

  • src/main/java/net/tirasa/connid/bundles/azure/AzureConnector.java

Affected Methods

1. fromUser()

  • Added getter-based fallback for 28 standard user attributes
  • Preserved existing special handling for passwordProfile, accountEnabled
  • Preserved existing collection attribute transformations (provisionedPlans, assignedLicenses, assignedPlans)
  • Backward compatibility maintained

Upstream Impact: Fixes executeQuery() for ObjectClass.ACCOUNT

2. fromGroup()

  • Added getter-based fallback for 18 standard group attributes
  • Support for mailNickname, displayName, groupTypes, securityEnabled, etc.
  • Backward compatibility maintained

Upstream Impact: Fixes executeQuery() for ObjectClass.GROUP

3. fromLicense()

  • Added getter-based fallback for 8 standard license attributes
  • Support for skuPartNumber, capabilityStatus, consumedUnits, prepaidUnits, etc.
  • Backward compatibility maintained

Upstream Impact: Fixes executeQuery() for AZURE_LICENSE

Added Imports

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

Testing

Before Fix (SDK v6.55.0)

User Query Result:
  Attributes: 2
  - __NAME__ = [email protected]
  - __UID__ = 12345678-1234-1234-1234-123456789012

After Fix (SDK v6.55.0)

User Query Result:
  Attributes: 31
  - displayName = John Doe
  - accountEnabled = true
  - surname = Doe
  - mail = [email protected]
  - department = Engineering
  - givenName = John
  - userPrincipalName = [email protected]
  - jobTitle = Software Engineer
  - city = Seattle
  - country = USA
  - mobilePhone = +1-555-0100
  - officeLocation = Building 42
  - businessPhones = [+1-555-0101]
  - assignedLicenses = [ENTERPRISEPACK, EMS]
  - assignedPlans = [service-plan-id-1, service-plan-id-2]
  - provisionedPlans = [Exchange, SharePoint]
  - __NAME__ = [email protected]
  - __UID__ = 12345678-1234-1234-1234-123456789012
  [... all requested attributes ...]

Compatibility

SDK Version Approach Status
v5.x and earlier Field-based reflection Preserved - Original code path unchanged
v6.1.0 - v6.55.0+ Getter-based reflection Fixed - New fallback implemented
Future versions Getter-based reflection Future-proof - Uses public API

No Breaking Changes

  • Existing functionality fully preserved
  • No changes to public API
  • No configuration changes required
  • Automatic detection and fallback

References


The Microsoft Graph SDK v6.x+ migrated to Kiota code generator which uses
a backingStore pattern instead of accessible fields via reflection. This
breaks the connector's field-based attribute extraction in fromUser(),
fromGroup(), and fromLicense() methods.

Impact on Upstream Operations:
- executeQuery() for ObjectClass.ACCOUNT - user search/get operations
- executeQuery() for ObjectClass.GROUP - group search/get operations
- executeQuery() for AZURE_LICENSE - license search/get operations
- All ConnId search(), get(), and filter operations affected
- Paginated result handling returns incomplete attributes

Root Cause:
- SDK v6.x+ uses backingStore pattern (Kiota generated code)
- Class.getDeclaredFields() returns empty array for User, Group, SubscribedSku
- Original field-based switch statements fail silently
- Only __NAME__ and __UID__ attributes returned (set outside try block)

Changes:
- Add getter-based fallback when getDeclaredFields() returns empty
- Apply fix consistently to fromUser(), fromGroup(), and fromLicense()
- fromUser(): Support 28 user attributes + 3 collection attributes
- fromGroup(): Support 18 group attributes
- fromLicense(): Support 8 license attributes
- Maintain backward compatibility with older SDK versions
- Use LOG.warn() for proper error handling

Fixes: All ConnId search/query operations for Users, Groups, and Licenses
       returned only 2 attributes (__NAME__, __UID__) instead of all
       requested attributes when using Microsoft Graph SDK v6.0.0+

Related: Microsoft Graph SDK Kiota migration
         Upgrade guide: https://github.com/microsoftgraph/msgraph-sdk-java/blob/main/docs/upgrade-to-v6.md
@ilgrosso
Copy link
Member

ilgrosso commented Nov 3, 2025

@KaiHongTan thank you for this PR.

Two things:

  1. please open an issue on JIRA with the information as provided above
  2. no need to keep compatibility with SDK v5.x, just remove that part from the PR

Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants