static void Main(string[] args) { //get governance variables such as warning duration and cutoff duration int warningDuration = Convert.ToInt32(ConfigurationManager.AppSettings["WarningDuration"]); int cutoffDuration = Convert.ToInt32(ConfigurationManager.AppSettings["CutoffDuration"]); string tenantName = ConfigurationManager.AppSettings["TenantName"]; string tenantUpnDomain = ConfigurationManager.AppSettings["TenantUpnDomain"]; Uri tenantAdminUri = new Uri(String.Format("https://{0}-admin.sharepoint.com", tenantName)); string webUrl = ""; #if DEBUG webUrl = "http://localhost:25440/"; #else webUrl = "https://sposharing.azurewebsites.net/"; #endif foreach (var siteUrl in sites) { //initialize a process date for this site and clean up to match SQL percision DateTime processDate = DateTime.Now; string stringTicks = processDate.Ticks.ToString(); int adjustmentTicks = Convert.ToInt32(stringTicks.Substring(stringTicks.Length - 5)); processDate = processDate.Subtract(TimeSpan.FromTicks(adjustmentTicks)); //use O365 Tenant Administration to get all the external sharing details for this site List <ExternalShareDetails> shares = new List <ExternalShareDetails>(); string adminRealm = TokenHelper.GetRealmFromTargetUrl(tenantAdminUri); var adminToken = TokenHelper.GetAppOnlyAccessToken(TokenHelper.SharePointPrincipal, tenantAdminUri.Authority, adminRealm).AccessToken; using (var clientContext = TokenHelper.GetClientContextWithAccessToken(tenantAdminUri.ToString(), adminToken)) { //load the tenant var tenant = new Office365Tenant(clientContext); clientContext.Load(tenant); clientContext.ExecuteQuery(); //initalize varables to going through the paged results int position = 0; bool hasMore = true; while (hasMore) { //get external users 50 at a time (this is the limit and why we are paging) var externalUsers = tenant.GetExternalUsersForSite(siteUrl, position, 50, String.Empty, SortOrder.Descending); clientContext.Load(externalUsers, i => i.TotalUserCount); clientContext.Load(externalUsers, i => i.ExternalUserCollection); clientContext.ExecuteQuery(); //convert each external user to our own entity foreach (var extUser in externalUsers.ExternalUserCollection) { position++; shares.Add(new ExternalShareDetails() { AcceptedAs = extUser.AcceptedAs.ToLower(), DisplayName = extUser.DisplayName, InvitedAs = extUser.InvitedAs.ToLower(), InvitedBy = (String.IsNullOrEmpty(extUser.InvitedBy)) ? null : extUser.InvitedBy.ToLower(), UserId = extUser.UserId, WhenCreated = extUser.WhenCreated }); } //determine if we have more pages to process hasMore = (externalUsers.TotalUserCount > position); } } //get an AppOnly accessToken and clientContext for the site collection Uri siteUri = new Uri(siteUrl); string realm = TokenHelper.GetRealmFromTargetUrl(siteUri); string accessToken = TokenHelper.GetAppOnlyAccessToken(TokenHelper.SharePointPrincipal, siteUri.Authority, realm).AccessToken; using (var clientContext = TokenHelper.GetClientContextWithAccessToken(siteUri.ToString(), accessToken)) { //first we need to load the site to determine if external sharing is enabled (Site.ShareByEmailEnabled) var site = clientContext.Site; var siteOwner = clientContext.Site.Owner; clientContext.Load(site); clientContext.Load(siteOwner); //include the site owner in case the share "InvitedBy" is null...we will send them email instead clientContext.ExecuteQuery(); //validate that the site has sharing turned on if (site.ShareByEmailEnabled) { //process all of the shares foreach (var externalShare in shares) { //look for an existing record in the database using (ExternalSharingDataEntities entities = new ExternalSharingDataEntities()) { var shareRecord = entities.ExternalShares.FirstOrDefault(i => i.LoginName.Equals(externalShare.AcceptedAs)); if (shareRecord != null) { //Update LastProcessedDate column of the record with the processDate shareRecord.LastProcessedDate = processDate; entities.SaveChanges(); } else { //get the original share date var details = getREST(accessToken, String.Format("{0}/_api/Web/SiteUserInfoList/Items({1})/FieldValuesAsText", siteUrl, externalShare.UserId)); externalShare.WhenCreated = Convert.ToDateTime(details.Descendants(ns + "Created").FirstOrDefault().Value); shareRecord = new ExternalShare() { UniqueIdentifier = Guid.NewGuid(), SiteCollectionUrl = siteUrl.ToLower(), LoginName = externalShare.AcceptedAs, UserId = externalShare.UserId, InvitedBy = (String.IsNullOrEmpty(externalShare.InvitedBy)) ? siteOwner.Email : externalShare.InvitedBy, OriginalSharedDate = externalShare.WhenCreated, LastProcessedDate = processDate }; entities.ExternalShares.Add(shareRecord); entities.SaveChanges(); } //check if the record falls inside the warnings double daysActive = processDate.Subtract(shareRecord.OriginalSharedDate).TotalDays; if (shareRecord.RefreshSharedDate != null) { daysActive = processDate.Subtract((DateTime)shareRecord.RefreshSharedDate).TotalDays; } //check for cutoff if (daysActive > cutoffDuration) { //remove the SPUser from the site clientContext.Web.SiteUsers.RemoveById(externalShare.UserId); clientContext.ExecuteQuery(); //delete the record entities.ExternalShares.Remove(shareRecord); entities.SaveChanges(); } else if (daysActive > warningDuration) { int expiresIn = Convert.ToInt32(cutoffDuration - daysActive); //send email to InvitedBy (which will be site collection owner when null) EmailProperties email = new EmailProperties(); email.To = new List <String>() { shareRecord.InvitedBy }; email.Subject = String.Format("Action Required: External sharing with {0} about to expire", externalShare.AcceptedAs); email.Body = String.Format("<html><body><p>You are receiving this message because you are the site administrator of <a href='{0}'>{0}</a> OR you shared it with {1}. The external access for this user is set to expire in {2} days. Use the link below to view additional details and perform actions to revoke OR extend access for another {3} days. If you do not act on this notice, the external access for this user to terminate in {2} days.</p><ul><li><a href='{4}Details/{5}'>View Details</a></li><li><a href='{4}Extend/{5}'>Extend {3} Days</a></li><li><a href='{4}Revoke/{5}'>Revoke Access</a></li></ul></body></html>", siteUrl, externalShare.AcceptedAs, expiresIn.ToString(), cutoffDuration.ToString(), webUrl, shareRecord.UniqueIdentifier); Utility.SendEmail(clientContext, email); clientContext.ExecuteQuery(); } } } } } //delete all database records for this site that have an older processDate...these should represent external users deleted by manually using (ExternalSharingDataEntities entities = new ExternalSharingDataEntities()) { var cleanUpRecords = entities.ExternalShares.Where(i => i.SiteCollectionUrl.Equals(siteUrl.ToLower()) && i.LastProcessedDate < processDate); foreach (var record in cleanUpRecords) { entities.ExternalShares.Remove(record); entities.SaveChanges(); } } } }
static void Main(string[] args) { //get governance variables such as warning duration and cutoff duration int warningDuration = Convert.ToInt32(ConfigurationManager.AppSettings["WarningDuration"]); int cutoffDuration = Convert.ToInt32(ConfigurationManager.AppSettings["CutoffDuration"]); string tenantName = ConfigurationManager.AppSettings["TenantName"]; string tenantUpnDomain = ConfigurationManager.AppSettings["TenantUpnDomain"]; Uri tenantAdminUri = new Uri(String.Format("https://{0}-admin.sharepoint.com", tenantName)); string webUrl = ""; #if DEBUG webUrl = "http://localhost:25440/"; #else webUrl = "https://sposharing.azurewebsites.net/"; #endif foreach (var siteUrl in sites) { //initialize a process date for this site and clean up to match SQL percision DateTime processDate = DateTime.Now; string stringTicks = processDate.Ticks.ToString(); int adjustmentTicks = Convert.ToInt32(stringTicks.Substring(stringTicks.Length - 5)); processDate = processDate.Subtract(TimeSpan.FromTicks(adjustmentTicks)); //use O365 Tenant Administration to get all the external sharing details for this site List<ExternalShareDetails> shares = new List<ExternalShareDetails>(); string adminRealm = TokenHelper.GetRealmFromTargetUrl(tenantAdminUri); var adminToken = TokenHelper.GetAppOnlyAccessToken(TokenHelper.SharePointPrincipal, tenantAdminUri.Authority, adminRealm).AccessToken; using (var clientContext = TokenHelper.GetClientContextWithAccessToken(tenantAdminUri.ToString(), adminToken)) { //load the tenant var tenant = new Office365Tenant(clientContext); clientContext.Load(tenant); clientContext.ExecuteQuery(); //initalize varables to going through the paged results int position = 0; bool hasMore = true; while (hasMore) { //get external users 50 at a time (this is the limit and why we are paging) var externalUsers = tenant.GetExternalUsersForSite(siteUrl, position, 50, String.Empty, SortOrder.Descending); clientContext.Load(externalUsers, i => i.TotalUserCount); clientContext.Load(externalUsers, i => i.ExternalUserCollection); clientContext.ExecuteQuery(); //convert each external user to our own entity foreach (var extUser in externalUsers.ExternalUserCollection) { position++; shares.Add(new ExternalShareDetails() { AcceptedAs = extUser.AcceptedAs.ToLower(), DisplayName = extUser.DisplayName, InvitedAs = extUser.InvitedAs.ToLower(), InvitedBy = (String.IsNullOrEmpty(extUser.InvitedBy)) ? null : extUser.InvitedBy.ToLower(), UserId = extUser.UserId, WhenCreated = extUser.WhenCreated }); } //determine if we have more pages to process hasMore = (externalUsers.TotalUserCount > position); } } //get an AppOnly accessToken and clientContext for the site collection Uri siteUri = new Uri(siteUrl); string realm = TokenHelper.GetRealmFromTargetUrl(siteUri); string accessToken = TokenHelper.GetAppOnlyAccessToken(TokenHelper.SharePointPrincipal, siteUri.Authority, realm).AccessToken; using (var clientContext = TokenHelper.GetClientContextWithAccessToken(siteUri.ToString(), accessToken)) { //first we need to load the site to determine if external sharing is enabled (Site.ShareByEmailEnabled) var site = clientContext.Site; var siteOwner = clientContext.Site.Owner; clientContext.Load(site); clientContext.Load(siteOwner); //include the site owner in case the share "InvitedBy" is null...we will send them email instead clientContext.ExecuteQuery(); //validate that the site has sharing turned on if (site.ShareByEmailEnabled) { //process all of the shares foreach (var externalShare in shares) { //look for an existing record in the database using (ExternalSharingDataEntities entities = new ExternalSharingDataEntities()) { var shareRecord = entities.ExternalShares.FirstOrDefault(i => i.LoginName.Equals(externalShare.AcceptedAs)); if (shareRecord != null) { //Update LastProcessedDate column of the record with the processDate shareRecord.LastProcessedDate = processDate; entities.SaveChanges(); } else { //get the original share date var details = getREST(accessToken, String.Format("{0}/_api/Web/SiteUserInfoList/Items({1})/FieldValuesAsText", siteUrl, externalShare.UserId)); externalShare.WhenCreated = Convert.ToDateTime(details.Descendants(ns + "Created").FirstOrDefault().Value); shareRecord = new ExternalShare() { UniqueIdentifier = Guid.NewGuid(), SiteCollectionUrl = siteUrl.ToLower(), LoginName = externalShare.AcceptedAs, UserId = externalShare.UserId, InvitedBy = (String.IsNullOrEmpty(externalShare.InvitedBy)) ? siteOwner.Email : externalShare.InvitedBy, OriginalSharedDate = externalShare.WhenCreated, LastProcessedDate = processDate }; entities.ExternalShares.Add(shareRecord); entities.SaveChanges(); } //check if the record falls inside the warnings double daysActive = processDate.Subtract(shareRecord.OriginalSharedDate).TotalDays; if (shareRecord.RefreshSharedDate != null) daysActive = processDate.Subtract((DateTime)shareRecord.RefreshSharedDate).TotalDays; //check for cutoff if (daysActive > cutoffDuration) { //remove the SPUser from the site clientContext.Web.SiteUsers.RemoveById(externalShare.UserId); clientContext.ExecuteQuery(); //delete the record entities.ExternalShares.Remove(shareRecord); entities.SaveChanges(); } else if (daysActive > warningDuration) { int expiresIn = Convert.ToInt32(cutoffDuration - daysActive); //send email to InvitedBy (which will be site collection owner when null) EmailProperties email = new EmailProperties(); email.To = new List<String>() { shareRecord.InvitedBy }; email.Subject = String.Format("Action Required: External sharing with {0} about to expire", externalShare.AcceptedAs); email.Body = String.Format("<html><body><p>You are receiving this message because you are the site administrator of <a href='{0}'>{0}</a> OR you shared it with {1}. The external access for this user is set to expire in {2} days. Use the link below to view additional details and perform actions to revoke OR extend access for another {3} days. If you do not act on this notice, the external access for this user to terminate in {2} days.</p><ul><li><a href='{4}Details/{5}'>View Details</a></li><li><a href='{4}Extend/{5}'>Extend {3} Days</a></li><li><a href='{4}Revoke/{5}'>Revoke Access</a></li></ul></body></html>", siteUrl, externalShare.AcceptedAs, expiresIn.ToString(), cutoffDuration.ToString(), webUrl, shareRecord.UniqueIdentifier); Utility.SendEmail(clientContext, email); clientContext.ExecuteQuery(); } } } } } //delete all database records for this site that have an older processDate...these should represent external users deleted by manually using (ExternalSharingDataEntities entities = new ExternalSharingDataEntities()) { var cleanUpRecords = entities.ExternalShares.Where(i => i.SiteCollectionUrl.Equals(siteUrl.ToLower()) && i.LastProcessedDate < processDate); foreach (var record in cleanUpRecords) { entities.ExternalShares.Remove(record); entities.SaveChanges(); } } } }