Validate scriban on new notifications (#2834)

* Add new command

* Enforce scriban at notification creation time

* fmt

* missed when merging
This commit is contained in:
Teo Voinea 2023-02-15 13:16:01 -05:00 committed by GitHub
parent 21374b36e6
commit e9f5a6a2e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 390 additions and 279 deletions

View File

@ -39,11 +39,11 @@ public class JinjaToScriban {
_log.Info($"Finding notifications to migrate");
var notifications = _context.NotificationOperations.SearchAll()
.Select(notification => {
.SelectAwait(async notification => {
var (didModify, config) = notification.Config switch {
TeamsTemplate => (false, notification.Config),
AdoTemplate adoTemplate => ConvertToScriban(adoTemplate),
GithubIssuesTemplate githubIssuesTemplate => ConvertToScriban(githubIssuesTemplate),
AdoTemplate adoTemplate => await JinjaTemplateAdapter.ConvertToScriban(adoTemplate),
GithubIssuesTemplate githubIssuesTemplate => await JinjaTemplateAdapter.ConvertToScriban(githubIssuesTemplate),
_ => throw new NotImplementedException("Unexpected notification configuration type")
};
@ -82,153 +82,4 @@ public class JinjaToScriban {
return await RequestHandling.Ok(req, new JinjaToScribanMigrationResponse(updatedNotificationsIds, failedNotificationIds));
}
private static (bool didModify, AdoTemplate template) ConvertToScriban(AdoTemplate template) {
var didModify = false;
if (JinjaTemplateAdapter.IsJinjaTemplate(template.Project)) {
didModify = true;
template = template with {
Project = JinjaTemplateAdapter.AdaptForScriban(template.Project)
};
}
foreach (var item in template.AdoFields) {
if (JinjaTemplateAdapter.IsJinjaTemplate(item.Value)) {
template.AdoFields[item.Key] = JinjaTemplateAdapter.AdaptForScriban(item.Value);
didModify = true;
}
}
if (JinjaTemplateAdapter.IsJinjaTemplate(template.Type)) {
didModify = true;
template = template with {
Type = JinjaTemplateAdapter.AdaptForScriban(template.Type)
};
}
if (template.Comment != null && JinjaTemplateAdapter.IsJinjaTemplate(template.Comment)) {
didModify = true;
template = template with {
Comment = JinjaTemplateAdapter.AdaptForScriban(template.Comment)
};
}
foreach (var item in template.OnDuplicate.AdoFields) {
if (JinjaTemplateAdapter.IsJinjaTemplate(item.Value)) {
template.OnDuplicate.AdoFields[item.Key] = JinjaTemplateAdapter.AdaptForScriban(item.Value);
didModify = true;
}
}
if (template.OnDuplicate.Comment != null && JinjaTemplateAdapter.IsJinjaTemplate(template.OnDuplicate.Comment)) {
didModify = true;
template = template with {
OnDuplicate = template.OnDuplicate with {
Comment = JinjaTemplateAdapter.AdaptForScriban(template.OnDuplicate.Comment)
}
};
}
return (didModify, template);
}
private static (bool didModify, GithubIssuesTemplate template) ConvertToScriban(GithubIssuesTemplate template) {
var didModify = false;
if (JinjaTemplateAdapter.IsJinjaTemplate(template.UniqueSearch.str)) {
didModify = true;
template = template with {
UniqueSearch = template.UniqueSearch with {
str = JinjaTemplateAdapter.AdaptForScriban(template.UniqueSearch.str)
}
};
}
if (!string.IsNullOrEmpty(template.UniqueSearch.Author) && JinjaTemplateAdapter.IsJinjaTemplate(template.UniqueSearch.Author)) {
didModify = true;
template = template with {
UniqueSearch = template.UniqueSearch with {
Author = JinjaTemplateAdapter.AdaptForScriban(template.UniqueSearch.Author)
}
};
}
if (JinjaTemplateAdapter.IsJinjaTemplate(template.Title)) {
didModify = true;
template = template with {
Title = JinjaTemplateAdapter.AdaptForScriban(template.Title)
};
}
if (JinjaTemplateAdapter.IsJinjaTemplate(template.Body)) {
didModify = true;
template = template with {
Body = JinjaTemplateAdapter.AdaptForScriban(template.Body)
};
}
if (!string.IsNullOrEmpty(template.OnDuplicate.Comment) && JinjaTemplateAdapter.IsJinjaTemplate(template.OnDuplicate.Comment)) {
didModify = true;
template = template with {
OnDuplicate = template.OnDuplicate with {
Comment = JinjaTemplateAdapter.AdaptForScriban(template.OnDuplicate.Comment)
}
};
}
if (template.OnDuplicate.Labels.Any()) {
template = template with {
OnDuplicate = template.OnDuplicate with {
Labels = template.OnDuplicate.Labels.Select(label => {
if (JinjaTemplateAdapter.IsJinjaTemplate(label)) {
didModify = true;
return JinjaTemplateAdapter.AdaptForScriban(label);
}
return label;
}).ToList()
}
};
}
if (template.Assignees.Any()) {
template = template with {
Assignees = template.Assignees.Select(assignee => {
if (JinjaTemplateAdapter.IsJinjaTemplate(assignee)) {
didModify = true;
return JinjaTemplateAdapter.AdaptForScriban(assignee);
}
return assignee;
}).ToList()
};
}
if (template.Labels.Any()) {
template = template with {
Labels = template.Labels.Select(label => {
if (JinjaTemplateAdapter.IsJinjaTemplate(label)) {
didModify = true;
return JinjaTemplateAdapter.AdaptForScriban(label);
}
return label;
}).ToList()
};
}
if (JinjaTemplateAdapter.IsJinjaTemplate(template.Organization)) {
didModify = true;
template = template with {
Organization = JinjaTemplateAdapter.AdaptForScriban(template.Organization)
};
}
if (JinjaTemplateAdapter.IsJinjaTemplate(template.Repository)) {
didModify = true;
template = template with {
Repository = JinjaTemplateAdapter.AdaptForScriban(template.Repository)
};
}
return (didModify, template);
}
}

View File

@ -17,19 +17,9 @@ public class ValidateScriban {
return await _context.RequestHandling.NotOk(req, request.ErrorV, "ValidateTemplate");
}
var instanceUrl = _context.ServiceConfiguration.OneFuzzInstance!;
try {
var (renderer, templateRenderContext) = await GenerateTemplateRenderContext(request.OkV.Context);
return await RequestHandling.Ok(req, await JinjaTemplateAdapter.ValidateScribanTemplate(_context, _log, request.OkV.Context, request.OkV.Template));
var renderedTemaplate = await renderer.Render(request.OkV.Template, new Uri(instanceUrl), strictRendering: true);
var response = new TemplateValidationResponse(
renderedTemaplate,
templateRenderContext
);
return await RequestHandling.Ok(req, response);
} catch (Exception e) {
return await new RequestHandling(_log).NotOk(
req,
@ -47,119 +37,4 @@ public class ValidateScriban {
_ => throw new InvalidOperationException("Unsupported HTTP method"),
};
}
private async Async.Task<(NotificationsBase.Renderer, TemplateRenderContext)> GenerateTemplateRenderContext(TemplateRenderContext? templateRenderContext) {
if (templateRenderContext != null) {
_log.Info($"Using the request's TemplateRenderContext");
} else {
_log.Info($"Generating TemplateRenderContext");
}
var targetUrl = templateRenderContext?.TargetUrl ?? new Uri("https://example.com/targetUrl");
var inputUrl = templateRenderContext?.InputUrl ?? new Uri("https://example.com/inputUrl");
var reportUrl = templateRenderContext?.ReportUrl ?? new Uri("https://example.com/reportUrl");
var executable = "target.exe";
var crashType = "some crash type";
var crashSite = "some crash site";
var callStack = new List<string>()
{
"stack frame 0",
"stack frame 1"
};
var callStackSha = "call stack sha";
var inputSha = "input sha";
var taskId = Guid.NewGuid();
var jobId = Guid.NewGuid();
var taskState = TaskState.Running;
var jobState = JobState.Enabled;
var os = Os.Linux;
var taskType = TaskType.LibfuzzerFuzz;
var duration = 100;
var project = "some project";
var jobName = "job name";
var buildName = "build name";
var reportContainer = templateRenderContext?.ReportContainer ?? Container.Parse("example-container-name");
var reportFileName = templateRenderContext?.ReportFilename ?? "example file name";
var reproCmd = templateRenderContext?.ReproCmd ?? "onefuzz command to create a repro";
var report = templateRenderContext?.Report ?? new Report(
inputUrl.ToString(),
null,
executable,
crashType,
crashSite,
callStack,
callStackSha,
inputSha,
null,
taskId,
jobId,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null
);
var task = new Task(
jobId,
taskId,
taskState,
os,
templateRenderContext?.Task ?? new TaskConfig(
jobId,
null,
new TaskDetails(
taskType,
duration
)
)
);
var job = new Job(
jobId,
jobState,
templateRenderContext?.Job ?? new JobConfig(
project,
jobName,
buildName,
duration,
null
)
);
var renderer = await NotificationsBase.Renderer.ConstructRenderer(
_context,
reportContainer,
reportFileName,
report,
_log,
task,
job,
targetUrl,
inputUrl,
reportUrl,
scribanOnlyOverride: true
);
templateRenderContext ??= new TemplateRenderContext(
report,
task.Config,
job.Config,
reportUrl,
inputUrl,
targetUrl,
reportContainer,
reportFileName,
reproCmd
);
return (renderer, templateRenderContext);
}
}

View File

@ -91,6 +91,11 @@ public class NotificationOperations : Orm<Notification>, INotificationOperations
return OneFuzzResult<Notification>.Error(ErrorCode.INVALID_REQUEST, "invalid container");
}
if (await _context.FeatureManagerSnapshot.IsEnabledAsync(FeatureFlagConstants.EnableScribanOnly) &&
!await JinjaTemplateAdapter.IsValidScribanNotificationTemplate(_context, _logTracer, config)) {
return OneFuzzResult<Notification>.Error(ErrorCode.INVALID_REQUEST, "The notification config is not a valid scriban template");
}
if (replaceExisting) {
var existing = this.SearchByRowKeys(new[] { container.String });
await foreach (var existingEntry in existing) {

View File

@ -11,4 +11,351 @@ public class JinjaTemplateAdapter {
.Replace("{%", "{{")
.Replace("%}", "}}");
}
public static async Async.Task<bool> IsValidScribanNotificationTemplate(IOnefuzzContext context, ILogTracer log, NotificationTemplate template) {
try {
var (didModify, _) = template switch {
TeamsTemplate => (false, template),
AdoTemplate adoTemplate => await ConvertToScriban(adoTemplate, attemptRender: true, context, log),
GithubIssuesTemplate githubTemplate => await ConvertToScriban(githubTemplate, attemptRender: true, context, log),
_ => throw new ArgumentOutOfRangeException(nameof(template), "Unexpected notification template type")
};
if (!didModify) {
return true;
}
return false;
} catch (Exception e) {
log.Exception(e);
return false;
}
}
public static async Async.Task<TemplateValidationResponse> ValidateScribanTemplate(IOnefuzzContext context, ILogTracer log, TemplateRenderContext? renderContext, string template) {
var instanceUrl = context.ServiceConfiguration.OneFuzzInstance!;
var (renderer, templateRenderContext) = await GenerateTemplateRenderContext(context, log, renderContext);
var renderedTemaplate = await renderer.Render(template, new Uri(instanceUrl), strictRendering: true);
return new TemplateValidationResponse(
renderedTemaplate,
templateRenderContext
);
}
private static async Async.Task<(NotificationsBase.Renderer, TemplateRenderContext)> GenerateTemplateRenderContext(IOnefuzzContext context, ILogTracer log, TemplateRenderContext? templateRenderContext) {
if (templateRenderContext != null) {
log.Info($"Using custom TemplateRenderContext");
} else {
log.Info($"Generating TemplateRenderContext");
}
var targetUrl = templateRenderContext?.TargetUrl ?? new Uri("https://example.com/targetUrl");
var inputUrl = templateRenderContext?.InputUrl ?? new Uri("https://example.com/inputUrl");
var reportUrl = templateRenderContext?.ReportUrl ?? new Uri("https://example.com/reportUrl");
var executable = "target.exe";
var crashType = "some crash type";
var crashSite = "some crash site";
var callStack = new List<string>()
{
"stack frame 0",
"stack frame 1"
};
var callStackSha = "call stack sha";
var inputSha = "input sha";
var taskId = Guid.NewGuid();
var jobId = Guid.NewGuid();
var taskState = TaskState.Running;
var jobState = JobState.Enabled;
var os = Os.Linux;
var taskType = TaskType.LibfuzzerFuzz;
var duration = 100;
var project = "some project";
var jobName = "job name";
var buildName = "build name";
var reportContainer = templateRenderContext?.ReportContainer ?? Container.Parse("example-container-name");
var reportFileName = templateRenderContext?.ReportFilename ?? "example file name";
var reproCmd = templateRenderContext?.ReproCmd ?? "onefuzz command to create a repro";
var report = templateRenderContext?.Report ?? new Report(
inputUrl.ToString(),
null,
executable,
crashType,
crashSite,
callStack,
callStackSha,
inputSha,
null,
taskId,
jobId,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null
);
var task = new Task(
jobId,
taskId,
taskState,
os,
templateRenderContext?.Task ?? new TaskConfig(
jobId,
null,
new TaskDetails(
taskType,
duration
)
)
);
var job = new Job(
jobId,
jobState,
templateRenderContext?.Job ?? new JobConfig(
project,
jobName,
buildName,
duration,
null
)
);
var renderer = await NotificationsBase.Renderer.ConstructRenderer(
context,
reportContainer,
reportFileName,
report,
log,
task,
job,
targetUrl,
inputUrl,
reportUrl,
scribanOnlyOverride: true
);
templateRenderContext ??= new TemplateRenderContext(
report,
task.Config,
job.Config,
reportUrl,
inputUrl,
targetUrl,
reportContainer,
reportFileName,
reproCmd
);
return (renderer, templateRenderContext);
}
public async static Async.Task<(bool didModify, AdoTemplate template)> ConvertToScriban(AdoTemplate template, bool attemptRender = false, IOnefuzzContext? context = null, ILogTracer? log = null) {
if (attemptRender) {
context = context.EnsureNotNull("Required to render");
log = log.EnsureNotNull("Required to render");
}
var didModify = false;
if (JinjaTemplateAdapter.IsJinjaTemplate(template.Project)) {
didModify = true;
template = template with {
Project = JinjaTemplateAdapter.AdaptForScriban(template.Project)
};
} else if (attemptRender) {
await ValidateScribanTemplate(context!, log!, null, template.Project).IgnoreResult();
}
foreach (var item in template.AdoFields) {
if (JinjaTemplateAdapter.IsJinjaTemplate(item.Value)) {
template.AdoFields[item.Key] = JinjaTemplateAdapter.AdaptForScriban(item.Value);
didModify = true;
} else if (attemptRender) {
await ValidateScribanTemplate(context!, log!, null, item.Value).IgnoreResult();
}
}
if (JinjaTemplateAdapter.IsJinjaTemplate(template.Type)) {
didModify = true;
template = template with {
Type = JinjaTemplateAdapter.AdaptForScriban(template.Type)
};
} else if (attemptRender) {
await ValidateScribanTemplate(context!, log!, null, template.Type).IgnoreResult();
}
if (template.Comment != null) {
if (JinjaTemplateAdapter.IsJinjaTemplate(template.Comment)) {
didModify = true;
template = template with {
Comment = JinjaTemplateAdapter.AdaptForScriban(template.Comment)
};
} else if (attemptRender) {
await ValidateScribanTemplate(context!, log!, null, template.Comment).IgnoreResult();
}
}
foreach (var item in template.OnDuplicate.AdoFields) {
if (JinjaTemplateAdapter.IsJinjaTemplate(item.Value)) {
template.OnDuplicate.AdoFields[item.Key] = JinjaTemplateAdapter.AdaptForScriban(item.Value);
didModify = true;
} else if (attemptRender) {
await ValidateScribanTemplate(context!, log!, null, item.Value).IgnoreResult();
}
}
if (template.OnDuplicate.Comment != null) {
if (JinjaTemplateAdapter.IsJinjaTemplate(template.OnDuplicate.Comment)) {
didModify = true;
template = template with {
OnDuplicate = template.OnDuplicate with {
Comment = JinjaTemplateAdapter.AdaptForScriban(template.OnDuplicate.Comment)
}
};
} else if (attemptRender) {
await ValidateScribanTemplate(context!, log!, null, template.OnDuplicate.Comment).IgnoreResult();
}
}
return (didModify, template);
}
public async static Async.Task<(bool didModify, GithubIssuesTemplate template)> ConvertToScriban(GithubIssuesTemplate template, bool attemptRender = false, IOnefuzzContext? context = null, ILogTracer? log = null) {
if (attemptRender) {
context = context.EnsureNotNull("Required to render");
log = log.EnsureNotNull("Required to render");
}
var didModify = false;
if (JinjaTemplateAdapter.IsJinjaTemplate(template.UniqueSearch.str)) {
didModify = true;
template = template with {
UniqueSearch = template.UniqueSearch with {
str = JinjaTemplateAdapter.AdaptForScriban(template.UniqueSearch.str)
}
};
} else if (attemptRender) {
await ValidateScribanTemplate(context!, log!, null, template.UniqueSearch.str).IgnoreResult();
}
if (!string.IsNullOrEmpty(template.UniqueSearch.Author)) {
if (JinjaTemplateAdapter.IsJinjaTemplate(template.UniqueSearch.Author)) {
didModify = true;
template = template with {
UniqueSearch = template.UniqueSearch with {
Author = JinjaTemplateAdapter.AdaptForScriban(template.UniqueSearch.Author)
}
};
} else if (attemptRender) {
await ValidateScribanTemplate(context!, log!, null, template.UniqueSearch.Author).IgnoreResult();
}
}
if (JinjaTemplateAdapter.IsJinjaTemplate(template.Title)) {
didModify = true;
template = template with {
Title = JinjaTemplateAdapter.AdaptForScriban(template.Title)
};
} else if (attemptRender) {
await ValidateScribanTemplate(context!, log!, null, template.Title).IgnoreResult();
}
if (JinjaTemplateAdapter.IsJinjaTemplate(template.Body)) {
didModify = true;
template = template with {
Body = JinjaTemplateAdapter.AdaptForScriban(template.Body)
};
} else if (attemptRender) {
await ValidateScribanTemplate(context!, log!, null, template.Body).IgnoreResult();
}
if (!string.IsNullOrEmpty(template.OnDuplicate.Comment)) {
if (JinjaTemplateAdapter.IsJinjaTemplate(template.OnDuplicate.Comment)) {
didModify = true;
template = template with {
OnDuplicate = template.OnDuplicate with {
Comment = JinjaTemplateAdapter.AdaptForScriban(template.OnDuplicate.Comment)
}
};
} else if (attemptRender) {
await ValidateScribanTemplate(context!, log!, null, template.OnDuplicate.Comment).IgnoreResult();
}
}
if (template.OnDuplicate.Labels.Any()) {
template = template with {
OnDuplicate = template.OnDuplicate with {
Labels = template.OnDuplicate.Labels.ToAsyncEnumerable().SelectAwait(async label => {
if (JinjaTemplateAdapter.IsJinjaTemplate(label)) {
didModify = true;
return JinjaTemplateAdapter.AdaptForScriban(label);
} else if (attemptRender) {
await ValidateScribanTemplate(context!, log!, null, label).IgnoreResult();
}
return label;
}).ToEnumerable().ToList()
}
};
}
if (template.Assignees.Any()) {
template = template with {
Assignees = template.Assignees.ToAsyncEnumerable().SelectAwait(async assignee => {
if (JinjaTemplateAdapter.IsJinjaTemplate(assignee)) {
didModify = true;
return JinjaTemplateAdapter.AdaptForScriban(assignee);
} else if (attemptRender) {
await ValidateScribanTemplate(context!, log!, null, assignee).IgnoreResult();
}
return assignee;
}).ToEnumerable().ToList()
};
}
if (template.Labels.Any()) {
template = template with {
Labels = template.Labels.ToAsyncEnumerable().SelectAwait(async label => {
if (JinjaTemplateAdapter.IsJinjaTemplate(label)) {
didModify = true;
return JinjaTemplateAdapter.AdaptForScriban(label);
} else if (attemptRender) {
await ValidateScribanTemplate(context!, log!, null, label).IgnoreResult();
}
return label;
}).ToEnumerable().ToList()
};
}
if (JinjaTemplateAdapter.IsJinjaTemplate(template.Organization)) {
didModify = true;
template = template with {
Organization = JinjaTemplateAdapter.AdaptForScriban(template.Organization)
};
} else if (attemptRender) {
await ValidateScribanTemplate(context!, log!, null, template.Organization).IgnoreResult();
}
if (JinjaTemplateAdapter.IsJinjaTemplate(template.Repository)) {
didModify = true;
template = template with {
Repository = JinjaTemplateAdapter.AdaptForScriban(template.Repository)
};
} else if (attemptRender) {
await ValidateScribanTemplate(context!, log!, null, template.Repository).IgnoreResult();
}
return (didModify, template);
}
}

View File

@ -41,6 +41,7 @@ public sealed class TestContext : IOnefuzzContext {
UserCredentials = new UserCredentials(logTracer, ConfigOperations);
NotificationOperations = new NotificationOperations(logTracer, this);
SecretsOperations = new TestSecretsOperations(Creds, ServiceConfiguration);
FeatureManagerSnapshot = new TestFeatureManagerSnapshot();
}
public TestEvents Events { get; set; } = new();
@ -92,6 +93,9 @@ public sealed class TestContext : IOnefuzzContext {
public ISecretsOperations SecretsOperations { get; }
public IFeatureManagerSnapshot FeatureManagerSnapshot { get; }
// -- Remainder not implemented --
public IConfig Config => throw new System.NotImplementedException();
@ -129,7 +133,6 @@ public sealed class TestContext : IOnefuzzContext {
public ITeams Teams => throw new NotImplementedException();
public IGithubIssues GithubIssues => throw new NotImplementedException();
public IAdo Ado => throw new NotImplementedException();
public IFeatureManagerSnapshot FeatureManagerSnapshot => throw new NotImplementedException();
public IConfigurationRefresher ConfigurationRefresher => throw new NotImplementedException();
}

View File

@ -0,0 +1,30 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.FeatureManagement;
namespace IntegrationTests.Fakes;
public class TestFeatureManagerSnapshot : IFeatureManagerSnapshot {
private static ConcurrentDictionary<string, bool> FeatureFlags = new();
public IAsyncEnumerable<string> GetFeatureNamesAsync() {
throw new System.NotImplementedException();
}
public Task<bool> IsEnabledAsync(string feature) {
return Task.FromResult(FeatureFlags.ContainsKey(feature) && FeatureFlags.TryGetValue(feature, out var enabled) && enabled);
}
public Task<bool> IsEnabledAsync<TContext>(string feature, TContext context) {
throw new System.NotImplementedException();
}
public static void AddFeatureFlag(string featureName, bool enabled = false) {
var _ = FeatureFlags.TryAdd(featureName, enabled);
}
public static void SetFeatureFlag(string featureName, bool enabled) {
var _ = FeatureFlags.TryUpdate(featureName, enabled, !enabled);
}
}