155 lines
6.1 KiB
C#
155 lines
6.1 KiB
C#
using System.Text;
|
|
using Dapper;
|
|
using Microsoft.AspNetCore.OutputCaching;
|
|
using PluginBuilder.DataModels;
|
|
using PluginBuilder.Events;
|
|
using PluginBuilder.JsonConverters;
|
|
using PluginBuilder.Util;
|
|
using PluginBuilder.Util.Extensions;
|
|
|
|
namespace PluginBuilder.Services;
|
|
|
|
public enum VersionLifecycleFailureCode
|
|
{
|
|
NotFound,
|
|
InvalidBuildState,
|
|
SignatureRequired,
|
|
ManifestUnavailable,
|
|
SignatureVerificationFailed
|
|
}
|
|
|
|
public readonly record struct VersionLifecycleResult(
|
|
bool Success,
|
|
VersionLifecycleFailureCode? FailureCode = null,
|
|
string? Message = null,
|
|
long? BuildId = null)
|
|
{
|
|
public static VersionLifecycleResult Ok(long? buildId = null)
|
|
{
|
|
return new VersionLifecycleResult(true, BuildId: buildId);
|
|
}
|
|
|
|
public static VersionLifecycleResult Fail(VersionLifecycleFailureCode failureCode, string message)
|
|
{
|
|
return new VersionLifecycleResult(false, failureCode, message);
|
|
}
|
|
}
|
|
|
|
public sealed class VersionLifecycleService(
|
|
DBConnectionFactory connectionFactory,
|
|
GPGKeyService gpgKeyService,
|
|
EventAggregator eventAggregator,
|
|
IOutputCacheStore outputCacheStore)
|
|
{
|
|
public async Task<VersionLifecycleResult> ReleaseAsync(
|
|
PluginSlug pluginSlug,
|
|
PluginVersion version,
|
|
string userId,
|
|
byte[]? signatureBytes)
|
|
{
|
|
await using var conn = await connectionFactory.Open();
|
|
var row = await conn.QueryFirstOrDefaultAsync<(long build_id, string state, string? manifest_info, string? settings)?>(
|
|
"""
|
|
SELECT v.build_id, b.state, b.manifest_info::text, p.settings::text
|
|
FROM versions v
|
|
JOIN builds b ON v.plugin_slug = b.plugin_slug AND v.build_id = b.id
|
|
JOIN plugins p ON v.plugin_slug = p.slug
|
|
WHERE v.plugin_slug = @pluginSlug AND v.ver = @version
|
|
LIMIT 1
|
|
""",
|
|
new { pluginSlug = pluginSlug.ToString(), version = version.VersionParts });
|
|
|
|
if (row is null)
|
|
return VersionLifecycleResult.Fail(VersionLifecycleFailureCode.NotFound, "Version not found");
|
|
|
|
if (row.Value.state != BuildStates.Uploaded.ToEventName())
|
|
{
|
|
return VersionLifecycleResult.Fail(
|
|
VersionLifecycleFailureCode.InvalidBuildState,
|
|
$"Build is in '{row.Value.state}' state and cannot be released");
|
|
}
|
|
|
|
if (signatureBytes is { Length: > 0 })
|
|
{
|
|
var niceManifest = ManifestHelper.NiceJson(row.Value.manifest_info);
|
|
var manifestHash = ManifestHelper.GetManifestHash(niceManifest, true);
|
|
if (string.IsNullOrEmpty(manifestHash))
|
|
{
|
|
return VersionLifecycleResult.Fail(
|
|
VersionLifecycleFailureCode.ManifestUnavailable,
|
|
"Manifest information for plugin not available");
|
|
}
|
|
|
|
var signatureVerification = await gpgKeyService.VerifyDetachedSignature(
|
|
pluginSlug.ToString(),
|
|
userId,
|
|
Encoding.UTF8.GetBytes(manifestHash),
|
|
signatureBytes);
|
|
|
|
if (!signatureVerification.valid)
|
|
{
|
|
return VersionLifecycleResult.Fail(
|
|
VersionLifecycleFailureCode.SignatureVerificationFailed,
|
|
signatureVerification.message);
|
|
}
|
|
|
|
var updated = await conn.UpdateVersionReleaseStatus(pluginSlug, "sign_release", version, signatureVerification.proof);
|
|
if (!updated)
|
|
return VersionLifecycleResult.Fail(VersionLifecycleFailureCode.NotFound, "Version not found");
|
|
}
|
|
else
|
|
{
|
|
var pluginSettings = SafeJson.Deserialize<PluginSettings>(row.Value.settings);
|
|
if (pluginSettings?.RequireGPGSignatureForRelease == true)
|
|
{
|
|
return VersionLifecycleResult.Fail(
|
|
VersionLifecycleFailureCode.SignatureRequired,
|
|
"A verified GPG signature is required to release this version");
|
|
}
|
|
|
|
var updated = await conn.UpdateVersionReleaseStatus(pluginSlug, "release", version);
|
|
if (!updated)
|
|
return VersionLifecycleResult.Fail(VersionLifecycleFailureCode.NotFound, "Version not found");
|
|
}
|
|
|
|
await outputCacheStore.EvictByTagAsync(CacheTags.Plugins, CancellationToken.None);
|
|
return VersionLifecycleResult.Ok();
|
|
}
|
|
|
|
public async Task<VersionLifecycleResult> UnreleaseAsync(PluginSlug pluginSlug, PluginVersion version)
|
|
{
|
|
await using var conn = await connectionFactory.Open();
|
|
var updated = await conn.UpdateVersionReleaseStatus(pluginSlug, "unrelease", version);
|
|
if (!updated)
|
|
return VersionLifecycleResult.Fail(VersionLifecycleFailureCode.NotFound, "Version not found");
|
|
|
|
await outputCacheStore.EvictByTagAsync(CacheTags.Plugins, CancellationToken.None);
|
|
return VersionLifecycleResult.Ok();
|
|
}
|
|
|
|
public async Task<VersionLifecycleResult> RemoveAsync(PluginSlug pluginSlug, PluginVersion version)
|
|
{
|
|
await using var conn = await connectionFactory.Open();
|
|
var buildId = await conn.QueryFirstOrDefaultAsync<long?>(
|
|
"SELECT build_id FROM versions WHERE plugin_slug=@pluginSlug AND ver=@version",
|
|
new { pluginSlug = pluginSlug.ToString(), version = version.VersionParts });
|
|
|
|
if (buildId is null)
|
|
return VersionLifecycleResult.Fail(VersionLifecycleFailureCode.NotFound, "Version not found");
|
|
|
|
await using var tx = await conn.BeginTransactionAsync();
|
|
var fullBuildId = new FullBuildId(pluginSlug, buildId.Value);
|
|
await conn.UpdateBuild(fullBuildId, BuildStates.Removed, null, tx: tx);
|
|
await conn.ExecuteAsync(
|
|
"DELETE FROM versions WHERE plugin_slug=@pluginSlug AND ver=@version",
|
|
new { pluginSlug = pluginSlug.ToString(), version = version.VersionParts },
|
|
tx);
|
|
await tx.CommitAsync();
|
|
|
|
eventAggregator.Publish(new BuildChanged(fullBuildId, BuildStates.Removed));
|
|
await outputCacheStore.EvictByTagAsync(CacheTags.Plugins, CancellationToken.None);
|
|
|
|
return VersionLifecycleResult.Ok(buildId.Value);
|
|
}
|
|
}
|