Support SABnzbd API key auth
This commit is contained in:
parent
1645320199
commit
49ab603708
3 changed files with 188 additions and 1 deletions
|
|
@ -196,7 +196,7 @@ RdtClient also emulates part of the SABnzbd API so Sonarr and Radarr can add NZB
|
||||||
1. Enter `6500` in the `Port` field.
|
1. Enter `6500` in the `Port` field.
|
||||||
1. Enable `Use SSL` only if you access RdtClient through HTTPS.
|
1. Enable `Use SSL` only if you access RdtClient through HTTPS.
|
||||||
1. Leave `URL Base` empty unless RdtClient is configured with a `BasePath`, for example `/rdt`.
|
1. Leave `URL Base` empty unless RdtClient is configured with a `BasePath`, for example `/rdt`.
|
||||||
1. If RdtClient authentication is enabled, leave `API Key` empty and enter your RdtClient username and password.
|
1. If RdtClient authentication is enabled, leave `API Key` empty and enter your RdtClient username and password. If your client only supports an API key, enter `{username}:{password}` in `API Key`.
|
||||||
1. If RdtClient authentication is disabled, enter any value in `API Key`, for example `rdtclient`, and leave username/password empty.
|
1. If RdtClient authentication is disabled, enter any value in `API Key`, for example `rdtclient`, and leave username/password empty.
|
||||||
1. Set the category to `sonarr` for Sonarr or `radarr` for Radarr.
|
1. Set the category to `sonarr` for Sonarr or `radarr` for Radarr.
|
||||||
1. Hit `Test` and then `Save` if all is well.
|
1. Hit `Test` and then `Save` if all is well.
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,36 @@ public class SabnzbdHandler(Authentication authentication, IHttpContextAccessor
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var apiKey = GetParam("apikey");
|
||||||
|
|
||||||
|
if (!String.IsNullOrWhiteSpace(apiKey))
|
||||||
|
{
|
||||||
|
var separatorIndex = apiKey.IndexOf(':');
|
||||||
|
|
||||||
|
if (separatorIndex <= 0 || separatorIndex == apiKey.Length - 1)
|
||||||
|
{
|
||||||
|
context.Fail();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var username = apiKey[..separatorIndex];
|
||||||
|
var password = apiKey[(separatorIndex + 1)..];
|
||||||
|
|
||||||
|
var loginResult = await authentication.Login(username, password);
|
||||||
|
|
||||||
|
if (loginResult.Succeeded)
|
||||||
|
{
|
||||||
|
context.Succeed(requirement);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Fail();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Authentication required but missing credentials
|
// Authentication required but missing credentials
|
||||||
context.Fail();
|
context.Fail();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,110 @@ public class SabnzbdHandlerTest
|
||||||
Assert.True(context.HasSucceeded, "HasSucceeded should be true for valid credentials");
|
Assert.True(context.HasSucceeded, "HasSucceeded should be true for valid credentials");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HandleAsync_ValidApiKeyCredentials_Succeeds()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||||
|
var httpContext = new DefaultHttpContext
|
||||||
|
{
|
||||||
|
Request =
|
||||||
|
{
|
||||||
|
QueryString = new("?apikey=user:pass")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext);
|
||||||
|
|
||||||
|
var context = CreateContext(httpContext);
|
||||||
|
_authenticationMock.Setup(a => a.Login("user", "pass")).ReturnsAsync(SignInResult.Success);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _handler.HandleAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(context.HasSucceeded, "HasSucceeded should be true for valid api key credentials");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HandleAsync_ValidApiKeyCredentialsFromForm_Succeeds()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
httpContext.Request.ContentType = "application/x-www-form-urlencoded";
|
||||||
|
httpContext.Request.Form = new FormCollection(new()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
"apikey", "user:pass"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext);
|
||||||
|
|
||||||
|
var context = CreateContext(httpContext);
|
||||||
|
_authenticationMock.Setup(a => a.Login("user", "pass")).ReturnsAsync(SignInResult.Success);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _handler.HandleAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(context.HasSucceeded, "HasSucceeded should be true for valid form api key credentials");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HandleAsync_ApiKeyPasswordContainingColon_Succeeds()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||||
|
var httpContext = new DefaultHttpContext
|
||||||
|
{
|
||||||
|
Request =
|
||||||
|
{
|
||||||
|
QueryString = new("?apikey=user:pass:with:colons")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext);
|
||||||
|
|
||||||
|
var context = CreateContext(httpContext);
|
||||||
|
_authenticationMock.Setup(a => a.Login("user", "pass:with:colons")).ReturnsAsync(SignInResult.Success);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _handler.HandleAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(context.HasSucceeded, "HasSucceeded should be true when the password contains colons");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HandleAsync_MaCredentialsTakePrecedenceOverApiKey()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||||
|
var httpContext = new DefaultHttpContext
|
||||||
|
{
|
||||||
|
Request =
|
||||||
|
{
|
||||||
|
QueryString = new("?ma_username=user&ma_password=wrong&apikey=user:pass")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext);
|
||||||
|
|
||||||
|
var context = CreateContext(httpContext);
|
||||||
|
_authenticationMock.Setup(a => a.Login("user", "wrong")).ReturnsAsync(SignInResult.Failed);
|
||||||
|
_authenticationMock.Setup(a => a.Login("user", "pass")).ReturnsAsync(SignInResult.Success);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _handler.HandleAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(context.HasSucceeded, "HasSucceeded should be false because ma_username/ma_password take precedence");
|
||||||
|
_authenticationMock.Verify(a => a.Login("user", "wrong"), Times.Once);
|
||||||
|
_authenticationMock.Verify(a => a.Login("user", "pass"), Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task HandleAsync_AlreadyAuthenticated_Succeeds()
|
public async Task HandleAsync_AlreadyAuthenticated_Succeeds()
|
||||||
{
|
{
|
||||||
|
|
@ -108,6 +212,59 @@ public class SabnzbdHandlerTest
|
||||||
Assert.False(context.HasSucceeded, "HasSucceeded should be false for invalid credentials");
|
Assert.False(context.HasSucceeded, "HasSucceeded should be false for invalid credentials");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HandleAsync_InvalidApiKeyCredentials_DoesNotSucceed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||||
|
var httpContext = new DefaultHttpContext
|
||||||
|
{
|
||||||
|
Request =
|
||||||
|
{
|
||||||
|
QueryString = new("?apikey=user:wrong")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext);
|
||||||
|
|
||||||
|
var context = CreateContext(httpContext);
|
||||||
|
_authenticationMock.Setup(a => a.Login("user", "wrong")).ReturnsAsync(SignInResult.Failed);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _handler.HandleAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(context.HasSucceeded, "HasSucceeded should be false for invalid api key credentials");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("missingdelimiter")]
|
||||||
|
[InlineData(":pass")]
|
||||||
|
[InlineData("user:")]
|
||||||
|
public async Task HandleAsync_MalformedApiKey_DoesNotSucceed(String apiKey)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword;
|
||||||
|
var httpContext = new DefaultHttpContext
|
||||||
|
{
|
||||||
|
Request =
|
||||||
|
{
|
||||||
|
QueryString = new($"?apikey={apiKey}")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext);
|
||||||
|
|
||||||
|
var context = CreateContext(httpContext);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _handler.HandleAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(context.HasSucceeded, "HasSucceeded should be false for malformed api keys");
|
||||||
|
_authenticationMock.Verify(a => a.Login(It.IsAny<String>(), It.IsAny<String>()), Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task HandleAsync_MissingCredentials_DoesNotSucceed()
|
public async Task HandleAsync_MissingCredentials_DoesNotSucceed()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue