From 9abc93c60c5e09892c7692f50c0a55cf887eafda Mon Sep 17 00:00:00 2001 From: Nataly Shrits Date: Sun, 7 Apr 2019 15:37:59 +0300 Subject: [PATCH] added option to include errors in tweek-api values result (#1132) * split files * return all error from jpad calculations * add option to propagate errors to tweek response * add e2e-integration .dockerignore file * add unit test * move smoke tests to integration tests * propagate error tests * added EnsureSuccess method to TweekValuesResult * rename propagateErrors -> includeErrors * split to files --- core/Engine/Tweek.Engine.Tests/EngineTests.cs | 96 +++++++++++++------ .../TestDrivers/ITestDriver.cs | 2 +- .../TestDrivers/InMemoryTestDriver.cs | 2 +- core/Engine/Tweek.Engine/FallbackRule.cs | 25 ----- core/Engine/Tweek.Engine/ITweek.cs | 15 +++ core/Engine/Tweek.Engine/Tweek.cs | 15 +++ .../{Api.cs => TweekExtensions.cs} | 65 +------------ core/Engine/Tweek.Engine/TweekRunner.cs | 57 +++++++++++ core/Engine/Tweek.Engine/TweekValuesResult.cs | 12 +++ .../TweekValuesResultExtensions.cs | 15 +++ e2e/integration/.dockerignore | 1 + .../spec/tweek-api/context.test.js | 93 ++++++++++++++++++ .../spec/tweek-api/delete-context.test.js | 28 ------ .../spec/tweek-api/values/hidden-keys.test.js | 32 +++++++ .../tweek-api/values/ignore-key-type.test.js | 31 ++++++ .../tweek-api/values/include-errors.test.js | 96 +++++++++++++++++++ e2e/ui/.dockerignore | 4 +- .../ContextTests.cs | 90 ----------------- .../GetConfigurations/HiddenKeysTests.cs | 72 -------------- .../GetConfigurations/IgnoreKeyTypesTests.cs | 56 ----------- .../Controllers/KeysController.cs | 30 ++++-- .../Security/AuthorizationDecider.cs | 31 ++++-- services/api/Tweek.ApiService/Startup.cs | 18 ++-- .../integration_tests/include_errors.jpad | 18 ++++ .../integration_tests/include_errors.json | 16 ++++ 25 files changed, 534 insertions(+), 386 deletions(-) delete mode 100644 core/Engine/Tweek.Engine/FallbackRule.cs create mode 100644 core/Engine/Tweek.Engine/ITweek.cs create mode 100644 core/Engine/Tweek.Engine/Tweek.cs rename core/Engine/Tweek.Engine/{Api.cs => TweekExtensions.cs} (53%) create mode 100644 core/Engine/Tweek.Engine/TweekRunner.cs create mode 100644 core/Engine/Tweek.Engine/TweekValuesResult.cs create mode 100644 core/Engine/Tweek.Engine/TweekValuesResultExtensions.cs create mode 100644 e2e/integration/.dockerignore create mode 100644 e2e/integration/spec/tweek-api/context.test.js delete mode 100644 e2e/integration/spec/tweek-api/delete-context.test.js create mode 100644 e2e/integration/spec/tweek-api/values/hidden-keys.test.js create mode 100644 e2e/integration/spec/tweek-api/values/ignore-key-type.test.js create mode 100644 e2e/integration/spec/tweek-api/values/include-errors.test.js delete mode 100644 services/api/Tweek.ApiService.SmokeTests/ContextTests.cs delete mode 100644 services/api/Tweek.ApiService.SmokeTests/GetConfigurations/HiddenKeysTests.cs delete mode 100644 services/api/Tweek.ApiService.SmokeTests/GetConfigurations/IgnoreKeyTypesTests.cs create mode 100644 services/git-service/BareRepository/tests-source/implementations/jpad/integration_tests/include_errors.jpad create mode 100644 services/git-service/BareRepository/tests-source/manifests/integration_tests/include_errors.json diff --git a/core/Engine/Tweek.Engine.Tests/EngineTests.cs b/core/Engine/Tweek.Engine.Tests/EngineTests.cs index ec35e5b12..d49a982e4 100644 --- a/core/Engine/Tweek.Engine.Tests/EngineTests.cs +++ b/core/Engine/Tweek.Engine.Tests/EngineTests.cs @@ -41,7 +41,7 @@ public EngineTests(TestDriverFixture fixture) async Task Run(Func test) { - var scope = driver.SetTestEnviornment(contexts, paths, rules); + var scope = driver.SetTestEnvironment(contexts, paths, rules); await scope.Run(test); } @@ -58,13 +58,13 @@ public async Task CalculateSingleValue() await Run(async (tweek, context) => { var val = await tweek.GetContextAndCalculate("_", NoIdentities, context); - Assert.Equal("SomeValue", val["abc/somepath"].Value.AsString()); + Assert.Equal("SomeValue", val.Data["abc/somepath"].Value.AsString()); val = await tweek.GetContextAndCalculate("abc/_", NoIdentities, context); - Assert.Equal( "SomeValue", val["abc/somepath"].Value.AsString()); + Assert.Equal( "SomeValue", val.Data["abc/somepath"].Value.AsString()); val = await tweek.GetContextAndCalculate("abc/somepath", NoIdentities, context); - Assert.Equal( "SomeValue", val["abc/somepath"].Value.AsString()); + Assert.Equal( "SomeValue", val.Data["abc/somepath"].Value.AsString()); }); } @@ -78,7 +78,7 @@ public async Task CalculateMultipleValues() await Run(async (tweek, context) => { - var val = await tweek.GetContextAndCalculate("abc/_", NoIdentities, context); + var val = (await tweek.GetContextAndCalculate("abc/_", NoIdentities, context)).Data; Assert.Equal(3, val.Count); Assert.Equal("SomeValue",val["abc/somepath"].Value.AsString()); Assert.Equal("SomeValue",val["abc/otherpath"].Value.AsString()); @@ -96,7 +96,7 @@ public async Task CalculateMultiplePathQueries() await Run(async (tweek, context) => { - var val = await tweek.GetContextAndCalculate(new List{"abc/_", "def/_"}, NoIdentities, context); + var val = (await tweek.GetContextAndCalculate(new List{"abc/_", "def/_"}, NoIdentities, context)).Data; Assert.Equal(4, val.Count); Assert.Equal("SomeValue", val["abc/somepath"].Value.AsString()); Assert.Equal("SomeValue", val["abc/otherpath"].Value.AsString()); @@ -115,7 +115,7 @@ public async Task CalculateMultiplePathQueriesWithOverlap() await Run(async (tweek, context) => { - var val = await tweek.GetContextAndCalculate(new List { "abc/_", "abc/nested/_" }, NoIdentities, context); + var val = (await tweek.GetContextAndCalculate(new List { "abc/_", "abc/nested/_" }, NoIdentities, context)).Data; Assert.Equal(3, val.Count); Assert.Equal("SomeValue", val["abc/somepath"].Value.AsString()); Assert.Equal("SomeValue", val["abc/otherpath"].Value.AsString()); @@ -142,13 +142,13 @@ public async Task CalculateFilterByMatcher() await Run(async (tweek, context) => { var val = await tweek.GetContextAndCalculate("abc/_", new HashSet { new Identity("device", "1") }, context); - Assert.Equal(0, val.Count); + Assert.Equal(0, val.Data.Count); val = await tweek.GetContextAndCalculate("abc/_", new HashSet { new Identity("device", "2") }, context); - Assert.Equal(0, val.Count); + Assert.Equal(0, val.Data.Count); val = await tweek.GetContextAndCalculate("abc/_", new HashSet { new Identity("device", "3") }, context); - Assert.Equal("SomeValue", val["abc/somepath"].Value.AsString()); + Assert.Equal("SomeValue", val.Data["abc/somepath"].Value.AsString()); }); } @@ -171,13 +171,13 @@ public async Task CalculateFilterByMatcherWithMultiIdentities() await Run(async (tweek, context) => { var val = await tweek.GetContextAndCalculate("abc/_", new HashSet { new Identity("device", "1") }, context); - Assert.Equal(0, val.Count); + Assert.Equal(0, val.Data.Count); val = await tweek.GetContextAndCalculate("abc/_", new HashSet { new Identity("user", "1") }, context); - Assert.Equal(0, val.Count); + Assert.Equal(0, val.Data.Count); val = await tweek.GetContextAndCalculate("abc/_", new HashSet { new Identity("device", "1"), new Identity("user", "1") }, context); - Assert.Equal("SomeValue", val["abc/somepath"].Value.AsString()); + Assert.Equal("SomeValue", val.Data["abc/somepath"].Value.AsString()); }); } @@ -196,7 +196,7 @@ public async Task MultipleRules() await Run(async (tweek, context) => { var val = await tweek.GetContextAndCalculate("abc/_", new HashSet { new Identity("device", "1") }, context); - Assert.Equal( "SomeValue", val["abc/somepath"].Value.AsString()); + Assert.Equal( "SomeValue", val.Data["abc/somepath"].Value.AsString()); }); } @@ -222,7 +222,7 @@ public async Task MultipleRulesWithFallback() await Run(async (tweek, context) => { var val = await tweek.GetContextAndCalculate("abc/_", new HashSet { new Identity("device", "1") }, context); - Assert.Equal("SomeValue", val["abc/somepath"].Value.AsString()); + Assert.Equal("SomeValue", val.Data["abc/somepath"].Value.AsString()); }); } @@ -248,12 +248,14 @@ public async Task CalculateWithMultiVariant() await Run(async (tweek, context) => { var val = await tweek.GetContextAndCalculate("abc/_", new HashSet { }, context); - Assert.Equal(0, val.Count); + Assert.Equal(0, val.Data.Count); val = await tweek.GetContextAndCalculate("abc/_", new HashSet { new Identity("device", "1")}, context); - Assert.True(val["abc/somepath"].Value.AsString() == "true" || val["abc/somepath"].Value.AsString() == "false"); + Assert.True(val.Data["abc/somepath"].Value.AsString() == "true" || val.Data["abc/somepath"].Value.AsString() == "false"); await Task.WhenAll(Enumerable.Range(0, 10).Select(async x => { - Assert.Equal((await tweek.GetContextAndCalculate("abc/_", new HashSet { new Identity("device", "1") }, context))["abc/somepath"].Value, val["abc/somepath"].Value); + var expected = val.Data["abc/somepath"].Value; + val = await tweek.GetContextAndCalculate("abc/_", new HashSet {new Identity("device", "1")}, context); + Assert.Equal(val.Data["abc/somepath"].Value, expected); })); }); } @@ -276,7 +278,7 @@ public async Task ContextKeysShouldBeCaseInsensitive() await Run(async (tweek, context) => { var val = await tweek.GetContextAndCalculate("abc/_", new HashSet { new Identity("device", "1") }, context); - Assert.Equal("true", val["abc/somepath"].Value.AsString()); + Assert.Equal("true", val.Data["abc/somepath"].Value.AsString()); }); } @@ -306,10 +308,10 @@ public async Task RuleUsingTimeBasedOperators() await Run(async (tweek, context) => { var val = await tweek.GetContextAndCalculate("abc/_", new HashSet { new Identity("device", "1") }, context); - Assert.Equal("true", val["abc/somepath"].Value.AsString()); + Assert.Equal("true", val.Data["abc/somepath"].Value.AsString()); val = await tweek.GetContextAndCalculate("abc/_", new HashSet { new Identity("device", "2") }, context); - Assert.Equal("false", val["abc/somepath"].Value.AsString()); + Assert.Equal("false", val.Data["abc/somepath"].Value.AsString()); }); } @@ -334,13 +336,13 @@ public async Task CalculateWithFixedValue() await Run(async (tweek, context) => { var val = await tweek.GetContextAndCalculate("abc/_", new HashSet { new Identity("device", "1") }, context); - Assert.Equal("FixedValue", val["abc/somepath"].Value.AsString()); + Assert.Equal("FixedValue", val.Data["abc/somepath"].Value.AsString()); val = await tweek.GetContextAndCalculate("abc/_", new HashSet { new Identity("device", "2") }, context); - Assert.Equal("RuleBasedValue", val["abc/somepath"].Value.AsString()); + Assert.Equal("RuleBasedValue", val.Data["abc/somepath"].Value.AsString()); val = await tweek.GetContextAndCalculate("abc/_", new HashSet { new Identity("device", "3") }, context); - Assert.Equal("FixedValue", val["abc/somepath"].Value.AsString()); + Assert.Equal("FixedValue", val.Data["abc/somepath"].Value.AsString()); }); } @@ -371,11 +373,11 @@ public async Task CalculateWithRecursiveMatcher() await Run(async (tweek, context) => { - var val = await tweek.GetContextAndCalculate("abc/_", new HashSet { new Identity("device", "1") }, context); + var val = (await tweek.GetContextAndCalculate("abc/_", new HashSet { new Identity("device", "1") }, context)).Data; Assert.Equal(1, val.Count); Assert.Equal("true", val["abc/dep_path1"].Value.AsString()); - val = await tweek.GetContextAndCalculate("abc/_", new HashSet { new Identity("device", "2") }, context); + val = (await tweek.GetContextAndCalculate("abc/_", new HashSet { new Identity("device", "2") }, context)).Data; Assert.Equal(3, val.Count); Assert.Equal("true", val["abc/dep_path1"].Value.AsString()); Assert.Equal("true", val["abc/dep_path2"].Value.AsString()); @@ -399,9 +401,49 @@ public async Task EmptyFixedKeyIsIgnoredInScan() await Run(async (tweek, context) => { var val = await tweek.GetContextAndCalculate("_", new HashSet { new Identity("device", "1")}, context); - Assert.Equal("SomeValue", val["abc/somepath"].Value.AsString()); + Assert.Equal("SomeValue", val.Data["abc/somepath"].Value.AsString()); }); } + [Fact] + public async Task BadKeyShouldReturnError() + { + contexts = ContextCreator.Create("device", "1", Tuple.Create("SomeDeviceProp", JsonValue.NewNumber(3))); + + paths = new[] { "abc/bad_path", "abc/good_path" }; + rules = new Dictionary + { + ["abc/bad_path"] = JPadGenerator.New() + .AddSingleVariantRule(JsonConvert.SerializeObject(new MatcherData {["device.SomeDeviceProp"] = new string[]{}}), "BadValue") + .Generate(), + ["abc/good_path"] = JPadGenerator.New().AddSingleVariantRule("{}", "SomeValue").Generate() + }; + + await Run(async (tweek, context) => + { + var identities = new HashSet {new Identity("device", "1")}; + + var val = await tweek.GetContextAndCalculate("_", identities, context); + Assert.Equal(1, val.Errors.Count); + Assert.True(val.Errors.ContainsKey("abc/bad_path")); + Assert.False(val.Data.ContainsKey("abc/bad_path")); + Assert.Equal("SomeValue", val.Data["abc/good_path"].Value.AsString()); + + val = await tweek.GetContextAndCalculate("abc/_", identities, context); + Assert.Equal(1, val.Errors.Count); + Assert.True(val.Errors.ContainsKey("abc/bad_path")); + Assert.False(val.Data.ContainsKey("abc/bad_path")); + Assert.Equal( "SomeValue", val.Data["abc/good_path"].Value.AsString()); + + val = await tweek.GetContextAndCalculate("abc/good_path", identities, context); + Assert.Equal(0, val.Errors.Count); + Assert.Equal( "SomeValue", val.Data["abc/good_path"].Value.AsString()); + + val = await tweek.GetContextAndCalculate("abc/bad_path", identities, context); + Assert.Equal(1, val.Errors.Count); + Assert.True(val.Errors.ContainsKey("abc/bad_path")); + Assert.Equal(0, val.Data.Count); + }); + } } } diff --git a/core/Engine/Tweek.Engine.Tests/TestDrivers/ITestDriver.cs b/core/Engine/Tweek.Engine.Tests/TestDrivers/ITestDriver.cs index 0bcac17c2..012cefa80 100644 --- a/core/Engine/Tweek.Engine.Tests/TestDrivers/ITestDriver.cs +++ b/core/Engine/Tweek.Engine.Tests/TestDrivers/ITestDriver.cs @@ -9,6 +9,6 @@ namespace Tweek.Engine.Tests.TestDrivers public interface ITestDriver { IContextDriver Context { get; } - TestScope SetTestEnviornment(Dictionary> contexts, string[] keys, Dictionary rules); + TestScope SetTestEnvironment(Dictionary> contexts, string[] keys, Dictionary rules); } } \ No newline at end of file diff --git a/core/Engine/Tweek.Engine.Tests/TestDrivers/InMemoryTestDriver.cs b/core/Engine/Tweek.Engine.Tests/TestDrivers/InMemoryTestDriver.cs index 3e2f73a5e..4b0b19866 100644 --- a/core/Engine/Tweek.Engine.Tests/TestDrivers/InMemoryTestDriver.cs +++ b/core/Engine/Tweek.Engine.Tests/TestDrivers/InMemoryTestDriver.cs @@ -81,7 +81,7 @@ private async Task InsertContextRows(Dictionary dictionary.Clear(); - public TestScope SetTestEnviornment(Dictionary> contexts, string[] keys, + public TestScope SetTestEnvironment(Dictionary> contexts, string[] keys, Dictionary rules) { return new TestScope(rules: new InMemoryRulesRepository(rules), context: Context, diff --git a/core/Engine/Tweek.Engine/FallbackRule.cs b/core/Engine/Tweek.Engine/FallbackRule.cs deleted file mode 100644 index e3208b22c..000000000 --- a/core/Engine/Tweek.Engine/FallbackRule.cs +++ /dev/null @@ -1,25 +0,0 @@ -using LanguageExt; -using Tweek.Engine.Core.Context; -using Tweek.Engine.Core.Rules; -using Tweek.Engine.Core.Utils; -using Tweek.Engine.DataTypes; - -namespace Tweek.Engine -{ - public class FallbackRule:IRule - { - private readonly IRule _l; - private readonly IRule _r; - - public FallbackRule(IRule l, IRule r) - { - _l = l; - _r = r; - } - - public Option GetValue(GetContextValue fullContext) - { - return _l.GetValue(fullContext).IfNone(() => _r.GetValue(fullContext)); - } - } -} diff --git a/core/Engine/Tweek.Engine/ITweek.cs b/core/Engine/Tweek.Engine/ITweek.cs new file mode 100644 index 000000000..a43c83a3a --- /dev/null +++ b/core/Engine/Tweek.Engine/ITweek.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using Tweek.Engine.Core.Context; +using Tweek.Engine.DataTypes; +using IdentityHashSet = System.Collections.Generic.HashSet; + +namespace Tweek.Engine +{ + public interface ITweek + { + TweekValuesResult Calculate( + ICollection pathQuery, + IdentityHashSet identities, GetLoadedContextByIdentityType context, + ConfigurationPath[] includeFixedPaths = null); + } +} \ No newline at end of file diff --git a/core/Engine/Tweek.Engine/Tweek.cs b/core/Engine/Tweek.Engine/Tweek.cs new file mode 100644 index 000000000..759b245f5 --- /dev/null +++ b/core/Engine/Tweek.Engine/Tweek.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; +using Tweek.Engine.Drivers.Rules; +using Tweek.Engine.Rules.Creation; + +namespace Tweek.Engine +{ + public static class Tweek + { + public static async Task Create(IRulesRepository rulesRepository, GetRuleParser parserResolver) + { + var rulesLoader = await RulesLoader.Factory(rulesRepository, parserResolver); + return new TweekRunner(rulesLoader); + } + } +} \ No newline at end of file diff --git a/core/Engine/Tweek.Engine/Api.cs b/core/Engine/Tweek.Engine/TweekExtensions.cs similarity index 53% rename from core/Engine/Tweek.Engine/Api.cs rename to core/Engine/Tweek.Engine/TweekExtensions.cs index 2b9d1f3b2..4d5f198d2 100644 --- a/core/Engine/Tweek.Engine/Api.cs +++ b/core/Engine/Tweek.Engine/TweekExtensions.cs @@ -4,19 +4,15 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Tweek.Engine.Core; using Tweek.Engine.Core.Context; -using Tweek.Engine.Core.Utils; using Tweek.Engine.DataTypes; using Tweek.Engine.Drivers.Context; -using Tweek.Engine.Drivers.Rules; -using Tweek.Engine.Rules.Creation; using ContextHelpers = Tweek.Engine.Context.ContextHelpers; using IdentityHashSet = System.Collections.Generic.HashSet; namespace Tweek.Engine { - public static class ITweekExtensions + public static class TweekExtensions { private static readonly ConfigurationPath Root = ConfigurationPath.New(""); public static Option SingleKey(this IDictionary results) => SingleKey(results, Root); @@ -26,7 +22,7 @@ public static Option SingleKey(this IDictionary> GetContextAndCalculate(this ITweek tweek, + public static Task GetContextAndCalculate(this ITweek tweek, ConfigurationPath pathQuery, IdentityHashSet identities, IContextReader contextDriver, @@ -35,7 +31,7 @@ public static Option SingleKey(this IDictionary> GetContextAndCalculate(this ITweek tweek, + public static async Task GetContextAndCalculate(this ITweek tweek, ICollection pathQuery, IdentityHashSet identities, IContextReader contextDriver, @@ -63,64 +59,11 @@ public static Option SingleKey(this IDictionary Calculate(this ITweek tweek, + public static TweekValuesResult Calculate(this ITweek tweek, ConfigurationPath pathQuery, IdentityHashSet identities, GetLoadedContextByIdentityType context, ConfigurationPath[] includeFixedPaths = null) { return tweek.Calculate(new[] { pathQuery }, identities, context, includeFixedPaths); } } - - public interface ITweek - { - Dictionary Calculate( - ICollection pathQuery, - IdentityHashSet identities, GetLoadedContextByIdentityType context, ConfigurationPath[] includeFixedPaths = null); - } - - public delegate IEnumerable PathExpander(ConfigurationPath path); - - public class TweekRunner : ITweek - { - private readonly Func<(GetRule, PathExpander)> _rulesLoader; - - public TweekRunner(Func<(GetRule, PathExpander)> rulesLoader) - { - _rulesLoader = rulesLoader; - } - - public Dictionary Calculate( - ICollection pathQuery, - IdentityHashSet identities, - GetLoadedContextByIdentityType context, - ConfigurationPath[] includeFixedPaths = null) - { - includeFixedPaths = includeFixedPaths ?? new ConfigurationPath[0]; - var (getRules, expandKey) = _rulesLoader(); - - var getRuleValue = EngineCore.GetRulesEvaluator(identities, context, getRules); - - var scanItems = pathQuery.Where(s => s.IsScan).ToList(); - var include = includeFixedPaths - .Where(path => !path.IsHidden() && scanItems.Any(query => query.Contains(path))); - var expandItems = scanItems.SelectMany(path => expandKey(path)); - - var paths = include.Concat(expandItems).Concat(pathQuery.Where(t => !t.IsScan)); - - return paths - .Distinct() - .Select(path => getRuleValue(path).Map(value => new { path, value })) - .SkipEmpty() - .ToDictionary(x => x.path, x => x.value); - } - } - - public static class Tweek - { - public static async Task Create(IRulesRepository rulesRepository, GetRuleParser parserResolver) - { - var rulesLoader = await RulesLoader.Factory(rulesRepository, parserResolver); - return new TweekRunner(rulesLoader); - } - } } diff --git a/core/Engine/Tweek.Engine/TweekRunner.cs b/core/Engine/Tweek.Engine/TweekRunner.cs new file mode 100644 index 000000000..9d5635b33 --- /dev/null +++ b/core/Engine/Tweek.Engine/TweekRunner.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Tweek.Engine.Core; +using Tweek.Engine.Core.Context; +using Tweek.Engine.DataTypes; + +namespace Tweek.Engine +{ + public delegate IEnumerable PathExpander(ConfigurationPath path); + + public class TweekRunner : ITweek + { + private readonly Func<(GetRule, PathExpander)> _rulesLoader; + + public TweekRunner(Func<(GetRule, PathExpander)> rulesLoader) + { + _rulesLoader = rulesLoader; + } + + public TweekValuesResult Calculate( + ICollection pathQuery, + HashSet identities, + GetLoadedContextByIdentityType context, + ConfigurationPath[] includeFixedPaths = null) + { + includeFixedPaths = includeFixedPaths ?? new ConfigurationPath[0]; + var (getRules, expandKey) = _rulesLoader(); + + var getRuleValue = EngineCore.GetRulesEvaluator(identities, context, getRules); + + var scanItems = pathQuery.Where(s => s.IsScan).ToList(); + var include = includeFixedPaths + .Where(path => !path.IsHidden() && scanItems.Any(query => query.Contains(path))); + var expandItems = scanItems.SelectMany(path => expandKey(path)); + + var paths = include.Concat(expandItems).Concat(pathQuery.Where(t => !t.IsScan)).Distinct(); + + var result = new TweekValuesResult(); + + foreach (var path in paths) + { + try + { + var ruleValue = getRuleValue(path); + ruleValue.IfSome(value => result.Data[path] = value); + } + catch (Exception e) + { + result.Errors[path] = e; + } + } + + return result; + } + } +} \ No newline at end of file diff --git a/core/Engine/Tweek.Engine/TweekValuesResult.cs b/core/Engine/Tweek.Engine/TweekValuesResult.cs new file mode 100644 index 000000000..c35824566 --- /dev/null +++ b/core/Engine/Tweek.Engine/TweekValuesResult.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using Tweek.Engine.DataTypes; + +namespace Tweek.Engine +{ + public class TweekValuesResult + { + public Dictionary Data { get; } = new Dictionary(); + public Dictionary Errors { get; } = new Dictionary(); + } +} \ No newline at end of file diff --git a/core/Engine/Tweek.Engine/TweekValuesResultExtensions.cs b/core/Engine/Tweek.Engine/TweekValuesResultExtensions.cs new file mode 100644 index 000000000..3adb0a8a4 --- /dev/null +++ b/core/Engine/Tweek.Engine/TweekValuesResultExtensions.cs @@ -0,0 +1,15 @@ +using System; + +namespace Tweek.Engine +{ + public static class TweekValuesResultExtensions + { + public static void EnsureSuccess(this TweekValuesResult result) + { + if (result.Errors.Count > 0) + { + throw new AggregateException(result.Errors.Values); + } + } + } +} \ No newline at end of file diff --git a/e2e/integration/.dockerignore b/e2e/integration/.dockerignore new file mode 100644 index 000000000..40b878db5 --- /dev/null +++ b/e2e/integration/.dockerignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/e2e/integration/spec/tweek-api/context.test.js b/e2e/integration/spec/tweek-api/context.test.js new file mode 100644 index 000000000..9026fb652 --- /dev/null +++ b/e2e/integration/spec/tweek-api/context.test.js @@ -0,0 +1,93 @@ +const uuid = require('uuid/v4'); +const { expect } = require('chai'); +const client = require('../../utils/client'); + +const identityType = 'user'; + +describe('tweek api - context', () => { + describe('append', () => { + it('should succeed appending fixed configuration', async () => { + const identityId = 'append-context-test-1'; + const url = `/api/v2/context/${identityType}/${identityId}`; + const expected = uuid(); + + await client + .post(url) + .send({ '@fixed:tests/fixed/some_fixed_configuration': expected }) + .expect(200); + + let result = await client.get( + `/api/v2/values/tests/fixed/some_fixed_configuration?${identityType}=${identityId}`, + ); + expect(result.body).to.equal(expected); + + const otherExpectedValue = uuid(); + + await client + .post(url) + .send({ + '@fixed:tests/fixed/additional_fixed_configuration1': otherExpectedValue, + '@fixed:tests/fixed/additional_fixed_configuration2': otherExpectedValue, + }) + .expect(200); + + result = await client.get(`/api/v2/values/tests/fixed/_?${identityType}=${identityId}`); + expect(result.body).to.deep.include({ + some_fixed_configuration: expected, + additional_fixed_configuration1: otherExpectedValue, + additional_fixed_configuration2: otherExpectedValue, + }); + }); + + it('should return bad response when trying to append context with bad property type', async () => { + await client + .post(`/api/v2/context/${identityType}/user-1`) + .send({ AgentVersion: 'not a version' }) + .expect(400); + }); + }); + + describe('delete', () => { + it('should delete context', async () => { + const identityId = 'delete_context_user'; + const url = `/api/v2/context/${identityType}/${identityId}`; + + await client + .post(url) + .send({ prop: 'value' }) + .expect(200); + + await client.delete(url).expect(200); + + await client.get(url).expect(200, {}); + }); + + it("should succeed deleting a context that doesn't exist", async () => { + const identityId = uuid(); + + const url = `/api/v2/context/${identityType}/${identityId}`; + + await client.delete(url).expect(200); + }); + + it('should succeed deleting fixed context', async () => { + const identityId = 'delete-context-test-1'; + const url = `/api/v2/context/${identityType}/${identityId}`; + const keyPath = 'tests/fixed/some_fixed_configuration_delete'; + const expected = uuid(); + + await client + .post(url) + .send({ [`@fixed:${keyPath}`]: expected }) + .expect(200); + + let result = await client.get(`/api/v2/values/${keyPath}?${identityType}=${identityId}`); + expect(result.body).to.equal(expected); + + await client.delete(`${url}/@fixed:${keyPath}`).expect(200); + + result = await client.get(`/api/v2/values/${keyPath}?${identityType}=${identityId}`); + expect(result.body).to.equal(null); + }); + }); +}); diff --git a/e2e/integration/spec/tweek-api/delete-context.test.js b/e2e/integration/spec/tweek-api/delete-context.test.js deleted file mode 100644 index f1b4f6360..000000000 --- a/e2e/integration/spec/tweek-api/delete-context.test.js +++ /dev/null @@ -1,28 +0,0 @@ -const uuid = require('uuid/v4'); -const client = require('../../utils/client'); - -const identityType = 'user'; - -describe('tweek api - delete context', () => { - it('should delete context', async () => { - const identityId = 'delete_context_user'; - const url = `/api/v2/context/${identityType}/${identityId}`; - - await client - .post(url) - .send({ prop: 'value' }) - .expect(200); - - await client.delete(url).expect(200); - - await client.get(url).expect(200, {}); - }); - - it("should succeed deleting a context that doesn't exist", async () => { - const identityId = uuid(); - - const url = `/api/v2/context/${identityType}/${identityId}`; - - await client.delete(url).expect(200); - }); -}); diff --git a/e2e/integration/spec/tweek-api/values/hidden-keys.test.js b/e2e/integration/spec/tweek-api/values/hidden-keys.test.js new file mode 100644 index 000000000..647196f9b --- /dev/null +++ b/e2e/integration/spec/tweek-api/values/hidden-keys.test.js @@ -0,0 +1,32 @@ +const client = require('../../../utils/client'); + +describe('tweek api - hidden keys', () => { + it('get scan folder should return only visible keys', async () => { + await client + .get('/api/v2/values/smoke_tests/not_hidden/_') + .expect(200, { some_key: 'some value' }); + }); + + it('get hidden scan folder should return the folder', async () => { + await client + .get('/api/v2/values/smoke_tests/not_hidden/@hidden/_') + .expect(200, { visible_key: 'visible value' }); + }); + + it('get scan folder include hidden folder should return hidden folder', async () => { + await client + .get('/api/v2/values/smoke_tests/not_hidden/_?$include=_&$include=@hidden/_') + .expect(200, { + some_key: 'some value', + '@hidden': { + visible_key: 'visible value', + }, + }); + }); + + it('get hidden key should return hidden key', async () => { + await client + .get('/api/v2/values/smoke_tests/not_hidden/@some_hidden_key') + .expect(200, '"some hidden value"'); + }); +}); diff --git a/e2e/integration/spec/tweek-api/values/ignore-key-type.test.js b/e2e/integration/spec/tweek-api/values/ignore-key-type.test.js new file mode 100644 index 000000000..2d1c6785e --- /dev/null +++ b/e2e/integration/spec/tweek-api/values/ignore-key-type.test.js @@ -0,0 +1,31 @@ +const { expect } = require('chai'); +const client = require('../../../utils/client'); + +describe('tweek api - ignore key type', () => { + const testCases = { + 'smoke_tests/ignore_key_types/string_type': 'hello', + 'smoke_tests/ignore_key_types/number_type': 15, + 'smoke_tests/ignore_key_types/boolean_type': true, + 'smoke_tests/ignore_key_types/object_type': { key: 'value' }, + 'smoke_tests/ignore_key_types/array_type': ['hello', 'world'], + }; + + Object.entries(testCases).forEach(([keyPath, value]) => { + it(`[$ignoreKeyTypes = true] - ${keyPath}`, async () => { + const expected = typeof value === 'string' ? value : JSON.stringify(value); + + const response = await client + .get(`/api/v2/values/${keyPath}?$ignoreKeyTypes=true`) + .expect(200); + expect(response.body).to.deep.equal(expected); + }); + + it(`[$ignoreKeyTypes = false] - ${keyPath}`, async () => { + const response = await client + .get(`/api/v2/values/${keyPath}?$ignoreKeyTypes=false`) + .expect(200); + + expect(response.body).to.deep.equal(value); + }); + }); +}); diff --git a/e2e/integration/spec/tweek-api/values/include-errors.test.js b/e2e/integration/spec/tweek-api/values/include-errors.test.js new file mode 100644 index 000000000..2f6d141b3 --- /dev/null +++ b/e2e/integration/spec/tweek-api/values/include-errors.test.js @@ -0,0 +1,96 @@ +const { expect } = require('chai'); +const client = require('../../../utils/client'); + +describe('tweek api - propagate errors', () => { + const identityType = 'user'; + const identityId = 'bad_context_user'; + const url = `/api/v2/values/integration_tests/include_errors`; + + before(async () => { + await client + .post(`/api/v2/context/${identityType}/${identityId}`) + .send({ BadProperty: 'SomeString' }) + .expect(200); + }); + + describe('[includeErrors = true]', () => { + it('should return null on single key error', async () => { + const result = await client + .get(`${url}?${identityType}=${identityId}&$includeErrors=true`) + .expect('X-Error-Count', '1') + .expect(200); + expect(result.body).to.eql({ + data: null, + errors: { + 'integration_tests/include_errors': 'non matching types', + }, + }); + }); + + it('should return correct value if no error', async () => { + const result = await client + .get(`${url}?$includeErrors=true`) + .expect('X-Error-Count', '0') + .expect(200); + expect(result.body).to.eql({ data: 'DefaultValue', errors: {} }); + }); + + it('should skip error key on scan', async () => { + const result = await client + .get(`/api/v2/values/integration_tests/_?${identityType}=${identityId}&$includeErrors=true`) + .expect('X-Error-Count', '1') + .expect(200); + + expect(result.body.data).to.not.have.property('include_errors'); + expect(result.body.errors).to.eql({ + 'integration_tests/include_errors': 'non matching types', + }); + }); + + it('should return correct value if no error in scan', async () => { + const result = await client + .get(`/api/v2/values/integration_tests/_?$includeErrors=true`) + .expect('X-Error-Count', '0') + .expect(200); + + expect(result.body.data).to.deep.include({ include_errors: 'DefaultValue' }); + expect(result.body.errors).to.eql({}); + }); + }); + + describe('[includeErrors = false]', () => { + it('should return null on single key error', async () => { + const result = await client + .get(`${url}?${identityType}=${identityId}&$includeErrors=false`) + .expect('X-Error-Count', '1') + .expect(200); + expect(result.body).to.eql(null); + }); + + it('should return correct value if no error', async () => { + const result = await client + .get(`${url}?$includeErrors=false`) + .expect('X-Error-Count', '0') + .expect(200); + expect(result.body).to.eql('DefaultValue'); + }); + + it('should skip error key on scan', async () => { + const result = await client + .get(`/api/v2/values/integration_tests/_?${identityType}=${identityId}&$includeErrors=true`) + .expect('X-Error-Count', '1') + .expect(200); + + expect(result.body).to.not.have.property('include_errors'); + }); + + it('should return correct value if no error in scan', async () => { + const result = await client + .get(`/api/v2/values/integration_tests/_?$includeErrors=false`) + .expect('X-Error-Count', '0') + .expect(200); + + expect(result.body).to.deep.include({ include_errors: 'DefaultValue' }); + }); + }); +}); diff --git a/e2e/ui/.dockerignore b/e2e/ui/.dockerignore index 961066c7e..7b34b2793 100644 --- a/e2e/ui/.dockerignore +++ b/e2e/ui/.dockerignore @@ -1,6 +1,6 @@ .git/ -node_modules -screenshots +node_modules/ +screenshots/ .gitignore .dockerignore diff --git a/services/api/Tweek.ApiService.SmokeTests/ContextTests.cs b/services/api/Tweek.ApiService.SmokeTests/ContextTests.cs deleted file mode 100644 index 7a3109ba5..000000000 --- a/services/api/Tweek.ApiService.SmokeTests/ContextTests.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; -using Newtonsoft.Json.Linq; -using Xunit; -using Xunit.Abstractions; -using static FSharpUtils.Newtonsoft.JsonValue; - -namespace Tweek.ApiService.SmokeTests -{ - public class ContextTests - { - private readonly ITweekApi mTweekApi; - - public ContextTests(ITestOutputHelper output) - { - mTweekApi = TweekApiServiceFactory.GetTweekApiClient(output); - } - - - [Fact(DisplayName = "Appending context (insert/upsert) with fixed configuration works properly")] - public async Task AppendContextWithFixedConfiguration() - { - var guid = Guid.NewGuid().ToString(); - await mTweekApi.AppendContext("test", "append-context-test-1", new Dictionary() - { - ["@fixed:tests/fixed/some_fixed_configuration"] = NewString(guid.ToString()) - }); - var results = await mTweekApi.GetConfigurations("tests/fixed/some_fixed_configuration", new Dictionary() - { - ["test"] = "append-context-test-1" - }); - - Assert.Equal(guid, results.ToString()); - var additonalGuid = Guid.NewGuid().ToString(); - await mTweekApi.AppendContext("test", "append-context-test-1", new Dictionary() - { - ["@fixed:tests/fixed/additional_fixed_configuration1"] = NewString(additonalGuid), - ["@fixed:tests/fixed/additional_fixed_configuration2"] = NewString(additonalGuid) - }); - - results = await mTweekApi.GetConfigurations("tests/fixed/_", new Dictionary() - { - ["test"] = "append-context-test-1" - }); - - Assert.Equal(additonalGuid, results["additional_fixed_configuration1"].ToString()); - Assert.Equal(additonalGuid, results["additional_fixed_configuration2"].ToString()); - Assert.Equal(guid, results["some_fixed_configuration"].ToString()); - } - - [Fact(DisplayName = "Deleting fixed configuration")] - public async Task DeleteContextWithFixedConfiguration() - { - var guid = Guid.NewGuid().ToString(); - await mTweekApi.AppendContext("test", "delete-context-test-1", new Dictionary() - { - ["@fixed:tests/fixed/some_fixed_configuration_delete"] = NewString(guid.ToString()) - }); - - var results = await mTweekApi.GetConfigurations("tests/fixed/some_fixed_configuration_delete", new Dictionary() - { - ["test"] = "delete-context-test-1" - }); - Assert.Equal(guid, results.ToString()); - - await mTweekApi.RemoveFromContext("test", "delete-context-test-1", - "@fixed:tests/fixed/some_fixed_configuration_delete"); - - results = await mTweekApi.GetConfigurations("tests/fixed/some_fixed_configuration_delete", new Dictionary() - { - ["test"] = "delete-context-test-1" - }); - - Assert.Equal(JTokenType.Null, results.Type); - } - - [Fact(DisplayName = "Append context with invalid property type should return 400 status code")] - public async Task AppendContextWithInvalidInput() - { - var response = await mTweekApi.AppendContext("user", "user-1", new Dictionary() - { - ["AgentVersion"] = NewString("undefined") - }); - - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - } - } -} diff --git a/services/api/Tweek.ApiService.SmokeTests/GetConfigurations/HiddenKeysTests.cs b/services/api/Tweek.ApiService.SmokeTests/GetConfigurations/HiddenKeysTests.cs deleted file mode 100644 index 42ca9168d..000000000 --- a/services/api/Tweek.ApiService.SmokeTests/GetConfigurations/HiddenKeysTests.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; -using Newtonsoft.Json.Linq; -using Xunit; -using Xunit.Abstractions; - -namespace Tweek.ApiService.SmokeTests.GetConfigurations -{ - public class HiddenKeysTests - { - private readonly ITweekApi mTweekApi; - - public HiddenKeysTests(ITestOutputHelper output) - { - mTweekApi = TweekApiServiceFactory.GetTweekApiClient(output); - } - - [Fact] - public async Task GetScanFolder_VisibleFolder_ShouldReturnVisibleKeys() - { - // Act - var response = await mTweekApi.GetConfigurations("smoke_tests/not_hidden/_", new Dictionary()); - - // Assert - Assert.Equal(JTokenType.Object, response.Type); - var expected = JToken.Parse("{\"some_key\":\"some value\"}"); - Assert.Equal(expected, response); - } - - [Fact] - public async Task GetScanFolder_HiddenFolder_ShouldReturnVisibleKeys() - { - // Act - var response = await mTweekApi.GetConfigurations("smoke_tests/not_hidden/@hidden/_", new Dictionary()); - - // Assert - Assert.Equal(JTokenType.Object, response.Type); - var expected = JToken.Parse("{\"visible_key\":\"visible value\"}"); - Assert.Equal(expected, response); - } - - [Fact] - public async Task GetScanFolder_IncludeHiddenFolder_ShouldReturnHiddenFolder() - { - // Act - var response = await mTweekApi.GetConfigurations("smoke_tests/not_hidden/_", new List> - { - new KeyValuePair("$include", "_"), - new KeyValuePair("$include", "@hidden/_") - }); - - // Assert - Assert.Equal(JTokenType.Object, response.Type); - var expected = JToken.Parse("{\"@hidden\":{\"visible_key\":\"visible value\"},\"some_key\":\"some value\"}"); - Assert.Equal(expected, response); - } - - [Fact] - public async Task GetKey_HiddenKey_ShouldReturnKey() - { - // Act - var response = await mTweekApi.GetConfigurations("smoke_tests/not_hidden/@some_hidden_key", new Dictionary()); - - // Assert - Assert.Equal(JTokenType.String, response.Type); - const string expected = "some hidden value"; - Assert.Equal(expected, response.ToString()); - } - } -} diff --git a/services/api/Tweek.ApiService.SmokeTests/GetConfigurations/IgnoreKeyTypesTests.cs b/services/api/Tweek.ApiService.SmokeTests/GetConfigurations/IgnoreKeyTypesTests.cs deleted file mode 100644 index e38d15a0a..000000000 --- a/services/api/Tweek.ApiService.SmokeTests/GetConfigurations/IgnoreKeyTypesTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Newtonsoft.Json.Linq; -using Xunit; -using Xunit.Abstractions; - -namespace Tweek.ApiService.SmokeTests.GetConfigurations -{ - public class IgnoreKeyTypesTests - { - private readonly ITweekApi mTweekApi; - - private const string STRING_KEY = "smoke_tests/ignore_key_types/string_type"; - private const string NUMBER_KEY = "smoke_tests/ignore_key_types/number_type"; - private const string BOOLEAN_KEY = "smoke_tests/ignore_key_types/boolean_type"; - private const string OBJECT_KEY = "smoke_tests/ignore_key_types/object_type"; - private const string ARRAY_KEY = "smoke_tests/ignore_key_types/array_type"; - - public IgnoreKeyTypesTests(ITestOutputHelper output) - { - mTweekApi = TweekApiServiceFactory.GetTweekApiClient(output); - } - - [Theory] - [InlineData(STRING_KEY, "hello")] - [InlineData(NUMBER_KEY, "15")] - [InlineData(BOOLEAN_KEY, "true")] - [InlineData(OBJECT_KEY, "{\"key\":\"value\"}")] - [InlineData(ARRAY_KEY, "[\"hello\",\"world\"]")] - public async Task GetStringKey_IgnoreKeyTypesTrue_ReturnsString(string key, string value) - { - // Act - var response = await mTweekApi.GetConfigurations(key, new Dictionary {{"$ignoreKeyTypes", "true"}}); - - // Assert - Assert.Equal(JTokenType.String, response.Type); - Assert.Equal(JToken.FromObject(value), response); - } - - [Theory] - [InlineData(STRING_KEY, JTokenType.String, "\"hello\"")] - [InlineData(NUMBER_KEY, JTokenType.Float, "15")] - [InlineData(BOOLEAN_KEY, JTokenType.Boolean, "true")] - [InlineData(OBJECT_KEY, JTokenType.Object, "{\"key\":\"value\"}")] - [InlineData(ARRAY_KEY, JTokenType.Array, "[\"hello\",\"world\"]")] - public async Task GetStringKey_IgnoreKeyTypesFalse_ReturnsString(string key, JTokenType type, string value) - { - // Act - var response = await mTweekApi.GetConfigurations(key, new Dictionary {{"$ignoreKeyTypes", "false"}}); - - // Assert - Assert.Equal(type, response.Type); - Assert.Equal(JToken.Parse(value), response); - } - } -} \ No newline at end of file diff --git a/services/api/Tweek.ApiService/Controllers/KeysController.cs b/services/api/Tweek.ApiService/Controllers/KeysController.cs index bc2b84d96..a6cf562b3 100644 --- a/services/api/Tweek.ApiService/Controllers/KeysController.cs +++ b/services/api/Tweek.ApiService/Controllers/KeysController.cs @@ -75,8 +75,8 @@ private static ConfigurationPath[] GetQuery(ConfigurationPath path, string[] inc [Produces("application/json")] [ProducesResponseType(typeof(object), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(void), (int)HttpStatusCode.Forbidden)] - public async Task GetAsyncSwagger([FromQuery] string keyPath, - [FromQuery( Name = "$flatten")] bool flatten = false, + public async Task GetAsyncSwagger([FromQuery] string keyPath, + [FromQuery( Name = "$flatten")] bool flatten = false, [FromQuery( Name = "$include")] List includeKeys = null) { if (System.String.IsNullOrWhiteSpace(keyPath)) return BadRequest("Missing key path"); @@ -93,6 +93,7 @@ public async Task GetAsync([FromRoute] string path) var allParams = PartitionByKey(HttpContext.Request.Query.ToDictionary(x => x.Key, x => x.Value), x => x.StartsWith("$")); var modifiers = allParams.Item1; var isFlatten = modifiers.TryGetValue("$flatten").Select(x => bool.Parse(x.First())).IfNone(false); + var includeErrors = modifiers.TryGetValue("$includeErrors").Select(x => bool.Parse(x.First())).IfNone(false); var ignoreKeyTypes = modifiers.TryGetValue("$ignoreKeyTypes").Select(x => bool.Parse(x.First())).IfNone(false); var includePaths = modifiers.TryGetValue("$include").Select(x => x.ToArray()).IfNone(new string[] {}); @@ -110,17 +111,30 @@ public async Task GetAsync([FromRoute] string path) var query = GetQuery(root, includePaths); - var data = await _tweek.GetContextAndCalculate(query, identities, _contextDriver, contextProps); + var values = await _tweek.GetContextAndCalculate(query, identities, _contextDriver, contextProps); + Response.Headers.Add("X-Error-Count", values.Errors.Count.ToString()); + object result = null; if (root.IsScan) { - var relativeData = data.ToDictionary(x => x.Key.ToRelative(root), x => x.Value); - return Json(!isFlatten ? (TreeResult.From(relativeData, translateValue)) : relativeData.ToDictionary(x => x.Key.ToString(), x => translateValue(x.Value))); + var relativeData = values.Data.ToDictionary(x => x.Key.ToRelative(root), x => x.Value); + result = !isFlatten + ? TreeResult.From(relativeData, translateValue) + : relativeData.ToDictionary(x => x.Key.ToString(), x => translateValue(x.Value)); } + else if (values.Data.TryGetValue(root, out var value)) + { + result = ignoreKeyTypes ? TranslateValueToString(value) : value.Value; + } + + if (!includeErrors) + { + return Json(result); + } + + var errors = values.Errors.ToDictionary(x => x.Key, x => x.Value.Message); - return data.Select(x => ignoreKeyTypes ? TranslateValueToString(x.Value) : x.Value.Value) - .FirstOrNone() - .Match(x => Json(x), () => Json(null)); + return Json(new Dictionary {{"data", result}, {"errors", errors}}); } } } diff --git a/services/api/Tweek.ApiService/Security/AuthorizationDecider.cs b/services/api/Tweek.ApiService/Security/AuthorizationDecider.cs index 1262619c0..ae3d27287 100644 --- a/services/api/Tweek.ApiService/Security/AuthorizationDecider.cs +++ b/services/api/Tweek.ApiService/Security/AuthorizationDecider.cs @@ -35,15 +35,28 @@ public static bool CheckAuthenticationForKey(ITweek tweek, string permissionType var identityType = tweekIdentity.Type; var key = $"@tweek/auth/{identityType}/{permissionType}"; - return identity.IsTweekIdentity() || - tweek.Calculate(key, new HashSet(), - type => type == "token" ? (GetContextValue)(q => Optional(identity.FindFirst(q)).Map(x=>x.Value).Map(JsonValue.NewString)) : _ => None) - .SingleKey(key) - .Map(j => j.AsString()) - .Match(x => match(x, - with("allow", _ => true), - with("deny", _ => false), - claim => Optional(identity.FindFirst(claim)).Match(c=> c.Value.Equals(tweekIdentity.Id,StringComparison.OrdinalIgnoreCase), ()=>false)), () => true); + if (identity.IsTweekIdentity()) + { + return true; + } + + var authValues = tweek.Calculate(key, new HashSet(), + type => type == "token" + ? (GetContextValue) (q => Optional(identity.FindFirst(q)).Map(x => x.Value).Map(JsonValue.NewString)) + : _ => None); + + if (authValues.Errors.ContainsKey(key)) + { + return false; + } + + return authValues.Data + .SingleKey(key) + .Map(j => j.AsString()) + .Match(x => match(x, + with("allow", _ => true), + with("deny", _ => false), + claim => Optional(identity.FindFirst(claim)).Match(c=> c.Value.Equals(tweekIdentity.Id,StringComparison.OrdinalIgnoreCase), ()=>false)), () => true); } public static CheckWriteContextAccess CreateWriteContextAccessChecker(ITweek tweek, TweekIdentityProvider identityProvider) diff --git a/services/api/Tweek.ApiService/Startup.cs b/services/api/Tweek.ApiService/Startup.cs index 3cca8427f..faae553aa 100644 --- a/services/api/Tweek.ApiService/Startup.cs +++ b/services/api/Tweek.ApiService/Startup.cs @@ -173,14 +173,20 @@ private SchemaValidation.Provider CreateSchemaProvider(ITweek tweek, IRulesRepo var logger = loggerFactory.CreateLogger("SchemaValidation.Provider"); SchemaValidation.Provider CreateValidationProvider(){ - logger.LogInformation("updateing schema"); - var schemaIdenetities = tweek.Calculate(new[] { new ConfigurationPath($"@tweek/schema/_") }, EmptyIdentitySet, - i => ContextHelpers.EmptyContext).ToDictionary(x=> x.Key.Name, x=> x.Value.Value); + logger.LogInformation("updating schema"); + var schemaValues = tweek.Calculate(new[] {new ConfigurationPath("@tweek/schema/_")}, EmptyIdentitySet, + i => ContextHelpers.EmptyContext); + schemaValues.EnsureSuccess(); + + var schemaIdentities = schemaValues.Data.ToDictionary(x=> x.Key.Name, x=> x.Value.Value); - var customTypes = tweek.Calculate(new[] { new ConfigurationPath($"@tweek/custom_types/_") }, EmptyIdentitySet, - i => ContextHelpers.EmptyContext).ToDictionary(x=>x.Key.Name, x=> CustomTypeDefinition.FromJsonValue(x.Value.Value)); + var customTypesValues = tweek.Calculate(new[] {new ConfigurationPath("@tweek/custom_types/_")}, EmptyIdentitySet, + i => ContextHelpers.EmptyContext); + customTypesValues.EnsureSuccess(); + + var customTypes = customTypesValues.Data.ToDictionary(x=>x.Key.Name, x=> CustomTypeDefinition.FromJsonValue(x.Value.Value)); - return SchemaValidation.Create(schemaIdenetities, customTypes, mode); + return SchemaValidation.Create(schemaIdentities, customTypes, mode); } var validationProvider = CreateValidationProvider(); diff --git a/services/git-service/BareRepository/tests-source/implementations/jpad/integration_tests/include_errors.jpad b/services/git-service/BareRepository/tests-source/implementations/jpad/integration_tests/include_errors.jpad new file mode 100644 index 000000000..5808769d3 --- /dev/null +++ b/services/git-service/BareRepository/tests-source/implementations/jpad/integration_tests/include_errors.jpad @@ -0,0 +1,18 @@ +{ + "partitions": [], + "valueType": "string", + "rules": [ + { + "Matcher": { + "user.BadProperty": [] + }, + "Value": "BadPropertyValue", + "Type": "SingleVariant" + }, + { + "Matcher": {}, + "Value": "DefaultValue", + "Type": "SingleVariant" + } + ] +} diff --git a/services/git-service/BareRepository/tests-source/manifests/integration_tests/include_errors.json b/services/git-service/BareRepository/tests-source/manifests/integration_tests/include_errors.json new file mode 100644 index 000000000..3168ce762 --- /dev/null +++ b/services/git-service/BareRepository/tests-source/manifests/integration_tests/include_errors.json @@ -0,0 +1,16 @@ +{ + "key_path": "integration_tests/include_errors", + "meta": { + "name": "integration_tests/include_errors", + "description": "", + "tags": [], + "readOnly": false, + "archived": false + }, + "implementation": { + "type": "file", + "format": "jpad" + }, + "valueType": "string", + "dependencies": [] +}