Files
AutopilotMonitor/src/Backend/AutopilotMonitor.Functions/Functions/Rules/Submissions/WithdrawRuleSubmissionFunction.cs
T
Oliver Kieselbach 61892592e4 feat(rules): community rule submissions — submit custom rules from the portal, review via GA page or MCP, publish by repo commit + reseed (D-236)
Tenant admins submit references to their own gather/analyze rules; the server freezes the rule, records pre-flight findings and fire stats, and stores it under a 12-character id (RuleSubmissions, SessionReports layout). Attribution is the submitter's frozen choice (anonymous by default) and becomes the published author. Review (approve with reserved id / decline with comment) through /admin/reports/rule-submissions or the MCP tools list_rule_submissions, get_rule_submission, review_rule_submission, reseed_rules_from_github; published is derived from the global catalog, never stored. Analyze rules now get the same server-side author stamp as gather rules; ANALYZE-ID-001 carries author Community. Privacy page names the credit; bumps internal and .claude/commands gitlinks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 22:53:32 +02:00

56 lines
2.4 KiB
C#

using AutopilotMonitor.Functions.Helpers;
using AutopilotMonitor.Functions.Services;
using AutopilotMonitor.Shared.Models;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
namespace AutopilotMonitor.Functions.Functions.Rules.Submissions
{
/// <summary>The submitter takes a pending submission back. The row stays (status withdrawn) for the audit trail.</summary>
public class WithdrawRuleSubmissionFunction
{
private readonly ILogger<WithdrawRuleSubmissionFunction> _logger;
private readonly RuleSubmissionService _service;
public WithdrawRuleSubmissionFunction(ILogger<WithdrawRuleSubmissionFunction> logger, RuleSubmissionService service)
{
_logger = logger;
_service = service;
}
[Function("WithdrawRuleSubmission")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "rules/submissions/{submissionId}")] HttpRequestData req,
string submissionId)
{
try
{
// Authentication + TenantAdminOrGA authorization enforced by PolicyEnforcementMiddleware
var requestCtx = req.GetRequestContext();
if (string.IsNullOrWhiteSpace(submissionId)) return await req.BadRequestAsync("submissionId is required.");
bool withdrawn;
try
{
// A foreign tenant's submission answers 404 for a non-GA — the row's existence is not theirs to learn.
withdrawn = await _service.WithdrawAsync(requestCtx.TenantId, submissionId, crossTenantAllowed: requestCtx.IsGlobalAdmin);
}
catch (InvalidOperationException ex)
{
return await req.ConflictAsync(ex.Message);
}
if (!withdrawn) return await req.NotFoundAsync("Submission not found.");
_logger.LogInformation("Rule submission {SubmissionId} withdrawn by {User}", submissionId, requestCtx.UserPrincipalName);
return await req.OkAsync(new SuccessMessageResponse { Success = true, Message = "Submission withdrawn." });
}
catch (Exception ex)
{
return await req.InternalServerErrorAsync(_logger, ex, "WithdrawRuleSubmission");
}
}
}
}