Merge pull request #984 from ALenfant/antonin/sabnzbd-api-key
Support SABnzbd API key auth
This commit is contained in:
commit
5b7b526390
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. 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. 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. Set the category to `sonarr` for Sonarr or `radarr` for Radarr.
|
||||
1. Hit `Test` and then `Save` if all is well.
|
||||
|
|
|
|||
|
|
@ -65,6 +65,36 @@ public class SabnzbdHandler(Authentication authentication, IHttpContextAccessor
|
|||
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
|
||||
context.Fail();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,110 @@ public class SabnzbdHandlerTest
|
|||
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]
|
||||
public async Task HandleAsync_AlreadyAuthenticated_Succeeds()
|
||||
{
|
||||
|
|
@ -108,6 +212,59 @@ public class SabnzbdHandlerTest
|
|||
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]
|
||||
public async Task HandleAsync_MissingCredentials_DoesNotSucceed()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue