diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Controllers/ApiController.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Controllers/ApiController.cs index 97900956484..c5f0ac72fe6 100644 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Controllers/ApiController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Controllers/ApiController.cs @@ -1,11 +1,14 @@ -using Microsoft.AspNetCore.Authorization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using OrchardCore.ContentManagement; using OrchardCore.Contents; -using System.Threading.Tasks; namespace OrchardCore.Content.Controllers { + [Route("api/content")] + [ApiController] + [Authorize(AuthenticationSchemes = "Api"), IgnoreAntiforgeryToken, AllowAnonymous] public class ApiController : Controller { private readonly IContentManager _contentManager; @@ -19,6 +22,7 @@ public ApiController( _contentManager = contentManager; } + [Route("{contentItemId}")] public async Task Get(string contentItemId) { var contentItem = await _contentManager.GetAsync(contentItemId); @@ -33,7 +37,72 @@ public async Task Get(string contentItemId) return Unauthorized(); } - return new ObjectResult(contentItem); + return Ok(contentItem); + } + + [HttpDelete] + [Route("{contentItemId}")] + public async Task Delete(string contentItemId) + { + var contentItem = await _contentManager.GetAsync(contentItemId); + + if (contentItem == null) + { + return StatusCode(204); + } + + if (!await _authorizationService.AuthorizeAsync(User, Permissions.DeleteContent, contentItem)) + { + return Unauthorized(); + } + + await _contentManager.RemoveAsync(contentItem); + + return Ok(contentItem); + } + + [HttpPost] + public async Task Post(ContentItem newContentItem, bool draft = false) + { + var contentItem = await _contentManager.GetAsync(newContentItem.ContentItemId, VersionOptions.DraftRequired); + + if (contentItem == null) + { + await _contentManager.CreateAsync(newContentItem, VersionOptions.DraftRequired); + + contentItem = newContentItem; + } + + if (!await _authorizationService.AuthorizeAsync(User, Permissions.EditContent, contentItem)) + { + return Unauthorized(); + } + + if (contentItem != newContentItem) + { + contentItem.DisplayText = newContentItem.DisplayText; + contentItem.ModifiedUtc = newContentItem.ModifiedUtc; + contentItem.PublishedUtc = newContentItem.PublishedUtc; + contentItem.CreatedUtc = newContentItem.CreatedUtc; + contentItem.Owner = newContentItem.Owner; + contentItem.Author = newContentItem.Author; + + contentItem.Apply(newContentItem); + + await _contentManager.UpdateAsync(contentItem); + } + + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + if (!draft) + { + await _contentManager.PublishAsync(contentItem); + } + + return Ok(contentItem); } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Demo/Controllers/ContentApiController.cs b/src/OrchardCore.Modules/OrchardCore.Demo/Controllers/ContentApiController.cs index 6390129a47e..56762713298 100644 --- a/src/OrchardCore.Modules/OrchardCore.Demo/Controllers/ContentApiController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Demo/Controllers/ContentApiController.cs @@ -5,6 +5,7 @@ namespace OrchardCore.Demo.Controllers { + [Authorize(AuthenticationSchemes = "Api"), IgnoreAntiforgeryToken, AllowAnonymous] public class ContentApiController : Controller { private readonly IAuthorizationService _authorizationService; @@ -28,7 +29,6 @@ public async Task GetById(string id) return new ObjectResult(contentItem); } - [Authorize] public async Task GetAuthorizedById(string id) { if (!await _authorizationService.AuthorizeAsync(User, Permissions.DemoAPIAccess)) @@ -52,9 +52,8 @@ public async Task GetAuthorizedById(string id) } [Authorize] - [IgnoreAntiforgeryToken] [HttpPost] - public async Task AddContent([FromBody]ContentItem contentItem) + public async Task AddContent(ContentItem contentItem) { if (!await _authorizationService.AuthorizeAsync(User, Permissions.DemoAPIAccess)) { diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Controllers/ApiController.cs b/src/OrchardCore.Modules/OrchardCore.Lucene/Controllers/ApiController.cs index 0c7168b5c1f..94d973ea117 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Controllers/ApiController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Lucene/Controllers/ApiController.cs @@ -7,6 +7,9 @@ namespace OrchardCore.Lucene.Controllers { + [Route("api/lucene")] + [ApiController] + [Authorize(AuthenticationSchemes = "Api"), IgnoreAntiforgeryToken, AllowAnonymous] public class ApiController : Controller { private readonly IAuthorizationService _authorizationService; @@ -24,6 +27,7 @@ public ApiController( } [HttpPost, HttpGet] + [Route("content")] public async Task Content( string indexName, string query, @@ -49,6 +53,7 @@ public async Task Content( } [HttpPost, HttpGet] + [Route("documents")] public async Task Documents( string indexName, string query, diff --git a/src/OrchardCore.Modules/OrchardCore.Lucene/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Lucene/Startup.cs index 6920b8faee9..b3e7e311449 100644 --- a/src/OrchardCore.Modules/OrchardCore.Lucene/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Lucene/Startup.cs @@ -67,20 +67,6 @@ public override void Configure(IApplicationBuilder app, IRouteBuilder routes, IS template: "Search/{id?}", defaults: new { controller = "Search", action = "Index", id = "" } ); - - routes.MapAreaRoute( - name: "Api.Lucene.Content", - areaName: "OrchardCore.Lucene", - template: "api/lucene/content", - defaults: new { controller = "Api", action = "Content" } - ); - - routes.MapAreaRoute( - name: "Api.Lucene.Documents", - areaName: "OrchardCore.Lucene", - template: "api/lucene/documents", - defaults: new { controller = "Api", action = "Documents" } - ); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Queries/Controllers/ApiController.cs b/src/OrchardCore.Modules/OrchardCore.Queries/Controllers/ApiController.cs index 7cb62e48de2..50e31694130 100644 --- a/src/OrchardCore.Modules/OrchardCore.Queries/Controllers/ApiController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Queries/Controllers/ApiController.cs @@ -6,6 +6,9 @@ namespace OrchardCore.Queries.Controllers { + [Route("api/queries")] + [ApiController] + [Authorize(AuthenticationSchemes = "Api"), IgnoreAntiforgeryToken, AllowAnonymous] public class ApiController : Controller { private readonly IAuthorizationService _authorizationService; @@ -21,6 +24,7 @@ IQueryManager queryManager } [HttpPost, HttpGet] + [Route("{name}")] public async Task Query( string name, string parameters) diff --git a/src/OrchardCore.Modules/OrchardCore.Queries/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Queries/Startup.cs index c6a38b55c58..1b61386133d 100644 --- a/src/OrchardCore.Modules/OrchardCore.Queries/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Queries/Startup.cs @@ -38,16 +38,6 @@ public override void ConfigureServices(IServiceCollection services) services.AddSingleton(new DeploymentStepFactory()); services.AddScoped, AllQueriesDeploymentStepDriver>(); } - - public override void Configure(IApplicationBuilder app, IRouteBuilder routes, IServiceProvider serviceProvider) - { - routes.MapAreaRoute( - name: "Api.Queries.Query", - areaName: "OrchardCore.Queries", - template: "api/queries/{name}", - defaults: new { controller = "Api", action = "Query" } - ); - } } diff --git a/src/OrchardCore.Modules/OrchardCore.Setup/Controllers/SetupController.cs b/src/OrchardCore.Modules/OrchardCore.Setup/Controllers/SetupController.cs index 0f23f48d392..167db759afa 100644 --- a/src/OrchardCore.Modules/OrchardCore.Setup/Controllers/SetupController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Setup/Controllers/SetupController.cs @@ -207,8 +207,8 @@ private async Task IsTokenValid(string token) { using (var scope = await _shellHost.GetScopeAsync(ShellHelper.DefaultShellName)) { - var dataProtectionProvider = scope.ServiceProvider.GetService(); - ITimeLimitedDataProtector dataProtector = dataProtectionProvider.CreateProtector("Tokens").ToTimeLimitedDataProtector(); + var dataProtectionProvider = scope.ServiceProvider.GetRequiredService(); + var dataProtector = dataProtectionProvider.CreateProtector("Tokens").ToTimeLimitedDataProtector(); var tokenValue = dataProtector.Unprotect(token, out var expiration); diff --git a/src/OrchardCore.Modules/OrchardCore.Tenants/Controllers/ApiController.cs b/src/OrchardCore.Modules/OrchardCore.Tenants/Controllers/ApiController.cs new file mode 100644 index 00000000000..80f4e51f56e --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Tenants/Controllers/ApiController.cs @@ -0,0 +1,324 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Localization; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Localization; +using OrchardCore.Data; +using OrchardCore.DisplayManagement.Notify; +using OrchardCore.Environment.Shell; +using OrchardCore.Environment.Shell.Models; +using OrchardCore.Hosting.ShellBuilders; +using OrchardCore.Modules; +using OrchardCore.Recipes.Models; +using OrchardCore.Recipes.Services; +using OrchardCore.Setup.Services; +using OrchardCore.Tenants.ViewModels; + +namespace OrchardCore.Tenants.Controllers +{ + [Route("api/tenants")] + [ApiController] + [Authorize(AuthenticationSchemes = "Api"), IgnoreAntiforgeryToken, AllowAnonymous] + public class ApiController : Controller + { + private readonly IShellHost _shellHost; + private readonly IShellSettingsManager _shellSettingsManager; + private readonly IEnumerable _databaseProviders; + private readonly IAuthorizationService _authorizationService; + private readonly IEnumerable _recipeHarvesters; + private readonly IDataProtectionProvider _dataProtectorProvider; + private readonly ISetupService _setupService; + private readonly ShellSettings _currentShellSettings; + private readonly IClock _clock; + private readonly INotifier _notifier; + + public ApiController( + IShellHost shellHost, + ShellSettings currentShellSettings, + IAuthorizationService authorizationService, + IShellSettingsManager shellSettingsManager, + IEnumerable databaseProviders, + IDataProtectionProvider dataProtectorProvider, + ISetupService setupService, + IClock clock, + INotifier notifier, + IEnumerable recipeHarvesters, + IStringLocalizer stringLocalizer, + IHtmlLocalizer htmlLocalizer) + { + _dataProtectorProvider = dataProtectorProvider; + _setupService = setupService; + _clock = clock; + _recipeHarvesters = recipeHarvesters; + _shellHost = shellHost; + _authorizationService = authorizationService; + _shellSettingsManager = shellSettingsManager; + _databaseProviders = databaseProviders; + _currentShellSettings = currentShellSettings; + _notifier = notifier; + + S = stringLocalizer; + H = htmlLocalizer; + } + + public IStringLocalizer S { get; set; } + public IHtmlLocalizer H { get; set; } + + [HttpPost] + [Route("create")] + public async Task Create(CreateApiViewModel model) + { + if (!IsDefaultShell()) + { + return Unauthorized(); + } + + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageTenants)) + { + return Unauthorized(); + } + + var allShells = await GetShellsAsync(); + + if (!string.IsNullOrEmpty(model.Name) && !Regex.IsMatch(model.Name, @"^\w+$")) + { + ModelState.AddModelError(nameof(CreateApiViewModel.Name), S["Invalid tenant name. Must contain characters only and no spaces."]); + } + + if (!IsDefaultShell() && string.IsNullOrWhiteSpace(model.RequestUrlHost) && string.IsNullOrWhiteSpace(model.RequestUrlPrefix)) + { + ModelState.AddModelError(nameof(CreateApiViewModel.RequestUrlPrefix), S["Host and url prefix can not be empty at the same time."]); + } + + if (!string.IsNullOrWhiteSpace(model.RequestUrlPrefix)) + { + if (model.RequestUrlPrefix.Contains('/')) + { + ModelState.AddModelError(nameof(CreateApiViewModel.RequestUrlPrefix), S["The url prefix can not contains more than one segment."]); + } + } + + if (ModelState.IsValid) + { + if (_shellHost.TryGetSettings(model.Name, out var shellSettings)) + { + // Site already exists, return 200 for indempotency purpose + + var token = CreateSetupToken(shellSettings); + + return StatusCode(201, GetTenantUrl(shellSettings, token)); + } + else + { + shellSettings = new ShellSettings + { + Name = model.Name, + RequestUrlPrefix = model.RequestUrlPrefix?.Trim(), + RequestUrlHost = model.RequestUrlHost, + ConnectionString = model.ConnectionString, + TablePrefix = model.TablePrefix, + DatabaseProvider = model.DatabaseProvider, + State = TenantState.Uninitialized, + Secret = Guid.NewGuid().ToString(), + RecipeName = model.RecipeName + }; + + _shellSettingsManager.SaveSettings(shellSettings); + var shellContext = await _shellHost.GetOrCreateShellContextAsync(shellSettings); + + var token = CreateSetupToken(shellSettings); + + return Ok(GetTenantUrl(shellSettings, token)); + } + } + + return BadRequest(ModelState); + } + + [HttpPost] + [Route("setup")] + public async Task Setup(SetupApiViewModel model) + { + if (!IsDefaultShell()) + { + return Unauthorized(); + } + + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageTenants)) + { + return Unauthorized(); + } + + if (!ModelState.IsValid) + { + return BadRequest(); + } + + if (!_shellHost.TryGetSettings(model.Name, out var shellSettings)) + { + ModelState.AddModelError(nameof(SetupApiViewModel.Name), S["Tenant not found: '{0}'", model.Name]); + } + + if (shellSettings.State == TenantState.Running) + { + return StatusCode(201); + } + + if (shellSettings.State != TenantState.Uninitialized) + { + return BadRequest(S["The tenant can't be setup."]); + } + + var selectedProvider = _databaseProviders.FirstOrDefault(x => String.Equals(x.Value, model.DatabaseProvider, StringComparison.OrdinalIgnoreCase)); + + if (selectedProvider == null) + { + return BadRequest(S["The database provider is not defined."]); + } + + var tablePrefix = shellSettings.TablePrefix; + + if (String.IsNullOrEmpty(tablePrefix)) + { + tablePrefix = model.TablePrefix; + } + + var connectionString = shellSettings.ConnectionString; + + if (String.IsNullOrEmpty(connectionString)) + { + connectionString = model.ConnectionString; + } + + if (selectedProvider.HasConnectionString && String.IsNullOrEmpty(connectionString)) + { + return BadRequest(S["The connection string is required for this database provider."]); + } + + var recipeName = shellSettings.RecipeName; + + if (String.IsNullOrEmpty(recipeName)) + { + recipeName = model.RecipeName; + } + + RecipeDescriptor recipeDescriptor = null; + + if (String.IsNullOrEmpty(recipeName)) + { + if (model.Recipe == null) + { + return BadRequest(S["Either 'Recipe' or 'RecipeName' is required."]); + } + + var tempFilename = Path.GetTempFileName(); + + using (var fs = System.IO.File.Create(tempFilename)) + { + await model.Recipe.CopyToAsync(fs); + } + + var fileProvider = new PhysicalFileProvider(Path.GetDirectoryName(tempFilename)); + + recipeDescriptor = new RecipeDescriptor + { + FileProvider = fileProvider, + BasePath = "", + RecipeFileInfo = fileProvider.GetFileInfo(Path.GetFileName(tempFilename)) + }; + } + else + { + var setupRecipes = await _setupService.GetSetupRecipesAsync(); + recipeDescriptor = setupRecipes.FirstOrDefault(x => String.Equals(x.Name, recipeName, StringComparison.OrdinalIgnoreCase)); + + if (recipeDescriptor == null) + { + return BadRequest(S["Recipe '{0}' not found.", recipeName]); + } + } + + var setupContext = new SetupContext + { + ShellSettings = shellSettings, + SiteName = model.SiteName, + EnabledFeatures = null, // default list, + AdminUsername = model.UserName, + AdminEmail = model.Email, + AdminPassword = model.Password, + Errors = new Dictionary(), + Recipe = recipeDescriptor, + SiteTimeZone = model.SiteTimeZone, + DatabaseProvider = selectedProvider.Name, + DatabaseConnectionString = connectionString, + DatabaseTablePrefix = tablePrefix + }; + + var executionId = await _setupService.SetupAsync(setupContext); + + // Check if a component in the Setup failed + if (setupContext.Errors.Any()) + { + foreach (var error in setupContext.Errors) + { + ModelState.AddModelError(error.Key, error.Value); + } + + return StatusCode(500, ModelState); + } + + return Ok(executionId); + } + + private async Task> GetShellsAsync() + { + return (await _shellHost.ListShellContextsAsync()).OrderBy(x => x.Settings.Name); + } + + private bool IsDefaultShell() + { + return string.Equals(_currentShellSettings.Name, ShellHelper.DefaultShellName, StringComparison.OrdinalIgnoreCase); + } + + public string GetTenantUrl(ShellSettings shellSettings, string token) + { + var requestHostInfo = Request.Host; + + var tenantUrlHost = shellSettings.RequestUrlHost?.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries).First() ?? requestHostInfo.Host; + if (requestHostInfo.Port.HasValue) + { + tenantUrlHost += ":" + requestHostInfo.Port; + } + + var result = $"{Request.Scheme}://{tenantUrlHost}"; + + if (!string.IsNullOrEmpty(shellSettings.RequestUrlPrefix)) + { + result += "/" + shellSettings.RequestUrlPrefix; + } + + if (!string.IsNullOrEmpty(token)) + { + result += "?token=" + WebUtility.UrlEncode(token); + } + + return result; + } + + private string CreateSetupToken(ShellSettings shellSettings) + { + // Create a public url to setup the new tenant + var dataProtector = _dataProtectorProvider.CreateProtector("Tokens").ToTimeLimitedDataProtector(); + var token = dataProtector.Protect(shellSettings.Secret, _clock.UtcNow.Add(new TimeSpan(24, 0, 0))); + return token; + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Tenants/OrchardCore.Tenants.csproj b/src/OrchardCore.Modules/OrchardCore.Tenants/OrchardCore.Tenants.csproj index 1fe8bfa8b0b..79eb775dbaf 100644 --- a/src/OrchardCore.Modules/OrchardCore.Tenants/OrchardCore.Tenants.csproj +++ b/src/OrchardCore.Modules/OrchardCore.Tenants/OrchardCore.Tenants.csproj @@ -12,6 +12,8 @@ + + diff --git a/src/OrchardCore.Modules/OrchardCore.Tenants/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Tenants/Startup.cs index 748c12ad055..05f2136bd06 100644 --- a/src/OrchardCore.Modules/OrchardCore.Tenants/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Tenants/Startup.cs @@ -8,6 +8,7 @@ using OrchardCore.Navigation; using OrchardCore.Environment.Shell; using OrchardCore.Modules; +using OrchardCore.Setup; namespace OrchardCore.Tenants { @@ -16,6 +17,7 @@ public class Startup : StartupBase public override void ConfigureServices(IServiceCollection services) { services.AddTransient(); + services.AddSetup(); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Tenants/ViewModels/CreateApiViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Tenants/ViewModels/CreateApiViewModel.cs new file mode 100644 index 00000000000..a34079d3d47 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Tenants/ViewModels/CreateApiViewModel.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace OrchardCore.Tenants.ViewModels +{ + public class CreateApiViewModel + { + [Required] + public string Name { get; set; } + public string DatabaseProvider { get; set; } + public string RequestUrlPrefix { get; set; } + public string RequestUrlHost { get; set; } + public string ConnectionString { get; set; } + public string TablePrefix { get; set; } + public string RecipeName { get; set; } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Tenants/ViewModels/SetupApiViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Tenants/ViewModels/SetupApiViewModel.cs new file mode 100644 index 00000000000..47576459bf7 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Tenants/ViewModels/SetupApiViewModel.cs @@ -0,0 +1,36 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Http; + +namespace OrchardCore.Tenants.ViewModels +{ + public class SetupApiViewModel + { + + [Required] + public string Name { get; set; } + + [Required] + public string SiteName { get; set; } + + public string DatabaseProvider { get; set; } + + public string ConnectionString { get; set; } + + public string TablePrefix { get; set; } + + [Required] + public string UserName { get; set; } + + [Required] + [EmailAddress] + public string Email { get; set; } + + [DataType(DataType.Password)] + public string Password { get; set; } + + public string RecipeName { get; set; } + public IFormFile Recipe { get; set; } + + public string SiteTimeZone { get; set; } + } +} diff --git a/src/OrchardCore/OrchardCore.ContentManagement.Abstractions/ContentExtensions.cs b/src/OrchardCore/OrchardCore.ContentManagement.Abstractions/ContentExtensions.cs index 5006019303b..f057e993284 100644 --- a/src/OrchardCore/OrchardCore.ContentManagement.Abstractions/ContentExtensions.cs +++ b/src/OrchardCore/OrchardCore.ContentManagement.Abstractions/ContentExtensions.cs @@ -107,6 +107,25 @@ public static ContentElement Apply(this ContentElement contentElement, string na return contentElement; } + /// + /// Updates the whole content. + /// + /// The content element instance to update. + /// The current instance. + public static ContentElement Apply(this ContentElement contentElement, ContentElement element) + { + if (contentElement.Data != null) + { + contentElement.Data.Merge(JObject.FromObject(element.Data), JsonMergeSettings); + } + else + { + contentElement.Data = JObject.FromObject(element.Data, ContentBuilderSettings.IgnoreDefaultValuesSerializer); + } + + return contentElement; + } + /// /// Modifies a new or existing content element by name. /// diff --git a/src/OrchardCore/OrchardCore.ContentManagement.Abstractions/ContentItemConverter.cs b/src/OrchardCore/OrchardCore.ContentManagement.Abstractions/ContentItemConverter.cs index ceba3d08c06..27ed19ecab3 100644 --- a/src/OrchardCore/OrchardCore.ContentManagement.Abstractions/ContentItemConverter.cs +++ b/src/OrchardCore/OrchardCore.ContentManagement.Abstractions/ContentItemConverter.cs @@ -8,11 +8,10 @@ public class ContentItemConverter : JsonConverter { public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { - ContentItem contentItem = (ContentItem)value; + var contentItem = (ContentItem)value; var o = new JObject(); // Write all well-known properties - o.Add(new JProperty(nameof(ContentItem.Id), contentItem.Id)); o.Add(new JProperty(nameof(ContentItem.ContentItemId), contentItem.ContentItemId)); o.Add(new JProperty(nameof(ContentItem.ContentItemVersionId), contentItem.ContentItemVersionId)); o.Add(new JProperty(nameof(ContentItem.ContentType), contentItem.ContentType)); @@ -54,9 +53,6 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist switch (propertyName) { - case nameof(ContentItem.Id) : - contentItem.Id = reader.ReadAsInt32() ?? 0; - break; case nameof(ContentItem.ContentItemId): contentItem.ContentItemId = reader.ReadAsString(); break; diff --git a/src/OrchardCore/OrchardCore.Infrastructure/Security/AuthenticationHandler.cs b/src/OrchardCore/OrchardCore.Infrastructure/Security/AuthenticationHandler.cs new file mode 100644 index 00000000000..69b718e82e5 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Infrastructure/Security/AuthenticationHandler.cs @@ -0,0 +1,42 @@ +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace OrchardCore.Security +{ + /// + /// Provides a delegating logic for API authentication. + /// If no specific scheme handler is found it returns an anonymous user. + /// + public class ApiAuthenticationHandler : AuthenticationHandler + { + private readonly IOptions _authenticationOptions; + + public ApiAuthenticationHandler( + IOptions authenticationOptions, + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock) + : base(options, logger, encoder, clock) + { + _authenticationOptions = authenticationOptions; + } + + protected override async Task HandleAuthenticateAsync() + { + if (!_authenticationOptions.Value.SchemeMap.ContainsKey("Bearer")) + { + return AuthenticateResult.NoResult(); + } + + return await Context.AuthenticateAsync("Bearer"); + } + } + + public class ApiAuthorizationOptions : AuthenticationSchemeOptions + { + } +} \ No newline at end of file diff --git a/src/OrchardCore/OrchardCore.Infrastructure/Security/ServiceCollectionExtensions.cs b/src/OrchardCore/OrchardCore.Infrastructure/Security/ServiceCollectionExtensions.cs index 5928b3016fb..45589378dd9 100644 --- a/src/OrchardCore/OrchardCore.Infrastructure/Security/ServiceCollectionExtensions.cs +++ b/src/OrchardCore/OrchardCore.Infrastructure/Security/ServiceCollectionExtensions.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; using OrchardCore.Security.AuthorizationHandlers; @@ -12,6 +13,8 @@ public static class ServiceCollectionExtensions public static IServiceCollection AddSecurity(this IServiceCollection services) { services.AddAuthorization(); + services.AddAuthentication().AddScheme("Api", null); + services.AddScoped(); services.AddScoped(); diff --git a/src/OrchardCore/OrchardCore.Setup.Core/SetupService.cs b/src/OrchardCore/OrchardCore.Setup.Core/SetupService.cs index 3da7ab18d90..9f25dfa5532 100644 --- a/src/OrchardCore/OrchardCore.Setup.Core/SetupService.cs +++ b/src/OrchardCore/OrchardCore.Setup.Core/SetupService.cs @@ -113,6 +113,11 @@ public async Task SetupInternalAsync(SetupContext context) shellSettings.TablePrefix = context.DatabaseTablePrefix; } + if (String.IsNullOrWhiteSpace(shellSettings.DatabaseProvider)) + { + throw new ArgumentException($"{nameof(shellSettings.DatabaseProvider)} is required"); + } + // Creating a standalone environment based on a "minimum shell descriptor". // In theory this environment can be used to resolve any normal components by interface, and those // components will exist entirely in isolation - no crossover between the safemode container currently in effect diff --git a/test/OrchardCore.Tests/Data/ContentItemTests.cs b/test/OrchardCore.Tests/Data/ContentItemTests.cs index a1687a4eef4..726f5a957a6 100644 --- a/test/OrchardCore.Tests/Data/ContentItemTests.cs +++ b/test/OrchardCore.Tests/Data/ContentItemTests.cs @@ -20,7 +20,7 @@ public void ShouldSerializeContent() var contentItem2 = JsonConvert.DeserializeObject(json); - Assert.Equal(contentItem.Id, contentItem2.Id); + Assert.Equal(0, contentItem2.Id); // Should be 0 as we dont serialize it. Assert.Equal(contentItem.ContentItemId, contentItem2.ContentItemId); Assert.Equal(contentItem.ContentType, contentItem2.ContentType); Assert.Equal(contentItem.Latest, contentItem2.Latest);