Skip to content

Instantly share code, notes, and snippets.

@merill
Last active September 30, 2020 22:57
Show Gist options
  • Save merill/97d274d074368ad4dcff69d591d15963 to your computer and use it in GitHub Desktop.
Save merill/97d274d074368ad4dcff69d591d15963 to your computer and use it in GitHub Desktop.
using Microsoft.Graph;
using System;
using System.Collections.Generic;
using System.Linq;
using App.Azure.GuestInvite.Infrastructure.Authentication;
using App.Azure.GuestInvite.Model;
namespace App.Azure.GuestInvite.Infrastructure
{
public class GraphClient : IGraphClient
{
private readonly GraphServiceClient _graphClient;
private IGraphServiceUsersCollectionPage _users;
public GraphClient(string tenant, string clientId, string[] scopes)
{
_graphClient = new MsalAuthenticationHelper().GetAuthenticatedClient(tenant, clientId, scopes);
}
public bool HasMoreUsers => _users.NextPageRequest != null;
public IGraphServiceUsersCollectionPage GetNextPageUsers()
{
return _users.NextPageRequest.GetAsync().Result;
}
private IGraphServiceUsersCollectionPage GetUsers(QueryOption option)
{
var options = new List<Option> { option };
_users = _graphClient.Users.Request(options).GetAsync().Result;
return _users;
}
public IGraphServiceUsersCollectionPage GetUsers(string fieldName, string value)
{
return GetUsers(GetQueryOption(GetODataFilter(fieldName, value)));
}
public Invitation InviteGuest(Invitation invitation)
{
return _graphClient.Invitations.Request().AddAsync(invitation).Result;
}
public void RemoveGuest(string id)
{
_graphClient.Users[id].Request().DeleteAsync().GetAwaiter().GetResult();
}
public IList<Store> GetStores(string tenantName, string siteCollectionId, string webId, string listId)
{
var result = _graphClient.Sites[$"{tenantName},{siteCollectionId},{webId}"].Lists[listId].Items.Request()
.Expand("fields($select%3DTitle,Code,Region,OrgUnitCode)").GetAsync().Result;
var stores = new List<Store>(result.Count);
do
{
stores.AddRange(result.CurrentPage.Select(item => new Store
{
Title = item.Fields.AdditionalData.ContainsKey("Title") ? item.Fields.AdditionalData["Title"] as string : string.Empty,
Code = item.Fields.AdditionalData.ContainsKey("Code") ? item.Fields.AdditionalData["Code"] as string : string.Empty,
Region = item.Fields.AdditionalData.ContainsKey("Region") ? item.Fields.AdditionalData["Region"] as string : string.Empty,
OrgUnitCode = item.Fields.AdditionalData.ContainsKey("OrgUnitCode") ? item.Fields.AdditionalData["OrgUnitCode"] as string : string.Empty,
}));
} while (result.NextPageRequest != null && (result = result.NextPageRequest.GetAsync().Result).Count > 0);
return stores;
}
public IList<User> GetExternalUsers(string externalEmailDomain, string tenantName)
{
var list = new List<User>();
// Remove the @ symbol
var domain = externalEmailDomain.Substring(1);
// External Users UserPrincipalName is of the form FirstName.LastName_externaldomain.com#EXT#@contoso.onmicrosoft.com
var filter = $"_{domain}#EXT#@{tenantName}";
var result = _graphClient.Users.Request().Select("id,displayName,userPrincipalName,userType").GetAsync().Result;
do
{
var users = result.CurrentPage;
foreach (var user in users)
{
if (user.UserPrincipalName.EndsWith(filter, StringComparison.OrdinalIgnoreCase))
{
// Need to convert the UPN to the normal email format without the #EXT#
user.UserPrincipalName = user.UserPrincipalName.Replace(filter, $"@{domain}");
list.Add(user);
}
}
}
while (result.NextPageRequest != null && (result = result.NextPageRequest.GetAsync().Result).Count > 0);
return list;
}
public IList<User> GetGroupMembers(string groupId)
{
var result = _graphClient.Groups[groupId].Members.Request().GetAsync().Result;
var members = new List<User>(result.Count);
do
{
members.AddRange(result.CurrentPage.OfType<User>());
}
while (result.NextPageRequest != null && (result = result.NextPageRequest.GetAsync().Result).Count > 0);
return members;
}
private string GetODataFilter(string fieldName, string value)
{
return $"{fieldName} eq '{value}'";
}
private QueryOption GetQueryOption(string filter)
{
return new QueryOption("$filter", filter);
}
}
}
using CsvHelper;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using App.Azure.GuestInvite.Infrastructure;
using App.Azure.GuestInvite.Model;
using File = System.IO.File;
namespace App.Azure.GuestInvite.Business
{
public class GraphQuery
{
private readonly IGraphClient _gc;
private readonly string[] _scopes = { "User.Read.All", "Sites.Read.All", "Directory.AccessAsUser.All" };
public GraphQuery()
{
_gc = new GraphClient(AppConfiguration.TeamApp.Tenant, AppConfiguration.TeamApp.ClientId, _scopes);
}
public GraphQuery(IGraphClient gc)
{
_gc = gc;
}
public void Connect()
{
//Run a quick test to confirm that we can connect to the tenant and user has required permissions.
//Use a random orgunit. This is just to check if user has access.
_gc.GetUsers(AppConfiguration.OrgUnitExtensionName, "000");
}
public void CreateGuestUsersFile()
{
var uniqueUsers = new Dictionary<string, UserInfo>();
PrepGuestUsersFile(AppConfiguration.GuestUsersFile);
var stores = GetStores();
foreach (var store in stores)
{
var orgUnit = store.OrgUnitCode;
Console.WriteLine("Processing " + orgUnit);
if (orgUnit is null)
{
throw new NullReferenceException($"Store OrgUnitCode is null. Store: {store.Title}, Code: {store.Code}, Region: {store.Region}");
}
AddUniqueUsers(GetUsersInOrgUnit(orgUnit), ref uniqueUsers);
// AppendUsersToCsv(users);
}
// Ad Hoc Users
AddUniqueUsers(GetAdHocUsers(), ref uniqueUsers);
AppendUsersToCsv(uniqueUsers.Values.ToList());
var msg = "Successfully finished creating GuestUsers.txt file.";
App.Log.Info(msg);
Console.WriteLine(msg);
}
private static void AddUniqueUsers(IList<UserInfo> users, ref Dictionary<string, UserInfo> uniqueUsers)
{
foreach (var user in users)
{
if (!uniqueUsers.ContainsKey(user.UserPrincipalName))
{
uniqueUsers[user.UserPrincipalName] = user;
}
else
{
App.Log.Info($"Duplicate OrgUnit user found: {user.UserPrincipalName}");
}
}
}
public IEnumerable<Store> GetStores()
{
App.Log.Debug("GetStores");
//return File.ReadAllLines(AppConfiguration.TeamApp.OrgUnitFilterFile);
try
{
return _gc.GetStores(
AppConfiguration.SharePointTenant,
AppConfiguration.SharePointSiteCollectionId,
AppConfiguration.SharePointWebId,
AppConfiguration.SharePointStoresListId);
}
catch (AggregateException ae)
when ((ae?.InnerException?.Message ?? string.Empty).Contains("Code: accessDenied"))
{
Console.WriteLine($"User does not have access to SharePoint List");
App.Log.Error($"User does not have access to SharePoint List");
throw ae.InnerException;
}
}
private void AppendUsersToCsv(IList<UserInfo> users)
{
using (var csv = new CsvWriter(new StreamWriter(AppConfiguration.GuestUsersFile, true)))
{
csv.Configuration.HasHeaderRecord = false;
csv.WriteRecords(users);
}
}
private List<UserInfo> GetUsersInOrgUnit(string orgUnit)
{
var filteredUsers = new List<UserInfo>();
var users = _gc.GetUsers(AppConfiguration.OrgUnitExtensionName, orgUnit);
var domainChecker = new DomainChecker(AppConfiguration.TeamApp.DomainFilter);
do
{
foreach (var user in users)
{
if (domainChecker.IsValidDomain(user.UserPrincipalName))
{
filteredUsers.Add(new UserInfo() { UserPrincipalName = user.UserPrincipalName, DisplayName = user.DisplayName, OrgUnit = orgUnit });
}
else
{
App.Log.Info($"Skipping. User does not match domain filter: {user.UserPrincipalName}");
}
}
}
while (_gc.HasMoreUsers && (users = _gc.GetNextPageUsers()).Count > 0);
return filteredUsers;
}
public IList<UserInfo> GetAdHocUsers()
{
var domainChecker = new DomainChecker(AppConfiguration.TeamApp.DomainFilter);
var filteredUsers = new List<UserInfo>();
var users = _gc.GetGroupMembers(AppConfiguration.UPOSApplicationSupportGroupId);
foreach (var user in users)
{
if (domainChecker.IsValidDomain(user.UserPrincipalName))
{
filteredUsers.Add(new UserInfo
{
UserPrincipalName = user.UserPrincipalName,
DisplayName = user.DisplayName,
UserType = "Guest",
});
}
else
{
App.Log.Info($"Skipping. Ad-Hoc user does not match domain filter: {user.UserPrincipalName}");
}
}
return filteredUsers;
}
private void PrepGuestUsersFile(string guestUsersFile)
{
App.Log.Debug("PrepGuestUsersFile - " + guestUsersFile);
if (File.Exists(guestUsersFile))
{
Console.WriteLine("WARNING: A guest users file already exists at the following location and will be OVERWRITTEN.");
Console.WriteLine(guestUsersFile);
File.Delete(guestUsersFile);
}
}
}
}
/* DomainChecker.cs */
using System;
using System.Collections.Generic;
using System.Linq;
namespace App.Azure.GuestInvite.Business
{
public class DomainChecker
{
private readonly IEnumerable<string> _domains;
public DomainChecker(string domainList)
{
_domains = domainList.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim());
}
public bool IsValidDomain(string email)
{
var userInDomain = false;
foreach (var domain in _domains)
{
if (email.EndsWith(domain, StringComparison.InvariantCultureIgnoreCase))
{
userInDomain = true;
break;
}
}
return userInDomain;
}
}
}
using System;
using CsvHelper;
using Microsoft.Graph;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using App.Azure.GuestInvite.Infrastructure;
using App.Azure.GuestInvite.Model;
using Microsoft.ApplicationInsights.DataContracts;
namespace App.Azure.GuestInvite.Business
{
public class InviteManager
{
private readonly IGraphClient _gc;
private readonly string _tenant;
// These three scopes are required to be able to invite guest users to the tenant
private readonly string[] _scopes = { "User.Invite.All", "User.ReadWrite.All", "Directory.ReadWrite.All" };
public InviteManager()
{
_tenant = AppConfiguration.RetailApp.Tenant;
_gc = new GraphClient(_tenant, AppConfiguration.RetailApp.ClientId, _scopes);
}
public InviteManager(IGraphClient gc)
{
_gc = gc;
}
public void Connect()
{
//Connectivity test. Add this fixed admin account which will always exist.
var user = new UserInfo {UserPrincipalName = "admin@App.com", DisplayName = "FirstName LastName", UserType = "Guest"};
InviteGuest(user);
}
public void DoInvites()
{
// Retrieve all users that should be invited from the CSV file
var users = GetUsersFromCsv(AppConfiguration.GuestUsersFile);
var domainChecker = new DomainChecker(AppConfiguration.TeamApp.DomainFilter);
foreach (var user in users)
{
if (domainChecker.IsValidDomain(user.UserPrincipalName))
{
if (IsUserInTenant(user))
{
var msg = "Skiping. User already in tenant: " + user.UserPrincipalName;
App.Log.Info(msg);
Console.WriteLine(msg);
}
else
{
InviteGuest(user);
}
}
else
{
App.Log.Info("Skipping. User does not match domain filter: " + user.UserPrincipalName);
}
}
var message = "Completed inviting users";
App.Log.Info(message);
Console.WriteLine(message);
}
/// <summary>Synchronizes the users.</summary>
public void SynchronizeUsers()
{
// Retrieve all users that should be invited from the CSV file
var usersThatShouldBeInvited = GetUsersFromCsv(AppConfiguration.GuestUsersFile);
var domainChecker = new DomainChecker(AppConfiguration.TeamApp.DomainFilter);
// Retrieve all external users already in tenant
var azureAdExternalUsers = _gc.GetExternalUsers(AppConfiguration.TeamApp.DomainFilter, AppConfiguration.RetailApp.Tenant)
.Select(u => new UserInfo()
{
UserPrincipalName = u.UserPrincipalName,
DisplayName = u.DisplayName,
UserType = u.UserType,
Id = u.Id
});
// Compare the two lists. Users in invitee list that are not in the AD list should be added
var usersToAdd = usersThatShouldBeInvited.Except(azureAdExternalUsers).ToList();
// Compare the two lists. Users in AD that are not in the invitee list should be removed
var usersToRemove = azureAdExternalUsers.Except(usersThatShouldBeInvited).ToList();
foreach (var user in usersToAdd)
{
if (domainChecker.IsValidDomain(user.UserPrincipalName))
{
if (!AppConfiguration.WhatIf)
{
InviteGuest(user);
}
else
{
Console.WriteLine($"Would have added user: {user.UserPrincipalName} ({(user.UserType == "Guest" ? "Ad-Hoc" : user.OrgUnit)})");
}
}
else
{
var message = $"Skipping invite. User does not match domain filter: { user.UserPrincipalName}";
App.Log.Warn(message);
Console.WriteLine(message);
}
}
foreach (var user in usersToRemove)
{
if (domainChecker.IsValidDomain(user.UserPrincipalName))
{
if (!AppConfiguration.WhatIf && AppConfiguration.EnableRemovals)
{
RemoveGuest(user);
}
else if (AppConfiguration.EnableRemovals)
{
Console.WriteLine($"Would have removed user: {user.UserPrincipalName}");
}
}
else
{
var message = $"Skipping remove. User does not match domain filtert: {user.UserPrincipalName}";
App.Log.Warn(message);
Console.WriteLine(message);
}
}
}
private static IEnumerable<UserInfo> GetUsersFromCsv(string guestUsersFile)
{
using (var csv = new CsvReader(new StreamReader(guestUsersFile), new CsvHelper.Configuration.Configuration
{
HasHeaderRecord = false,
HeaderValidated = null,
MissingFieldFound = null
}))
{
var users = csv.GetRecords<UserInfo>().ToList();
return users;
}
}
private bool IsUserInTenant(UserInfo user)
{
var userInTenant = false;
try
{
var users = _gc.GetUsers("mail", user.UserPrincipalName);
userInTenant = users.Count > 0;
}
catch (Exception ex)
{
App.Log.Error("Error searching for user " + user.UserPrincipalName, ex);
}
return userInTenant;
}
private void InviteGuest(UserInfo user)
{
var invite = new Invitation
{
InvitedUserEmailAddress = user.UserPrincipalName,
InvitedUserDisplayName = user.DisplayName,
InviteRedirectUrl = "https://myapps.microsoft.com",
InvitedUserType = user.UserType,
SendInvitationMessage = false
};
var isInvited = false;
var status = string.Empty;
try
{
var inviteResult = _gc.InviteGuest(invite);
isInvited = (inviteResult.Status == "Accepted" || inviteResult.Status == "Completed");
status = inviteResult.Status;
AppConfiguration.TelementryClient.TrackEvent(
"User Invited",
new Dictionary<string, string> {
{ "UserPrincipalName", user.UserPrincipalName }});
}
catch (Exception ex)
{
App.Log.Error("Error inviting guest", ex);
}
var message = isInvited
? user.UserPrincipalName + " - added successfully as guest"
: user.UserPrincipalName + " - failed adding as guest. Status:" + status;
App.Log.Info(message);
Console.WriteLine(message);
}
private void RemoveGuest(UserInfo user)
{
try
{
_gc.RemoveGuest(user.Id);
AppConfiguration.TelementryClient.TrackEvent(
"User Removed",
new Dictionary<string, string> {
{ "UserPrincipalName", user.UserPrincipalName }});
var message = $"{user.UserPrincipalName} removed from {_tenant}";
App.Log.Info(message);
Console.WriteLine(message);
}
catch (Exception e)
{
App.Log.Error($"Error removing guest {user.UserPrincipalName}", e);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment