-
Notifications
You must be signed in to change notification settings - Fork 6
update how azure gets the userid #216
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -9,11 +9,14 @@ import ( | |||||||||||||||||||||||||
| "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization" | ||||||||||||||||||||||||||
| "github.com/google/uuid" | ||||||||||||||||||||||||||
| msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go" | ||||||||||||||||||||||||||
| "github.com/microsoftgraph/msgraph-sdk-go/users" | ||||||||||||||||||||||||||
| "github.com/sirupsen/logrus" | ||||||||||||||||||||||||||
| "github.com/thand-io/agent/internal/data" | ||||||||||||||||||||||||||
| "github.com/thand-io/agent/internal/models" | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| var azureUserIDCache = make(map[string]string) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Never synchronize roles from Azure as they are | ||||||||||||||||||||||||||
| // statically defined by Azure and cannot be modified | ||||||||||||||||||||||||||
| func (p *azureProvider) CanSynchronizeRoles() bool { | ||||||||||||||||||||||||||
|
|
@@ -89,6 +92,15 @@ func (p *azureProvider) createRoleAssignment(ctx context.Context, user *models.U | |||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| roleAssignmentID := uuid.New().String() | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Log the principal ID for debugging | ||||||||||||||||||||||||||
| logrus.WithFields(logrus.Fields{ | ||||||||||||||||||||||||||
| "principal_id": principalID, | ||||||||||||||||||||||||||
| "user_email": user.Email, | ||||||||||||||||||||||||||
| "role_id": roleDefinitionID, | ||||||||||||||||||||||||||
| "scope": scope, | ||||||||||||||||||||||||||
| }).Info("Creating Azure role assignment") | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| roleAssignment := armauthorization.RoleAssignmentCreateParameters{ | ||||||||||||||||||||||||||
| Properties: &armauthorization.RoleAssignmentProperties{ | ||||||||||||||||||||||||||
| RoleDefinitionID: &roleDefinitionID, | ||||||||||||||||||||||||||
|
|
@@ -147,15 +159,15 @@ func (p *azureProvider) getUserPrincipalID(ctx context.Context, user *models.Use | |||||||||||||||||||||||||
| return "", fmt.Errorf("user email is required for Azure role assignments") | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // If the user's ID field already contains an Azure object ID (GUID format), use it | ||||||||||||||||||||||||||
| if len(user.ID) > 0 && len(user.ID) >= 32 { | ||||||||||||||||||||||||||
| // Validate it looks like a GUID | ||||||||||||||||||||||||||
| if _, err := uuid.Parse(user.ID); err == nil { | ||||||||||||||||||||||||||
| logrus.WithField("user_id", user.ID).Debug("Using existing Azure object ID from user.ID field") | ||||||||||||||||||||||||||
| return user.ID, nil | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| // Check cache first | ||||||||||||||||||||||||||
| if objectID, found := azureUserIDCache[user.Email]; found { | ||||||||||||||||||||||||||
| logrus.WithField("email", user.Email).Debug("Using cached Azure AD object ID") | ||||||||||||||||||||||||||
| return objectID, nil | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // NOTE: We always use Microsoft Graph API to lookup the user's Azure AD object ID | ||||||||||||||||||||||||||
| // even if user.ID is set, because user.ID may be a Thand-internal ID, not an Azure AD object ID. | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Use Microsoft Graph API to lookup the user by email and get their object ID | ||||||||||||||||||||||||||
| logrus.WithField("email", user.Email).Debug("Looking up Azure AD object ID via Microsoft Graph API") | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
@@ -165,23 +177,111 @@ func (p *azureProvider) getUserPrincipalID(ctx context.Context, user *models.Use | |||||||||||||||||||||||||
| return "", fmt.Errorf("failed to create Microsoft Graph client: %w", err) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Query the user by their email address (UPN) | ||||||||||||||||||||||||||
| // GET https://graph.microsoft.com/v1.0/users/{email} | ||||||||||||||||||||||||||
| graphUser, err := client.Users().ByUserId(user.Email).Get(ctx, nil) | ||||||||||||||||||||||||||
| // Sanitize email to prevent OData filter injection | ||||||||||||||||||||||||||
| // Escape single quotes by doubling them (OData escaping standard) | ||||||||||||||||||||||||||
| sanitizedEmail := strings.ReplaceAll(user.Email, "'", "''") | ||||||||||||||||||||||||||
|
Comment on lines
+180
to
+182
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Query the user by their email address using a filter | ||||||||||||||||||||||||||
| // This searches both 'mail' and 'userPrincipalName' fields | ||||||||||||||||||||||||||
| // GET https://graph.microsoft.com/v1.0/users?$filter=mail eq 'email' or userPrincipalName eq 'email' | ||||||||||||||||||||||||||
| filter := fmt.Sprintf("mail eq '%s' or userPrincipalName eq '%s'", sanitizedEmail, sanitizedEmail) | ||||||||||||||||||||||||||
| requestConfig := &users.UsersRequestBuilderGetRequestConfiguration{ | ||||||||||||||||||||||||||
| QueryParameters: &users.UsersRequestBuilderGetQueryParameters{ | ||||||||||||||||||||||||||
| Filter: &filter, | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| userList, err := client.Users().Get(ctx, requestConfig) | ||||||||||||||||||||||||||
|
Comment on lines
+184
to
+194
|
||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||
| return "", fmt.Errorf("failed to lookup user '%s' in Azure AD via Microsoft Graph API: %w", user.Email, err) | ||||||||||||||||||||||||||
| return "", fmt.Errorf("failed to search for user '%s' in Azure AD via Microsoft Graph API: %w", user.Email, err) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Check if we found any users | ||||||||||||||||||||||||||
| if userList == nil || len(userList.GetValue()) == 0 { | ||||||||||||||||||||||||||
| return "", fmt.Errorf("user '%s' not found in Azure AD", user.Email) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Resolve the matching user from the result set, handling multiple matches | ||||||||||||||||||||||||||
| usersValue := userList.GetValue() | ||||||||||||||||||||||||||
| var graphUser = usersValue[0] // Default to first user | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if len(usersValue) == 1 { | ||||||||||||||||||||||||||
| // Only one user returned, use it directly | ||||||||||||||||||||||||||
| logrus.WithField("email", user.Email).Debug("Single Azure AD user found") | ||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||
| // Multiple users returned; try to find an exact match on mail or userPrincipalName | ||||||||||||||||||||||||||
| logrus.WithFields(logrus.Fields{ | ||||||||||||||||||||||||||
| "email": user.Email, | ||||||||||||||||||||||||||
| "matches_returned": len(usersValue), | ||||||||||||||||||||||||||
| }).Warn("Multiple Azure AD users matched email filter; attempting exact match") | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| var exactMatches []int // Store indices of exact matches | ||||||||||||||||||||||||||
| for i, u := range usersValue { | ||||||||||||||||||||||||||
| if u == nil { | ||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Check for exact match on mail field | ||||||||||||||||||||||||||
| if mail := u.GetMail(); mail != nil && strings.EqualFold(*mail, user.Email) { | ||||||||||||||||||||||||||
| exactMatches = append(exactMatches, i) | ||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| // Check for exact match on userPrincipalName field | ||||||||||||||||||||||||||
| if upn := u.GetUserPrincipalName(); upn != nil && strings.EqualFold(*upn, user.Email) { | ||||||||||||||||||||||||||
| exactMatches = append(exactMatches, i) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+224
to
+232
|
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| switch len(exactMatches) { | ||||||||||||||||||||||||||
| case 0: | ||||||||||||||||||||||||||
| // No exact matches; use the first result but log a warning | ||||||||||||||||||||||||||
| logrus.WithFields(logrus.Fields{ | ||||||||||||||||||||||||||
| "email": user.Email, | ||||||||||||||||||||||||||
| "matches_returned": len(usersValue), | ||||||||||||||||||||||||||
| "user_id": usersValue[0], | ||||||||||||||||||||||||||
|
Comment on lines
+238
to
+241
|
||||||||||||||||||||||||||
| logrus.WithFields(logrus.Fields{ | |
| "email": user.Email, | |
| "matches_returned": len(usersValue), | |
| "user_id": usersValue[0], | |
| var firstUserID interface{} | |
| if len(usersValue) > 0 && usersValue[0] != nil && usersValue[0].GetId() != nil { | |
| firstUserID = *usersValue[0].GetId() | |
| } | |
| logrus.WithFields(logrus.Fields{ | |
| "email": user.Email, | |
| "matches_returned": len(usersValue), | |
| "user_id": firstUserID, |
Copilot
AI
Jan 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The objectID is stored in the cache before being validated as a proper GUID. If the validation fails (lines 272-279), the invalid objectID will remain in the cache and be returned on subsequent calls for the same email. Move the cache population (line 269) to after the GUID validation succeeds to ensure only valid object IDs are cached.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The azureUserIDCache is a package-level map that is accessed concurrently without synchronization. This can lead to race conditions when multiple goroutines call getUserPrincipalID simultaneously for different users. Consider using sync.Map or protecting this map with a sync.RWMutex to ensure thread-safe access.