mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-10-07 22:03:14 -04:00
360 lines
13 KiB
C#
360 lines
13 KiB
C#
using System.Text;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using allstarr.Middleware;
|
|
using allstarr.Services.Admin;
|
|
|
|
namespace allstarr.Tests;
|
|
|
|
public class AdminAuthenticationMiddlewareTests
|
|
{
|
|
[Fact]
|
|
public async Task InvokeAsync_UnauthenticatedAdminRequest_Returns401()
|
|
{
|
|
var sessionService = AdminAuthSessionTestSupport.Create();
|
|
var nextInvoked = false;
|
|
|
|
var middleware = new AdminAuthenticationMiddleware(
|
|
_ =>
|
|
{
|
|
nextInvoked = true;
|
|
return Task.CompletedTask;
|
|
},
|
|
sessionService,
|
|
NullLogger<AdminAuthenticationMiddleware>.Instance);
|
|
|
|
var context = CreateContext(
|
|
path: "/api/admin/config",
|
|
method: HttpMethods.Get,
|
|
localPort: 5275);
|
|
|
|
await middleware.InvokeAsync(context);
|
|
|
|
Assert.False(nextInvoked);
|
|
Assert.Equal(StatusCodes.Status401Unauthorized, context.Response.StatusCode);
|
|
|
|
var body = await ReadResponseBodyAsync(context);
|
|
Assert.Contains("Authentication required", body);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InvokeAsync_NonAdminUser_AllowedRoute_PassesThrough()
|
|
{
|
|
var sessionService = AdminAuthSessionTestSupport.Create();
|
|
var session = await sessionService.CreateSessionAsync(
|
|
userId: "user-1",
|
|
userName: "josh",
|
|
isAdministrator: false,
|
|
jellyfinAccessToken: "token",
|
|
jellyfinServerId: "server");
|
|
|
|
var nextInvoked = false;
|
|
var middleware = new AdminAuthenticationMiddleware(
|
|
context =>
|
|
{
|
|
nextInvoked = true;
|
|
context.Response.StatusCode = StatusCodes.Status204NoContent;
|
|
return Task.CompletedTask;
|
|
},
|
|
sessionService,
|
|
NullLogger<AdminAuthenticationMiddleware>.Instance);
|
|
|
|
var context = CreateContext(
|
|
path: "/api/admin/jellyfin/playlists",
|
|
method: HttpMethods.Get,
|
|
localPort: 5275,
|
|
sessionIdCookie: session.SessionId);
|
|
|
|
await middleware.InvokeAsync(context);
|
|
|
|
Assert.True(nextInvoked);
|
|
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
|
Assert.True(context.Items.ContainsKey(AdminAuthSessionService.HttpContextSessionItemKey));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/api/admin/ui/schema", "GET")]
|
|
[InlineData("/api/admin/ui/home", "GET")]
|
|
[InlineData("/api/admin/provider-accounts", "GET")]
|
|
[InlineData("/api/admin/provider-accounts", "POST")]
|
|
[InlineData("/api/admin/provider-accounts/019f48f2-5f28-7b11-b42d-0d9b76b73b40", "DELETE")]
|
|
[InlineData("/api/admin/provider-accounts/019f48f2-5f28-7b11-b42d-0d9b76b73b40/secret", "PUT")]
|
|
[InlineData("/api/admin/provider-accounts/019f48f2-5f28-7b11-b42d-0d9b76b73b40/audience", "PUT")]
|
|
[InlineData("/api/admin/provider-accounts/019f48f2-5f28-7b11-b42d-0d9b76b73b40", "PATCH")]
|
|
[InlineData("/api/admin/scrobbling/lastfm/authenticate", "POST")]
|
|
public async Task InvokeAsync_NonAdminUser_ProviderSelfServiceRoutesPassToScopedControllers(
|
|
string path,
|
|
string method)
|
|
{
|
|
var sessionService = AdminAuthSessionTestSupport.Create();
|
|
var session = await sessionService.CreateSessionAsync(
|
|
userId: "user-1",
|
|
userName: "josh",
|
|
isAdministrator: false,
|
|
jellyfinAccessToken: "token",
|
|
jellyfinServerId: "server");
|
|
var invoked = false;
|
|
var middleware = new AdminAuthenticationMiddleware(
|
|
_ =>
|
|
{
|
|
invoked = true;
|
|
return Task.CompletedTask;
|
|
},
|
|
sessionService,
|
|
NullLogger<AdminAuthenticationMiddleware>.Instance);
|
|
var context = CreateContext(path, method, 5275, session.SessionId);
|
|
|
|
await middleware.InvokeAsync(context);
|
|
|
|
Assert.True(invoked);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/api/admin/playlist-sources", "GET")]
|
|
[InlineData("/api/admin/playlist-sources/019f48f2-5f28-7b11-b42d-0d9b76b73b40/playlists", "GET")]
|
|
[InlineData("/api/admin/media-targets", "GET")]
|
|
[InlineData("/api/admin/media-targets/019f48f2-5f28-7b11-b42d-0d9b76b73b40/playlists", "GET")]
|
|
[InlineData("/api/admin/library-index/counts", "GET")]
|
|
[InlineData("/api/admin/library-index/enqueue", "POST")]
|
|
[InlineData("/api/admin/playlist-links", "GET")]
|
|
[InlineData("/api/admin/playlist-links", "POST")]
|
|
[InlineData("/api/admin/playlist-links/019f48f2-5f28-7b11-b42d-0d9b76b73b40", "PUT")]
|
|
[InlineData("/api/admin/playlist-links/019f48f2-5f28-7b11-b42d-0d9b76b73b40", "DELETE")]
|
|
[InlineData("/api/admin/playlist-links/rematch/preview", "GET")]
|
|
[InlineData("/api/admin/playlist-links/019f48f2-5f28-7b11-b42d-0d9b76b73b40/source-update/apply", "POST")]
|
|
public async Task InvokeAsync_NonAdminUser_PlaylistSelfServiceRoutesPassToScopedControllers(
|
|
string path,
|
|
string method)
|
|
{
|
|
var sessionService = AdminAuthSessionTestSupport.Create();
|
|
var session = await sessionService.CreateSessionAsync(
|
|
userId: "user-1",
|
|
userName: "josh",
|
|
isAdministrator: false,
|
|
jellyfinAccessToken: "token",
|
|
jellyfinServerId: "server");
|
|
var invoked = false;
|
|
var middleware = new AdminAuthenticationMiddleware(
|
|
_ =>
|
|
{
|
|
invoked = true;
|
|
return Task.CompletedTask;
|
|
},
|
|
sessionService,
|
|
NullLogger<AdminAuthenticationMiddleware>.Instance);
|
|
var context = CreateContext(path, method, 5275, session.SessionId);
|
|
|
|
await middleware.InvokeAsync(context);
|
|
|
|
Assert.True(invoked);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/api/admin/ui/schema", "POST")]
|
|
[InlineData("/api/admin/ui/home", "POST")]
|
|
[InlineData("/api/admin/ui/home-extra", "GET")]
|
|
[InlineData("/api/admin/ui/now-playing", "GET")]
|
|
[InlineData("/api/admin/config", "GET")]
|
|
[InlineData("/api/admin/status", "GET")]
|
|
[InlineData("/api/admin/library-index-spoof", "POST")]
|
|
[InlineData("/api/admin/playlist-linkspoof", "GET")]
|
|
[InlineData("/api/admin/provider-accounts", "PUT")]
|
|
[InlineData("/api/admin/provider-accounts/not-a-guid", "DELETE")]
|
|
[InlineData("/api/admin/provider-accounts/not-a-guid/audience", "PUT")]
|
|
[InlineData("/api/admin/provider-accounts/019f48f2-5f28-7b11-b42d-0d9b76b73b40/audience", "GET")]
|
|
[InlineData("/api/admin/scrobbling/status", "GET")]
|
|
[InlineData("/api/admin/scrobbling/lastfm/authenticate", "GET")]
|
|
[InlineData("/api/admin/scrobbling/lastfm/authenticate-extra", "POST")]
|
|
[InlineData("/api/admin/provider-accounts/019f48f2-5f28-7b11-b42d-0d9b76b73b40/secret", "GET")]
|
|
public async Task InvokeAsync_NonAdminUser_RemainsBlockedFromAdminAndInvalidAccountRoutes(
|
|
string path,
|
|
string method)
|
|
{
|
|
var sessionService = AdminAuthSessionTestSupport.Create();
|
|
var session = await sessionService.CreateSessionAsync(
|
|
userId: "user-1",
|
|
userName: "josh",
|
|
isAdministrator: false,
|
|
jellyfinAccessToken: "token",
|
|
jellyfinServerId: "server");
|
|
var invoked = false;
|
|
var middleware = new AdminAuthenticationMiddleware(
|
|
_ =>
|
|
{
|
|
invoked = true;
|
|
return Task.CompletedTask;
|
|
},
|
|
sessionService,
|
|
NullLogger<AdminAuthenticationMiddleware>.Instance);
|
|
var context = CreateContext(path, method, 5275, session.SessionId);
|
|
|
|
await middleware.InvokeAsync(context);
|
|
|
|
Assert.False(invoked);
|
|
Assert.Equal(StatusCodes.Status403Forbidden, context.Response.StatusCode);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/api/admin/jobs", "GET")]
|
|
[InlineData("/api/admin/jobs/019f48f2-5f28-7b11-b42d-0d9b76b73b40", "GET")]
|
|
[InlineData("/api/admin/jobs/019f48f2-5f28-7b11-b42d-0d9b76b73b40/cancel", "POST")]
|
|
[InlineData("/api/admin/favorite-action-policies", "GET")]
|
|
[InlineData("/api/admin/favorite-action-policies/me", "PUT")]
|
|
[InlineData("/api/admin/intelligence", "GET")]
|
|
[InlineData("/api/admin/intelligence/policy", "PUT")]
|
|
[InlineData("/api/admin/intelligence/runs", "POST")]
|
|
[InlineData("/api/admin/intelligence/generated-sets", "POST")]
|
|
[InlineData("/api/admin/intelligence/data", "DELETE")]
|
|
[InlineData("/api/admin/updates/stream", "GET")]
|
|
public async Task InvokeAsync_NonAdminUser_OwnJobRoutesPassToControllerScopeChecks(
|
|
string path,
|
|
string method)
|
|
{
|
|
var sessionService = AdminAuthSessionTestSupport.Create();
|
|
var session = await sessionService.CreateSessionAsync(
|
|
userId: "user-1",
|
|
userName: "josh",
|
|
isAdministrator: false,
|
|
jellyfinAccessToken: "token",
|
|
jellyfinServerId: "server");
|
|
var invoked = false;
|
|
var middleware = new AdminAuthenticationMiddleware(
|
|
_ =>
|
|
{
|
|
invoked = true;
|
|
return Task.CompletedTask;
|
|
},
|
|
sessionService,
|
|
NullLogger<AdminAuthenticationMiddleware>.Instance);
|
|
var context = CreateContext(path, method, 5275, session.SessionId);
|
|
|
|
await middleware.InvokeAsync(context);
|
|
|
|
Assert.True(invoked);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InvokeAsync_NonAdminUser_DisallowedRoute_Returns403()
|
|
{
|
|
var sessionService = AdminAuthSessionTestSupport.Create();
|
|
var session = await sessionService.CreateSessionAsync(
|
|
userId: "user-1",
|
|
userName: "josh",
|
|
isAdministrator: false,
|
|
jellyfinAccessToken: "token",
|
|
jellyfinServerId: "server");
|
|
|
|
var nextInvoked = false;
|
|
var middleware = new AdminAuthenticationMiddleware(
|
|
_ =>
|
|
{
|
|
nextInvoked = true;
|
|
return Task.CompletedTask;
|
|
},
|
|
sessionService,
|
|
NullLogger<AdminAuthenticationMiddleware>.Instance);
|
|
|
|
var context = CreateContext(
|
|
path: "/api/admin/config",
|
|
method: HttpMethods.Get,
|
|
localPort: 5275,
|
|
sessionIdCookie: session.SessionId);
|
|
|
|
await middleware.InvokeAsync(context);
|
|
|
|
Assert.False(nextInvoked);
|
|
Assert.Equal(StatusCodes.Status403Forbidden, context.Response.StatusCode);
|
|
|
|
var body = await ReadResponseBodyAsync(context);
|
|
Assert.Contains("Administrator permissions required", body);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InvokeAsync_AdminUser_DisallowedForUserButAllowedForAdmin_PassesThrough()
|
|
{
|
|
var sessionService = AdminAuthSessionTestSupport.Create();
|
|
var session = await sessionService.CreateSessionAsync(
|
|
userId: "admin-1",
|
|
userName: "admin",
|
|
isAdministrator: true,
|
|
jellyfinAccessToken: "token",
|
|
jellyfinServerId: "server");
|
|
|
|
var nextInvoked = false;
|
|
var middleware = new AdminAuthenticationMiddleware(
|
|
context =>
|
|
{
|
|
nextInvoked = true;
|
|
context.Response.StatusCode = StatusCodes.Status204NoContent;
|
|
return Task.CompletedTask;
|
|
},
|
|
sessionService,
|
|
NullLogger<AdminAuthenticationMiddleware>.Instance);
|
|
|
|
var context = CreateContext(
|
|
path: "/api/admin/config",
|
|
method: HttpMethods.Get,
|
|
localPort: 5275,
|
|
sessionIdCookie: session.SessionId);
|
|
|
|
await middleware.InvokeAsync(context);
|
|
|
|
Assert.True(nextInvoked);
|
|
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InvokeAsync_AdminApiOnMainPort_PassesThroughForDownstreamFilter()
|
|
{
|
|
var sessionService = AdminAuthSessionTestSupport.Create();
|
|
var nextInvoked = false;
|
|
|
|
var middleware = new AdminAuthenticationMiddleware(
|
|
context =>
|
|
{
|
|
nextInvoked = true;
|
|
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
|
return Task.CompletedTask;
|
|
},
|
|
sessionService,
|
|
NullLogger<AdminAuthenticationMiddleware>.Instance);
|
|
|
|
var context = CreateContext(
|
|
path: "/api/admin/config",
|
|
method: HttpMethods.Get,
|
|
localPort: 5274);
|
|
|
|
await middleware.InvokeAsync(context);
|
|
|
|
Assert.True(nextInvoked);
|
|
Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode);
|
|
}
|
|
|
|
private static DefaultHttpContext CreateContext(
|
|
string path,
|
|
string method,
|
|
int localPort,
|
|
string? sessionIdCookie = null)
|
|
{
|
|
var context = new DefaultHttpContext();
|
|
context.Request.Path = path;
|
|
context.Request.Method = method;
|
|
context.Connection.LocalPort = localPort;
|
|
context.Response.Body = new MemoryStream();
|
|
|
|
if (!string.IsNullOrWhiteSpace(sessionIdCookie))
|
|
{
|
|
context.Request.Headers.Cookie = $"{AdminAuthSessionService.SessionCookieName}={sessionIdCookie}";
|
|
}
|
|
|
|
return context;
|
|
}
|
|
|
|
private static async Task<string> ReadResponseBodyAsync(HttpContext context)
|
|
{
|
|
context.Response.Body.Seek(0, SeekOrigin.Begin);
|
|
using var reader = new StreamReader(context.Response.Body, Encoding.UTF8, leaveOpen: true);
|
|
return await reader.ReadToEndAsync();
|
|
}
|
|
}
|