Browse Source

Optimize permission parsing.

pull/544/head
Sebastian 6 years ago
parent
commit
c0cfabca12
  1. 136
      backend/src/Squidex.Infrastructure/Security/Permission.Part.cs
  2. 11
      backend/tests/Squidex.Infrastructure.Tests/Security/PermissionTests.cs
  3. 9
      backend/tools/TestSuite/TestSuite.LoadTests/ReadingBenchmarks.cs
  4. 10
      backend/tools/TestSuite/TestSuite.LoadTests/ReadingContentBenchmarks.cs
  5. 4
      backend/tools/TestSuite/TestSuite.LoadTests/ReadingFixture.cs
  6. 21
      backend/tools/TestSuite/TestSuite.LoadTests/Run.cs
  7. 2
      backend/tools/TestSuite/TestSuite.Shared/Fixtures/ContentQueryFixture.cs

136
backend/src/Squidex.Infrastructure/Security/Permission.Part.cs

@ -6,22 +6,23 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Linq;
namespace Squidex.Infrastructure.Security namespace Squidex.Infrastructure.Security
{ {
public sealed partial class Permission public sealed partial class Permission
{ {
internal struct Part internal readonly struct Part
{ {
private static readonly char[] AlternativeSeparators = { '|' }; private const char SeparatorAlternative = '|';
private static readonly char[] MainSeparators = { '.' }; private const char SeparatorMain = '.';
private const char CharAny = '*';
private const char CharExclude = '^';
public readonly string[]? Alternatives; public readonly ReadOnlyMemory<char>[]? Alternatives;
public readonly bool Exclusion; public readonly bool Exclusion;
public Part(string[]? alternatives, bool exclusion) public Part(ReadOnlyMemory<char>[]? alternatives, bool exclusion)
{ {
Alternatives = alternatives; Alternatives = alternatives;
@ -30,37 +31,118 @@ namespace Squidex.Infrastructure.Security
public static Part[] ParsePath(string path) public static Part[] ParsePath(string path)
{ {
var parts = path.Split(MainSeparators, StringSplitOptions.RemoveEmptyEntries); if (string.IsNullOrWhiteSpace(path))
{
return Array.Empty<Part>();
}
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; return result;
} }
public static Part Parse(string part) public static Part Parse(ReadOnlyMemory<char> current)
{ {
var currentSpan = current.Span;
if (currentSpan.Length == 0)
{
return new Part(Array.Empty<ReadOnlyMemory<char>>(), false);
}
var isExclusion = false; var isExclusion = false;
if (part.StartsWith(Exclude, StringComparison.OrdinalIgnoreCase)) if (currentSpan[0] == CharExclude)
{ {
isExclusion = true; isExclusion = true;
part = part.Substring(1); current = current.Slice(1);
currentSpan = current.Span;
}
if (currentSpan.Length == 0)
{
return new Part(Array.Empty<ReadOnlyMemory<char>>(), isExclusion);
}
if (current.Length > 1 || currentSpan[0] != CharAny)
{
var alternatives = new ReadOnlyMemory<char>[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<char> 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) public static bool Intersects(ref Part lhs, ref Part rhs, bool allowNull)
@ -70,14 +152,28 @@ namespace Squidex.Infrastructure.Security
return true; return true;
} }
if (allowNull && rhs.Alternatives == null) if (rhs.Alternatives == null)
{ {
return true; return allowNull;
} }
var shouldIntersect = !(lhs.Exclusion ^ rhs.Exclusion); 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;
} }
} }
} }

11
backend/tests/Squidex.Infrastructure.Tests/Security/PermissionTests.cs

@ -166,5 +166,16 @@ namespace Squidex.Infrastructure.Security
Assert.Equal(new List<Permission> { source[2], source[1], source[0] }, sorted); Assert.Equal(new List<Permission> { 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));
}
} }
} }

9
backend/tools/TestSuite/TestSuite.LoadTests/ReadingBenchmarks.cs

@ -9,6 +9,7 @@ using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using TestSuite.Fixtures; using TestSuite.Fixtures;
using Xunit; using Xunit;
using Xunit.Abstractions;
#pragma warning disable SA1300 // Element should begin with upper-case letter #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 #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<CreatedAppFixture> public class ReadingBenchmarks : IClassFixture<CreatedAppFixture>
{ {
private readonly ITestOutputHelper testOutput;
public CreatedAppFixture _ { get; } public CreatedAppFixture _ { get; }
public ReadingBenchmarks(CreatedAppFixture fixture) public ReadingBenchmarks(CreatedAppFixture fixture, ITestOutputHelper testOutput)
{ {
this.testOutput = testOutput;
_ = fixture; _ = fixture;
} }
@ -65,7 +70,7 @@ namespace TestSuite.LoadTests
await Run.Parallel(numUsers, numIterationsPerUser, async () => await Run.Parallel(numUsers, numIterationsPerUser, async () =>
{ {
await _.Apps.GetClientsAsync(_.AppName); await _.Apps.GetClientsAsync(_.AppName);
}); }, 100, testOutput);
} }
} }
} }

10
backend/tools/TestSuite/TestSuite.LoadTests/ReadingContentBenchmarks.cs

@ -64,7 +64,7 @@ namespace TestSuite.LoadTests
{ {
await Run.Parallel(numUsers, numIterationsPerUser, async () => 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 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 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 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 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" });
}); });
} }
} }

4
backend/tools/TestSuite/TestSuite.LoadTests/ReadingFixture.cs

@ -9,10 +9,10 @@ using TestSuite.Fixtures;
namespace TestSuite.LoadTests namespace TestSuite.LoadTests
{ {
public sealed class ReadingFixture : ContentFixture public sealed class ReadingFixture : ContentQueryFixture
{ {
public ReadingFixture() public ReadingFixture()
: base("benchmark_reading") : base("benchmark-reading")
{ {
} }
} }

21
backend/tools/TestSuite/TestSuite.LoadTests/Run.cs

@ -13,13 +13,16 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Xunit; using Xunit;
using Xunit.Abstractions;
namespace TestSuite.LoadTests namespace TestSuite.LoadTests
{ {
public static class Run public static class Run
{ {
public static async Task Parallel(int numUsers, int numIterationsPerUser, Func<Task> action, int expectedAvg = 100) public static async Task Parallel(int numUsers, int numIterationsPerUser, Func<Task> action, int expectedAvg = 100, ITestOutputHelper testOutput = null)
{ {
await action();
var elapsedMs = new ConcurrentBag<long>(); var elapsedMs = new ConcurrentBag<long>();
var errors = 0; var errors = 0;
@ -56,16 +59,18 @@ namespace TestSuite.LoadTests
var count = elapsedMs.Count; var count = elapsedMs.Count;
var max = elapsedMs.Max();
var min = elapsedMs.Min();
var avg = elapsedMs.Average(); var avg = elapsedMs.Average();
Assert.Equal(0, errors); if (testOutput != null)
Assert.Equal(count, numUsers * numIterationsPerUser); {
testOutput.WriteLine("Total Errors: {0}/{1}", errors, numUsers * numIterationsPerUser);
testOutput.WriteLine("Total Count: {0}/{1}", count, numUsers * numIterationsPerUser);
Assert.InRange(max, 0, expectedAvg * 10); testOutput.WriteLine(string.Empty);
Assert.InRange(min, 0, expectedAvg); 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); Assert.InRange(avg, 0, expectedAvg);
} }

2
backend/tools/TestSuite/TestSuite.Shared/Fixtures/ContentQueryFixture.cs

@ -13,7 +13,7 @@ namespace TestSuite.Fixtures
public class ContentQueryFixture : ContentFixture public class ContentQueryFixture : ContentFixture
{ {
public ContentQueryFixture() public ContentQueryFixture()
: this("my-reads") : this("my-reads")
{ {
} }

Loading…
Cancel
Save