Apply suggestions from code review

This commit is contained in:
Matt Montgomery 2020-08-17 16:36:45 -05:00
parent c49a357f85
commit 5f1a863241
6 changed files with 40 additions and 55 deletions

View File

@ -3,17 +3,16 @@ using System.Collections.Concurrent;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.QuickConnect; using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Security;
using MediaBrowser.Model.QuickConnect; using MediaBrowser.Model.QuickConnect;
using Microsoft.AspNetCore.Http;
using MediaBrowser.Common;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Authentication;
namespace Emby.Server.Implementations.QuickConnect namespace Emby.Server.Implementations.QuickConnect
{ {
@ -60,7 +59,7 @@ namespace Emby.Server.Implementations.QuickConnect
public int CodeLength { get; set; } = 6; public int CodeLength { get; set; } = 6;
/// <inheritdoc/> /// <inheritdoc/>
public string TokenNamePrefix { get; set; } = "QuickConnect-"; public string TokenName { get; set; } = "QuickConnect";
/// <inheritdoc/> /// <inheritdoc/>
public QuickConnectState State { get; private set; } = QuickConnectState.Unavailable; public QuickConnectState State { get; private set; } = QuickConnectState.Unavailable;
@ -82,7 +81,7 @@ namespace Emby.Server.Implementations.QuickConnect
/// <inheritdoc/> /// <inheritdoc/>
public void Activate() public void Activate()
{ {
DateActivated = DateTime.Now; DateActivated = DateTime.UtcNow;
SetState(QuickConnectState.Active); SetState(QuickConnectState.Active);
} }
@ -101,7 +100,7 @@ namespace Emby.Server.Implementations.QuickConnect
} }
/// <inheritdoc/> /// <inheritdoc/>
public QuickConnectResult TryConnect(string friendlyName) public QuickConnectResult TryConnect()
{ {
ExpireRequests(); ExpireRequests();
@ -111,14 +110,11 @@ namespace Emby.Server.Implementations.QuickConnect
throw new AuthenticationException("Quick connect is not active on this server"); throw new AuthenticationException("Quick connect is not active on this server");
} }
_logger.LogDebug("Got new quick connect request from {friendlyName}", friendlyName);
var code = GenerateCode(); var code = GenerateCode();
var result = new QuickConnectResult() var result = new QuickConnectResult()
{ {
Secret = GenerateSecureRandom(), Secret = GenerateSecureRandom(),
FriendlyName = friendlyName, DateAdded = DateTime.UtcNow,
DateAdded = DateTime.Now,
Code = code Code = code
}; };
@ -162,13 +158,11 @@ namespace Emby.Server.Implementations.QuickConnect
} }
/// <inheritdoc/> /// <inheritdoc/>
public bool AuthorizeRequest(HttpRequest request, string code) public bool AuthorizeRequest(Guid userId, string code)
{ {
ExpireRequests(); ExpireRequests();
AssertActive(); AssertActive();
var auth = _authContext.GetAuthorizationInfo(request);
if (!_currentRequests.TryGetValue(code, out QuickConnectResult result)) if (!_currentRequests.TryGetValue(code, out QuickConnectResult result))
{ {
throw new ResourceNotFoundException("Unable to find request"); throw new ResourceNotFoundException("Unable to find request");
@ -182,21 +176,21 @@ namespace Emby.Server.Implementations.QuickConnect
result.Authentication = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); result.Authentication = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
// Change the time on the request so it expires one minute into the future. It can't expire immediately as otherwise some clients wouldn't ever see that they have been authenticated. // Change the time on the request so it expires one minute into the future. It can't expire immediately as otherwise some clients wouldn't ever see that they have been authenticated.
var added = result.DateAdded ?? DateTime.Now.Subtract(new TimeSpan(0, Timeout, 0)); var added = result.DateAdded ?? DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(Timeout));
result.DateAdded = added.Subtract(new TimeSpan(0, Timeout - 1, 0)); result.DateAdded = added.Subtract(TimeSpan.FromMinutes(Timeout - 1));
_authenticationRepository.Create(new AuthenticationInfo _authenticationRepository.Create(new AuthenticationInfo
{ {
AppName = TokenNamePrefix + result.FriendlyName, AppName = TokenName,
AccessToken = result.Authentication, AccessToken = result.Authentication,
DateCreated = DateTime.UtcNow, DateCreated = DateTime.UtcNow,
DeviceId = _appHost.SystemId, DeviceId = _appHost.SystemId,
DeviceName = _appHost.FriendlyName, DeviceName = _appHost.FriendlyName,
AppVersion = _appHost.ApplicationVersionString, AppVersion = _appHost.ApplicationVersionString,
UserId = auth.UserId UserId = userId
}); });
_logger.LogInformation("Allowing device {FriendlyName} to login as user {Username} with quick connect code {Code}", result.FriendlyName, auth.User.Username, result.Code); _logger.LogDebug("Authorizing device with code {Code} to login as user {userId}", code, userId);
return true; return true;
} }
@ -210,7 +204,7 @@ namespace Emby.Server.Implementations.QuickConnect
UserId = user UserId = user
}); });
var tokens = raw.Items.Where(x => x.AppName.StartsWith(TokenNamePrefix, StringComparison.CurrentCulture)); var tokens = raw.Items.Where(x => x.AppName.StartsWith(TokenName, StringComparison.CurrentCulture));
var removed = 0; var removed = 0;
foreach (var token in tokens) foreach (var token in tokens)
@ -256,7 +250,7 @@ namespace Emby.Server.Implementations.QuickConnect
public void ExpireRequests(bool expireAll = false) public void ExpireRequests(bool expireAll = false)
{ {
// Check if quick connect should be deactivated // Check if quick connect should be deactivated
if (State == QuickConnectState.Active && DateTime.Now > DateActivated.AddMinutes(Timeout) && !expireAll) if (State == QuickConnectState.Active && DateTime.UtcNow > DateActivated.AddMinutes(Timeout) && !expireAll)
{ {
_logger.LogDebug("Quick connect time expired, deactivating"); _logger.LogDebug("Quick connect time expired, deactivating");
SetState(QuickConnectState.Available); SetState(QuickConnectState.Available);
@ -270,7 +264,7 @@ namespace Emby.Server.Implementations.QuickConnect
for (int i = 0; i < values.Count; i++) for (int i = 0; i < values.Count; i++)
{ {
var added = values[i].DateAdded ?? DateTime.UnixEpoch; var added = values[i].DateAdded ?? DateTime.UnixEpoch;
if (DateTime.Now > added.AddMinutes(Timeout) || expireAll) if (DateTime.UtcNow > added.AddMinutes(Timeout) || expireAll)
{ {
code = values[i].Code; code = values[i].Code;
_logger.LogDebug("Removing expired request {code}", code); _logger.LogDebug("Removing expired request {code}", code);

View File

@ -1433,7 +1433,7 @@ namespace Emby.Server.Implementations.Session
Limit = 1 Limit = 1
}); });
if (result.TotalRecordCount < 1) if (result.TotalRecordCount == 0)
{ {
throw new SecurityException("Unknown quick connect token"); throw new SecurityException("Unknown quick connect token");
} }

View File

@ -1,8 +1,8 @@
using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.QuickConnect; using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Model.QuickConnect; using MediaBrowser.Model.QuickConnect;
@ -18,22 +18,18 @@ namespace Jellyfin.Api.Controllers
public class QuickConnectController : BaseJellyfinApiController public class QuickConnectController : BaseJellyfinApiController
{ {
private readonly IQuickConnect _quickConnect; private readonly IQuickConnect _quickConnect;
private readonly IUserManager _userManager;
private readonly IAuthorizationContext _authContext; private readonly IAuthorizationContext _authContext;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="QuickConnectController"/> class. /// Initializes a new instance of the <see cref="QuickConnectController"/> class.
/// </summary> /// </summary>
/// <param name="quickConnect">Instance of the <see cref="IQuickConnect"/> interface.</param> /// <param name="quickConnect">Instance of the <see cref="IQuickConnect"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
public QuickConnectController( public QuickConnectController(
IQuickConnect quickConnect, IQuickConnect quickConnect,
IUserManager userManager,
IAuthorizationContext authContext) IAuthorizationContext authContext)
{ {
_quickConnect = quickConnect; _quickConnect = quickConnect;
_userManager = userManager;
_authContext = authContext; _authContext = authContext;
} }
@ -53,15 +49,14 @@ namespace Jellyfin.Api.Controllers
/// <summary> /// <summary>
/// Initiate a new quick connect request. /// Initiate a new quick connect request.
/// </summary> /// </summary>
/// <param name="friendlyName">Device friendly name.</param>
/// <response code="200">Quick connect request successfully created.</response> /// <response code="200">Quick connect request successfully created.</response>
/// <response code="401">Quick connect is not active on this server.</response> /// <response code="401">Quick connect is not active on this server.</response>
/// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns> /// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns>
[HttpGet("Initiate")] [HttpGet("Initiate")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QuickConnectResult> Initiate([FromQuery] string? friendlyName) public ActionResult<QuickConnectResult> Initiate()
{ {
return _quickConnect.TryConnect(friendlyName); return _quickConnect.TryConnect();
} }
/// <summary> /// <summary>
@ -74,12 +69,11 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Connect")] [HttpGet("Connect")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QuickConnectResult> Connect([FromQuery] string? secret) public ActionResult<QuickConnectResult> Connect([FromQuery, Required] string secret)
{ {
try try
{ {
var result = _quickConnect.CheckRequestStatus(secret); return _quickConnect.CheckRequestStatus(secret);
return result;
} }
catch (ResourceNotFoundException) catch (ResourceNotFoundException)
{ {
@ -117,9 +111,9 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Available")] [HttpPost("Available")]
[Authorize(Policy = Policies.RequiresElevation)] [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult Available([FromQuery] QuickConnectState? status) public ActionResult Available([FromQuery] QuickConnectState status = QuickConnectState.Available)
{ {
_quickConnect.SetState(status ?? QuickConnectState.Available); _quickConnect.SetState(status);
return NoContent(); return NoContent();
} }
@ -127,16 +121,22 @@ namespace Jellyfin.Api.Controllers
/// Authorizes a pending quick connect request. /// Authorizes a pending quick connect request.
/// </summary> /// </summary>
/// <param name="code">Quick connect code to authorize.</param> /// <param name="code">Quick connect code to authorize.</param>
/// <param name="userId">User id.</param>
/// <response code="200">Quick connect result authorized successfully.</response> /// <response code="200">Quick connect result authorized successfully.</response>
/// <response code="400">Missing quick connect code.</response> /// <response code="403">User is not allowed to authorize quick connect requests.</response>
/// <returns>Boolean indicating if the authorization was successful.</returns> /// <returns>Boolean indicating if the authorization was successful.</returns>
[HttpPost("Authorize")] [HttpPost("Authorize")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult<bool> Authorize([FromQuery, Required] string? code) public ActionResult<bool> Authorize([FromQuery, Required] string code, [FromQuery, Required] Guid userId)
{ {
return _quickConnect.AuthorizeRequest(Request, code); if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{
return Forbid("User is not allowed to authorize quick connect requests.");
}
return _quickConnect.AuthorizeRequest(userId, code);
} }
/// <summary> /// <summary>

View File

@ -239,11 +239,9 @@ namespace Jellyfin.Api.Controllers
DeviceName = auth.Device, DeviceName = auth.Device,
}; };
var result = await _sessionManager.AuthenticateQuickConnect( return await _sessionManager.AuthenticateQuickConnect(
authRequest, authRequest,
request.Token).ConfigureAwait(false); request.Token).ConfigureAwait(false);
return result;
} }
catch (SecurityException e) catch (SecurityException e)
{ {

View File

@ -1,6 +1,5 @@
using System; using System;
using MediaBrowser.Model.QuickConnect; using MediaBrowser.Model.QuickConnect;
using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.QuickConnect namespace MediaBrowser.Controller.QuickConnect
{ {
@ -15,9 +14,9 @@ namespace MediaBrowser.Controller.QuickConnect
int CodeLength { get; set; } int CodeLength { get; set; }
/// <summary> /// <summary>
/// Gets or sets the string to prefix internal access tokens with. /// Gets or sets the name of internal access tokens.
/// </summary> /// </summary>
string TokenNamePrefix { get; set; } string TokenName { get; set; }
/// <summary> /// <summary>
/// Gets the current state of quick connect. /// Gets the current state of quick connect.
@ -48,9 +47,8 @@ namespace MediaBrowser.Controller.QuickConnect
/// <summary> /// <summary>
/// Initiates a new quick connect request. /// Initiates a new quick connect request.
/// </summary> /// </summary>
/// <param name="friendlyName">Friendly device name to display in the request UI.</param>
/// <returns>A quick connect result with tokens to proceed or throws an exception if not active.</returns> /// <returns>A quick connect result with tokens to proceed or throws an exception if not active.</returns>
QuickConnectResult TryConnect(string friendlyName); QuickConnectResult TryConnect();
/// <summary> /// <summary>
/// Checks the status of an individual request. /// Checks the status of an individual request.
@ -62,10 +60,10 @@ namespace MediaBrowser.Controller.QuickConnect
/// <summary> /// <summary>
/// Authorizes a quick connect request to connect as the calling user. /// Authorizes a quick connect request to connect as the calling user.
/// </summary> /// </summary>
/// <param name="request">HTTP request object.</param> /// <param name="userId">User id.</param>
/// <param name="code">Identifying code for the request.</param> /// <param name="code">Identifying code for the request.</param>
/// <returns>A boolean indicating if the authorization completed successfully.</returns> /// <returns>A boolean indicating if the authorization completed successfully.</returns>
bool AuthorizeRequest(HttpRequest request, string code); bool AuthorizeRequest(Guid userId, string code);
/// <summary> /// <summary>
/// Expire quick connect requests that are over the time limit. If <paramref name="expireAll"/> is true, all requests are unconditionally expired. /// Expire quick connect requests that are over the time limit. If <paramref name="expireAll"/> is true, all requests are unconditionally expired.

View File

@ -22,11 +22,6 @@ namespace MediaBrowser.Model.QuickConnect
/// </summary> /// </summary>
public string? Code { get; set; } public string? Code { get; set; }
/// <summary>
/// Gets or sets the device friendly name.
/// </summary>
public string? FriendlyName { get; set; }
/// <summary> /// <summary>
/// Gets or sets the private access token. /// Gets or sets the private access token.
/// </summary> /// </summary>