diff --git a/backend/src/Squidex.Infrastructure/Security/Permission.Part.cs b/backend/src/Squidex.Infrastructure/Security/Permission.Part.cs index eb63501b3..8b6934dd0 100644 --- a/backend/src/Squidex.Infrastructure/Security/Permission.Part.cs +++ b/backend/src/Squidex.Infrastructure/Security/Permission.Part.cs @@ -6,22 +6,23 @@ // ========================================================================== using System; -using System.Linq; namespace Squidex.Infrastructure.Security { public sealed partial class Permission { - internal struct Part + internal readonly struct Part { - private static readonly char[] AlternativeSeparators = { '|' }; - private static readonly char[] MainSeparators = { '.' }; + private const char SeparatorAlternative = '|'; + private const char SeparatorMain = '.'; + private const char CharAny = '*'; + private const char CharExclude = '^'; - public readonly string[]? Alternatives; + public readonly ReadOnlyMemory[]? Alternatives; public readonly bool Exclusion; - public Part(string[]? alternatives, bool exclusion) + public Part(ReadOnlyMemory[]? alternatives, bool exclusion) { Alternatives = alternatives; @@ -30,37 +31,118 @@ namespace Squidex.Infrastructure.Security public static Part[] ParsePath(string path) { - var parts = path.Split(MainSeparators, StringSplitOptions.RemoveEmptyEntries); + if (string.IsNullOrWhiteSpace(path)) + { + return Array.Empty(); + } + + var current = path.AsMemory(); + var currentSpan = current.Span; - var result = new Part[parts.Length]; + var result = new Part[CountOf(currentSpan, SeparatorMain) + 1]; - for (var i = 0; i < result.Length; i++) + if (result.Length == 1) { - result[i] = Parse(parts[i]); + result[0] = Parse(current); + } + else + { + for (int i = 0, j = 0; i < currentSpan.Length; i++) + { + if (currentSpan[i] == SeparatorMain) + { + result[j] = Parse(current.Slice(0, i)); + + current = current.Slice(i + 1); + currentSpan = current.Span; + + i = 0; + j++; + } + else if (i == currentSpan.Length - 1 || currentSpan[i] == SeparatorMain) + { + result[j] = Parse(current); + } + } } return result; } - public static Part Parse(string part) + public static Part Parse(ReadOnlyMemory current) { + var currentSpan = current.Span; + + if (currentSpan.Length == 0) + { + return new Part(Array.Empty>(), false); + } + var isExclusion = false; - if (part.StartsWith(Exclude, StringComparison.OrdinalIgnoreCase)) + if (currentSpan[0] == CharExclude) { isExclusion = true; - part = part.Substring(1); + current = current.Slice(1); + currentSpan = current.Span; + } + + if (currentSpan.Length == 0) + { + return new Part(Array.Empty>(), isExclusion); + } + + if (current.Length > 1 || currentSpan[0] != CharAny) + { + var alternatives = new ReadOnlyMemory[CountOf(currentSpan, SeparatorAlternative) + 1]; + + if (alternatives.Length == 1) + { + alternatives[0] = current; + } + else + { + for (int i = 0, j = 0; i < current.Length; i++) + { + if (currentSpan[i] == SeparatorAlternative) + { + alternatives[j] = current.Slice(0, i); + + current = current.Slice(i + 1); + currentSpan = current.Span; + + i = 0; + j++; + } + else if (i == current.Length - 1) + { + alternatives[j] = current; + } + } + } + + return new Part(alternatives, isExclusion); } + else + { + return new Part(null, isExclusion); + } + } - string[]? alternatives = null; + private static int CountOf(ReadOnlySpan text, char character) + { + var count = 0; - if (part != Any) + for (var i = 0; i < text.Length; i++) { - alternatives = part.Split(AlternativeSeparators, StringSplitOptions.RemoveEmptyEntries); + if (text[i] == character) + { + count++; + } } - return new Part(alternatives, isExclusion); + return count; } public static bool Intersects(ref Part lhs, ref Part rhs, bool allowNull) @@ -70,14 +152,28 @@ namespace Squidex.Infrastructure.Security return true; } - if (allowNull && rhs.Alternatives == null) + if (rhs.Alternatives == null) { - return true; + return allowNull; } var shouldIntersect = !(lhs.Exclusion ^ rhs.Exclusion); - return rhs.Alternatives != null && lhs.Alternatives.Intersect(rhs.Alternatives).Any() == shouldIntersect; + var isIntersected = false; + + for (var i = 0; i < lhs.Alternatives.Length; i++) + { + for (var j = 0; j < rhs.Alternatives.Length; j++) + { + if (lhs.Alternatives[i].Span.Equals(rhs.Alternatives[j].Span, StringComparison.OrdinalIgnoreCase)) + { + isIntersected = true; + break; + } + } + } + + return isIntersected == shouldIntersect; } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Security/PermissionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Security/PermissionTests.cs index c3802ca81..6286f0223 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Security/PermissionTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Security/PermissionTests.cs @@ -166,5 +166,16 @@ namespace Squidex.Infrastructure.Security Assert.Equal(new List { source[2], source[1], source[0] }, sorted); } + + [Theory] + [InlineData("permission")] + [InlineData("permission...")] + [InlineData("permission.||..")] + public void Should_parse_invalid_permissions(string source) + { + var permission = new Permission(source); + + permission.Allows(new Permission(Permission.Any)); + } } } diff --git a/backend/tools/TestSuite/TestSuite.LoadTests/ReadingBenchmarks.cs b/backend/tools/TestSuite/TestSuite.LoadTests/ReadingBenchmarks.cs index ecfa0ee6a..f15cdbc24 100644 --- a/backend/tools/TestSuite/TestSuite.LoadTests/ReadingBenchmarks.cs +++ b/backend/tools/TestSuite/TestSuite.LoadTests/ReadingBenchmarks.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using TestSuite.Fixtures; using Xunit; +using Xunit.Abstractions; #pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1507 // Code should not contain multiple blank lines in a row @@ -17,10 +18,14 @@ namespace TestSuite.LoadTests { public class ReadingBenchmarks : IClassFixture { + private readonly ITestOutputHelper testOutput; + public CreatedAppFixture _ { get; } - public ReadingBenchmarks(CreatedAppFixture fixture) + public ReadingBenchmarks(CreatedAppFixture fixture, ITestOutputHelper testOutput) { + this.testOutput = testOutput; + _ = fixture; } @@ -65,7 +70,7 @@ namespace TestSuite.LoadTests await Run.Parallel(numUsers, numIterationsPerUser, async () => { await _.Apps.GetClientsAsync(_.AppName); - }); + }, 100, testOutput); } } } diff --git a/backend/tools/TestSuite/TestSuite.LoadTests/ReadingContentBenchmarks.cs b/backend/tools/TestSuite/TestSuite.LoadTests/ReadingContentBenchmarks.cs index 8fe47b336..3a6156a47 100644 --- a/backend/tools/TestSuite/TestSuite.LoadTests/ReadingContentBenchmarks.cs +++ b/backend/tools/TestSuite/TestSuite.LoadTests/ReadingContentBenchmarks.cs @@ -64,7 +64,7 @@ namespace TestSuite.LoadTests { await Run.Parallel(numUsers, numIterationsPerUser, async () => { - await _.Contents.GetAsync(new ContentQuery { OrderBy = "data/value/iv asc" }); + await _.Contents.GetAsync(new ContentQuery { OrderBy = "data/number/iv asc" }); }); } @@ -74,7 +74,7 @@ namespace TestSuite.LoadTests { await Run.Parallel(numUsers, numIterationsPerUser, async () => { - await _.Contents.GetAsync(new ContentQuery { Skip = 5, OrderBy = "data/value/iv asc" }); + await _.Contents.GetAsync(new ContentQuery { Skip = 5, OrderBy = "data/number/iv asc" }); }); } @@ -84,7 +84,7 @@ namespace TestSuite.LoadTests { await Run.Parallel(numUsers, numIterationsPerUser, async () => { - await _.Contents.GetAsync(new ContentQuery { Skip = 2, Top = 5, OrderBy = "data/value/iv asc" }); + await _.Contents.GetAsync(new ContentQuery { Skip = 2, Top = 5, OrderBy = "data/number/iv asc" }); }); } @@ -94,7 +94,7 @@ namespace TestSuite.LoadTests { await Run.Parallel(numUsers, numIterationsPerUser, async () => { - await _.Contents.GetAsync(new ContentQuery { Skip = 2, Top = 5, OrderBy = "data/value/iv desc" }); + await _.Contents.GetAsync(new ContentQuery { Skip = 2, Top = 5, OrderBy = "data/number/iv desc" }); }); } @@ -104,7 +104,7 @@ namespace TestSuite.LoadTests { await Run.Parallel(numUsers, numIterationsPerUser, async () => { - await _.Contents.GetAsync(new ContentQuery { Filter = "data/value/iv gt 3 and data/value/iv lt 7", OrderBy = "data/value/iv asc" }); + await _.Contents.GetAsync(new ContentQuery { Filter = "data/number/iv gt 3 and data/number/iv lt 7", OrderBy = "data/number/iv asc" }); }); } } diff --git a/backend/tools/TestSuite/TestSuite.LoadTests/ReadingFixture.cs b/backend/tools/TestSuite/TestSuite.LoadTests/ReadingFixture.cs index d1765c6a3..2e97d0b52 100644 --- a/backend/tools/TestSuite/TestSuite.LoadTests/ReadingFixture.cs +++ b/backend/tools/TestSuite/TestSuite.LoadTests/ReadingFixture.cs @@ -9,10 +9,10 @@ using TestSuite.Fixtures; namespace TestSuite.LoadTests { - public sealed class ReadingFixture : ContentFixture + public sealed class ReadingFixture : ContentQueryFixture { public ReadingFixture() - : base("benchmark_reading") + : base("benchmark-reading") { } } diff --git a/backend/tools/TestSuite/TestSuite.LoadTests/Run.cs b/backend/tools/TestSuite/TestSuite.LoadTests/Run.cs index f3d421d6a..95f150b29 100644 --- a/backend/tools/TestSuite/TestSuite.LoadTests/Run.cs +++ b/backend/tools/TestSuite/TestSuite.LoadTests/Run.cs @@ -13,13 +13,16 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Xunit; +using Xunit.Abstractions; namespace TestSuite.LoadTests { public static class Run { - public static async Task Parallel(int numUsers, int numIterationsPerUser, Func action, int expectedAvg = 100) + public static async Task Parallel(int numUsers, int numIterationsPerUser, Func action, int expectedAvg = 100, ITestOutputHelper testOutput = null) { + await action(); + var elapsedMs = new ConcurrentBag(); var errors = 0; @@ -56,16 +59,18 @@ namespace TestSuite.LoadTests var count = elapsedMs.Count; - var max = elapsedMs.Max(); - var min = elapsedMs.Min(); - var avg = elapsedMs.Average(); - Assert.Equal(0, errors); - Assert.Equal(count, numUsers * numIterationsPerUser); + if (testOutput != null) + { + testOutput.WriteLine("Total Errors: {0}/{1}", errors, numUsers * numIterationsPerUser); + testOutput.WriteLine("Total Count: {0}/{1}", count, numUsers * numIterationsPerUser); - Assert.InRange(max, 0, expectedAvg * 10); - Assert.InRange(min, 0, expectedAvg); + testOutput.WriteLine(string.Empty); + testOutput.WriteLine("Performance Average: {0}", avg); + testOutput.WriteLine("Performance Max: {0}", elapsedMs.Max()); + testOutput.WriteLine("Performance Min: {0}", elapsedMs.Min()); + } Assert.InRange(avg, 0, expectedAvg); } diff --git a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ContentQueryFixture.cs b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ContentQueryFixture.cs index 13e834f58..3d88e3463 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ContentQueryFixture.cs +++ b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ContentQueryFixture.cs @@ -13,7 +13,7 @@ namespace TestSuite.Fixtures public class ContentQueryFixture : ContentFixture { public ContentQueryFixture() - : this("my-reads") + : this("my-reads") { }