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.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<char>[]? Alternatives;
public readonly bool Exclusion;
public Part(string[]? alternatives, bool exclusion)
public Part(ReadOnlyMemory<char>[]? 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<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;
}
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;
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<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)
@ -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;
}
}
}

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);
}
[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 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<CreatedAppFixture>
{
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);
}
}
}

10
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" });
});
}
}

4
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")
{
}
}

21
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<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 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);
}

2
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")
{
}

Loading…
Cancel
Save