private async Task GroupMemberToCSEntryChange(CSEntryChange c, SchemaType schemaType) { if (schemaType.Attributes.Contains("member")) { List <DirectoryObject> members = await GraphHelperGroups.GetGroupMembers(this.client, c.DN, this.token); List <object> memberIds = members.Where(u => !this.userFilter.ShouldExclude(u.Id, this.token)).Select(t => t.Id).ToList <object>(); if (memberIds.Count > 0) { c.AttributeChanges.Add(AttributeChange.CreateAttributeAdd("member", memberIds)); } } if (schemaType.Attributes.Contains("owner")) { List <DirectoryObject> owners = await GraphHelperGroups.GetGroupOwners(this.client, c.DN, this.token); List <object> ownerIds = owners.Where(u => !this.userFilter.ShouldExclude(u.Id, this.token)).Select(t => t.Id).ToList <object>(); if (ownerIds.Count > 0) { c.AttributeChanges.Add(AttributeChange.CreateAttributeAdd("owner", ownerIds)); } } }
/// <summary> /// Group delta imports have a few problems that need to be resolved. /// 1. You can't currently filter on teams-type groups only. You have to get all groups and then filter yourself /// 2. This isn't so much of a problem apart from that you have to specify your attribute selection on the initial query to include members and owners. This means you get all members and owners for all groups /// 3. Membership information comes in chunks of 20 members, however chunks for each group can be returned in any order. This breaks the way FIM works, as we would have to hold all group objects in memory, wait to see if duplicates arrive in the stream, merge them, and only once we have all groups with all members, confidently pass them back to the sync engine in one massive batch /// </summary> /// <param name="context"></param> /// <param name="target"></param> /// <returns></returns> private async Task ProduceObjectsDelta(ITargetBlock <Group> target) { string newDeltaLink; if (this.context.InDelta) { if (!this.context.IncomingWatermark.Contains("group")) { throw new WarningNoWatermarkException(); } Watermark watermark = this.context.IncomingWatermark["group"]; if (watermark.Value == null) { throw new WarningNoWatermarkException(); } newDeltaLink = await GraphHelperGroups.GetGroups(this.client, watermark.Value, target, this.token); } else { newDeltaLink = await GraphHelperGroups.GetGroups(this.client, target, this.token, "displayName", "resourceProvisioningOptions", "id", "mailNickname", "description", "visibility", "members", "owners"); } if (newDeltaLink != null) { logger.Trace($"Got delta link {newDeltaLink}"); this.context.OutgoingWatermark.Add(new Watermark("group", newDeltaLink, "string")); } target.Complete(); }
private async Task ProduceObjects(ITargetBlock <Beta.Group> target) { var client = ((GraphConnectionContext)this.context.ConnectionContext).BetaClient; await GraphHelperGroups.GetGroups(client, target, this.context.ConfigParameters[ConfigParameterNames.FilterQuery].Value, this.token, "displayName", "resourceProvisioningOptions", "id", "mailNickname", "description", "visibility"); target.Complete(); }
private async Task PutGroupMembersMailNickname(CSEntryChange csentry, string teamID) { if (csentry.HasAttributeChange("mailNickname")) { Group group = new Group(); group.MailNickname = csentry.GetValueAdd <string>("mailNickname"); try { await GraphHelperGroups.UpdateGroup(this.client, teamID, group, this.token); logger.Info($"{csentry.DN}: Updated group {teamID}"); } catch (ServiceException ex) { if (MicrosoftTeamsMAConfigSection.Configuration.DeleteAddConflictingGroup && ex.StatusCode == HttpStatusCode.BadRequest && ex.Message.IndexOf("mailNickname", 0, StringComparison.Ordinal) > 0) { string mailNickname = csentry.GetValueAdd <string>("mailNickname"); logger.Warn($"{csentry.DN}: Deleting group with conflicting mailNickname '{mailNickname}'"); string existingGroup = await GraphHelperGroups.GetGroupIdByMailNickname(this.client, mailNickname, this.token); await GraphHelperGroups.DeleteGroup(this.client, existingGroup, this.token); await Task.Delay(TimeSpan.FromSeconds(MicrosoftTeamsMAConfigSection.Configuration.PostGroupCreateDelay), this.token); await GraphHelperGroups.UpdateGroup(this.client, teamID, group, this.token); logger.Info($"{csentry.DN}: Updated group {group.Id}"); } else { throw; } } } IList <string> members = csentry.GetValueAdds <string>("member") ?? new List <string>(); IList <string> owners = csentry.GetValueAdds <string>("owner") ?? new List <string>(); if (owners.Count > 0) { // Remove the first owner, as we already added them during team creation string initialOwner = owners[0]; owners.RemoveAt(0); // Teams API unhelpfully adds the initial owner as a member as well, so we need to undo that. await GraphHelperGroups.RemoveGroupMembers(this.client, teamID, new List <string> { initialOwner }, false, this.token); } if (owners.Count > 100) { throw new UnsupportedAttributeModificationException($"The group creation request {csentry.DN} contained more than 100 owners"); } await this.ApplyMembership(teamID, teamID, members, owners); }
private async Task ApplyMembership(string csentryDN, string groupid, IList <string> members, IList <string> owners) { if (members.Count > 0) { logger.Trace($"{csentryDN}: Adding {members.Count} members"); await GraphHelperGroups.AddGroupMembers(this.client, groupid, members, true, this.token); } if (owners.Count > 0) { logger.Trace($"{csentryDN}: Adding {owners.Count} owners"); await GraphHelperGroups.AddGroupOwners(this.client, groupid, owners, true, this.token); } }
public static async Task UpdateGroupOwners(GraphServiceClient client, string groupid, IList <string> adds, IList <string> deletes, bool ignoreMemberExists, bool ignoreNotFound, CancellationToken token) { // If we try to delete the last owner on a channel, the operation will fail. If we are swapping out the full set of owners (eg an add/delete of 100 owners), this will never succeed if we do a 'delete' operation first. // If we do an 'add' operation first, and the channel already has the maximum number of owners, the call will fail. // So the order of events should be to // 1) Process all membership removals except for one owner (100-99 = 1 owner) // 2) Process all membership adds except for one owner (1 + 99 = 100 owners) // 3) Remove the final owner (100 - 1 = 99 owners) // 4) Add the final owner (99 + 1 = 100 owners) string lastOwnerToRemove = null; if (deletes.Count > 0) { if (adds.Count > 0) { // We only need to deal with the above condition if we are processing deletes and adds at the same time lastOwnerToRemove = deletes[0]; deletes.RemoveAt(0); } await GraphHelperGroups.RemoveGroupOwners(client, groupid, deletes, true, token); } string lastOwnerToAdd = null; if (adds.Count > 0) { if (deletes.Count > 0) { // We only need to deal with the above condition if we are processing deletes and adds at the same time lastOwnerToAdd = adds[0]; adds.RemoveAt(0); } await GraphHelperGroups.AddGroupOwners(client, groupid, adds, true, token); } if (lastOwnerToRemove != null) { await GraphHelperGroups.RemoveGroupOwners(client, groupid, new List <string>() { lastOwnerToRemove }, ignoreNotFound, token); } if (lastOwnerToAdd != null) { await GraphHelperGroups.AddGroupOwners(client, groupid, new List <string>() { lastOwnerToAdd }, ignoreMemberExists, token); } }
private async Task <CSEntryChangeResult> PutCSEntryChangeAdd(CSEntryChange csentry) { string teamid = null; try { IList <string> owners = csentry.GetValueAdds <string>("owner") ?? new List <string>(); if (owners.Count == 0) { throw new InvalidProvisioningStateException("At least one owner is required to create a team"); } teamid = await this.CreateTeam(csentry, this.betaClient, owners.First()); await this.PutGroupMembersMailNickname(csentry, teamid); } catch { try { if (teamid != null) { logger.Error($"{csentry.DN}: An exception occurred while creating the team, rolling back by deleting it"); await Task.Delay(TimeSpan.FromSeconds(MicrosoftTeamsMAConfigSection.Configuration.PostGroupCreateDelay), this.token); await GraphHelperGroups.DeleteGroup(this.client, teamid, CancellationToken.None); logger.Info($"{csentry.DN}: The group was deleted"); } } catch (Exception ex2) { logger.Error(ex2, $"{csentry.DN}: An exception occurred while rolling back the team"); } throw; } List <AttributeChange> anchorChanges = new List <AttributeChange>(); anchorChanges.Add(AttributeChange.CreateAttributeAdd("id", teamid)); return(CSEntryChangeResult.Create(csentry.Identifier, anchorChanges, MAExportError.Success)); }
private async Task <CSEntryChangeResult> PutCSEntryChangeDelete(CSEntryChange csentry) { try { await GraphHelperGroups.DeleteGroup(this.client, csentry.DN, this.token); } catch (ServiceException ex) { if (ex.StatusCode == HttpStatusCode.NotFound) { logger.Warn($"The request to delete the group {csentry.DN} failed because the group doesn't exist"); } else { throw; } } return(CSEntryChangeResult.Create(csentry.Identifier, null, MAExportError.Success)); }
private async Task ProcessDeferredMembership(IList <string> deferredMembers, Group result, IList <string> deferredOwners, string csentryDN) { bool success = false; while (!success) { if (deferredMembers.Count > 0) { logger.Trace($"{csentryDN}: Adding {deferredMembers.Count} deferred members"); await GraphHelperGroups.AddGroupMembers(this.client, result.Id, deferredMembers, true, this.token); } if (deferredOwners.Count > 0) { logger.Trace($"{csentryDN}: Adding {deferredOwners.Count} deferred owners"); await GraphHelperGroups.AddGroupOwners(this.client, result.Id, deferredOwners, true, this.token); } success = true; } }
private async Task <CSEntryChangeResult> PutCSEntryChangeAdd(CSEntryChange csentry) { Group result = null; try { result = await this.CreateGroup(csentry); await this.CreateTeam(csentry, result.Id); } catch { try { if (result != null) { logger.Error($"{csentry.DN}: An exception occurred while creating the team, rolling back the group by deleting it"); await Task.Delay(TimeSpan.FromSeconds(MicrosoftTeamsMAConfigSection.Configuration.PostGroupCreateDelay)); await GraphHelperGroups.DeleteGroup(this.client, result.Id, CancellationToken.None); logger.Info($"{csentry.DN}: The group was deleted"); } } catch (Exception ex2) { logger.Error(ex2, $"{csentry.DN}: An exception occurred while rolling back the team"); } throw; } List <AttributeChange> anchorChanges = new List <AttributeChange>(); anchorChanges.Add(AttributeChange.CreateAttributeAdd("id", result.Id)); return(CSEntryChangeResult.Create(csentry.Identifier, anchorChanges, MAExportError.Success)); }
private async Task PutAttributeChangeMembers(CSEntryChange c, AttributeChange change) { IList <string> valueDeletes = change.GetValueDeletes <string>(); IList <string> valueAdds = change.GetValueAdds <string>(); if (change.ModificationType == AttributeModificationType.Delete) { if (change.Name == "member") { List <DirectoryObject> result = await GraphHelperGroups.GetGroupMembers(this.client, c.DN, this.token); valueDeletes = result.Where(u => !this.userFilter.ShouldExclude(u.Id, this.token)).Select(t => t.Id).ToList(); } else { List <DirectoryObject> result = await GraphHelperGroups.GetGroupOwners(this.client, c.DN, this.token); valueDeletes = result.Where(u => !this.userFilter.ShouldExclude(u.Id, this.token)).Select(t => t.Id).ToList(); } } if (change.Name == "member") { await GraphHelperGroups.AddGroupMembers(this.client, c.DN, valueAdds, true, this.token); await GraphHelperGroups.RemoveGroupMembers(this.client, c.DN, valueDeletes, true, this.token); logger.Info($"Membership modification for group {c.DN} completed. Members added: {valueAdds.Count}, members removed: {valueDeletes.Count}"); } else { await GraphHelperGroups.AddGroupOwners(this.client, c.DN, valueAdds, true, this.token); await GraphHelperGroups.RemoveGroupOwners(this.client, c.DN, valueDeletes, true, this.token); logger.Info($"Owner modification for group {c.DN} completed. Owners added: {valueAdds.Count}, owners removed: {valueDeletes.Count}"); } }
private async Task <CSEntryChangeResult> PutCSEntryChangeDelete(CSEntryChange csentry) { try { string teamid = csentry.GetAnchorValueOrDefault <string>("id"); await GraphHelperGroups.DeleteGroup(this.client, teamid, this.token); logger.Info($"The team {teamid} was deleted"); } catch (ServiceException ex) { if (ex.StatusCode == HttpStatusCode.NotFound) { logger.Warn($"The request to delete the team {csentry.DN} failed because the group doesn't exist"); } else { throw; } } return(CSEntryChangeResult.Create(csentry.Identifier, null, MAExportError.Success)); }
private async Task PutCSEntryChangeUpdateGroup(CSEntryChange csentry) { Group group = new Group(); bool changed = false; string teamid = csentry.GetAnchorValueOrDefault <string>("id"); foreach (AttributeChange change in csentry.AttributeChanges) { if (SchemaProvider.GroupMemberProperties.Contains(change.Name)) { await this.PutAttributeChangeMembers(teamid, change); continue; } if (!SchemaProvider.GroupFromTeamProperties.Contains(change.Name)) { continue; } if (change.Name == "displayName") { if (change.ModificationType == AttributeModificationType.Delete) { throw new UnsupportedAttributeDeleteException(change.Name); } group.DisplayName = change.GetValueAdd <string>(); } else if (change.Name == "description") { if (change.ModificationType == AttributeModificationType.Delete) { group.AssignNullToProperty(change.Name); } else { group.Description = change.GetValueAdd <string>(); } } else if (change.Name == "mailNickname") { if (change.ModificationType == AttributeModificationType.Delete) { throw new UnsupportedAttributeDeleteException(change.Name); } group.MailNickname = change.GetValueAdd <string>(); } else { continue; } changed = true; } if (changed) { logger.Trace($"{csentry.DN}:Updating group data: {JsonConvert.SerializeObject(group)}"); await GraphHelperGroups.UpdateGroup(this.client, teamid, group, this.token); logger.Info($"{csentry.DN}: Updated group {teamid}"); } }
public static async Task AddGroupMembers(GraphServiceClient client, string groupid, IList <string> members, bool ignoreMemberExists, CancellationToken token) { if (members.Count == 0) { return; } Dictionary <string, Func <BatchRequestStep> > requests = new Dictionary <string, Func <BatchRequestStep> >(); foreach (string member in members) { requests.Add(member, () => GraphHelper.GenerateBatchRequestStepJsonContent(HttpMethod.Post, member, client.Groups[groupid].Members.References.Request().RequestUrl, GraphHelperGroups.GetUserODataId(member))); } logger.Trace($"Adding {requests.Count} members in batch request for group {groupid}"); await GraphHelper.SubmitAsBatches(client, requests, false, ignoreMemberExists, token); }
private async Task PutCSEntryChangeUpdateGroup(CSEntryChange csentry) { Group group = new Group(); bool changed = false; foreach (AttributeChange change in csentry.AttributeChanges) { if (SchemaProvider.GroupMemberProperties.Contains(change.Name)) { await this.PutAttributeChangeMembers(csentry, change); continue; } if (!SchemaProvider.GroupProperties.Contains(change.Name)) { continue; } if (change.ModificationType == AttributeModificationType.Delete) { group.AssignNullToProperty(change.Name); continue; } if (change.Name == "visibility") { throw new UnexpectedDataException("The visibility parameter can only be supplied during an 'add' operation"); } else if (change.Name == "displayName") { group.DisplayName = change.GetValueAdd <string>(); } else if (change.Name == "description") { group.Description = change.GetValueAdd <string>(); } else if (change.Name == "mailNickname") { group.MailNickname = change.GetValueAdd <string>(); } else if (change.Name == "isArchived") { group.IsArchived = change.GetValueAdd <bool>(); } else { continue; } changed = true; } if (changed) { logger.Trace($"{csentry.DN}:Updating group data: {JsonConvert.SerializeObject(group)}"); await GraphHelperGroups.UpdateGroup(this.client, csentry.DN, group, this.token); logger.Info($"{csentry.DN}: Updated group {csentry.DN}"); } }
private async Task <Group> CreateGroup(CSEntryChange csentry) { Group group = new Group(); group.DisplayName = csentry.GetValueAdd <string>("displayName") ?? throw new UnexpectedDataException("The group must have a displayName"); group.GroupTypes = new[] { "Unified" }; group.MailEnabled = true; group.Description = csentry.GetValueAdd <string>("description"); group.MailNickname = csentry.GetValueAdd <string>("mailNickname") ?? throw new UnexpectedDataException("The group must have a mailNickname"); group.SecurityEnabled = false; group.AdditionalData = new Dictionary <string, object>(); group.Id = csentry.DN; group.Visibility = csentry.GetValueAdd <string>("visibility"); IList <string> members = csentry.GetValueAdds <string>("member") ?? new List <string>(); IList <string> owners = csentry.GetValueAdds <string>("owner") ?? new List <string>(); IList <string> deferredMembers = new List <string>(); IList <string> createOpMembers = new List <string>(); IList <string> deferredOwners = new List <string>(); IList <string> createOpOwners = new List <string>(); int memberCount = 0; if (owners.Count > 100) { throw new UnexpectedDataException($"The group creation request {csentry.DN} contained more than 100 owners"); } foreach (string owner in owners) { if (memberCount >= MaxReferencesPerCreateRequest) { deferredOwners.Add(owner); } else { createOpOwners.Add($"https://graph.microsoft.com/v1.0/users/{owner}"); memberCount++; } } foreach (string member in members) { if (memberCount >= MaxReferencesPerCreateRequest) { deferredMembers.Add(member); } else { createOpMembers.Add($"https://graph.microsoft.com/v1.0/users/{member}"); memberCount++; } } if (createOpMembers.Count > 0) { group.AdditionalData.Add("*****@*****.**", createOpMembers.ToArray()); } if (createOpOwners.Count > 0) { group.AdditionalData.Add("*****@*****.**", createOpOwners.ToArray()); } logger.Trace($"{csentry.DN}: Creating group {group.MailNickname} with {createOpMembers.Count} members and {createOpOwners.Count} owners (Deferring {deferredMembers.Count} members and {deferredOwners.Count} owners until after group creation)"); logger.Trace($"{csentry.DN}: Group data: {JsonConvert.SerializeObject(group)}"); Group result; try { result = await GraphHelperGroups.CreateGroup(this.client, group, this.token); } catch (ServiceException ex) { if (MicrosoftTeamsMAConfigSection.Configuration.DeleteAddConflictingGroup && ex.StatusCode == HttpStatusCode.BadRequest && ex.Message.IndexOf("mailNickname", 0, StringComparison.Ordinal) > 0) { string mailNickname = csentry.GetValueAdd <string>("mailNickname"); logger.Warn($"{csentry.DN}: Delete/Adding conflicting group with mailNickname '{mailNickname}'"); string existingGroup = await GraphHelperGroups.GetGroupIdByMailNickname(this.client, mailNickname, this.token); await GraphHelperGroups.DeleteGroup(this.client, existingGroup, this.token); await Task.Delay(TimeSpan.FromSeconds(MicrosoftTeamsMAConfigSection.Configuration.PostGroupCreateDelay)); result = await GraphHelperGroups.CreateGroup(this.client, group, this.token); } else { throw; } } logger.Info($"{csentry.DN}: Created group {group.Id}"); await Task.Delay(TimeSpan.FromSeconds(MicrosoftTeamsMAConfigSection.Configuration.PostGroupCreateDelay), this.token); try { await this.ProcessDeferredMembership(deferredMembers, result, deferredOwners, csentry.DN); } catch (Exception ex) { logger.Error(ex, $"{csentry.DN}: An exception occurred while modifying the membership, rolling back the group by deleting it"); await Task.Delay(TimeSpan.FromSeconds(5), this.token); await GraphHelperGroups.DeleteGroup(this.client, csentry.DN, CancellationToken.None); logger.Info($"{csentry.DN}: The group was deleted"); throw; } return(result); }