Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,16 @@ public virtual async Task<IActionResult> ReplaceAsync([FromRoute] string id, Can
throw new HttpException(StatusCodes.Status404NotFound);
}

await AuthorizeRequestAsync(TableOperation.Update, entity, cancellationToken).ConfigureAwait(false);

await AuthorizeRequestAsync(TableOperation.Update, existing, cancellationToken).ConfigureAwait(false);
if (Options.EnableSoftDelete && existing.Deleted && !Request.ShouldIncludeDeletedEntities())
{
Logger.LogWarning("ReplaceAsync: {id} statusCode=410 deleted", id);
throw new HttpException(StatusCodes.Status410Gone);
}

Request.ParseConditionalRequest(existing, out byte[] version);

await AccessControlProvider.PreCommitHookAsync(TableOperation.Update, entity, cancellationToken).ConfigureAwait(false);

await Repository.ReplaceAsync(entity, version, cancellationToken).ConfigureAwait(false);

await PostCommitHookAsync(TableOperation.Update, entity, cancellationToken).ConfigureAwait(false);

Logger.LogInformation("ReplaceAsync: replaced {entity}", entity.ToJsonString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ public void GetAllTableControllers_AlternateAssembly()
{
// Adjust this as necessary to the list of controllers in the TestService.
string[] expected = [
"AuthorizedMovieController",
"InMemoryKitchenSinkController",
"InMemoryMovieController",
"InMemoryPagedMovieController",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using CommunityToolkit.Datasync.Common;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using NSubstitute;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using CommunityToolkit.Datasync.Server.InMemory;
using CommunityToolkit.Datasync.TestCommon.Databases;
using CommunityToolkit.Datasync.TestCommon.Models;
using CommunityToolkit.Datasync.TestService.AccessControlProviders;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -26,6 +27,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
internal string MovieEndpoint = "api/in-memory/movies";
internal string PagedMovieEndpoint = "api/in-memory/pagedmovies";
internal string SoftDeletedMovieEndpoint = "api/in-memory/softmovies";
internal string AuthorizedMovieEndpoint = "api/authorized/movies";

internal void RunWithRepository<TEntity>(Action<InMemoryRepository<TEntity>> action) where TEntity : InMemoryTableData
{
Expand Down Expand Up @@ -55,6 +57,20 @@ internal TEntity GetServerEntityById<TEntity>(string id) where TEntity : InMemor
return repository.GetEntity(id);
}

internal void SetupAccessControlProvider(bool isAuthorized)
{
using IServiceScope scope = Services.CreateScope();
IAccessControlProvider<InMemoryMovie> provider = scope.ServiceProvider.GetRequiredService<IAccessControlProvider<InMemoryMovie>>();
(provider as MovieAccessControlProvider<InMemoryMovie>).CanBeAuthorized = isAuthorized;
}

internal InMemoryMovie GetAuthorizedEntity()
{
using IServiceScope scope = Services.CreateScope();
IAccessControlProvider<InMemoryMovie> provider = scope.ServiceProvider.GetRequiredService<IAccessControlProvider<InMemoryMovie>>();
return (provider as MovieAccessControlProvider<InMemoryMovie>).LastEntity as InMemoryMovie;
}

internal void SoftDelete<TEntity>(TEntity entity, bool deleted = true) where TEntity : InMemoryTableData
{
using IServiceScope scope = Services.CreateScope();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using CommunityToolkit.Datasync.TestCommon;
using CommunityToolkit.Datasync.TestCommon.Databases;
using CommunityToolkit.Datasync.TestCommon.Models;
using CommunityToolkit.Datasync.TestService.AccessControlProviders;
using Microsoft.AspNetCore.Localization;
using System.Net;
using System.Net.Http.Json;
using System.Text;
Expand Down Expand Up @@ -133,4 +135,24 @@ public async Task Replace_NonJsonData_Returns415()
HttpResponseMessage response = await this.client.PutAsync($"{this.factory.MovieEndpoint}/1", new StringContent(content, Encoding.UTF8, "text/html"));
response.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType);
}

/// <summary>
/// Given an existing entity and an access provider, ensure that we can replace the entity
/// and that the access provider is called with the existing entity, not the new entity.
/// </summary>
[Fact]
public async Task Replace_Unauthorized_Returns401()
{
// Set up the movie access control provider so that is returns false for the update operation
this.factory.SetupAccessControlProvider(false);

InMemoryMovie inMemoryMovie = this.factory.GetRandomMovie();
ClientMovie existingMovie = new(inMemoryMovie) { Title = "New Title" };
HttpResponseMessage response = await this.client.PutAsJsonAsync($"{this.factory.AuthorizedMovieEndpoint}/{existingMovie.Id}", existingMovie, this.serializerOptions);
response.Should().HaveStatusCode(HttpStatusCode.Unauthorized);

// Ensure that the access provider was called with the existing movie, not the new movie
InMemoryMovie lastEntity = this.factory.GetAuthorizedEntity();
lastEntity.Should().HaveEquivalentMetadataTo(inMemoryMovie).And.BeEquivalentTo<IMovie>(inMemoryMovie);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using CommunityToolkit.Datasync.Server;
using CommunityToolkit.Datasync.TestCommon.Models;

namespace CommunityToolkit.Datasync.TestService.AccessControlProviders;

public class MovieAccessControlProvider<T> : AccessControlProvider<T> where T : class, IMovie, ITableData
{
/// <summary>
/// The last entity that was authorized.
/// </summary>
public object LastEntity { get; private set; } = null;

/// <summary>
/// Determines if the entity can be authorized.
/// </summary>
public bool CanBeAuthorized { get; set; } = true;

/// <inheritdoc />
public override ValueTask<bool> IsAuthorizedAsync(TableOperation operation, T entity, CancellationToken cancellationToken = default)
{
LastEntity = entity;
return ValueTask.FromResult(CanBeAuthorized);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using CommunityToolkit.Datasync.Server;
using CommunityToolkit.Datasync.TestCommon.Databases;
using Microsoft.AspNetCore.Mvc;

namespace CommunityToolkit.Datasync.TestService.Controllers;

[ExcludeFromCodeCoverage]
[Route("api/authorized/movies")]
public class AuthorizedMovieController : TableController<InMemoryMovie>
{
public AuthorizedMovieController(IRepository<InMemoryMovie> repository, IAccessControlProvider<InMemoryMovie> provider) : base(repository)
{
AccessControlProvider = provider;
}
}
3 changes: 3 additions & 0 deletions tests/CommunityToolkit.Datasync.TestService/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using TestData = CommunityToolkit.Datasync.TestCommon.TestData;
using CommunityToolkit.Datasync.TestCommon.Databases;
using Microsoft.OData.ModelBuilder;
using CommunityToolkit.Datasync.TestService.AccessControlProviders;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

Expand All @@ -22,6 +23,8 @@
modelBuilder.AddEntityType(typeof(InMemoryKitchenSink));
builder.Services.AddDatasyncServices(modelBuilder.GetEdmModel());

builder.Services.AddSingleton<IAccessControlProvider<InMemoryMovie>>(new MovieAccessControlProvider<InMemoryMovie>());

builder.Services.AddControllers();

WebApplication app = builder.Build();
Expand Down
Loading