private static void LogReporting(ProvisioningActionModel action, string provisioningEnvironment, DateTime startProvisioning, DomainModel.Package package, Int32 outcome, String details = null) { // Prepare the reporting event var provisioningEvent = new { EventId = action.CorrelationId, EventStartDateTime = startProvisioning, EventEndDateTime = DateTime.Now, EventOutcome = outcome, EventDetails = details, EventFromProduction = provisioningEnvironment.ToUpper() == "PROD" ? 1 : 0, TemplateId = action.PackageId, TemplateDisplayName = package?.DisplayName, }; try { // Make the Azure Function call for reporting HttpHelper.MakePostRequest(ConfigurationManager.AppSettings["SPPA:ReportingFunctionUrl"], provisioningEvent, "application/json", null); } catch { // Intentionally ignore any reporting issue } }
private static async Task LoadThemesFromTenant(ProvisioningActionModel model, string tokenId, SharePointSite rootSite, string graphAccessToken) { // Retrieve the SPO URL for the Admin Site var adminSiteUrl = rootSite.WebUrl.Replace(".sharepoint.com", "-admin.sharepoint.com"); // Retrieve the SPO Access Token var spoAccessToken = await ProvisioningAppManager.AccessTokenProvider.GetAccessTokenAsync( tokenId, adminSiteUrl, ConfigurationManager.AppSettings["ida:ClientId"], ConfigurationManager.AppSettings["ida:ClientSecret"], ConfigurationManager.AppSettings["ida:AppUrl"]); // Connect to SPO and retrieve the list of available Themes AuthenticationManager authManager = new AuthenticationManager(); using (ClientContext spoContext = authManager.GetAzureADAccessTokenAuthenticatedContext(adminSiteUrl, spoAccessToken)) { TenantAdmin.Tenant tenant = new TenantAdmin.Tenant(spoContext); var themes = tenant.GetAllTenantThemes(); spoContext.Load(themes); spoContext.ExecuteQueryRetry(); model.Themes = themes.Select(t => t.Name).ToList(); } }
public async Task <ActionResult> Provision(ProvisioningActionModel model) { CheckBetaFlag(); PrepareHeaderData(model.ReturnUrl); if (model != null && ModelState.IsValid) { // Enqueue the provisioning request await ProvisioningAppManager.EnqueueProvisioningRequest( model, (Request.Files != null && Request.Files.Count > 0 && Request.Files[0].ContentLength > 0)?Request.Files[0].FileName : null, (Request.Files != null && Request.Files.Count > 0 && Request.Files[0].ContentLength > 0)?Request.Files[0].InputStream : null ); // Get the service description content var context = GetDataContext(); var contentPage = context.ContentPages.FirstOrDefault(cp => cp.Id == "system/pages/ProvisioningScheduled.md"); if (contentPage != null) { model.ProvisionDescription = contentPage.Content; } } return(View("ProvisionQueued", model)); }
private static async Task LoadThemesFromTenant(ProvisioningActionModel model, string tokenId, string graphAccessToken) { // 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); // Store the SPO Root Site URL in the Model model.SPORootSiteUrl = rootSite.WebUrl; // Retrieve the SPO URL for the Admin Site var adminSiteUrl = rootSite.WebUrl.Replace(".sharepoint.com", "-admin.sharepoint.com"); // Retrieve the SPO Access Token var spoAccessToken = await ProvisioningAppManager.AccessTokenProvider.GetAccessTokenAsync( tokenId, adminSiteUrl, ConfigurationManager.AppSettings["ida:ClientId"], ConfigurationManager.AppSettings["ida:ClientSecret"], ConfigurationManager.AppSettings["ida:AppUrl"]); // Connect to SPO and retrieve the list of available Themes AuthenticationManager authManager = new AuthenticationManager(); using (ClientContext spoContext = authManager.GetAzureADAccessTokenAuthenticatedContext(adminSiteUrl, spoAccessToken)) { TenantAdmin.Tenant tenant = new TenantAdmin.Tenant(spoContext); var themes = tenant.GetAllTenantThemes(); spoContext.Load(themes); spoContext.ExecuteQueryRetry(); model.Themes = themes.Select(t => t.Name).ToList(); } }
public async Task <ActionResult> Provision(String packageId = null) { if (String.IsNullOrEmpty(packageId)) { throw new ArgumentNullException("packageId"); } ProvisioningActionModel model = new ProvisioningActionModel(); if (IsValidUser()) { var issuer = (System.Threading.Thread.CurrentPrincipal as System.Security.Claims.ClaimsPrincipal)?.FindFirst("iss"); if (issuer != null && !String.IsNullOrEmpty(issuer.Value)) { var issuerValue = issuer.Value.Substring(0, issuer.Value.Length - 1); var tenantId = issuerValue.Substring(issuerValue.LastIndexOf("/") + 1); var upn = (System.Threading.Thread.CurrentPrincipal as System.Security.Claims.ClaimsPrincipal)?.FindFirst(ClaimTypes.Upn)?.Value; if (this.IsAllowedUpnTenant(upn)) { #region Prepare model generic context data // Prepare the model data model.TenantId = tenantId; model.UserPrincipalName = upn; model.PackageId = packageId; model.ApplyTheme = false; model.ApplyCustomTheme = false; String provisioningScope = ConfigurationManager.AppSettings["SPPA:ProvisioningScope"]; String provisioningEnvironment = ConfigurationManager.AppSettings["SPPA:ProvisioningEnvironment"]; var tokenId = $"{model.TenantId}-{model.UserPrincipalName.GetHashCode()}-{provisioningScope}-{provisioningEnvironment}"; var graphAccessToken = await ProvisioningAppManager.AccessTokenProvider.GetAccessTokenAsync( tokenId, "https://graph.microsoft.com/"); model.UserIsTenantAdmin = Utilities.UserIsTenantGlobalAdmin(graphAccessToken); model.UserIsSPOAdmin = Utilities.UserIsSPOAdmin(graphAccessToken); model.NotificationEmail = upn; #endregion // If the current user is an admin, we can get the available Themes if (model.UserIsTenantAdmin || model.UserIsSPOAdmin) { await LoadThemesFromTenant(model, tokenId, graphAccessToken); } LoadPackageDataIntoModel(packageId, model); } else { throw new ApplicationException("Invalid request, the current tenant is not allowed to use this solution!"); } } } return(View("Provision", model)); }
public async Task <ActionResult> Provision(ProvisioningActionModel model) { if (model != null && ModelState.IsValid) { if (!String.IsNullOrEmpty(model.MetadataPropertiesJson)) { model.MetadataProperties = JsonConvert.DeserializeObject <Dictionary <String, MetadataProperty> >(model.MetadataPropertiesJson); } // If there is an input file for the logo if (Request.Files != null && Request.Files.Count > 0 && Request.Files[0].ContentLength > 0) { // Generate a random file name model.CustomLogo = $"{Guid.NewGuid()}-{Request.Files[0].FileName}"; // Get a reference to the blob storage account var blobLogosConnectionString = ConfigurationManager.AppSettings["BlobLogosProvider:ConnectionString"]; var blobLogosContainerName = ConfigurationManager.AppSettings["BlobLogosProvider:ContainerName"]; CloudStorageAccount csaLogos; if (!CloudStorageAccount.TryParse(blobLogosConnectionString, out csaLogos)) { throw new ArgumentException("Cannot create cloud storage account from given connection string."); } CloudBlobClient blobLogosClient = csaLogos.CreateCloudBlobClient(); CloudBlobContainer containerLogos = blobLogosClient.GetContainerReference(blobLogosContainerName); // Store the file in the blob storage CloudBlockBlob blobLogo = containerLogos.GetBlockBlobReference(model.CustomLogo); await blobLogo.UploadFromStreamAsync(Request.Files[0].InputStream); } // Get a reference to the blob storage queue CloudStorageAccount storageAccount = CloudStorageAccount.Parse( CloudConfigurationManager.GetSetting("SPPA:StorageConnectionString")); // Get queue... create if does not exist. CloudQueueClient queueClient = storageAccount.CreateCloudQueueClient(); CloudQueue queue = queueClient.GetQueueReference( CloudConfigurationManager.GetSetting("SPPA:StorageQueueName")); queue.CreateIfNotExists(); // add message to the queue queue.AddMessage(new CloudQueueMessage(JsonConvert.SerializeObject(model))); } // Get the service description content var context = GetDataContext(); var contentPage = context.ContentPages.FirstOrDefault(cp => cp.Id == "system/pages/ProvisioningScheduled.md"); if (contentPage != null) { model.ProvisionDescription = contentPage.Content; } return(View("ProvisionQueued", model)); }
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 static async Task EnqueueProvisioningRequest(ProvisioningActionModel model, String logoFileName = null, Stream logoFile = null) { if (!String.IsNullOrEmpty(model.MetadataPropertiesJson)) { model.MetadataProperties = JsonConvert.DeserializeObject <Dictionary <String, MetadataProperty> >(model.MetadataPropertiesJson); } // If there is an input file for the logo if (logoFile != null) { // Generate a random file name model.CustomLogo = $"{Guid.NewGuid()}-{logoFileName}"; // Get a reference to the blob storage account var blobLogosConnectionString = ConfigurationManager.AppSettings["BlobLogosProvider:ConnectionString"]; var blobLogosContainerName = ConfigurationManager.AppSettings["BlobLogosProvider:ContainerName"]; CloudStorageAccount csaLogos; if (!CloudStorageAccount.TryParse(blobLogosConnectionString, out csaLogos)) { throw new ArgumentException("Cannot create cloud storage account from given connection string."); } CloudBlobClient blobLogosClient = csaLogos.CreateCloudBlobClient(); CloudBlobContainer containerLogos = blobLogosClient.GetContainerReference(blobLogosContainerName); // Store the file in the blob storage CloudBlockBlob blobLogo = containerLogos.GetBlockBlobReference(model.CustomLogo); await blobLogo.UploadFromStreamAsync(logoFile); } var queueTarget = CloudConfigurationManager.GetSetting("SPPA:QueueTarget")?.ToUpper() ?? "BLOB"; switch (queueTarget) { case "SERVICEBUS": var sbConnectionString = CloudConfigurationManager.GetSetting("SPPA:ServiceBusConnectionString"); var sbQueueName = CloudConfigurationManager.GetSetting("SPPA:ServiceBusQueueName"); await ServiceBusQueueUtility.EnqueueMessageAsync(sbConnectionString, sbQueueName, model); break; case "BLOB": default: var blobConnectionString = CloudConfigurationManager.GetSetting("SPPA:StorageConnectionString"); var blobQueueName = CloudConfigurationManager.GetSetting("SPPA:StorageQueueName"); await BlobStorageQueueUtility.EnqueueMessageAsync(blobConnectionString, blobQueueName, model); break; } }
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(); } }
public static async Task EnqueueProvisioningRequest(ProvisioningActionModel model, String logoFileName = null, Stream logoFile = null) { if (!String.IsNullOrEmpty(model.MetadataPropertiesJson)) { model.MetadataProperties = JsonConvert.DeserializeObject <Dictionary <String, MetadataProperty> >(model.MetadataPropertiesJson); } // If there is an input file for the logo if (logoFile != null) { // Generate a random file name model.CustomLogo = $"{Guid.NewGuid()}-{logoFileName}"; // Get a reference to the blob storage account var blobLogosConnectionString = ConfigurationManager.AppSettings["BlobLogosProvider:ConnectionString"]; var blobLogosContainerName = ConfigurationManager.AppSettings["BlobLogosProvider:ContainerName"]; CloudStorageAccount csaLogos; if (!CloudStorageAccount.TryParse(blobLogosConnectionString, out csaLogos)) { throw new ArgumentException("Cannot create cloud storage account from given connection string."); } CloudBlobClient blobLogosClient = csaLogos.CreateCloudBlobClient(); CloudBlobContainer containerLogos = blobLogosClient.GetContainerReference(blobLogosContainerName); // Store the file in the blob storage CloudBlockBlob blobLogo = containerLogos.GetBlockBlobReference(model.CustomLogo); await blobLogo.UploadFromStreamAsync(logoFile); } // Get a reference to the blob storage queue CloudStorageAccount storageAccount = CloudStorageAccount.Parse( CloudConfigurationManager.GetSetting("SPPA:StorageConnectionString")); // Get queue... create if does not exist. CloudQueueClient queueClient = storageAccount.CreateCloudQueueClient(); CloudQueue queue = queueClient.GetQueueReference( CloudConfigurationManager.GetSetting("SPPA:StorageQueueName")); queue.CreateIfNotExists(); // add message to the queue queue.AddMessage(new CloudQueueMessage(JsonConvert.SerializeObject(model))); }
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 static void LogSourceTrackingProvisionedSites(ProvisioningActionModel action, string spoAccessToken, AuthenticationManager authManager, List <Tuple <string, string> > provisionedSites) { // Log source tracking (2 = Provisioned) for every provisioned site foreach (var provisionedSite in provisionedSites) { // Get the provisioned site URL var provisionedSiteUrl = provisionedSite.Item2; // Connect to the provisioned site using (var provisionedSiteContext = authManager.GetAzureADAccessTokenAuthenticatedContext(provisionedSiteUrl, spoAccessToken)) { // Retrieve the Site ID var provisionedSiteId = provisionedSiteContext.Site.EnsureProperty(s => s.Id); // Log the source tracking event LogSourceTracking(action.Source, 2, null, action.PackageId, action.TenantId, provisionedSiteId.ToString()); } } }
/// <summary> /// Notifies an exception through the configured webhooks /// </summary> /// <param name="action">The provisioning action</param> /// <param name="ex">The exception that occurred</param> private static void ProcessWebhooksExceptionNotification(ProvisioningActionModel action, Exception ex) { if (action.Webhooks != null && action.Webhooks.Count > 0) { foreach (var wh in action.Webhooks) { var provisioningWebhook = new OfficeDevPnP.Core.Framework.Provisioning.Model.ProvisioningWebhook { Kind = ProvisioningTemplateWebhookKind.ExceptionOccurred, Url = wh.Url, Method = (ProvisioningTemplateWebhookMethod)Enum.Parse(typeof(ProvisioningTemplateWebhookMethod), wh.Method.ToString(), true), BodyFormat = ProvisioningTemplateWebhookBodyFormat.Json, // force JSON format Async = false, // force sync webhooks Parameters = wh.Parameters, }; var httpClient = new HttpClient(); WebhookSender.InvokeWebhook(provisioningWebhook, httpClient, ProvisioningTemplateWebhookKind.ExceptionOccurred, exception: ex); } } }
private static async Task <Dictionary <string, string> > PrepareAccessTokensAsync(ProvisioningActionModel model) { // Prepare the variable to hold the result var accessTokens = new Dictionary <string, string>(); // Retrieve the Microsoft Graph Access Token var graphAccessToken = await ProvisioningAppManager.AccessTokenProvider.GetAccessTokenAsync( AuthenticationConfig.GetGraphScopes()); // Retrieve the SPO Access Token var spoAccessToken = await ProvisioningAppManager.AccessTokenProvider.GetAccessTokenAsync( AuthenticationConfig.ClientId, AuthenticationConfig.ClientSecret, AuthenticationConfig.RedirectUri, AuthenticationConfig.GetSpoScopes(model.SPORootSiteUrl)); // Retrieve the SPO URL for the Admin Site var adminSiteUrl = model.SPORootSiteUrl.Replace(".sharepoint.com", "-admin.sharepoint.com"); // Retrieve the SPO Access Token var spoAdminAccessToken = await ProvisioningAppManager.AccessTokenProvider.GetAccessTokenAsync( AuthenticationConfig.ClientId, AuthenticationConfig.ClientSecret, AuthenticationConfig.RedirectUri, AuthenticationConfig.GetSpoScopes(adminSiteUrl)); // Configure the resulting dictionary accessTokens.Add(new Uri(AuthenticationConfig.GraphBaseUrl).Authority, graphAccessToken); accessTokens.Add(new Uri(model.SPORootSiteUrl).Authority, spoAccessToken); accessTokens.Add(new Uri(adminSiteUrl).Authority, spoAdminAccessToken); return(accessTokens); }
private void LoadPackageDataIntoModel(string packageId, ProvisioningActionModel model) { var context = GetDataContext(); DomainModel.Package package = null; // Get the package if (Boolean.Parse(ConfigurationManager.AppSettings["TestEnvironment"])) { // Process all packages in the test environment package = context.Packages.FirstOrDefault(p => p.Id == new Guid(packageId)); } else { // Process not-preview packages in the production environment package = context.Packages.FirstOrDefault(p => p.Id == new Guid(packageId) && p.Preview == false); } if (package != null) { if ((package.PackageType == PackageType.Tenant && !this.Request.Url.AbsolutePath.Contains("/tenant/")) || (package.PackageType == PackageType.SiteCollection && !this.Request.Url.AbsolutePath.Contains("/site/"))) { throw new ApplicationException("Invalid request, the requested package/template is not valid for the current request!"); } model.DisplayName = package.DisplayName; model.ActionType = package.PackageType == PackageType.SiteCollection ? ActionType.Site : ActionType.Tenant; // Configure content for instructions model.Instructions = package.Instructions; // If we don't have specific instructions if (model.Instructions == null) { // Get the default instructions var instructionsPage = context.ContentPages.FirstOrDefault(cp => cp.Id == "system/pages/GenericInstructions.md"); if (instructionsPage != null) { model.Instructions = instructionsPage.Content; } } // Configure content for provisioning recap model.ProvisionRecap = package.ProvisionRecap; // If we don't have specific provisioning recap if (String.IsNullOrEmpty(model.ProvisionRecap)) { // Get the default provisioning recap var provisionRecapPage = context.ContentPages.FirstOrDefault(cp => cp.Id == "system/pages/GenericProvisioning.md"); if (provisionRecapPage != null) { model.ProvisionRecap = provisionRecapPage.Content; } } // Retrieve parameters from the package/template definition var packageFileUrl = new Uri(package.PackageUrl); var packageLocalFolder = packageFileUrl.AbsolutePath.Substring(1, packageFileUrl.AbsolutePath.LastIndexOf('/') - 1); var packageFileName = packageFileUrl.AbsolutePath.Substring(packageLocalFolder.Length + 2); ProvisioningHierarchy hierarchy = GetHierarchyFromStorage(packageLocalFolder, packageFileName); // If we have the hierarchy and its parameters if (hierarchy != null && hierarchy.Parameters != null) { // Use them model.PackageProperties = hierarchy.Parameters; } else { // Otherwise, use an empty list of parameters model.PackageProperties = new Dictionary <string, string>(); } // Configure the metadata properties var metadata = new { properties = new[] { new { name = "", caption = "", description = "", editor = "", editorSettings = "", } } }; var metadataProperties = JsonConvert.DeserializeAnonymousType(package.PropertiesMetadata, metadata); model.MetadataProperties = metadataProperties.properties.ToDictionary( i => i.name, i => new MetadataProperty { Name = i.name, Caption = i.caption, Description = i.description, Editor = i.editor, EditorSettings = i.editorSettings }); model.MetadataPropertiesJson = JsonConvert.SerializeObject(model.MetadataProperties); // Get the service description content var contentPage = context.ContentPages.FirstOrDefault(cp => cp.Id == "system/pages/ProvisioningIntro.md"); if (contentPage != null) { model.ProvisionDescription = contentPage.Content; } // Get the pre-reqs Header content var preReqsHeaderPage = context.ContentPages.FirstOrDefault(cp => cp.Id == "system/pages/CanProvisionPreReqsHeader.md"); if (preReqsHeaderPage != null) { model.MissingPreReqsHeader = preReqsHeaderPage.Content; } // Get the pre-reqs Footer content var preReqsFooterPage = context.ContentPages.FirstOrDefault(cp => cp.Id == "system/pages/CanProvisionPreReqsFooter.md"); if (preReqsFooterPage != null) { model.MissingPreReqsFooter = preReqsFooterPage.Content; } } else { throw new ApplicationException("Invalid request, the requested package/template is not available!"); } }
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(); } }
private static void GetProvisionTextMessages(SharePointPnP.ProvisioningApp.DomainModel.Package package, ProvisioningActionModel model) { var settings = Newtonsoft.Json.JsonConvert.DeserializeObject <SharePointPnP.ProvisioningApp.DomainModel.TemplateSettingsMetadata>(package.PropertiesMetadata); if (settings?.displayInfo?.provisionMessages != null) { model.ProvisionPageTitle = settings.displayInfo.provisionMessages.provisionPageTitle; model.ProvisionPageSubTitle = settings.displayInfo.provisionMessages.provisionPageSubTitle; model.ProvisionPageText = settings.displayInfo.provisionMessages.provisionPageText; } }
public async Task <ProvisionContentPackResponse> ProvisionContentPack(ProvisionContentPackRequest provisionRequest) { var provisionResponse = new ProvisionContentPackResponse(); // If the input paramenters are missing, raise a BadRequest response if (provisionRequest == null) { ThrowEmptyRequest(); } // If the TenantId input argument is missing, raise a BadRequest response if (String.IsNullOrEmpty(provisionRequest.TenantId)) { ThrowMissingArgument("TenantId"); } // If the UserPrincipalName input argument is missing, raise a BadRequest response if (String.IsNullOrEmpty(provisionRequest.UserPrincipalName)) { ThrowMissingArgument("UserPrincipalName"); } // If the PackageIds input argument is missing, raise a BadRequest response if (provisionRequest.Packages == null || provisionRequest.Packages.Count == 0) { ThrowMissingArgument("Packages"); } if (provisionRequest != null && !String.IsNullOrEmpty(provisionRequest.TenantId) && !String.IsNullOrEmpty(provisionRequest.UserPrincipalName) && provisionRequest.Packages != null && provisionRequest.Packages.Count > 0) { try { // Process the AuthorizationCode request String provisioningScope = ConfigurationManager.AppSettings["SPPA:ProvisioningScope"]; String provisioningEnvironment = ConfigurationManager.AppSettings["SPPA:ProvisioningEnvironment"]; var tokenId = $"{provisionRequest.TenantId}-{provisionRequest.UserPrincipalName.ToLower().GetHashCode()}-{provisioningScope}-{provisioningEnvironment}"; try { // Retrieve the refresh token and store it in the KeyVault await ProvisioningAppManager.AccessTokenProvider.SetupSecurityFromAuthorizationCodeAsync( tokenId, provisionRequest.AuthorizationCode, provisionRequest.TenantId, ConfigurationManager.AppSettings["ida:ClientId"], ConfigurationManager.AppSettings["ida:ClientSecret"], "https://graph.microsoft.com/", provisionRequest.RedirectUri); } catch (Exception ex) { // In case of any authorization exception, raise an Unauthorized exception ThrowUnauthorized(ex); } // Validate the Package IDs var context = dbContext; DomainModel.Package package = null; // Get the first item to provision var item = provisionRequest.Packages.First(); // And remove it from the whole list of items provisionRequest.Packages.RemoveAt(0); var childrenItems = provisionRequest.Packages; // Get the package if (ProvisioningAppManager.IsTestingEnvironment) { // Process all packages in the test environment package = context.Packages.FirstOrDefault(p => p.Id == new Guid(item.PackageId)); } else { // Process not-preview packages in the production environment package = context.Packages.FirstOrDefault(p => p.Id == new Guid(item.PackageId) && p.Preview == false); } // If the package is not valid if (package == null) { // Throw an exception accordingly throw new ArgumentException("Invalid Package Id!"); } // First of all, validate the provisioning request for pre-requirements provisionResponse.CanProvisionResult = await CanProvisionInternal( new CanProvisionModel { PackageId = item.PackageId, TenantId = provisionRequest.TenantId, UserPrincipalName = provisionRequest.UserPrincipalName, SPORootSiteUrl = provisionRequest.SPORootSiteUrl, UserIsSPOAdmin = true, // We assume that the request comes from an Admin UserIsTenantAdmin = true, // We assume that the request comes from an Admin }); // If the package can be provisioned onto the target if (provisionResponse.CanProvisionResult.CanProvision) { // Prepare the provisioning request var request = new ProvisioningActionModel(); request.ActionType = ActionType.Tenant; // Do we want to support site/tenant or just one? request.ApplyCustomTheme = false; request.ApplyTheme = false; // Do we need to apply any special theme? request.CorrelationId = Guid.NewGuid(); request.CustomLogo = null; request.DisplayName = $"Provision Content Pack {item.PackageId}"; request.PackageId = item.PackageId; request.TargetSiteAlreadyExists = false; // Do we want to check this? request.TargetSiteBaseTemplateId = null; request.TenantId = provisionRequest.TenantId; request.UserIsSPOAdmin = true; // We don't use this in the job request.UserIsTenantAdmin = true; // We don't use this in the job request.UserPrincipalName = provisionRequest.UserPrincipalName.ToLower(); request.NotificationEmail = provisionRequest.NotificationEmail; request.PackageProperties = item.Parameters; request.ChildrenItems = childrenItems; request.Webhooks = provisionRequest.Webhooks; // Enqueue the provisioning request await ProvisioningAppManager.EnqueueProvisioningRequest(request); } // Set the status of the provisioning request provisionResponse.ProvisioningStarted = true; } catch (HttpResponseException) { throw; } catch (Exception ex) { // In case of any other exception, raise an InternalServerError exception ThrowInternalServerError(ex); } } // Return to the requested URL return(provisionResponse); }
public async Task <ActionResult> Provision(String packageId = null, String returnUrl = null, String source = null) { if (String.IsNullOrEmpty(packageId)) { throw new ArgumentNullException("packageId"); } if (String.IsNullOrEmpty(source)) { source = "default"; } CheckBetaFlag(); PrepareHeaderData(returnUrl); LogSourceTracking(source, 0, Request.Url.ToString(), packageId); // 0 = PageView ProvisioningActionModel model = new ProvisioningActionModel(); try { if (IsValidUser()) { var issuer = (System.Threading.Thread.CurrentPrincipal as System.Security.Claims.ClaimsPrincipal)?.FindFirst("iss"); if (issuer != null && !String.IsNullOrEmpty(issuer.Value)) { var issuerValue = issuer.Value.Substring(0, issuer.Value.Length - 1); var tenantId = issuerValue.Substring(issuerValue.LastIndexOf("/") + 1); var upn = (System.Threading.Thread.CurrentPrincipal as System.Security.Claims.ClaimsPrincipal)?.FindFirst(ClaimTypes.Upn)?.Value; if (this.IsAllowedUpnTenant(upn)) { #region Prepare model generic context data // Prepare the model data model.TenantId = tenantId; model.UserPrincipalName = upn; model.PackageId = packageId; model.ApplyTheme = false; model.ApplyCustomTheme = false; String provisioningScope = ConfigurationManager.AppSettings["SPPA:ProvisioningScope"]; String provisioningEnvironment = ConfigurationManager.AppSettings["SPPA:ProvisioningEnvironment"]; var tokenId = $"{model.TenantId}-{model.UserPrincipalName.ToLower().GetHashCode()}-{provisioningScope}-{provisioningEnvironment}"; var graphAccessToken = await ProvisioningAppManager.AccessTokenProvider.GetAccessTokenAsync( tokenId, "https://graph.microsoft.com/"); if (string.IsNullOrEmpty(graphAccessToken)) { throw new ApplicationException($"Cannot retrieve a valid Access Token for user {model.UserPrincipalName.ToLower()} in tenant {model.TenantId}"); } model.UserIsTenantAdmin = Utilities.UserIsTenantGlobalAdmin(graphAccessToken); model.UserIsSPOAdmin = Utilities.UserIsSPOAdmin(graphAccessToken); model.NotificationEmail = upn; model.ReturnUrl = returnUrl; model.Source = source; #endregion // 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); // Store the SPO Root Site URL in the Model model.SPORootSiteUrl = rootSite.WebUrl; // If the current user is an admin, we can get the available Themes if (model.UserIsTenantAdmin || model.UserIsSPOAdmin) { await LoadThemesFromTenant(model, tokenId, rootSite, graphAccessToken); } LoadPackageDataIntoModel(packageId, model); } else { throw new ApplicationException("Invalid request, the current tenant is not allowed to use this solution!"); } } } } catch (Exception ex) { throw new Exception(ex.Message); } return(View("Provision", model)); }