-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add blacklist feature and API key authentication
- Introduced a blacklist feature to prevent processing of unwanted torrents. - Added API key authentication for securing API endpoints. - Updated database schema to include BlacklistedItems table. - Enhanced DmmScraping and GenericIngestionProcessor to filter out blacklisted torrents. - Implemented new endpoints for adding and removing items from the blacklist. - Updated configuration to support API key generation and management. - Improved Kubernetes service discovery with authentication type options. - Enhanced Swagger documentation with API key security scheme.
- Loading branch information
1 parent
ae73304
commit 562bd14
Showing
34 changed files
with
889 additions
and
55 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 7 additions & 0 deletions
7
src/Zilean.ApiService/Features/Authentication/ApiKeyAuthentication.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
namespace Zilean.ApiService.Features.Authentication; | ||
|
||
public static class ApiKeyAuthentication | ||
{ | ||
public const string Scheme = "ApiKey"; | ||
public const string Policy = "ApiKeyPolicy"; | ||
} |
32 changes: 32 additions & 0 deletions
32
src/Zilean.ApiService/Features/Authentication/ApiKeyAuthenticationHandler.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
namespace Zilean.ApiService.Features.Authentication; | ||
|
||
public class ApiKeyAuthenticationHandler( | ||
IOptionsMonitor<AuthenticationSchemeOptions> options, | ||
ILoggerFactory logger, | ||
UrlEncoder encoder, | ||
ISystemClock clock, | ||
ZileanConfiguration configuration) | ||
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder, clock) | ||
{ | ||
protected override Task<AuthenticateResult> HandleAuthenticateAsync() | ||
{ | ||
if (!Request.Headers.TryGetValue("X-API-KEY", out var extractedApiKey)) | ||
{ | ||
return Task.FromResult(AuthenticateResult.Fail("API Key was not provided")); | ||
} | ||
|
||
var configuredApiKey = configuration.ApiKey; | ||
if (string.IsNullOrEmpty(configuredApiKey) || extractedApiKey != configuredApiKey) | ||
{ | ||
return Task.FromResult(AuthenticateResult.Fail("Invalid API Key")); | ||
} | ||
|
||
var claims = new[] { new Claim(ClaimTypes.Name, "ApiKeyUser") }; | ||
var identity = new ClaimsIdentity(claims, Scheme.Name); | ||
var principal = new ClaimsPrincipal(identity); | ||
var ticket = new AuthenticationTicket(principal, Scheme.Name); | ||
|
||
return Task.FromResult(AuthenticateResult.Success(ticket)); | ||
} | ||
} | ||
|
45 changes: 45 additions & 0 deletions
45
src/Zilean.ApiService/Features/Authentication/ApiKeyDocumentTransformer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
public class ApiKeyDocumentTransformer : IOpenApiDocumentTransformer | ||
{ | ||
public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken) | ||
{ | ||
// Define the API key security scheme | ||
var apiKeyScheme = new OpenApiSecurityScheme | ||
{ | ||
Type = SecuritySchemeType.ApiKey, | ||
Name = "X-API-KEY", | ||
In = ParameterLocation.Header, | ||
Description = "API Key required for accessing protected endpoints." | ||
}; | ||
|
||
document.Components ??= new OpenApiComponents(); | ||
document.Components.SecuritySchemes[ApiKeyAuthentication.Scheme] = apiKeyScheme; | ||
|
||
foreach (var group in context.DescriptionGroups) | ||
{ | ||
foreach (var apiDescription in group.Items) | ||
{ | ||
var metadata = apiDescription.ActionDescriptor.EndpointMetadata? | ||
.OfType<OpenApiSecurityMetadata>() | ||
.FirstOrDefault(); | ||
|
||
if (metadata is { SecurityScheme: ApiKeyAuthentication.Scheme }) | ||
{ | ||
var route = apiDescription.RelativePath; | ||
if (document.Paths.TryGetValue("/" + route, out var pathItem)) | ||
{ | ||
foreach (var operation in pathItem.Operations.Values) | ||
{ | ||
operation.Security ??= []; | ||
operation.Security.Add(new OpenApiSecurityRequirement | ||
{ | ||
[apiKeyScheme] = Array.Empty<string>() | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
return Task.CompletedTask; | ||
} | ||
} |
6 changes: 6 additions & 0 deletions
6
src/Zilean.ApiService/Features/Authentication/OpenApiSecurityMetadata.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
namespace Zilean.ApiService.Features.Authentication; | ||
|
||
public class OpenApiSecurityMetadata(string securityScheme) | ||
{ | ||
public string SecurityScheme { get; } = securityScheme; | ||
} |
116 changes: 116 additions & 0 deletions
116
src/Zilean.ApiService/Features/Blacklist/BlacklistEndpoints.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
namespace Zilean.ApiService.Features.Blacklist; | ||
|
||
public static class BlacklistEndpoints | ||
{ | ||
private const string GroupName = "blacklist"; | ||
private const string Add = "/add"; | ||
private const string Remove = "/remove"; | ||
|
||
public static WebApplication MapBlacklistEndpoints(this WebApplication app) | ||
{ | ||
app.MapGroup(GroupName) | ||
.WithTags(GroupName) | ||
.Torrents() | ||
.DisableAntiforgery() | ||
.RequireAuthorization(ApiKeyAuthentication.Policy) | ||
.WithMetadata(new OpenApiSecurityMetadata(ApiKeyAuthentication.Scheme)); | ||
|
||
return app; | ||
} | ||
|
||
private static RouteGroupBuilder Torrents(this RouteGroupBuilder group) | ||
{ | ||
group.MapPut(Add, AddBlacklistItem) | ||
.Produces(StatusCodes.Status204NoContent) | ||
.Produces<string>(StatusCodes.Status400BadRequest) | ||
.Produces<string>(StatusCodes.Status409Conflict); | ||
|
||
group.MapDelete(Remove, RemoveBlacklistItem) | ||
.Produces(StatusCodes.Status204NoContent) | ||
.Produces<string>(StatusCodes.Status404NotFound) | ||
.Produces<string>(StatusCodes.Status400BadRequest); | ||
|
||
return group; | ||
} | ||
|
||
private static async Task<IResult> RemoveBlacklistItem(HttpContext context, ZileanDbContext dbContext, ILogger<BlacklistLogger> logger, [FromQuery] string infoHash) | ||
{ | ||
try | ||
{ | ||
if (string.IsNullOrWhiteSpace(infoHash)) | ||
{ | ||
logger.LogWarning("Attempted to remove blacklisted item with empty info hash"); | ||
return Results.BadRequest("InfoHash is required"); | ||
} | ||
|
||
var item = await dbContext.BlacklistedItems.FirstOrDefaultAsync(x => x.InfoHash == infoHash); | ||
|
||
if (item == null) | ||
{ | ||
logger.LogWarning("Attempted to remove non-existent blacklisted item {InfoHash}", infoHash); | ||
return Results.NotFound(); | ||
} | ||
|
||
dbContext.BlacklistedItems.Remove(item); | ||
await dbContext.SaveChangesAsync(); | ||
|
||
logger.LogInformation("Removed blacklisted item {InfoHash}", infoHash); | ||
|
||
return Results.NoContent(); | ||
} | ||
catch (Exception e) | ||
{ | ||
logger.LogError(e, "An error occurred while removing a blacklisted item"); | ||
return Results.BadRequest("An error occurred while removing a blacklisted item"); | ||
} | ||
} | ||
|
||
private static async Task<IResult> AddBlacklistItem(HttpContext context, ZileanDbContext dbContext, [AsParameters] BlacklistItemRequest request, ILogger<BlacklistLogger> logger) | ||
{ | ||
try | ||
{ | ||
if (string.IsNullOrWhiteSpace(request.info_hash)) | ||
{ | ||
return Results.BadRequest("info_hash is required"); | ||
} | ||
|
||
if (string.IsNullOrWhiteSpace(request.reason)) | ||
{ | ||
return Results.BadRequest("reason is required"); | ||
} | ||
|
||
if (await dbContext.BlacklistedItems.AnyAsync(x => x.InfoHash == request.info_hash)) | ||
{ | ||
return Results.Conflict("Item already blacklisted"); | ||
} | ||
|
||
var blacklistedItem = new BlacklistedItem | ||
{ | ||
InfoHash = request.info_hash, | ||
Reason = request.reason, | ||
BlacklistedAt = DateTime.UtcNow | ||
}; | ||
|
||
dbContext.BlacklistedItems.Add(blacklistedItem); | ||
|
||
var torrentInfo = await dbContext.Torrents.FirstOrDefaultAsync(x => x.InfoHash == request.info_hash); | ||
|
||
if (torrentInfo != null) | ||
{ | ||
dbContext.Torrents.Remove(torrentInfo); | ||
logger.LogInformation("Removed torrent {InfoHash} from database", request.info_hash); | ||
} | ||
|
||
await dbContext.SaveChangesAsync(); | ||
|
||
return Results.NoContent(); | ||
} | ||
catch (Exception e) | ||
{ | ||
logger.LogError(e, "An error occurred while adding a blacklisted item"); | ||
return Results.BadRequest("An error occurred while adding a blacklisted item"); | ||
} | ||
} | ||
|
||
private abstract class BlacklistLogger; | ||
} |
8 changes: 8 additions & 0 deletions
8
src/Zilean.ApiService/Features/Blacklist/BlacklistItemRequest.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
// ReSharper disable InconsistentNaming | ||
namespace Zilean.ApiService.Features.Blacklist; | ||
|
||
public class BlacklistItemRequest | ||
{ | ||
public required string info_hash { get; set; } | ||
public required string reason { get; set; } | ||
} |
47 changes: 47 additions & 0 deletions
47
src/Zilean.ApiService/Features/Bootstrapping/ConfigurationUpdaterService.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
namespace Zilean.ApiService.Features.Bootstrapping; | ||
|
||
public class ConfigurationUpdaterService(ZileanConfiguration configuration, ILogger<ConfigurationUpdaterService> logger) : IHostedService | ||
{ | ||
private const string ResetApiKeyEnvVar = "ZILEAN__NEW__API__KEY"; | ||
|
||
public async Task StartAsync(CancellationToken cancellationToken) | ||
{ | ||
bool firstRun = configuration.FirstRun; | ||
|
||
if (firstRun) | ||
{ | ||
configuration.FirstRun = false; | ||
} | ||
|
||
if (Environment.GetEnvironmentVariable(ResetApiKeyEnvVar) is "1" or "true") | ||
{ | ||
configuration.ApiKey = ApiKey.Generate(); | ||
logger.LogInformation("API Key regenerated:'{ApiKey}'", configuration.ApiKey); | ||
logger.LogInformation("Please keep this key safe and secure."); | ||
} | ||
|
||
var configurationFolderPath = Path.Combine(AppContext.BaseDirectory, ConfigurationLiterals.ConfigurationFolder); | ||
var configurationFilePath = Path.Combine(configurationFolderPath, ConfigurationLiterals.SettingsConfigFilename); | ||
|
||
var configWrapper = new Dictionary<string, object> | ||
{ | ||
[ConfigurationLiterals.MainSettingsSectionName] = configuration, | ||
}; | ||
|
||
await File.WriteAllTextAsync(configurationFilePath, | ||
JsonSerializer.Serialize(configWrapper, | ||
new JsonSerializerOptions | ||
{ | ||
WriteIndented = true, | ||
PropertyNamingPolicy = null, | ||
}), cancellationToken); | ||
|
||
if (firstRun) | ||
{ | ||
logger.LogInformation("Zilean API Key: '{ApiKey}'", configuration.ApiKey); | ||
logger.LogInformation("Please keep this key safe and secure."); | ||
} | ||
} | ||
|
||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; | ||
} |
Oops, something went wrong.