public ActionResult Details(String packageId) { DetailsViewModel model = new DetailsViewModel(); ProvisioningAppDBContext context = new ProvisioningAppDBContext(); // Get the package if (Boolean.Parse(ConfigurationManager.AppSettings["TestEnvironment"])) { // Show any package in the test environment model.Package = context.Packages.Include("Categories").FirstOrDefault(p => p.Id == new Guid(packageId)); } else { // Show not-preview packages in the production environment model.Package = context.Packages.Include("Categories").FirstOrDefault(p => p.Id == new Guid(packageId) && p.Preview == false); } if (model.Package == null) { throw new ApplicationException("There is no Package with the provided packageId!"); } return(View("Details", model)); }
public ActionResult DetailsByPath(String packageUrl) { ProvisioningAppDBContext context = new ProvisioningAppDBContext(); Package targetPackage = null; packageUrl.Replace("-", "/"); // Get the package if (Boolean.Parse(ConfigurationManager.AppSettings["TestEnvironment"])) { // Show any package in the test environment targetPackage = context.Packages.FirstOrDefault(p => p.PackageUrl == packageUrl); } else { // Show not-preview packages in the production environment targetPackage = context.Packages.FirstOrDefault(p => p.PackageUrl == packageUrl && p.Preview == false); } if (targetPackage != null) { return(RedirectToAction("Details", targetPackage.Id)); } else { throw new ApplicationException("There is no Package with the provided packageUrl!"); } }
private async Task SyncCategories() { using (ProvisioningAppDBContext context = GetContext()) { // Find the category file ITemplateFile file = (await _cloneProvider.GetAsync(SYSTEM_PATH, WriteLog)).FindFile(CATEGORIES_NAME); if (file == null) { throw new InvalidOperationException($"Cannot find file {CATEGORIES_NAME}"); } // Deserialize the json var categories = await file.DownloadAsJsonAsync(new { categories = new[] { new { id = "", displayName = "" } } }); var existingDbCategories = context.Categories.ToDictionary(c => c.Id, StringComparer.OrdinalIgnoreCase); foreach (var category in categories.categories) { // Update category, if already exists if (existingDbCategories.TryGetValue(category.id, out Category dbCategory)) { dbCategory.DisplayName = category.displayName; context.Entry(dbCategory).State = EntityState.Modified; } else { // Add new category dbCategory = new Category { Id = category.id, DisplayName = category.displayName }; context.Entry(dbCategory).State = EntityState.Added; } existingDbCategories.Remove(category.id); } // Remove exceed categories var objectStateManager = ((IObjectContextAdapter)context).ObjectContext.ObjectStateManager; foreach (var dbCategory in existingDbCategories) { await context.Entry(dbCategory.Value).Collection(d => d.Packages).LoadAsync(); foreach (var dbPackage in dbCategory.Value.Packages.ToArray()) { objectStateManager.ChangeRelationshipState(dbCategory.Value, dbPackage, d => d.Packages, EntityState.Deleted); } context.Entry(dbCategory.Value).State = EntityState.Deleted; } await context.SaveChangesAsync(); } }
public ActionResult Index() { IndexViewModel model = new IndexViewModel(); ProvisioningAppDBContext context = new ProvisioningAppDBContext(); // Get the packages if (Boolean.Parse(ConfigurationManager.AppSettings["TestEnvironment"])) { // Show all packages in the test environment model.Packages = context.Packages.Include("Categories").ToList(); } else { // Show not-preview packages in the production environment model.Packages = context.Packages.Include("Categories").Where(p => p.Preview == false).ToList(); } // Get the service description content var contentPage = context.ContentPages.FirstOrDefault(cp => cp.Id == "system/pages/ServiceDescription.md"); if (contentPage != null) { model.ServiceDescription = contentPage.Content; } // Get the categories model.Categories = context.Categories.ToDictionary(c => c.Id, c => c.DisplayName); return(View(model)); }
private async Task SyncPlatforms() { using (ProvisioningAppDBContext context = GetContext()) { // Find the platforms file ITemplateFile file = (await _cloneProvider.GetAsync(SYSTEM_PATH, WriteLog)).FindFile(PLATFORMS_NAME); if (file == null) { throw new InvalidOperationException($"Cannot find file {PLATFORMS_NAME}"); } // Deserialize the json var platforms = await file.DownloadAsJsonAsync(new { platforms = new[] { new { id = "", displayName = "" } } }); var existingDbPlatforms = context.Platforms.ToDictionary(c => c.Id, StringComparer.OrdinalIgnoreCase); foreach (var platform in platforms.platforms) { // Update platform, if already exists if (existingDbPlatforms.TryGetValue(platform.id, out Platform dbPlatform)) { dbPlatform.DisplayName = platform.displayName; context.Entry(dbPlatform).State = EntityState.Modified; } else { // Add new platform dbPlatform = new Platform { Id = platform.id, DisplayName = platform.displayName, }; context.Entry(dbPlatform).State = EntityState.Added; } existingDbPlatforms.Remove(platform.id); } // Remove exceed platforms var objectStateManager = ((IObjectContextAdapter)context).ObjectContext.ObjectStateManager; foreach (var dbPlatform in existingDbPlatforms) { await context.Entry(dbPlatform.Value).Collection(p => p.Packages).LoadAsync(); foreach (var dbPackage in dbPlatform.Value.Packages.ToArray()) { objectStateManager.ChangeRelationshipState(dbPlatform.Value, dbPackage, p => p.Packages, EntityState.Deleted); } context.Entry(dbPlatform.Value).State = EntityState.Deleted; } await context.SaveChangesAsync(); } }
/// <summary> /// Checks the Authorization for an API request /// </summary> /// <param name="allowAppOnly">Defines whether an app-only request is allowed or not</param> public static void CheckRequestAuthorization(Boolean allowAppOnly) { // Read the current audience from the configuration var audience = AuthenticationConfig.Audience; // Read the permission scopes from the configuration var scope = AuthenticationConfig.ApiScope; // If there isn't a current user identity, there is a security issue if (ClaimsPrincipal.Current == null || ClaimsPrincipal.Current.Identity == null) { ThrowAuthorizationException(); } // Otherwise try to get the permission scope and the appid claims var scopeClaim = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/scope"); var appIdClaim = ClaimsPrincipal.Current.FindFirst("appid"); var audienceClaim = ClaimsPrincipal.Current.FindFirst("aud"); // Check if the audience claim matches the current audience, otherwise there is a security issue if (audienceClaim == null || audienceClaim.Value != audience) { ThrowAuthorizationException(); } // If we have the AppId, check it against the ConsumerApps collection if (appIdClaim != null) { var dbContext = new ProvisioningAppDBContext(); var consumerAppId = Guid.Parse(appIdClaim.Value); var consumerApp = dbContext.ConsumerApps.FirstOrDefault(a => a.Id == consumerAppId); if (consumerApp == null) { // If the AppId is not "supported", there is a security issue ThrowAuthorizationException(); } } else { // If we don't have the AppId, there is a security issue ThrowAuthorizationException(); } // If we're not allowed to accept an app-only request if (!allowAppOnly) { // and there is no scope claim or the scope claim is wrong, there is a security issue if (scopeClaim == null || (scopeClaim != null && scopeClaim.Value != scope)) { ThrowAuthorizationException(); } } }
private ProvisioningAppDBContext GetContext() { var context = new ProvisioningAppDBContext(); context.Configuration.ProxyCreationEnabled = false; context.Configuration.LazyLoadingEnabled = false; context.Configuration.AutoDetectChangesEnabled = false; return(context); }
private async Task SyncFirstReleaseTenants() { using (ProvisioningAppDBContext context = GetContext()) { // Find the category file ITemplateFile file = (await _cloneProvider.GetAsync(SYSTEM_PATH, WriteLog)).FindFile(TENANTS_NAME); if (file == null) { throw new InvalidOperationException($"Cannot find file {TENANTS_NAME}"); } // Deserialize the json var tenantsList = await file.DownloadAsJsonAsync(new { tenants = new[] { new { id = "", tenantName = "", referenceOwner = "" } } }); var existingDbTenants = context.Tenants.ToDictionary(t => t.Id, StringComparer.OrdinalIgnoreCase); foreach (var tenant in tenantsList.tenants) { // Update tenant, if already exists if (existingDbTenants.TryGetValue(tenant.id, out Tenant dbTenant)) { dbTenant.TenantName = tenant.tenantName; dbTenant.ReferenceOwner = tenant.referenceOwner; context.Entry(dbTenant).State = EntityState.Modified; } else { // Add new tenant dbTenant = new Tenant { Id = tenant.id, TenantName = tenant.tenantName, ReferenceOwner = tenant.referenceOwner }; context.Entry(dbTenant).State = EntityState.Added; } existingDbTenants.Remove(tenant.id); } // Remove exceed categories var objectStateManager = ((IObjectContextAdapter)context).ObjectContext.ObjectStateManager; foreach (var dbTenant in existingDbTenants) { context.Entry(dbTenant.Value).State = EntityState.Deleted; } await context.SaveChangesAsync(); } }
private async Task SyncContentPages() { using (ProvisioningAppDBContext context = GetContext()) { // Find the content pages files var files = await _cloneProvider.GetAsync(CONTENT_PATH, WriteLog); if (files == null || files.Count() == 0) { throw new InvalidOperationException($"Cannot find files in folder {CONTENT_PATH}"); } // Consider system pages only var existingDbContentPages = context.ContentPages .ToList() .Where(cp => cp.Id.StartsWith("system", StringComparison.InvariantCultureIgnoreCase)) .ToDictionary(cp => cp.Id, StringComparer.OrdinalIgnoreCase); foreach (ITemplateFile file in files) { // Get the file content String fileContent = await GetHtmlContentAsync(CONTENT_PATH, file.Path.Substring(file.Path.LastIndexOf('/') + 1)); // Update Content Page, if already exists if (existingDbContentPages.TryGetValue(file.Path, out ContentPage dbContentPage)) { dbContentPage.Content = fileContent; context.Entry(dbContentPage).State = EntityState.Modified; } else { // Add new Content Page dbContentPage = new ContentPage { Id = file.Path, Content = fileContent, }; context.Entry(dbContentPage).State = EntityState.Added; } existingDbContentPages.Remove(file.Path); } // Remove leftover pages var objectStateManager = ((IObjectContextAdapter)context).ObjectContext.ObjectStateManager; foreach (var dbContentPage in existingDbContentPages) { context.Entry(dbContentPage.Value).State = EntityState.Deleted; } await context.SaveChangesAsync(); } }
private static void CleanupCurrentActionItem(ProvisioningActionModel action, ProvisioningAppDBContext dbContext) { // Check if there is the action item for the current action var existingItem = dbContext.ProvisioningActionItems.FirstOrDefault(i => i.Id == action.CorrelationId); // And in case it does exist and it is not failed if (existingItem != null && !existingItem.FailedOn.HasValue) { // Delete it dbContext.ProvisioningActionItems.Remove(existingItem); dbContext.SaveChanges(); } }
public ActionResult ContentPage(String contentPageId) { ContentPageViewModel model = new ContentPageViewModel(); ProvisioningAppDBContext context = new ProvisioningAppDBContext(); var targetContentPageId = $"system/pages/{contentPageId}.md"; var contentPage = context.ContentPages.FirstOrDefault(cp => cp.Id == targetContentPageId); if (contentPage == null) { throw new ApplicationException("There is no Content Page with the provided contentPageId!"); } model.Content = contentPage.Content; return(View("ContentPage", model)); }
//private static async Task CleanupKeyVault() //{ // // Get the reference date for removal of keys (expired at least 2 hours ago) // var referenceDateTime = DateTime.Now.AddHours(-2); // // This job cleans up the expired keys from the Azure Key Vault // var vault = new KeyVaultService(); // // Get all the keys stored in Key Vault // var allKeys = await vault.ListKeysAsync(); // // Check for expired keys // foreach (var key in allKeys) // { // var currentKey = await vault.GetFullKeyAsync(key); // if (currentKey.Attributes.Expires <= referenceDateTime) // { // // The key is expired, let's remove it // await vault.RemoveKeyAsync(key); // // Log the action // Console.WriteLine($"Deleted key {key} that expired on {currentKey.Attributes.Expires}"); // } // else // { // Console.WriteLine($"Key {key} is not yet expired"); // } // // Delay to avoid throttling // System.Threading.Thread.Sleep(TimeSpan.FromSeconds(1)); // } //} //private static async Task CleanupSqlVault() //{ // Console.WriteLine("Cleaning up SQL Vault"); // // This job cleans up the expired keys from the Sql Azure Database // var sqlVault = new SqlServerVaultService(); // await sqlVault.CleanupTokensAsync(); // Console.WriteLine("Cleaned SQL Vault"); //} private static async Task CleanupSqlServer() { // Get the expired actions var dbContext = new ProvisioningAppDBContext(); var now = DateTime.Now; var expiredItems = dbContext.ProvisioningActionItems.Where(i => i.ExpiresOn <= now); // And delete any expired item foreach (var i in expiredItems) { Console.WriteLine($"Deleting action item {i.Id} because it is expired"); // Delete it dbContext.ProvisioningActionItems.Remove(i); } await dbContext.SaveChangesAsync(); }
public ActionResult CategoriesMenu(String returnUrl = null) { CategoriesMenuViewModel model = new CategoriesMenuViewModel(); // Let's see if we need to filter the output categories var slbHost = System.Configuration.ConfigurationManager.AppSettings["SPLBSiteHost"]; var testEnvironment = Boolean.Parse(ConfigurationManager.AppSettings["TestEnvironment"]); string targetPlatform = null; if (!String.IsNullOrEmpty(returnUrl) && !String.IsNullOrEmpty(slbHost) && returnUrl.Contains(slbHost)) { model.BaseLinksUrl = returnUrl.Substring(0, returnUrl.LastIndexOf(@"/") + 1); targetPlatform = "LOOKBOOK"; } else { model.BaseLinksUrl = String.Empty; targetPlatform = "SPPNP"; } // Get all the Categories together with the Packages ProvisioningAppDBContext context = new ProvisioningAppDBContext(); var tempCategories = context.Categories .AsNoTracking() .Where(c => c.Packages.Any( p => p.Visible && (testEnvironment || !p.Preview) && p.TargetPlatforms.Any(pf => pf.Id == targetPlatform) )) .OrderBy(c => c.Order) .Include("Packages") .ToList(); model.Categories = tempCategories; return(PartialView("CategoriesMenu", model)); }
private async Task <DomainModel.Package> GetPackageAsync(ITemplateFolder folder, ProvisioningAppDBContext context) { var items = await _cloneProvider.GetAsync(folder.Path, WriteLog); // Read settings file ITemplateFile settingsFile = items.OfType <ITemplateFile>().FindFile(SETTINGS_NAME); if (settingsFile == null) { WriteLog($"Cannot find file {SETTINGS_NAME}"); return(null); } #region Prepare the settings file and its outline var settings = await settingsFile.DownloadAsJsonAsync(new TemplateSettings()); #endregion // Read the package file ITemplateFile packageFile = items.FindFile(settings.packageFile); if (packageFile == null) { WriteLog($"Cannot find file {settings.packageFile}"); return(null); } #region Fix the Preview Image URLs // Fix the URLs in the metadata settings if (settings?.metadata?.displayInfo?.previewImages != null) { int previewImagesCount = settings.metadata.displayInfo.previewImages.Length; for (var n = 0; n < previewImagesCount; n++) { var previewImageUrl = settings.metadata.displayInfo.previewImages[n].url; settings.metadata.displayInfo.previewImages[n].url = ChangeUri(packageFile.DownloadUri, previewImageUrl); } } if (settings?.metadata?.displayInfo?.detailItemCategories != null) { int detailItemCategoriesCount = settings.metadata.displayInfo.detailItemCategories.Length; for (var n = 0; n < detailItemCategoriesCount; n++) { if (settings.metadata.displayInfo.detailItemCategories[n].items != null) { var detailItemCategoryItemsCount = settings.metadata.displayInfo.detailItemCategories[n].items.Length; for (var m = 0; m < detailItemCategoryItemsCount; m++) { var detailItemCategoryItemPreviewImageUrl = settings.metadata.displayInfo.detailItemCategories[n].items[m].previewImage; if (!String.IsNullOrEmpty(detailItemCategoryItemPreviewImageUrl)) { settings.metadata.displayInfo.detailItemCategories[n].items[m].previewImage = ChangeUri(packageFile.DownloadUri, detailItemCategoryItemPreviewImageUrl); } } } } } #endregion var package = new DomainModel.Package { Id = !string.IsNullOrEmpty(settings.templateId) ? new Guid(settings.templateId) : Guid.NewGuid(), PackageUrl = packageFile.DownloadUri.ToString(), // New properties for Wave2 Promoted = settings.promoted, Preview = settings.preview, TimesApplied = 0, PropertiesMetadata = JsonConvert.SerializeObject(settings.metadata), // New properties for Wave 5 Abstract = settings.@abstract, SortOrder = settings.sortOrder, SortOrderPromoted = settings.sortOrderPromoted, RepositoryRelativeUrl = folder.Path, MatchingSiteBaseTemplateId = settings.matchingSiteBaseTemplateId, ForceNewSite = settings.forceNewSite, Visible = settings.visible, PageTemplateId = settings.metadata?.displayInfo?.pageTemplateId, // New properties for Wave 12 DisplayName = settings.metadata?.displayInfo?.siteTitle, ForceExistingSite = settings.forceExistingSite, }; // Read the instructions.md and the provisioning.md files String instructionsContent = await GetHtmlContentAsync(folder.Path, INSTRUCTIONS_NAME); String provisioningContent = await GetHtmlContentAsync(folder.Path, PROVISIONING_NAME); package.Instructions = instructionsContent; package.ProvisionRecap = provisioningContent; // Read any pre-requirement content files var preRequirementContents = items.FindFiles(i => i.Path.ToLower().Contains("prerequirement-") && i.Path.EndsWith(".md", StringComparison.InvariantCultureIgnoreCase)); // Process content for pre-requirements, if any if (preRequirementContents != null) { // Get the existing content pages for the current folder var existingDbContentPages = context.ContentPages .ToList() .Where(cp => cp.Id.StartsWith(folder.Path, StringComparison.InvariantCultureIgnoreCase)) .ToDictionary(cp => cp.Id, StringComparer.OrdinalIgnoreCase); foreach (var prc in preRequirementContents) { // Get the file content String fileContent = await GetHtmlContentAsync(folder.Path, prc.Path.Substring(prc.Path.LastIndexOf('/') + 1)); // Update Content Page, if already exists if (existingDbContentPages.TryGetValue(prc.Path, out ContentPage dbContentPage)) { dbContentPage.Content = fileContent; context.Entry(dbContentPage).State = EntityState.Modified; } else { // Add new Content Page dbContentPage = new ContentPage { Id = prc.Path, Content = fileContent, }; context.Entry(dbContentPage).State = EntityState.Added; } existingDbContentPages.Remove(prc.Path); } // Remove leftover content pages, if any var objectStateManager = ((IObjectContextAdapter)context).ObjectContext.ObjectStateManager; foreach (var dbContentPage in existingDbContentPages) { context.Entry(dbContentPage.Value).State = EntityState.Deleted; } } // Find the categories to apply if (settings.categories != null && settings.categories.Length > 0) { var dbCategories = settings.categories.Select(c => { Category dbCategory = context.Categories.Find(c); if (dbCategory == null) { WriteLog($"Cannot find category with id {c}"); } return(dbCategory); }).ToArray(); package.Categories.AddRange(dbCategories); } // Find the platforms to apply if (settings.platforms != null && settings.platforms.Length > 0) { var dbPlatforms = settings.platforms.Select(p => { Platform dbPlatform = context.Platforms.Find(p); if (dbPlatform == null) { WriteLog($"Cannot find platform with id {p}"); } return(dbPlatform); }).ToArray(); package.TargetPlatforms.AddRange(dbPlatforms); } // Find then author and fill her/his information await FillAuthorAsync(package, packageFile.Path); // Open the package and set info await FillPackageAsync(package, packageFile); return(package); }
private static Boolean CheckIfActionIsAlreadyRunning(ProvisioningActionModel action, ProvisioningAppDBContext dbContext) { var result = false; var tenantId = Guid.Parse(action.TenantId); var packageId = Guid.Parse(action.PackageId); // Check if there is already a pending action item with the same settings and not yet expired var alreadyExistingItems = from i in dbContext.ProvisioningActionItems where i.TenantId == tenantId && i.PackageId == packageId && i.ExpiresOn > DateTime.Now && i.FailedOn == null select i; // Prepare the action properties as JSON var currentActionProperties = action.PackageProperties != null?JsonConvert.SerializeObject(action.PackageProperties) : null; // Verify if the same package, with the same properties is already running in the same tenant foreach (var item in alreadyExistingItems) { if (item.PackageProperties == currentActionProperties) { result = true; break; } } if (!result) { // Add a ProvisioningActionItem record for tracking purposes dbContext.ProvisioningActionItems.Add(new ProvisioningActionItem { Id = action.CorrelationId, PackageId = packageId, TenantId = tenantId, PackageProperties = action.PackageProperties != null ? JsonConvert.SerializeObject(action.PackageProperties) : null, CreatedOn = DateTime.Now, ExpiresOn = DateTime.Now.AddHours(2), }); dbContext.SaveChanges(); } return(result); }
private async Task SyncTemplatesAsync(string path, PackageType type) { using (ProvisioningAppDBContext context = GetContext()) { var objectStateManager = ((IObjectContextAdapter)context).ObjectContext.ObjectStateManager; // Find packages inside folder IEnumerable <ITemplateItem> items = await _cloneProvider.GetAsync(path, WriteLog); var packages = await FindPackagesAsync(items, context); var existingDbPackages = context.Packages .Include(c => c.Categories) .Include(c => c.TargetPlatforms) // .Where(p => p.PackageType == type) .ToDictionary(c => c.Id); foreach (DomainModel.Package package in packages) { package.PackageType = type; // Update package, if already exists if (existingDbPackages.TryGetValue(package.Id, out DomainModel.Package dbPackage)) { // Copy info into existing item dbPackage.DisplayName = package.DisplayName; dbPackage.Author = package.Author; dbPackage.AuthorLink = package.AuthorLink; dbPackage.Description = package.Description; dbPackage.ImagePreviewUrl = package.ImagePreviewUrl; dbPackage.PackageUrl = package.PackageUrl; dbPackage.Version = package.Version; // New properties for Wave2 dbPackage.Promoted = package.Promoted; dbPackage.Preview = package.Preview; dbPackage.PropertiesMetadata = package.PropertiesMetadata; // Keep times applied from the DB // dbPackage.TimesApplied = package.TimesApplied; // New properties for Wave 4 dbPackage.Instructions = package.Instructions; dbPackage.ProvisionRecap = package.ProvisionRecap; // New properties for Wave 5 dbPackage.SortOrder = package.SortOrder; dbPackage.SortOrderPromoted = package.SortOrderPromoted; dbPackage.RepositoryRelativeUrl = package.RepositoryRelativeUrl; dbPackage.Abstract = package.Abstract; // New properties for Wave 6 dbPackage.ForceNewSite = package.ForceNewSite; dbPackage.MatchingSiteBaseTemplateId = package.MatchingSiteBaseTemplateId; // New properties for Wave 7 dbPackage.Visible = package.Visible; // New properties for Wave 8 dbPackage.PackageProperties = package.PackageProperties; // Wave 12 - Allowed to move from one type to another dbPackage.PackageType = package.PackageType; dbPackage.ForceExistingSite = package.ForceExistingSite; context.Entry(dbPackage).State = EntityState.Modified; // Add new categories foreach (var c in package.Categories.Except(dbPackage.Categories).ToArray()) { objectStateManager.ChangeRelationshipState(dbPackage, c, d => d.Categories, EntityState.Added); } // Remove old categories foreach (var c in dbPackage.Categories.Except(package.Categories).ToArray()) { objectStateManager.ChangeRelationshipState(dbPackage, c, d => d.Categories, EntityState.Deleted); } // Add new platforms foreach (var c in package.TargetPlatforms.Except(dbPackage.TargetPlatforms).ToArray()) { objectStateManager.ChangeRelationshipState(dbPackage, c, d => d.TargetPlatforms, EntityState.Added); } // Remove old platforms foreach (var c in dbPackage.TargetPlatforms.Except(package.TargetPlatforms).ToArray()) { objectStateManager.ChangeRelationshipState(dbPackage, c, d => d.TargetPlatforms, EntityState.Deleted); } existingDbPackages.Remove(dbPackage.Id); } else { // Add new package context.Entry(package).State = EntityState.Added; } } // Remove leftover packages from current category foreach (var dbPackage in existingDbPackages.Where(p => p.Value.PackageType == type)) { await context.Entry(dbPackage.Value).Collection(d => d.Categories).LoadAsync(); foreach (var dbCategory in dbPackage.Value.Categories.ToArray()) { objectStateManager.ChangeRelationshipState(dbPackage.Value, dbCategory, d => d.Categories, EntityState.Deleted); } context.Entry(dbPackage.Value).State = EntityState.Deleted; } await context.SaveChangesAsync(); } }
private async Task <IReadOnlyList <DomainModel.Package> > FindPackagesAsync(IEnumerable <ITemplateItem> items, ProvisioningAppDBContext context) { var packages = new List <DomainModel.Package>(); foreach (ITemplateFolder folder in items.OfType <ITemplateFolder>()) { WriteLog($"Processing folder {folder.Path}..."); try { DomainModel.Package package = await GetPackageAsync(folder, context); if (package == null) { continue; } if (!String.IsNullOrEmpty(package.DisplayName)) { packages.Add(package); } } catch (Exception ex) { WriteLog($"Error processing folder {folder.Path}: {ex.Message} - {ex.StackTrace}"); } } return(packages); }
private static void MarkCurrentActionItemAsFailed(ProvisioningActionModel action, ProvisioningAppDBContext dbContext) { // Check if there is the action item for the current action var existingItem = dbContext.ProvisioningActionItems.FirstOrDefault(i => i.Id == action.CorrelationId); // And in case it does exist if (existingItem != null) { // Set the failure date and time existingItem.FailedOn = DateTime.Now; // Update the persistence storage dbContext.SaveChanges(); } }
private async Task SyncPageTemplates() { using (ProvisioningAppDBContext context = GetContext()) { // Find the category file var pageTemplatesFolders = await _cloneProvider.GetAsync(PAGE_TEMPLATES_PATH, WriteLog); if (pageTemplatesFolders == null || pageTemplatesFolders.Count() == 0) { throw new InvalidOperationException($"Cannot find Page Template folders in folder {PAGE_TEMPLATES_PATH}"); } var existingDbPageTemplates = context.PageTemplates.ToDictionary(cp => cp.Id, StringComparer.OrdinalIgnoreCase); foreach (ITemplateFolder pageTemplateFolder in pageTemplatesFolders) { // Prepare content variables String htmlContent = null; String cssContent = null; // Get the page template folder content files var pageTemplateFiles = await _cloneProvider.GetAsync(pageTemplateFolder.Path, WriteLog); if (pageTemplateFiles == null || pageTemplateFiles.Count() == 0) { throw new InvalidOperationException($"Cannot find template files in Page Template folder {pageTemplateFolder.Path}"); } foreach (ITemplateFile pageTemplateFile in pageTemplateFiles) { // If the content file is an HTML file if (pageTemplateFile.Path.EndsWith(".html", StringComparison.InvariantCultureIgnoreCase)) { // Set the HTML content htmlContent = await GetFileContentAsync(pageTemplateFolder.Path, pageTemplateFile.Path.Substring(pageTemplateFile.Path.LastIndexOf('/') + 1)); } else if (pageTemplateFile.Path.EndsWith(".css", StringComparison.InvariantCultureIgnoreCase)) { // Set the CSS content cssContent = await GetFileContentAsync(pageTemplateFolder.Path, pageTemplateFile.Path.Substring(pageTemplateFile.Path.LastIndexOf('/') + 1)); } if (!String.IsNullOrEmpty(htmlContent) && !String.IsNullOrEmpty(cssContent)) { // If we have both HTML and CSS content, just break the foreach loop break; } } // Update Page Template, if already exists if (existingDbPageTemplates.TryGetValue(pageTemplateFolder.Path, out PageTemplate dbPageTemplate)) { dbPageTemplate.Html = htmlContent; dbPageTemplate.Css = cssContent; context.Entry(dbPageTemplate).State = EntityState.Modified; } else { // Add new Content Page dbPageTemplate = new PageTemplate { Id = pageTemplateFolder.Path, Html = htmlContent, Css = cssContent, }; context.Entry(dbPageTemplate).State = EntityState.Added; } existingDbPageTemplates.Remove(pageTemplateFolder.Path); } // Remove exceed categories var objectStateManager = ((IObjectContextAdapter)context).ObjectContext.ObjectStateManager; foreach (var dbPageTemplate in existingDbPageTemplates) { context.Entry(dbPageTemplate.Value).State = EntityState.Deleted; } await context.SaveChangesAsync(); } }
private async Task <DomainModel.Package> GetPackageAsync(ITemplateFolder folder, ProvisioningAppDBContext context) { var items = await _cloneProvider.GetAsync(folder.Path, WriteLog); // Read settings file ITemplateFile settingsFile = items.OfType <ITemplateFile>().FindFile(SETTINGS_NAME); if (settingsFile == null) { WriteLog($"Cannot find file {SETTINGS_NAME}"); return(null); } var settings = await settingsFile.DownloadAsJsonAsync(new { @abstract = "", sortOrder = 0, categories = new string[0], packageFile = "", promoted = false, sortOrderPromoted = 0, preview = false, metadata = new { properties = new[] { new { name = "", caption = "", description = "", editor = "", editorSettings = "", } } } }); // Read the package file ITemplateFile packageFile = items.FindFile(settings.packageFile); if (packageFile == null) { WriteLog($"Cannot find file {settings.packageFile}"); return(null); } var package = new DomainModel.Package { Id = Guid.NewGuid(), PackageUrl = packageFile.DownloadUri.ToString(), // New properties for Wave2 Promoted = settings.promoted, Preview = settings.preview, TimesApplied = 0, PropertiesMetadata = JsonConvert.SerializeObject(settings.metadata), // New properties for Wave 5 Abstract = settings.@abstract, SortOrder = settings.sortOrder, SortOrderPromoted = settings.sortOrderPromoted, RepositoryRelativeUrl = folder.Path, }; // Read the instructions.md and the provisioning.md files String instructionsContent = await GetHtmlContentAsync(folder.Path, INSTRUCTIONS_NAME); String provisioningContent = await GetHtmlContentAsync(folder.Path, PROVISIONING_NAME); package.Instructions = instructionsContent; package.ProvisionRecap = provisioningContent; // Find the categories to apply var dbCategories = settings.categories.Select(c => { Category dbCategory = context.Categories.Find(c); if (dbCategory == null) { WriteLog($"Cannot find category with id {c}"); } return(dbCategory); }).ToArray(); package.Categories.AddRange(dbCategories); // Find then author and fill his informations await FillAuthorAsync(package, packageFile.Path); // Open the package and set info await FillPackageAsync(package, packageFile); return(package); }
private async Task <DomainModel.Package> GetPackageAsync(ITemplateFolder folder, ProvisioningAppDBContext context) { var items = await _cloneProvider.GetAsync(folder.Path, WriteLog); // Read settings file ITemplateFile settingsFile = items.OfType <ITemplateFile>().FindFile(SETTINGS_NAME); if (settingsFile == null) { WriteLog($"Cannot find file {SETTINGS_NAME}"); return(null); } #region Prepare the settings file and its outline var settings = await settingsFile.DownloadAsJsonAsync(new TemplateSettings()); #endregion // Read the package file ITemplateFile packageFile = items.FindFile(settings.packageFile); if (packageFile == null) { WriteLog($"Cannot find file {settings.packageFile}"); return(null); } #region Fix the Preview Image URLs // Fix the URLs in the metadata settings if (settings?.metadata?.displayInfo?.previewImages != null) { int previewImagesCount = settings.metadata.displayInfo.previewImages.Length; for (var n = 0; n < previewImagesCount; n++) { var previewImageUrl = settings.metadata.displayInfo.previewImages[n].url; settings.metadata.displayInfo.previewImages[n].url = ChangeUri(packageFile.DownloadUri, previewImageUrl); } } if (settings?.metadata?.displayInfo?.detailItemCategories != null) { int detailItemCategoriesCount = settings.metadata.displayInfo.detailItemCategories.Length; for (var n = 0; n < detailItemCategoriesCount; n++) { if (settings.metadata.displayInfo.detailItemCategories[n].items != null) { var detailItemCategoryItemsCount = settings.metadata.displayInfo.detailItemCategories[n].items.Length; for (var m = 0; m < detailItemCategoryItemsCount; m++) { var detailItemCategoryItemPreviewImageUrl = settings.metadata.displayInfo.detailItemCategories[n].items[m].previewImage; if (!String.IsNullOrEmpty(detailItemCategoryItemPreviewImageUrl)) { settings.metadata.displayInfo.detailItemCategories[n].items[m].previewImage = ChangeUri(packageFile.DownloadUri, detailItemCategoryItemPreviewImageUrl); } } } } } #endregion var package = new DomainModel.Package { Id = Guid.NewGuid(), PackageUrl = packageFile.DownloadUri.ToString(), // New properties for Wave2 Promoted = settings.promoted, Preview = settings.preview, TimesApplied = 0, PropertiesMetadata = JsonConvert.SerializeObject(settings.metadata), // New properties for Wave 5 Abstract = settings.@abstract, SortOrder = settings.sortOrder, SortOrderPromoted = settings.sortOrderPromoted, RepositoryRelativeUrl = folder.Path, MatchingSiteBaseTemplateId = settings.matchingSiteBaseTemplateId, ForceNewSite = settings.forceNewSite, Visible = settings.visible, PageTemplateId = settings.metadata?.displayInfo?.pageTemplateId, }; // Read the instructions.md and the provisioning.md files String instructionsContent = await GetHtmlContentAsync(folder.Path, INSTRUCTIONS_NAME); String provisioningContent = await GetHtmlContentAsync(folder.Path, PROVISIONING_NAME); package.Instructions = instructionsContent; package.ProvisionRecap = provisioningContent; // Find the categories to apply if (settings.categories != null && settings.categories.Length > 0) { var dbCategories = settings.categories.Select(c => { Category dbCategory = context.Categories.Find(c); if (dbCategory == null) { WriteLog($"Cannot find category with id {c}"); } return(dbCategory); }).ToArray(); package.Categories.AddRange(dbCategories); } // Find the platforms to apply if (settings.platforms != null && settings.platforms.Length > 0) { var dbPlatforms = settings.platforms.Select(p => { Platform dbPlatform = context.Platforms.Find(p); if (dbPlatform == null) { WriteLog($"Cannot find platform with id {p}"); } return(dbPlatform); }).ToArray(); package.TargetPlatforms.AddRange(dbPlatforms); } // Find then author and fill his informations await FillAuthorAsync(package, packageFile.Path); // Open the package and set info await FillPackageAsync(package, packageFile); return(package); }
public static async Task RunAsync([QueueTrigger("actions")] ProvisioningActionModel action, TextWriter log) { var startProvisioning = DateTime.Now; String provisioningEnvironment = ConfigurationManager.AppSettings["SPPA:ProvisioningEnvironment"]; log.WriteLine($"Processing queue trigger function for tenant {action.TenantId}"); log.WriteLine($"PnP Correlation ID: {action.CorrelationId.ToString()}"); // Instantiate and use the telemetry model TelemetryUtility telemetry = new TelemetryUtility((s) => { log.WriteLine(s); }); Dictionary <string, string> telemetryProperties = new Dictionary <string, string>(); // Configure telemetry properties // telemetryProperties.Add("UserPrincipalName", action.UserPrincipalName); telemetryProperties.Add("TenantId", action.TenantId); telemetryProperties.Add("PnPCorrelationId", action.CorrelationId.ToString()); telemetryProperties.Add("TargetSiteAlreadyExists", action.TargetSiteAlreadyExists.ToString()); telemetryProperties.Add("TargetSiteBaseTemplateId", action.TargetSiteBaseTemplateId); // Get a reference to the data context ProvisioningAppDBContext dbContext = new ProvisioningAppDBContext(); try { // Log telemetry event telemetry?.LogEvent("ProvisioningFunction.Start"); if (CheckIfActionIsAlreadyRunning(action, dbContext)) { throw new ConcurrentProvisioningException("The requested package is currently provisioning in the selected target tenant and cannot be applied in parallel. Please wait for the previous provisioning action to complete."); } var tokenId = $"{action.TenantId}-{action.UserPrincipalName.ToLower().GetHashCode()}-{action.ActionType.ToString().ToLower()}-{provisioningEnvironment}"; // Retrieve the SPO target tenant via Microsoft Graph var graphAccessToken = await ProvisioningAppManager.AccessTokenProvider.GetAccessTokenAsync( tokenId, "https://graph.microsoft.com/", ConfigurationManager.AppSettings[$"{action.ActionType}:ClientId"], ConfigurationManager.AppSettings[$"{action.ActionType}:ClientSecret"], ConfigurationManager.AppSettings[$"{action.ActionType}:AppUrl"]); log.WriteLine($"Retrieved target Microsoft Graph Access Token."); if (!String.IsNullOrEmpty(graphAccessToken)) { #region Get current context data (User, SPO Tenant, SPO Access Token) // Get the currently connected user name and email (UPN) var jwtAccessToken = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken(graphAccessToken); String delegatedUPN = String.Empty; var upnClaim = jwtAccessToken.Claims.FirstOrDefault(c => c.Type == "upn"); if (upnClaim != null && !String.IsNullOrEmpty(upnClaim.Value)) { delegatedUPN = upnClaim.Value; } String delegatedUserName = String.Empty; var nameClaim = jwtAccessToken.Claims.FirstOrDefault(c => c.Type == "name"); if (nameClaim != null && !String.IsNullOrEmpty(nameClaim.Value)) { delegatedUserName = nameClaim.Value; } // Determine the URL of the root SPO site for the current tenant var rootSiteJson = HttpHelper.MakeGetRequestForString("https://graph.microsoft.com/v1.0/sites/root", graphAccessToken); SharePointSite rootSite = JsonConvert.DeserializeObject <SharePointSite>(rootSiteJson); String spoTenant = rootSite.WebUrl; log.WriteLine($"Target SharePoint Online Tenant: {spoTenant}"); // Configure telemetry properties telemetryProperties.Add("SPOTenant", spoTenant); // Retrieve the SPO Access Token var spoAccessToken = await ProvisioningAppManager.AccessTokenProvider.GetAccessTokenAsync( tokenId, rootSite.WebUrl, ConfigurationManager.AppSettings[$"{action.ActionType}:ClientId"], ConfigurationManager.AppSettings[$"{action.ActionType}:ClientSecret"], ConfigurationManager.AppSettings[$"{action.ActionType}:AppUrl"]); log.WriteLine($"Retrieved target SharePoint Online Access Token."); #endregion // Connect to SPO, create and provision site AuthenticationManager authManager = new AuthenticationManager(); using (ClientContext context = authManager.GetAzureADAccessTokenAuthenticatedContext(spoTenant, spoAccessToken)) { // Telemetry and startup var web = context.Web; context.ClientTag = $"SPDev:ProvisioningPortal-{provisioningEnvironment}"; context.Load(web, w => w.Title, w => w.Id); await context.ExecuteQueryAsync(); // Save the current SPO Correlation ID telemetryProperties.Add("SPOCorrelationId", context.TraceCorrelationId); log.WriteLine($"SharePoint Online Root Site Collection title: {web.Title}"); #region Store the main site URL in KeyVault // Store the main site URL in the vault var vault = ProvisioningAppManager.SecurityTokensServiceProvider; // Read any existing properties for the current tenantId var properties = await vault.GetAsync(tokenId); if (properties == null) { // If there are no properties, create a new dictionary properties = new Dictionary <String, String>(); } // Set/Update the RefreshToken value properties["SPORootSite"] = spoTenant; // Add or Update the Key Vault accordingly await vault.AddOrUpdateAsync(tokenId, properties); #endregion #region Provision the package var package = dbContext.Packages.FirstOrDefault(p => p.Id == new Guid(action.PackageId)); if (package != null) { // Update the Popularity of the package package.TimesApplied++; dbContext.SaveChanges(); #region Get the Provisioning Hierarchy file // Determine reference path variables var blobConnectionString = ConfigurationManager.AppSettings["BlobTemplatesProvider:ConnectionString"]; var blobContainerName = ConfigurationManager.AppSettings["BlobTemplatesProvider:ContainerName"]; var packageFileName = package.PackageUrl.Substring(package.PackageUrl.LastIndexOf('/') + 1); var packageFileUri = new Uri(package.PackageUrl); var packageFileRelativePath = packageFileUri.AbsolutePath.Substring(2 + blobContainerName.Length); var packageFileRelativeFolder = packageFileRelativePath.Substring(0, packageFileRelativePath.LastIndexOf('/')); // Configure telemetry properties telemetryProperties.Add("PackageFileName", packageFileName); telemetryProperties.Add("PackageFileUri", packageFileUri.ToString()); // Read the main provisioning file from the Blob Storage CloudStorageAccount csa; if (!CloudStorageAccount.TryParse(blobConnectionString, out csa)) { throw new ArgumentException("Cannot create cloud storage account from given connection string."); } CloudBlobClient blobClient = csa.CreateCloudBlobClient(); CloudBlobContainer blobContainer = blobClient.GetContainerReference(blobContainerName); var blockBlob = blobContainer.GetBlockBlobReference(packageFileRelativePath); // Crate an in-memory copy of the source stream MemoryStream mem = new MemoryStream(); await blockBlob.DownloadToStreamAsync(mem); mem.Position = 0; // Prepare the output hierarchy ProvisioningHierarchy hierarchy = null; if (packageFileName.EndsWith(".xml", StringComparison.InvariantCultureIgnoreCase)) { // That's an XML Provisioning Template file XDocument xml = XDocument.Load(mem); mem.Position = 0; // Deserialize the stream into a provisioning hierarchy reading any // dependecy with the Azure Blob Storage connector var formatter = XMLPnPSchemaFormatter.GetSpecificFormatter(xml.Root.Name.NamespaceName); var templateLocalFolder = $"{blobContainerName}/{packageFileRelativeFolder}"; var provider = new XMLAzureStorageTemplateProvider( blobConnectionString, templateLocalFolder); formatter.Initialize(provider); // Get the full hierarchy hierarchy = ((IProvisioningHierarchyFormatter)formatter).ToProvisioningHierarchy(mem); hierarchy.Connector = provider.Connector; } else if (packageFileName.EndsWith(".pnp", StringComparison.InvariantCultureIgnoreCase)) { // That's a PnP Package file // Get a provider based on the in-memory .PNP Open XML file OpenXMLConnector openXmlConnector = new OpenXMLConnector(mem); XMLTemplateProvider provider = new XMLOpenXMLTemplateProvider( openXmlConnector); // Get the .xml provisioning template file name var xmlTemplateFileName = openXmlConnector.Info?.Properties?.TemplateFileName ?? packageFileName.Substring(packageFileName.LastIndexOf('/') + 1) .ToLower().Replace(".pnp", ".xml"); // Get the full hierarchy hierarchy = provider.GetHierarchy(xmlTemplateFileName); hierarchy.Connector = provider.Connector; } #endregion #region Apply the template // Prepare variable to collect provisioned sites var provisionedSites = new List <Tuple <String, String> >(); // If we have a hierarchy with at least one Sequence if (hierarchy != null) // && hierarchy.Sequences != null && hierarchy.Sequences.Count > 0) { Console.WriteLine($"Provisioning hierarchy \"{hierarchy.DisplayName}\""); var tenantUrl = UrlUtilities.GetTenantAdministrationUrl(context.Url); // Retrieve the SPO Access Token var spoAdminAccessToken = await ProvisioningAppManager.AccessTokenProvider.GetAccessTokenAsync( tokenId, tenantUrl, ConfigurationManager.AppSettings[$"{action.ActionType}:ClientId"], ConfigurationManager.AppSettings[$"{action.ActionType}:ClientSecret"], ConfigurationManager.AppSettings[$"{action.ActionType}:AppUrl"]); log.WriteLine($"Retrieved target SharePoint Online Admin Center Access Token."); using (var tenantContext = authManager.GetAzureADAccessTokenAuthenticatedContext(tenantUrl, spoAdminAccessToken)) { using (var pnpTenantContext = PnPClientContext.ConvertFrom(tenantContext)) { var tenant = new Microsoft.Online.SharePoint.TenantAdministration.Tenant(pnpTenantContext); // Prepare a dictionary to hold the access tokens var accessTokens = new Dictionary <String, String>(); // Prepare logging for hierarchy application var ptai = new ProvisioningTemplateApplyingInformation(); ptai.MessagesDelegate += delegate(string message, ProvisioningMessageType messageType) { log.WriteLine($"{messageType} - {message}"); }; ptai.ProgressDelegate += delegate(string message, int step, int total) { log.WriteLine($"{step:00}/{total:00} - {message}"); }; ptai.SiteProvisionedDelegate += delegate(string title, string url) { log.WriteLine($"Fully provisioned site '{title}' with URL: {url}"); var provisionedSite = new Tuple <string, string>(title, url); if (!provisionedSites.Contains(provisionedSite)) { provisionedSites.Add(provisionedSite); } }; //#if !DEBUG // // Set the default delay for sites creations to 5 mins // ptai.DelayAfterModernSiteCreation = 60 * 5; //#endif // Configure the OAuth Access Tokens for the client context accessTokens.Add(new Uri(tenantUrl).Authority, spoAdminAccessToken); accessTokens.Add(new Uri(spoTenant).Authority, spoAccessToken); // Configure the OAuth Access Tokens for the PnPClientContext, too pnpTenantContext.PropertyBag["AccessTokens"] = accessTokens; ptai.AccessTokens = accessTokens; #region Theme handling // Process the graphical Theme if (action.ApplyTheme) { // If we don't have any custom Theme if (!action.ApplyCustomTheme) { // Associate the selected already existing Theme to all the sites of the hierarchy foreach (var sc in hierarchy.Sequences[0].SiteCollections) { sc.Theme = action.SelectedTheme; foreach (var s in sc.Sites) { UpdateChildrenSitesTheme(s, action.SelectedTheme); } } } } #endregion // Configure provisioning parameters if (action.PackageProperties != null) { foreach (var key in action.PackageProperties.Keys) { if (hierarchy.Parameters.ContainsKey(key.ToString())) { hierarchy.Parameters[key.ToString()] = action.PackageProperties[key].ToString(); } else { hierarchy.Parameters.Add(key.ToString(), action.PackageProperties[key].ToString()); } // Configure telemetry properties telemetryProperties.Add($"PackageProperty.{key}", action.PackageProperties[key].ToString()); } } // Log telemetry event telemetry?.LogEvent("ProvisioningFunction.BeginProvisioning", telemetryProperties); // Define a PnPProvisioningContext scope to share the security context across calls using (var pnpProvisioningContext = new PnPProvisioningContext(async(r, s) => { if (accessTokens.ContainsKey(r)) { // In this scenario we just use the dictionary of access tokens // in fact the overall operation for sure will take less than 1 hour return(await Task.FromResult(accessTokens[r])); } else { // Try to get a fresh new Access Token var token = await ProvisioningAppManager.AccessTokenProvider.GetAccessTokenAsync( tokenId, $"https://{r}", ConfigurationManager.AppSettings[$"{action.ActionType}:ClientId"], ConfigurationManager.AppSettings[$"{action.ActionType}:ClientSecret"], ConfigurationManager.AppSettings[$"{action.ActionType}:AppUrl"]); accessTokens.Add(r, token); return(token); } })) { // Configure the webhooks, if any if (action.Webhooks != null && action.Webhooks.Count > 0) { foreach (var t in hierarchy.Templates) { foreach (var wh in action.Webhooks) { AddProvisioningTemplateWebhook(t, wh, ProvisioningTemplateWebhookKind.ProvisioningTemplateStarted); AddProvisioningTemplateWebhook(t, wh, ProvisioningTemplateWebhookKind.ObjectHandlerProvisioningStarted); AddProvisioningTemplateWebhook(t, wh, ProvisioningTemplateWebhookKind.ObjectHandlerProvisioningCompleted); AddProvisioningTemplateWebhook(t, wh, ProvisioningTemplateWebhookKind.ProvisioningTemplateCompleted); AddProvisioningTemplateWebhook(t, wh, ProvisioningTemplateWebhookKind.ExceptionOccurred); } } foreach (var wh in action.Webhooks) { AddProvisioningWebhook(hierarchy, wh, ProvisioningTemplateWebhookKind.ProvisioningStarted); AddProvisioningWebhook(hierarchy, wh, ProvisioningTemplateWebhookKind.ProvisioningCompleted); AddProvisioningWebhook(hierarchy, wh, ProvisioningTemplateWebhookKind.ProvisioningExceptionOccurred); } } // Apply the hierarchy log.WriteLine($"Hierarchy Provisioning Started: {DateTime.Now:hh.mm.ss}"); tenant.ApplyProvisionHierarchy(hierarchy, (hierarchy.Sequences != null && hierarchy.Sequences.Count > 0) ? hierarchy.Sequences[0].ID : null, ptai); log.WriteLine($"Hierarchy Provisioning Completed: {DateTime.Now:hh.mm.ss}"); } if (action.ApplyTheme && action.ApplyCustomTheme) { if (!String.IsNullOrEmpty(action.ThemePrimaryColor) && !String.IsNullOrEmpty(action.ThemeBodyTextColor) && !String.IsNullOrEmpty(action.ThemeBodyBackgroundColor)) { log.WriteLine($"Applying custom Theme to provisioned sites"); #region Palette generation for Theme var jsonPalette = ThemeUtility.GetThemeAsJSON( action.ThemePrimaryColor, action.ThemeBodyTextColor, action.ThemeBodyBackgroundColor); #endregion // Apply the custom theme to all of the provisioned sites foreach (var ps in provisionedSites) { using (var provisionedSiteContext = authManager.GetAzureADAccessTokenAuthenticatedContext(ps.Item2, spoAccessToken)) { if (provisionedSiteContext.Web.ApplyTheme(jsonPalette)) { log.WriteLine($"Custom Theme applied on site '{ps.Item1}' with URL: {ps.Item2}"); } else { log.WriteLine($"Failed to apply custom Theme on site '{ps.Item1}' with URL: {ps.Item2}"); } } } } } // Log telemetry event telemetry?.LogEvent("ProvisioningFunction.EndProvisioning", telemetryProperties); // Notify user about the provisioning outcome if (!String.IsNullOrEmpty(action.NotificationEmail)) { var appOnlyAccessToken = await ProvisioningAppManager.AccessTokenProvider.GetAppOnlyAccessTokenAsync( "https://graph.microsoft.com/", ConfigurationManager.AppSettings["OfficeDevPnP:TenantId"], ConfigurationManager.AppSettings["OfficeDevPnP:ClientId"], ConfigurationManager.AppSettings["OfficeDevPnP:ClientSecret"], ConfigurationManager.AppSettings["OfficeDevPnP:AppUrl"]); MailHandler.SendMailNotification( "ProvisioningCompleted", action.NotificationEmail, null, new { TemplateName = action.DisplayName, ProvisionedSites = provisionedSites, }, appOnlyAccessToken); } // Log reporting event (1 = Success) LogReporting(action, provisioningEnvironment, startProvisioning, package, 1); } } } else { throw new ApplicationException($"The requested package does not contain a valid PnP Hierarchy!"); } #endregion } else { throw new ApplicationException($"Cannot find the package with ID: {action.PackageId}"); } #endregion #region Process any children items // If there are children items if (action.ChildrenItems != null && action.ChildrenItems.Count > 0) { // Prepare any further child provisioning request action.PackageId = action.ChildrenItems[0].PackageId; action.PackageProperties = action.ChildrenItems[0].Parameters; action.ChildrenItems.RemoveAt(0); // Enqueue any further child provisioning request await ProvisioningAppManager.EnqueueProvisioningRequest(action); } #endregion log.WriteLine($"Function successfully executed!"); // Log telemetry event telemetry?.LogEvent("ProvisioningFunction.End", telemetryProperties); } } else { var noTokensErrorMessage = $"Cannot retrieve Refresh Token or Access Token for {action.CorrelationId} in tenant {action.TenantId}!"; log.WriteLine(noTokensErrorMessage); throw new ApplicationException(noTokensErrorMessage); } } catch (Exception ex) { // Skip logging exception for Recycled Site if (ex is RecycledSiteException) { // rather log an event telemetry?.LogEvent("ProvisioningFunction.RecycledSite", telemetryProperties); // Log reporting event (3 = RecycledSite) LogReporting(action, provisioningEnvironment, startProvisioning, null, 3, ex.ToDetailedString()); } // Skip logging exception for Concurrent Provisioning else if (ex is ConcurrentProvisioningException) { // rather log an event telemetry?.LogEvent("ProvisioningFunction.ConcurrentProvisioning", telemetryProperties); // Log reporting event (4 = ConcurrentProvisioningException) LogReporting(action, provisioningEnvironment, startProvisioning, null, 4, ex.ToDetailedString()); } else { // Log telemetry event telemetry?.LogException(ex, "ProvisioningFunction.RunAsync", telemetryProperties); // Log reporting event (2 = Failed) LogReporting(action, provisioningEnvironment, startProvisioning, null, 2, ex.ToDetailedString()); } if (!String.IsNullOrEmpty(action.NotificationEmail)) { var appOnlyAccessToken = await ProvisioningAppManager.AccessTokenProvider.GetAppOnlyAccessTokenAsync( "https://graph.microsoft.com/", ConfigurationManager.AppSettings["OfficeDevPnP:TenantId"], ConfigurationManager.AppSettings["OfficeDevPnP:ClientId"], ConfigurationManager.AppSettings["OfficeDevPnP:ClientSecret"], ConfigurationManager.AppSettings["OfficeDevPnP:AppUrl"]); // Notify user about the provisioning outcome MailHandler.SendMailNotification( "ProvisioningFailed", action.NotificationEmail, null, new { TemplateName = action.DisplayName, ExceptionDetails = SimplifyException(ex), PnPCorrelationId = action.CorrelationId.ToString(), }, appOnlyAccessToken); } ProcessWebhooksExceptionNotification(action, ex); // Track the failure in the local action log MarkCurrentActionItemAsFailed(action, dbContext); throw ex; } finally { // Try to cleanup the pending action item, if any CleanupCurrentActionItem(action, dbContext); telemetry?.Flush(); } }