Migrate authentication db to EF Core

This commit is contained in:
Patrick Barron 2021-05-20 23:56:59 -04:00
parent b03f2353d8
commit a0c6f72762
38 changed files with 1172 additions and 716 deletions

View file

@ -36,7 +36,6 @@ using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.Plugins;
using Emby.Server.Implementations.QuickConnect;
using Emby.Server.Implementations.ScheduledTasks;
using Emby.Server.Implementations.Security;
using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SyncPlay;
@ -57,7 +56,6 @@ using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
@ -73,7 +71,6 @@ using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Controller.Subtitles;
@ -599,8 +596,6 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
ServiceCollection.AddSingleton<EncodingHelper>();
@ -657,8 +652,7 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
ServiceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
ServiceCollection.AddSingleton<ISessionContext, SessionContext>();
ServiceCollection.AddScoped<ISessionContext, SessionContext>();
ServiceCollection.AddSingleton<IAuthService, AuthService>();
ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
@ -687,8 +681,6 @@ namespace Emby.Server.Implementations
_mediaEncoder = Resolve<IMediaEncoder>();
_sessionManager = Resolve<ISessionManager>();
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
SetStaticProperties();
var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();

View file

@ -1,5 +1,6 @@
#pragma warning disable CS1591
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net;
@ -17,9 +18,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
_authorizationContext = authorizationContext;
}
public AuthorizationInfo Authenticate(HttpRequest request)
public async Task<AuthorizationInfo> Authenticate(HttpRequest request)
{
var auth = _authorizationContext.GetAuthorizationInfo(request);
var auth = await _authorizationContext.GetAuthorizationInfo(request).ConfigureAwait(false);
if (!auth.HasToken)
{

View file

@ -24,12 +24,18 @@ namespace Emby.Server.Implementations.HttpServer.Security
_sessionManager = sessionManager;
}
public Task<SessionInfo> GetSession(HttpContext requestContext)
public async Task<SessionInfo> GetSession(HttpContext requestContext)
{
var authorization = _authContext.GetAuthorizationInfo(requestContext);
var authorization = await _authContext.GetAuthorizationInfo(requestContext).ConfigureAwait(false);
var user = authorization.User;
return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp().ToString(), user);
return await _sessionManager.LogSessionActivity(
authorization.Client,
authorization.Version,
authorization.DeviceId,
authorization.Device,
requestContext.GetNormalizedRemoteIp().ToString(),
user).ConfigureAwait(false);
}
public Task<SessionInfo> GetSession(object requestContext)

View file

@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.HttpServer
/// <inheritdoc />
public async Task WebSocketRequestHandler(HttpContext context)
{
_ = _authService.Authenticate(context.Request);
_ = await _authService.Authenticate(context.Request).ConfigureAwait(false);
try
{
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);

View file

@ -1,407 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Emby.Server.Implementations.Data;
using Jellyfin.Data.Entities.Security;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Security
{
public class AuthenticationRepository : BaseSqliteRepository, IAuthenticationRepository
{
public AuthenticationRepository(ILogger<AuthenticationRepository> logger, IServerConfigurationManager config)
: base(logger)
{
DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "authentication.db");
}
public void Initialize()
{
string[] queries =
{
"create table if not exists Tokens (Id INTEGER PRIMARY KEY, AccessToken TEXT NOT NULL, DeviceId TEXT NOT NULL, AppName TEXT NOT NULL, AppVersion TEXT NOT NULL, DeviceName TEXT NOT NULL, UserId TEXT, UserName TEXT, IsActive BIT NOT NULL, DateCreated DATETIME NOT NULL, DateLastActivity DATETIME NOT NULL)",
"create table if not exists Devices (Id TEXT NOT NULL PRIMARY KEY, CustomName TEXT, Capabilities TEXT)",
"drop index if exists idx_AccessTokens",
"drop index if exists Tokens1",
"drop index if exists Tokens2",
"create index if not exists Tokens3 on Tokens (AccessToken, DateLastActivity)",
"create index if not exists Tokens4 on Tokens (Id, DateLastActivity)",
"create index if not exists Devices1 on Devices (Id)"
};
using (var connection = GetConnection())
{
var tableNewlyCreated = !TableExists(connection, "Tokens");
connection.RunQueries(queries);
TryMigrate(connection, tableNewlyCreated);
}
}
private void TryMigrate(ManagedConnection connection, bool tableNewlyCreated)
{
try
{
if (tableNewlyCreated && TableExists(connection, "AccessTokens"))
{
connection.RunInTransaction(
db =>
{
var existingColumnNames = GetColumnNames(db, "AccessTokens");
AddColumn(db, "AccessTokens", "UserName", "TEXT", existingColumnNames);
AddColumn(db, "AccessTokens", "DateLastActivity", "DATETIME", existingColumnNames);
AddColumn(db, "AccessTokens", "AppVersion", "TEXT", existingColumnNames);
}, TransactionMode);
connection.RunQueries(new[]
{
"update accesstokens set DateLastActivity=DateCreated where DateLastActivity is null",
"update accesstokens set DeviceName='Unknown' where DeviceName is null",
"update accesstokens set AppName='Unknown' where AppName is null",
"update accesstokens set AppVersion='1' where AppVersion is null",
"INSERT INTO Tokens (AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity) SELECT AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity FROM AccessTokens where deviceid not null and devicename not null and appname not null and isactive=1"
});
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Error migrating authentication database");
}
}
public void Create(AuthenticationInfo info)
{
if (info == null)
{
throw new ArgumentNullException(nameof(info));
}
using (var connection = GetConnection())
{
connection.RunInTransaction(
db =>
{
using (var statement = db.PrepareStatement("insert into Tokens (AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity) values (@AccessToken, @DeviceId, @AppName, @AppVersion, @DeviceName, @UserId, @UserName, @IsActive, @DateCreated, @DateLastActivity)"))
{
statement.TryBind("@AccessToken", info.AccessToken);
statement.TryBind("@DeviceId", info.DeviceId);
statement.TryBind("@AppName", info.AppName);
statement.TryBind("@AppVersion", info.AppVersion);
statement.TryBind("@DeviceName", info.DeviceName);
statement.TryBind("@UserId", info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N", CultureInfo.InvariantCulture));
statement.TryBind("@UserName", info.UserName);
statement.TryBind("@IsActive", true);
statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue());
statement.TryBind("@DateLastActivity", info.DateLastActivity.ToDateTimeParamValue());
statement.MoveNext();
}
}, TransactionMode);
}
}
public void Update(AuthenticationInfo info)
{
if (info == null)
{
throw new ArgumentNullException(nameof(info));
}
using (var connection = GetConnection())
{
connection.RunInTransaction(
db =>
{
using (var statement = db.PrepareStatement("Update Tokens set AccessToken=@AccessToken, DeviceId=@DeviceId, AppName=@AppName, AppVersion=@AppVersion, DeviceName=@DeviceName, UserId=@UserId, UserName=@UserName, DateCreated=@DateCreated, DateLastActivity=@DateLastActivity where Id=@Id"))
{
statement.TryBind("@Id", info.Id);
statement.TryBind("@AccessToken", info.AccessToken);
statement.TryBind("@DeviceId", info.DeviceId);
statement.TryBind("@AppName", info.AppName);
statement.TryBind("@AppVersion", info.AppVersion);
statement.TryBind("@DeviceName", info.DeviceName);
statement.TryBind("@UserId", info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N", CultureInfo.InvariantCulture));
statement.TryBind("@UserName", info.UserName);
statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue());
statement.TryBind("@DateLastActivity", info.DateLastActivity.ToDateTimeParamValue());
statement.MoveNext();
}
}, TransactionMode);
}
}
public void Delete(AuthenticationInfo info)
{
if (info == null)
{
throw new ArgumentNullException(nameof(info));
}
using (var connection = GetConnection())
{
connection.RunInTransaction(
db =>
{
using (var statement = db.PrepareStatement("Delete from Tokens where Id=@Id"))
{
statement.TryBind("@Id", info.Id);
statement.MoveNext();
}
}, TransactionMode);
}
}
private const string BaseSelectText = "select Tokens.Id, AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, DateCreated, DateLastActivity, Devices.CustomName from Tokens left join Devices on Tokens.DeviceId=Devices.Id";
private static void BindAuthenticationQueryParams(AuthenticationInfoQuery query, IStatement statement)
{
if (!string.IsNullOrEmpty(query.AccessToken))
{
statement.TryBind("@AccessToken", query.AccessToken);
}
if (!query.UserId.Equals(Guid.Empty))
{
statement.TryBind("@UserId", query.UserId.ToString("N", CultureInfo.InvariantCulture));
}
if (!string.IsNullOrEmpty(query.DeviceId))
{
statement.TryBind("@DeviceId", query.DeviceId);
}
}
public QueryResult<AuthenticationInfo> Get(AuthenticationInfoQuery query)
{
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
var commandText = BaseSelectText;
var whereClauses = new List<string>();
if (!string.IsNullOrEmpty(query.AccessToken))
{
whereClauses.Add("AccessToken=@AccessToken");
}
if (!string.IsNullOrEmpty(query.DeviceId))
{
whereClauses.Add("DeviceId=@DeviceId");
}
if (!query.UserId.Equals(Guid.Empty))
{
whereClauses.Add("UserId=@UserId");
}
if (query.HasUser.HasValue)
{
if (query.HasUser.Value)
{
whereClauses.Add("UserId not null");
}
else
{
whereClauses.Add("UserId is null");
}
}
var whereTextWithoutPaging = whereClauses.Count == 0 ?
string.Empty :
" where " + string.Join(" AND ", whereClauses.ToArray());
commandText += whereTextWithoutPaging;
commandText += " ORDER BY DateLastActivity desc";
if (query.Limit.HasValue || query.StartIndex.HasValue)
{
var offset = query.StartIndex ?? 0;
if (query.Limit.HasValue || offset > 0)
{
commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
}
if (offset > 0)
{
commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
}
}
var statementTexts = new[]
{
commandText,
"select count (Id) from Tokens" + whereTextWithoutPaging
};
var list = new List<AuthenticationInfo>();
var result = new QueryResult<AuthenticationInfo>();
using (var connection = GetConnection(true))
{
connection.RunInTransaction(
db =>
{
var statements = PrepareAll(db, statementTexts);
using (var statement = statements[0])
{
BindAuthenticationQueryParams(query, statement);
foreach (var row in statement.ExecuteQuery())
{
list.Add(Get(row));
}
using (var totalCountStatement = statements[1])
{
BindAuthenticationQueryParams(query, totalCountStatement);
result.TotalRecordCount = totalCountStatement.ExecuteQuery()
.SelectScalarInt()
.First();
}
}
},
ReadTransactionMode);
}
result.Items = list;
return result;
}
private static AuthenticationInfo Get(IReadOnlyList<IResultSetValue> reader)
{
var info = new AuthenticationInfo
{
Id = reader[0].ToInt64(),
AccessToken = reader[1].ToString()
};
if (reader[2].SQLiteType != SQLiteType.Null)
{
info.DeviceId = reader[2].ToString();
}
if (reader[3].SQLiteType != SQLiteType.Null)
{
info.AppName = reader[3].ToString();
}
if (reader[4].SQLiteType != SQLiteType.Null)
{
info.AppVersion = reader[4].ToString();
}
if (reader[5].SQLiteType != SQLiteType.Null)
{
info.DeviceName = reader[5].ToString();
}
if (reader[6].SQLiteType != SQLiteType.Null)
{
info.UserId = new Guid(reader[6].ToString());
}
if (reader[7].SQLiteType != SQLiteType.Null)
{
info.UserName = reader[7].ToString();
}
info.DateCreated = reader[8].ReadDateTime();
if (reader[9].SQLiteType != SQLiteType.Null)
{
info.DateLastActivity = reader[9].ReadDateTime();
}
else
{
info.DateLastActivity = info.DateCreated;
}
if (reader[10].SQLiteType != SQLiteType.Null)
{
info.DeviceName = reader[10].ToString();
}
return info;
}
public DeviceOptions GetDeviceOptions(string deviceId)
{
using (var connection = GetConnection(true))
{
return connection.RunInTransaction(
db =>
{
using (var statement = base.PrepareStatement(db, "select CustomName from Devices where Id=@DeviceId"))
{
statement.TryBind("@DeviceId", deviceId);
var result = new DeviceOptions(deviceId);
foreach (var row in statement.ExecuteQuery())
{
if (row[0].SQLiteType != SQLiteType.Null)
{
result.CustomName = row[0].ToString();
}
}
return result;
}
}, ReadTransactionMode);
}
}
public void UpdateDeviceOptions(string deviceId, DeviceOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
using (var connection = GetConnection())
{
connection.RunInTransaction(
db =>
{
using (var statement = db.PrepareStatement("replace into devices (Id, CustomName, Capabilities) VALUES (@Id, @CustomName, (Select Capabilities from Devices where Id=@Id))"))
{
statement.TryBind("@Id", deviceId);
if (string.IsNullOrWhiteSpace(options.CustomName))
{
statement.TryBindNull("@CustomName");
}
else
{
statement.TryBind("@CustomName", options.CustomName);
}
statement.MoveNext();
}
}, TransactionMode);
}
}
}
}

View file

@ -11,6 +11,7 @@ using Jellyfin.Data.Entities;
using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using Jellyfin.Data.Queries;
using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
@ -23,7 +24,6 @@ using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Events.Session;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@ -52,7 +52,6 @@ namespace Emby.Server.Implementations.Session
private readonly IImageProcessor _imageProcessor;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerApplicationHost _appHost;
private readonly IAuthenticationRepository _authRepo;
private readonly IDeviceManager _deviceManager;
/// <summary>
@ -75,7 +74,6 @@ namespace Emby.Server.Implementations.Session
IDtoService dtoService,
IImageProcessor imageProcessor,
IServerApplicationHost appHost,
IAuthenticationRepository authRepo,
IDeviceManager deviceManager,
IMediaSourceManager mediaSourceManager)
{
@ -88,7 +86,6 @@ namespace Emby.Server.Implementations.Session
_dtoService = dtoService;
_imageProcessor = imageProcessor;
_appHost = appHost;
_authRepo = authRepo;
_deviceManager = deviceManager;
_mediaSourceManager = mediaSourceManager;
@ -1486,7 +1483,7 @@ namespace Emby.Server.Implementations.Session
throw new SecurityException("User is at their maximum number of sessions.");
}
var token = GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName);
var token = await GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName).ConfigureAwait(false);
var session = await LogSessionActivity(
request.App,
@ -1509,21 +1506,21 @@ namespace Emby.Server.Implementations.Session
return returnResult;
}
private string GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName)
private async Task<string> GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName)
{
var existing = _authRepo.Get(
new AuthenticationInfoQuery
var existing = (await _deviceManager.GetDevices(
new DeviceQuery
{
DeviceId = deviceId,
UserId = user.Id,
Limit = 1
}).Items.FirstOrDefault();
}).ConfigureAwait(false)).Items.FirstOrDefault();
var allExistingForDevice = _authRepo.Get(
new AuthenticationInfoQuery
var allExistingForDevice = (await _deviceManager.GetDevices(
new DeviceQuery
{
DeviceId = deviceId
}).Items;
}).ConfigureAwait(false)).Items;
foreach (var auth in allExistingForDevice)
{
@ -1531,7 +1528,7 @@ namespace Emby.Server.Implementations.Session
{
try
{
Logout(auth);
await Logout(auth).ConfigureAwait(false);
}
catch (Exception ex)
{
@ -1546,29 +1543,14 @@ namespace Emby.Server.Implementations.Session
return existing.AccessToken;
}
var now = DateTime.UtcNow;
var newToken = new AuthenticationInfo
{
AppName = app,
AppVersion = appVersion,
DateCreated = now,
DateLastActivity = now,
DeviceId = deviceId,
DeviceName = deviceName,
UserId = user.Id,
AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
UserName = user.Username
};
_logger.LogInformation("Creating new access token for user {0}", user.Id);
_authRepo.Create(newToken);
var device = await _deviceManager.CreateDevice(new Device(user.Id, app, appVersion, deviceName, deviceId)).ConfigureAwait(false);
return newToken.AccessToken;
return device.AccessToken;
}
/// <inheritdoc />
public void Logout(string accessToken)
public async Task Logout(string accessToken)
{
CheckDisposed();
@ -1577,27 +1559,27 @@ namespace Emby.Server.Implementations.Session
throw new ArgumentNullException(nameof(accessToken));
}
var existing = _authRepo.Get(
new AuthenticationInfoQuery
var existing = (await _deviceManager.GetDevices(
new DeviceQuery
{
Limit = 1,
AccessToken = accessToken
}).Items;
}).ConfigureAwait(false)).Items;
if (existing.Count > 0)
{
Logout(existing[0]);
await Logout(existing[0]).ConfigureAwait(false);
}
}
/// <inheritdoc />
public void Logout(AuthenticationInfo existing)
public async Task Logout(Device existing)
{
CheckDisposed();
_logger.LogInformation("Logging out access token {0}", existing.AccessToken);
_authRepo.Delete(existing);
await _deviceManager.DeleteDevice(existing).ConfigureAwait(false);
var sessions = Sessions
.Where(i => string.Equals(i.DeviceId, existing.DeviceId, StringComparison.OrdinalIgnoreCase))
@ -1617,30 +1599,24 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
public void RevokeUserTokens(Guid userId, string currentAccessToken)
public async Task RevokeUserTokens(Guid userId, string currentAccessToken)
{
CheckDisposed();
var existing = _authRepo.Get(new AuthenticationInfoQuery
var existing = await _deviceManager.GetDevices(new DeviceQuery
{
UserId = userId
});
}).ConfigureAwait(false);
foreach (var info in existing.Items)
{
if (!string.Equals(currentAccessToken, info.AccessToken, StringComparison.OrdinalIgnoreCase))
{
Logout(info);
await Logout(info).ConfigureAwait(false);
}
}
}
/// <inheritdoc />
public void RevokeToken(string token)
{
Logout(token);
}
/// <summary>
/// Reports the capabilities.
/// </summary>
@ -1792,7 +1768,7 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
public Task<SessionInfo> GetSessionByAuthenticationToken(AuthenticationInfo info, string deviceId, string remoteEndpoint, string appVersion)
public Task<SessionInfo> GetSessionByAuthenticationToken(Device info, string deviceId, string remoteEndpoint, string appVersion)
{
if (info == null)
{
@ -1825,20 +1801,20 @@ namespace Emby.Server.Implementations.Session
}
/// <inheritdoc />
public Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint)
public async Task<SessionInfo> GetSessionByAuthenticationToken(string token, string deviceId, string remoteEndpoint)
{
var items = _authRepo.Get(new AuthenticationInfoQuery
var items = (await _deviceManager.GetDevices(new DeviceQuery
{
AccessToken = token,
Limit = 1
}).Items;
}).ConfigureAwait(false)).Items;
if (items.Count == 0)
{
return null;
}
return GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null);
return await GetSessionByAuthenticationToken(items[0], deviceId, remoteEndpoint, null).ConfigureAwait(false);
}
/// <inheritdoc />

View file

@ -40,11 +40,11 @@ namespace Jellyfin.Api.Auth
}
/// <inheritdoc />
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
try
{
var authorizationInfo = _authService.Authenticate(Request);
var authorizationInfo = await _authService.Authenticate(Request).ConfigureAwait(false);
var role = UserRoles.User;
if (authorizationInfo.IsApiKey || authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
{
@ -68,16 +68,16 @@ namespace Jellyfin.Api.Auth
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(ticket));
return AuthenticateResult.Success(ticket);
}
catch (AuthenticationException ex)
{
_logger.LogDebug(ex, "Error authenticating with {Handler}", nameof(CustomAuthenticationHandler));
return Task.FromResult(AuthenticateResult.NoResult());
return AuthenticateResult.NoResult();
}
catch (SecurityException ex)
{
return Task.FromResult(AuthenticateResult.Fail(ex));
return AuthenticateResult.Fail(ex);
}
}
}

View file

@ -58,7 +58,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] Guid? parentId,
[FromQuery] bool isLocked = false)
{
var userId = _authContext.GetAuthorizationInfo(Request).UserId;
var userId = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).UserId;
var item = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
{

View file

@ -3,8 +3,8 @@ using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Queries;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Querying;
@ -21,22 +21,18 @@ namespace Jellyfin.Api.Controllers
public class DevicesController : BaseJellyfinApiController
{
private readonly IDeviceManager _deviceManager;
private readonly IAuthenticationRepository _authenticationRepository;
private readonly ISessionManager _sessionManager;
/// <summary>
/// Initializes a new instance of the <see cref="DevicesController"/> class.
/// </summary>
/// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
/// <param name="authenticationRepository">Instance of <see cref="IAuthenticationRepository"/> interface.</param>
/// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
public DevicesController(
IDeviceManager deviceManager,
IAuthenticationRepository authenticationRepository,
ISessionManager sessionManager)
{
_deviceManager = deviceManager;
_authenticationRepository = authenticationRepository;
_sessionManager = sessionManager;
}
@ -111,7 +107,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery, Required] string id,
[FromBody, Required] DeviceOptions deviceOptions)
{
var existingDeviceOptions = _deviceManager.GetDeviceOptions(id);
var existingDeviceOptions = await _deviceManager.GetDeviceOptions(id).ConfigureAwait(false);
if (existingDeviceOptions == null)
{
return NotFound();
@ -131,19 +127,19 @@ namespace Jellyfin.Api.Controllers
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult DeleteDevice([FromQuery, Required] string id)
public async Task<ActionResult> DeleteDevice([FromQuery, Required] string id)
{
var existingDevice = _deviceManager.GetDevice(id);
var existingDevice = await _deviceManager.GetDevice(id).ConfigureAwait(false);
if (existingDevice == null)
{
return NotFound();
}
var sessions = _authenticationRepository.Get(new AuthenticationInfoQuery { DeviceId = id }).Items;
var sessions = await _deviceManager.GetDevices(new DeviceQuery { DeviceId = id }).ConfigureAwait(false);
foreach (var session in sessions)
foreach (var session in sessions.Items)
{
_sessionManager.Logout(session);
await _sessionManager.Logout(session).ConfigureAwait(false);
}
return NoContent();

View file

@ -97,7 +97,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] ImageType imageType,
[FromQuery] int? index = null)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
}
@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] ImageType imageType,
[FromRoute] int index)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
}
@ -190,7 +190,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] ImageType imageType,
[FromQuery] int? index = null)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
}
@ -234,7 +234,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] ImageType imageType,
[FromRoute] int index)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
}

View file

@ -331,10 +331,10 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public ActionResult DeleteItem(Guid itemId)
public async Task<ActionResult> DeleteItem(Guid itemId)
{
var item = _libraryManager.GetItemById(itemId);
var auth = _authContext.GetAuthorizationInfo(Request);
var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
var user = auth.User;
if (!item.CanDelete(user))
@ -361,7 +361,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
public async Task<ActionResult> DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
{
if (ids.Length == 0)
{
@ -371,7 +371,7 @@ namespace Jellyfin.Api.Controllers
foreach (var i in ids)
{
var item = _libraryManager.GetItemById(i);
var auth = _authContext.GetAuthorizationInfo(Request);
var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
var user = auth.User;
if (!item.CanDelete(user))
@ -627,7 +627,7 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
var auth = _authContext.GetAuthorizationInfo(Request);
var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
var user = auth.User;

View file

@ -123,7 +123,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery, ParameterObsolete] bool? allowAudioStreamCopy,
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] PlaybackInfoDto? playbackInfoDto)
{
var authInfo = _authContext.GetAuthorizationInfo(Request);
var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
var profile = playbackInfoDto?.DeviceProfile;
_logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile);

View file

@ -171,7 +171,8 @@ namespace Jellyfin.Api.Controllers
_logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId))
{
await _transcodingJobHelper.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
await _transcodingJobHelper.KillTranscodingJobs(authInfo.DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
}
playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);
@ -320,7 +321,8 @@ namespace Jellyfin.Api.Controllers
_logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId))
{
await _transcodingJobHelper.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
var authInfo = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
await _transcodingJobHelper.KillTranscodingJobs(authInfo.DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
}
playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _authContext, Request).ConfigureAwait(false);

View file

@ -471,11 +471,11 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Sessions/Logout")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult ReportSessionEnded()
public async Task<ActionResult> ReportSessionEnded()
{
AuthorizationInfo auth = _authContext.GetAuthorizationInfo(Request);
AuthorizationInfo auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
_sessionManager.Logout(auth.Token);
await _sessionManager.Logout(auth.Token).ConfigureAwait(false);
return NoContent();
}

View file

@ -361,7 +361,7 @@ namespace Jellyfin.Api.Controllers
long positionTicks = 0;
var accessToken = _authContext.GetAuthorizationInfo(Request).Token;
var accessToken = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Token;
while (positionTicks < runtime)
{

View file

@ -116,9 +116,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool enableRedirection = true)
{
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
_authorizationContext.GetAuthorizationInfo(Request).DeviceId = deviceId;
(await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).DeviceId = deviceId;
var authInfo = _authorizationContext.GetAuthorizationInfo(Request);
var authInfo = await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
_logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);

View file

@ -77,11 +77,11 @@ namespace Jellyfin.Api.Controllers
[HttpGet]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<UserDto>> GetUsers(
public async Task<ActionResult<IEnumerable<UserDto>>> GetUsers(
[FromQuery] bool? isHidden,
[FromQuery] bool? isDisabled)
{
var users = Get(isHidden, isDisabled, false, false);
var users = await Get(isHidden, isDisabled, false, false).ConfigureAwait(false);
return Ok(users);
}
@ -92,15 +92,15 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="IEnumerable{UserDto}"/> containing the public users.</returns>
[HttpGet("Public")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<UserDto>> GetPublicUsers()
public async Task<ActionResult<IEnumerable<UserDto>>> GetPublicUsers()
{
// If the startup wizard hasn't been completed then just return all users
if (!_config.Configuration.IsStartupWizardCompleted)
{
return Ok(Get(false, false, false, false));
return Ok(await Get(false, false, false, false).ConfigureAwait(false));
}
return Ok(Get(false, false, true, true));
return Ok(await Get(false, false, true, true).ConfigureAwait(false));
}
/// <summary>
@ -141,7 +141,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> DeleteUser([FromRoute, Required] Guid userId)
{
var user = _userManager.GetUserById(userId);
_sessionManager.RevokeUserTokens(user.Id, null);
await _sessionManager.RevokeUserTokens(user.Id, null).ConfigureAwait(false);
await _userManager.DeleteUserAsync(userId).ConfigureAwait(false);
return NoContent();
}
@ -195,7 +195,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<AuthenticationResult>> AuthenticateUserByName([FromBody, Required] AuthenticateUserByName request)
{
var auth = _authContext.GetAuthorizationInfo(Request);
var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
try
{
@ -230,7 +230,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<AuthenticationResult>> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request)
{
var auth = _authContext.GetAuthorizationInfo(Request);
var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
try
{
@ -271,7 +271,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid userId,
[FromBody, Required] UpdateUserPassword request)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the password.");
}
@ -303,9 +303,9 @@ namespace Jellyfin.Api.Controllers
await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
var currentToken = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Token;
_sessionManager.RevokeUserTokens(user.Id, currentToken);
await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false);
}
return NoContent();
@ -325,11 +325,11 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateUserEasyPassword(
public async Task<ActionResult> UpdateUserEasyPassword(
[FromRoute, Required] Guid userId,
[FromBody, Required] UpdateUserEasyPassword request)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true).ConfigureAwait(false))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the easy password.");
}
@ -343,11 +343,11 @@ namespace Jellyfin.Api.Controllers
if (request.ResetPassword)
{
_userManager.ResetEasyPassword(user);
await _userManager.ResetEasyPassword(user).ConfigureAwait(false);
}
else
{
_userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword);
await _userManager.ChangeEasyPassword(user, request.NewPw, request.NewPassword).ConfigureAwait(false);
}
return NoContent();
@ -371,7 +371,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid userId,
[FromBody, Required] UserDto updateUser)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false).ConfigureAwait(false))
{
return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed.");
}
@ -431,8 +431,8 @@ namespace Jellyfin.Api.Controllers
return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one enabled user in the system.");
}
var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
_sessionManager.RevokeUserTokens(user.Id, currentToken);
var currentToken = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Token;
await _sessionManager.RevokeUserTokens(user.Id, currentToken).ConfigureAwait(false);
}
await _userManager.UpdatePolicyAsync(userId, newPolicy).ConfigureAwait(false);
@ -456,7 +456,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid userId,
[FromBody, Required] UserConfiguration userConfig)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
if (!await RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false).ConfigureAwait(false))
{
return StatusCode(StatusCodes.Status403Forbidden, "User configuration update not allowed");
}
@ -555,7 +555,7 @@ namespace Jellyfin.Api.Controllers
return _userManager.GetUserDto(user);
}
private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork)
private async Task<IEnumerable<UserDto>> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork)
{
var users = _userManager.Users;
@ -571,7 +571,7 @@ namespace Jellyfin.Api.Controllers
if (filterByDevice)
{
var deviceId = _authContext.GetAuthorizationInfo(Request).DeviceId;
var deviceId = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).DeviceId;
if (!string.IsNullOrWhiteSpace(deviceId))
{

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.UserViewDtos;
@ -64,7 +65,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="OkResult"/> containing the user views.</returns>
[HttpGet("Users/{userId}/Views")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetUserViews(
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetUserViews(
[FromRoute, Required] Guid userId,
[FromQuery] bool? includeExternalContent,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews,
@ -86,7 +87,7 @@ namespace Jellyfin.Api.Controllers
query.PresetViews = presetViews;
}
var app = _authContext.GetAuthorizationInfo(Request).Client ?? string.Empty;
var app = (await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).Client ?? string.Empty;
if (app.IndexOf("emby rt", StringComparison.OrdinalIgnoreCase) != -1)
{
query.PresetViews = new[] { CollectionType.Movies, CollectionType.TvShows };

View file

@ -468,7 +468,7 @@ namespace Jellyfin.Api.Helpers
/// <returns>A <see cref="Task"/> containing the <see cref="LiveStreamResponse"/>.</returns>
public async Task<LiveStreamResponse> OpenMediaSource(HttpRequest httpRequest, LiveStreamRequest request)
{
var authInfo = _authContext.GetAuthorizationInfo(httpRequest);
var authInfo = await _authContext.GetAuthorizationInfo(httpRequest).ConfigureAwait(false);
var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);

View file

@ -60,9 +60,9 @@ namespace Jellyfin.Api.Helpers
/// <param name="userId">The user id.</param>
/// <param name="restrictUserPreferences">Whether to restrict the user preferences.</param>
/// <returns>A <see cref="bool"/> whether the user can update the entry.</returns>
internal static bool AssertCanUpdateUser(IAuthorizationContext authContext, HttpRequest requestContext, Guid userId, bool restrictUserPreferences)
internal static async Task<bool> AssertCanUpdateUser(IAuthorizationContext authContext, HttpRequest requestContext, Guid userId, bool restrictUserPreferences)
{
var auth = authContext.GetAuthorizationInfo(requestContext);
var auth = await authContext.GetAuthorizationInfo(requestContext).ConfigureAwait(false);
var authenticatedUser = auth.User;
@ -78,7 +78,7 @@ namespace Jellyfin.Api.Helpers
internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IAuthorizationContext authContext, HttpRequest request)
{
var authorization = authContext.GetAuthorizationInfo(request);
var authorization = await authContext.GetAuthorizationInfo(request).ConfigureAwait(false);
var user = authorization.User;
var session = await sessionManager.LogSessionActivity(
authorization.Client,

View file

@ -17,9 +17,7 @@ using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
@ -101,7 +99,7 @@ namespace Jellyfin.Api.Helpers
EnableDlnaHeaders = enableDlnaHeaders
};
var auth = authorizationContext.GetAuthorizationInfo(httpRequest);
var auth = await authorizationContext.GetAuthorizationInfo(httpRequest).ConfigureAwait(false);
if (!auth.UserId.Equals(Guid.Empty))
{
state.User = userManager.GetUserById(auth.UserId);

View file

@ -489,7 +489,7 @@ namespace Jellyfin.Api.Helpers
if (state.VideoRequest != null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
{
var auth = _authorizationContext.GetAuthorizationInfo(request);
var auth = await _authorizationContext.GetAuthorizationInfo(request).ConfigureAwait(false);
if (auth.User != null && !auth.User.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
{
this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);

View file

@ -6,6 +6,7 @@ using Jellyfin.Data.Entities;
using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using Jellyfin.Data.Queries;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Devices;
@ -55,6 +56,17 @@ namespace Jellyfin.Server.Implementations.Devices
DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, options)));
}
/// <inheritdoc />
public async Task<Device> CreateDevice(Device device)
{
await using var dbContext = _dbProvider.CreateContext();
dbContext.Devices.Add(device);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
return device;
}
/// <inheritdoc />
public async Task<DeviceOptions?> GetDeviceOptions(string deviceId)
{
@ -90,6 +102,61 @@ namespace Jellyfin.Server.Implementations.Devices
return deviceInfo;
}
/// <inheritdoc />
public async Task<QueryResult<Device>> GetDevices(DeviceQuery query)
{
await using var dbContext = _dbProvider.CreateContext();
var devices = dbContext.Devices.AsQueryable();
if (query.UserId.HasValue)
{
devices = devices.Where(device => device.UserId == query.UserId.Value);
}
if (query.DeviceId != null)
{
devices = devices.Where(device => device.DeviceId == query.DeviceId);
}
if (query.AccessToken != null)
{
devices = devices.Where(device => device.AccessToken == query.AccessToken);
}
if (query.Skip.HasValue)
{
devices = devices.Skip(query.Skip.Value);
}
var count = await devices.CountAsync().ConfigureAwait(false);
if (query.Limit.HasValue)
{
devices = devices.Take(query.Limit.Value);
}
return new QueryResult<Device>
{
Items = await devices.ToListAsync().ConfigureAwait(false),
StartIndex = query.Skip ?? 0,
TotalRecordCount = count
};
}
/// <inheritdoc />
public async Task<QueryResult<DeviceInfo>> GetDeviceInfos(DeviceQuery query)
{
var devices = await GetDevices(query).ConfigureAwait(false);
return new QueryResult<DeviceInfo>
{
Items = devices.Items.Select(ToDeviceInfo).ToList(),
StartIndex = devices.StartIndex,
TotalRecordCount = devices.TotalRecordCount
};
}
/// <inheritdoc />
public async Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId, bool? supportsSync)
{
@ -117,6 +184,12 @@ namespace Jellyfin.Server.Implementations.Devices
return new QueryResult<DeviceInfo>(array);
}
/// <inheritdoc />
public async Task DeleteDevice(Device device)
{
await using var dbContext = _dbProvider.CreateContext();
}
/// <inheritdoc />
public bool CanAccessDevice(User user, string deviceId)
{

View file

@ -214,6 +214,15 @@ namespace Jellyfin.Server.Implementations
modelBuilder.Entity<Device>()
.HasIndex(entity => new { entity.DeviceId, entity.DateLastActivity });
modelBuilder.Entity<Device>()
.HasIndex(entity => new { entity.AccessToken, entity.DateLastActivity });
modelBuilder.Entity<Device>()
.HasIndex(entity => new { entity.UserId, entity.DeviceId });
modelBuilder.Entity<Device>()
.HasIndex(entity => entity.DeviceId);
modelBuilder.Entity<DeviceOptions>()
.HasIndex(entity => entity.DeviceId)
.IsUnique();

View file

@ -0,0 +1,645 @@
#pragma warning disable CS1591
// <auto-generated />
using System;
using Jellyfin.Server.Implementations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
[DbContext(typeof(JellyfinDb))]
[Migration("20210521032224_AddDevices")]
partial class AddDevices
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("jellyfin")
.HasAnnotation("ProductVersion", "5.0.5");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("DayOfWeek")
.HasColumnType("INTEGER");
b.Property<double>("EndHour")
.HasColumnType("REAL");
b.Property<double>("StartHour")
.HasColumnType("REAL");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AccessSchedules");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<string>("ItemId")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<int>("LogSeverity")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<string>("Overview")
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("ShortOverview")
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ActivityLogs");
});
modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Client")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId", "ItemId", "Client", "Key")
.IsUnique();
b.ToTable("CustomItemDisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ChromecastVersion")
.HasColumnType("INTEGER");
b.Property<string>("Client")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<string>("DashboardTheme")
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<bool>("EnableNextVideoInfoOverlay")
.HasColumnType("INTEGER");
b.Property<int?>("IndexBy")
.HasColumnType("INTEGER");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<int>("ScrollDirection")
.HasColumnType("INTEGER");
b.Property<bool>("ShowBackdrop")
.HasColumnType("INTEGER");
b.Property<bool>("ShowSidebar")
.HasColumnType("INTEGER");
b.Property<int>("SkipBackwardLength")
.HasColumnType("INTEGER");
b.Property<int>("SkipForwardLength")
.HasColumnType("INTEGER");
b.Property<string>("TvHome")
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId", "ItemId", "Client")
.IsUnique();
b.ToTable("DisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("DisplayPreferencesId")
.HasColumnType("INTEGER");
b.Property<int>("Order")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("DisplayPreferencesId");
b.ToTable("HomeSection");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<Guid?>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("ImageInfos");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Client")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<int?>("IndexBy")
.HasColumnType("INTEGER");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<bool>("RememberIndexing")
.HasColumnType("INTEGER");
b.Property<bool>("RememberSorting")
.HasColumnType("INTEGER");
b.Property<string>("SortBy")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.Property<int>("ViewType")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("ItemDisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Kind")
.HasColumnType("INTEGER");
b.Property<Guid?>("Permission_Permissions_Guid")
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<Guid?>("UserId")
.HasColumnType("TEXT");
b.Property<bool>("Value")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId", "Kind")
.IsUnique()
.HasFilter("[UserId] IS NOT NULL");
b.ToTable("Permissions");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Kind")
.HasColumnType("INTEGER");
b.Property<Guid?>("Preference_Preferences_Guid")
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<Guid?>("UserId")
.HasColumnType("TEXT");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(65535)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId", "Kind")
.IsUnique()
.HasFilter("[UserId] IS NOT NULL");
b.ToTable("Preferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<Guid>("AccessToken")
.HasColumnType("TEXT");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AccessToken")
.IsUnique();
b.ToTable("ApiKeys");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AccessToken")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("AppName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<string>("AppVersion")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<DateTime>("DateLastActivity")
.HasColumnType("TEXT");
b.Property<string>("DeviceId")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("DeviceName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DeviceId");
b.HasIndex("AccessToken", "DateLastActivity");
b.HasIndex("DeviceId", "DateLastActivity");
b.HasIndex("UserId", "DeviceId");
b.ToTable("Devices");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CustomName")
.HasColumnType("TEXT");
b.Property<string>("DeviceId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DeviceId")
.IsUnique();
b.ToTable("DeviceOptions");
});
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AudioLanguagePreference")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("AuthenticationProviderId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<bool>("DisplayCollectionsView")
.HasColumnType("INTEGER");
b.Property<bool>("DisplayMissingEpisodes")
.HasColumnType("INTEGER");
b.Property<string>("EasyPassword")
.HasMaxLength(65535)
.HasColumnType("TEXT");
b.Property<bool>("EnableAutoLogin")
.HasColumnType("INTEGER");
b.Property<bool>("EnableLocalPassword")
.HasColumnType("INTEGER");
b.Property<bool>("EnableNextEpisodeAutoPlay")
.HasColumnType("INTEGER");
b.Property<bool>("EnableUserPreferenceAccess")
.HasColumnType("INTEGER");
b.Property<bool>("HidePlayedInLatest")
.HasColumnType("INTEGER");
b.Property<long>("InternalId")
.HasColumnType("INTEGER");
b.Property<int>("InvalidLoginAttemptCount")
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastActivityDate")
.HasColumnType("TEXT");
b.Property<DateTime?>("LastLoginDate")
.HasColumnType("TEXT");
b.Property<int?>("LoginAttemptsBeforeLockout")
.HasColumnType("INTEGER");
b.Property<int>("MaxActiveSessions")
.HasColumnType("INTEGER");
b.Property<int?>("MaxParentalAgeRating")
.HasColumnType("INTEGER");
b.Property<bool>("MustUpdatePassword")
.HasColumnType("INTEGER");
b.Property<string>("Password")
.HasMaxLength(65535)
.HasColumnType("TEXT");
b.Property<string>("PasswordResetProviderId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<bool>("PlayDefaultAudioTrack")
.HasColumnType("INTEGER");
b.Property<bool>("RememberAudioSelections")
.HasColumnType("INTEGER");
b.Property<bool>("RememberSubtitleSelections")
.HasColumnType("INTEGER");
b.Property<int?>("RemoteClientBitrateLimit")
.HasColumnType("INTEGER");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("SubtitleLanguagePreference")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<int>("SubtitleMode")
.HasColumnType("INTEGER");
b.Property<int>("SyncPlayAccess")
.HasColumnType("INTEGER");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT")
.UseCollation("NOCASE");
b.HasKey("Id");
b.HasIndex("Username")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("AccessSchedules")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("DisplayPreferences")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
{
b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
.WithMany("HomeSections")
.HasForeignKey("DisplayPreferencesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithOne("ProfileImage")
.HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("ItemDisplayPreferences")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Permissions")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Preferences")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.Navigation("HomeSections");
});
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Navigation("AccessSchedules");
b.Navigation("DisplayPreferences");
b.Navigation("ItemDisplayPreferences");
b.Navigation("Permissions");
b.Navigation("Preferences");
b.Navigation("ProfileImage");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,126 @@
#pragma warning disable CS1591
#pragma warning disable SA1601
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Jellyfin.Server.Implementations.Migrations
{
public partial class AddDevices : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ApiKeys",
schema: "jellyfin",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
DateCreated = table.Column<DateTime>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
AccessToken = table.Column<Guid>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ApiKeys", x => x.Id);
});
migrationBuilder.CreateTable(
name: "DeviceOptions",
schema: "jellyfin",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
DeviceId = table.Column<string>(type: "TEXT", nullable: false),
CustomName = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_DeviceOptions", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Devices",
schema: "jellyfin",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UserId = table.Column<Guid>(type: "TEXT", nullable: false),
AccessToken = table.Column<string>(type: "TEXT", nullable: false),
AppName = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
AppVersion = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
DeviceName = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
DeviceId = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false),
IsActive = table.Column<bool>(type: "INTEGER", nullable: false),
DateCreated = table.Column<DateTime>(type: "TEXT", nullable: false),
DateLastActivity = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Devices", x => x.Id);
table.ForeignKey(
name: "FK_Devices_Users_UserId",
column: x => x.UserId,
principalSchema: "jellyfin",
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_ApiKeys_AccessToken",
schema: "jellyfin",
table: "ApiKeys",
column: "AccessToken",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_DeviceOptions_DeviceId",
schema: "jellyfin",
table: "DeviceOptions",
column: "DeviceId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Devices_AccessToken_DateLastActivity",
schema: "jellyfin",
table: "Devices",
columns: new[] { "AccessToken", "DateLastActivity" });
migrationBuilder.CreateIndex(
name: "IX_Devices_DeviceId",
schema: "jellyfin",
table: "Devices",
column: "DeviceId");
migrationBuilder.CreateIndex(
name: "IX_Devices_DeviceId_DateLastActivity",
schema: "jellyfin",
table: "Devices",
columns: new[] { "DeviceId", "DateLastActivity" });
migrationBuilder.CreateIndex(
name: "IX_Devices_UserId_DeviceId",
schema: "jellyfin",
table: "Devices",
columns: new[] { "UserId", "DeviceId" });
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ApiKeys",
schema: "jellyfin");
migrationBuilder.DropTable(
name: "DeviceOptions",
schema: "jellyfin");
migrationBuilder.DropTable(
name: "Devices",
schema: "jellyfin");
}
}
}

View file

@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("jellyfin")
.HasAnnotation("ProductVersion", "5.0.3");
.HasAnnotation("ProductVersion", "5.0.5");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@ -332,6 +332,107 @@ namespace Jellyfin.Server.Implementations.Migrations
b.ToTable("Preferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<Guid>("AccessToken")
.HasColumnType("TEXT");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AccessToken")
.IsUnique();
b.ToTable("ApiKeys");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AccessToken")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("AppName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<string>("AppVersion")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<DateTime>("DateLastActivity")
.HasColumnType("TEXT");
b.Property<string>("DeviceId")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("DeviceName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DeviceId");
b.HasIndex("AccessToken", "DateLastActivity");
b.HasIndex("DeviceId", "DateLastActivity");
b.HasIndex("UserId", "DeviceId");
b.ToTable("Devices");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CustomName")
.HasColumnType("TEXT");
b.Property<string>("DeviceId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DeviceId")
.IsUnique();
b.ToTable("DeviceOptions");
});
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Property<Guid>("Id")
@ -505,6 +606,17 @@ namespace Jellyfin.Server.Implementations.Migrations
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.Navigation("HomeSections");

View file

@ -4,39 +4,39 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer.Security
namespace Jellyfin.Server.Implementations.Security
{
public class AuthorizationContext : IAuthorizationContext
{
private readonly IAuthenticationRepository _authRepo;
private readonly JellyfinDb _jellyfinDb;
private readonly IUserManager _userManager;
public AuthorizationContext(IAuthenticationRepository authRepo, IUserManager userManager)
public AuthorizationContext(JellyfinDb jellyfinDb, IUserManager userManager)
{
_authRepo = authRepo;
_jellyfinDb = jellyfinDb;
_userManager = userManager;
}
public AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext)
public Task<AuthorizationInfo> GetAuthorizationInfo(HttpContext requestContext)
{
if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached))
if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached) && cached != null)
{
return (AuthorizationInfo)cached;
return Task.FromResult((AuthorizationInfo)cached);
}
return GetAuthorization(requestContext);
}
public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
public async Task<AuthorizationInfo> GetAuthorizationInfo(HttpRequest requestContext)
{
var auth = GetAuthorizationDictionary(requestContext);
var authInfo = GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
var authInfo = await GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query).ConfigureAwait(false);
return authInfo;
}
@ -45,35 +45,37 @@ namespace Emby.Server.Implementations.HttpServer.Security
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private AuthorizationInfo GetAuthorization(HttpContext httpReq)
private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpReq)
{
var auth = GetAuthorizationDictionary(httpReq);
var authInfo = GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
var authInfo = await GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query).ConfigureAwait(false);
httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
return authInfo;
}
private AuthorizationInfo GetAuthorizationInfoFromDictionary(
in Dictionary<string, string> auth,
in IHeaderDictionary headers,
in IQueryCollection queryString)
private async Task<AuthorizationInfo> GetAuthorizationInfoFromDictionary(
IReadOnlyDictionary<string, string>? auth,
IHeaderDictionary headers,
IQueryCollection queryString)
{
string deviceId = null;
string device = null;
string client = null;
string version = null;
string token = null;
string? deviceId = null;
string? deviceName = null;
string? client = null;
string? version = null;
string? token = null;
if (auth != null)
{
auth.TryGetValue("DeviceId", out deviceId);
auth.TryGetValue("Device", out device);
auth.TryGetValue("Device", out deviceName);
auth.TryGetValue("Client", out client);
auth.TryGetValue("Version", out version);
auth.TryGetValue("Token", out token);
}
#pragma warning disable CA1508
// headers can return StringValues.Empty
if (string.IsNullOrEmpty(token))
{
token = headers["X-Emby-Token"];
@ -98,7 +100,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
var authInfo = new AuthorizationInfo
{
Client = client,
Device = device,
Device = deviceName,
DeviceId = deviceId,
Version = version,
Token = token,
@ -111,80 +113,69 @@ namespace Emby.Server.Implementations.HttpServer.Security
// Request doesn't contain a token.
return authInfo;
}
#pragma warning restore CA1508
authInfo.HasToken = true;
var result = _authRepo.Get(new AuthenticationInfoQuery
{
AccessToken = token
});
var device = await _jellyfinDb.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false);
if (result.Items.Count > 0)
if (device != null)
{
authInfo.IsAuthenticated = true;
}
var originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
if (originalAuthenticationInfo != null)
if (device != null)
{
var updateToken = false;
// TODO: Remove these checks for IsNullOrWhiteSpace
if (string.IsNullOrWhiteSpace(authInfo.Client))
{
authInfo.Client = originalAuthenticationInfo.AppName;
authInfo.Client = device.AppName;
}
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
{
authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
authInfo.DeviceId = device.DeviceId;
}
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device
var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
var allowTokenInfoUpdate = !authInfo.Client.Contains("chromecast", StringComparison.OrdinalIgnoreCase);
if (string.IsNullOrWhiteSpace(authInfo.Device))
{
authInfo.Device = originalAuthenticationInfo.DeviceName;
authInfo.Device = device.DeviceName;
}
else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
else if (!string.Equals(authInfo.Device, device.DeviceName, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{
updateToken = true;
originalAuthenticationInfo.DeviceName = authInfo.Device;
device.DeviceName = authInfo.Device;
}
}
if (string.IsNullOrWhiteSpace(authInfo.Version))
{
authInfo.Version = originalAuthenticationInfo.AppVersion;
authInfo.Version = device.AppVersion;
}
else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
else if (!string.Equals(authInfo.Version, device.AppVersion, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{
updateToken = true;
originalAuthenticationInfo.AppVersion = authInfo.Version;
device.AppVersion = authInfo.Version;
}
}
if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
if ((DateTime.UtcNow - device.DateLastActivity).TotalMinutes > 3)
{
originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
device.DateLastActivity = DateTime.UtcNow;
updateToken = true;
}
if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
if (!device.UserId.Equals(Guid.Empty))
{
authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
{
originalAuthenticationInfo.UserName = authInfo.User.Username;
updateToken = true;
}
authInfo.User = _userManager.GetUserById(device.UserId);
authInfo.IsApiKey = false;
}
else
@ -194,7 +185,8 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (updateToken)
{
_authRepo.Update(originalAuthenticationInfo);
_jellyfinDb.Devices.Update(device);
await _jellyfinDb.SaveChangesAsync().ConfigureAwait(false);
}
}
@ -206,7 +198,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private Dictionary<string, string> GetAuthorizationDictionary(HttpContext httpReq)
private Dictionary<string, string>? GetAuthorizationDictionary(HttpContext httpReq)
{
var auth = httpReq.Request.Headers["X-Emby-Authorization"];
@ -223,7 +215,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private Dictionary<string, string> GetAuthorizationDictionary(HttpRequest httpReq)
private Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
{
var auth = httpReq.Headers["X-Emby-Authorization"];
@ -240,7 +232,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
/// </summary>
/// <param name="authorizationHeader">The authorization header.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private Dictionary<string, string> GetAuthorization(string authorizationHeader)
private Dictionary<string, string>? GetAuthorization(string? authorizationHeader)
{
if (authorizationHeader == null)
{

View file

@ -4,10 +4,10 @@ using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using Jellyfin.Data.Queries;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
namespace Jellyfin.Server.Implementations.Users
@ -15,14 +15,12 @@ namespace Jellyfin.Server.Implementations.Users
public sealed class DeviceAccessEntryPoint : IServerEntryPoint
{
private readonly IUserManager _userManager;
private readonly IAuthenticationRepository _authRepo;
private readonly IDeviceManager _deviceManager;
private readonly ISessionManager _sessionManager;
public DeviceAccessEntryPoint(IUserManager userManager, IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionManager sessionManager)
public DeviceAccessEntryPoint(IUserManager userManager, IDeviceManager deviceManager, ISessionManager sessionManager)
{
_userManager = userManager;
_authRepo = authRepo;
_deviceManager = deviceManager;
_sessionManager = sessionManager;
}
@ -38,27 +36,27 @@ namespace Jellyfin.Server.Implementations.Users
{
}
private void OnUserUpdated(object? sender, GenericEventArgs<User> e)
private async void OnUserUpdated(object? sender, GenericEventArgs<User> e)
{
var user = e.Argument;
if (!user.HasPermission(PermissionKind.EnableAllDevices))
{
UpdateDeviceAccess(user);
await UpdateDeviceAccess(user).ConfigureAwait(false);
}
}
private void UpdateDeviceAccess(User user)
private async Task UpdateDeviceAccess(User user)
{
var existing = _authRepo.Get(new AuthenticationInfoQuery
var existing = (await _deviceManager.GetDevices(new DeviceQuery
{
UserId = user.Id
}).Items;
}).ConfigureAwait(false)).Items;
foreach (var authInfo in existing)
foreach (var device in existing)
{
if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId))
if (!string.IsNullOrEmpty(device.DeviceId) && !_deviceManager.CanAccessDevice(user, device.DeviceId))
{
_sessionManager.Logout(authInfo);
await _sessionManager.Logout(device).ConfigureAwait(false);
}
}
}

View file

@ -11,6 +11,7 @@ using Jellyfin.Server.Implementations;
using Jellyfin.Server.Implementations.Activity;
using Jellyfin.Server.Implementations.Devices;
using Jellyfin.Server.Implementations.Events;
using Jellyfin.Server.Implementations.Security;
using Jellyfin.Server.Implementations.Users;
using MediaBrowser.Controller;
using MediaBrowser.Controller.BaseItemManager;
@ -94,6 +95,8 @@ namespace Jellyfin.Server
ServiceCollection.AddSingleton<IWebSocketListener, ScheduledTasksWebSocketListener>();
ServiceCollection.AddSingleton<IWebSocketListener, SessionInfoWebSocketListener>();
ServiceCollection.AddScoped<IAuthorizationContext, AuthorizationContext>();
base.RegisterServices();
}

View file

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Events;
using Jellyfin.Data.Queries;
using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Session;
@ -17,6 +18,13 @@ namespace MediaBrowser.Controller.Devices
{
event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
/// <summary>
/// Creates a new device.
/// </summary>
/// <param name="device">The device to create.</param>
/// <returns>A <see cref="Task{Device}"/> representing the creation of the device.</returns>
Task<Device> CreateDevice(Device device);
/// <summary>
/// Saves the capabilities.
/// </summary>
@ -38,6 +46,15 @@ namespace MediaBrowser.Controller.Devices
/// <returns>DeviceInfo.</returns>
Task<DeviceInfo> GetDevice(string id);
/// <summary>
/// Gets devices based on the provided query.
/// </summary>
/// <param name="query">The device query.</param>
/// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the devices.</returns>
Task<QueryResult<Device>> GetDevices(DeviceQuery query);
Task<QueryResult<DeviceInfo>> GetDeviceInfos(DeviceQuery query);
/// <summary>
/// Gets the devices.
/// </summary>
@ -46,6 +63,8 @@ namespace MediaBrowser.Controller.Devices
/// <returns>IEnumerable&lt;DeviceInfo&gt;.</returns>
Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId, bool? supportsSync);
Task DeleteDevice(Device device);
/// <summary>
/// Determines whether this instance [can access device] the specified user identifier.
/// </summary>

View file

@ -1,3 +1,4 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net
@ -12,6 +13,6 @@ namespace MediaBrowser.Controller.Net
/// </summary>
/// <param name="request">The request.</param>
/// <returns>Authorization information. Null if unauthenticated.</returns>
AuthorizationInfo Authenticate(HttpRequest request);
Task<AuthorizationInfo> Authenticate(HttpRequest request);
}
}

View file

@ -1,3 +1,4 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net
@ -11,14 +12,14 @@ namespace MediaBrowser.Controller.Net
/// Gets the authorization information.
/// </summary>
/// <param name="requestContext">The request context.</param>
/// <returns>AuthorizationInfo.</returns>
AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext);
/// <returns>A task containing the authorization info.</returns>
Task<AuthorizationInfo> GetAuthorizationInfo(HttpContext requestContext);
/// <summary>
/// Gets the authorization information.
/// </summary>
/// <param name="requestContext">The request context.</param>
/// <returns>AuthorizationInfo.</returns>
AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext);
/// <returns>A <see cref="Task"/> containing the authorization info.</returns>
Task<AuthorizationInfo> GetAuthorizationInfo(HttpRequest requestContext);
}
}

View file

@ -1,53 +0,0 @@
#nullable disable
#pragma warning disable CS1591
using System;
namespace MediaBrowser.Controller.Security
{
public class AuthenticationInfoQuery
{
/// <summary>
/// Gets or sets the device identifier.
/// </summary>
/// <value>The device identifier.</value>
public string DeviceId { get; set; }
/// <summary>
/// Gets or sets the user identifier.
/// </summary>
/// <value>The user identifier.</value>
public Guid UserId { get; set; }
/// <summary>
/// Gets or sets the access token.
/// </summary>
/// <value>The access token.</value>
public string AccessToken { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance is active.
/// </summary>
/// <value><c>null</c> if [is active] contains no value, <c>true</c> if [is active]; otherwise, <c>false</c>.</value>
public bool? IsActive { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance has user.
/// </summary>
/// <value><c>null</c> if [has user] contains no value, <c>true</c> if [has user]; otherwise, <c>false</c>.</value>
public bool? HasUser { get; set; }
/// <summary>
/// Gets or sets the start index.
/// </summary>
/// <value>The start index.</value>
public int? StartIndex { get; set; }
/// <summary>
/// Gets or sets the limit.
/// </summary>
/// <value>The limit.</value>
public int? Limit { get; set; }
}
}

View file

@ -1,39 +0,0 @@
#nullable disable
#pragma warning disable CS1591
using Jellyfin.Data.Entities.Security;
using MediaBrowser.Model.Querying;
namespace MediaBrowser.Controller.Security
{
public interface IAuthenticationRepository
{
/// <summary>
/// Creates the specified information.
/// </summary>
/// <param name="info">The information.</param>
/// <returns>Task.</returns>
void Create(AuthenticationInfo info);
/// <summary>
/// Updates the specified information.
/// </summary>
/// <param name="info">The information.</param>
/// <returns>Task.</returns>
void Update(AuthenticationInfo info);
/// <summary>
/// Gets the specified query.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>QueryResult{AuthenticationInfo}.</returns>
QueryResult<AuthenticationInfo> Get(AuthenticationInfoQuery query);
void Delete(AuthenticationInfo info);
DeviceOptions GetDeviceOptions(string deviceId);
void UpdateDeviceOptions(string deviceId, DeviceOptions options);
}
}

View file

@ -6,10 +6,12 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
@ -325,26 +327,23 @@ namespace MediaBrowser.Controller.Session
/// <param name="remoteEndpoint">The remote endpoint.</param>
/// <param name="appVersion">The application version.</param>
/// <returns>Task&lt;SessionInfo&gt;.</returns>
Task<SessionInfo> GetSessionByAuthenticationToken(AuthenticationInfo info, string deviceId, string remoteEndpoint, string appVersion);
Task<SessionInfo> GetSessionByAuthenticationToken(Device info, string deviceId, string remoteEndpoint, string appVersion);
/// <summary>
/// Logouts the specified access token.
/// </summary>
/// <param name="accessToken">The access token.</param>
void Logout(string accessToken);
/// <returns>A <see cref="Task"/> representing the log out process.</returns>
Task Logout(string accessToken);
void Logout(AuthenticationInfo accessToken);
Task Logout(Device accessToken);
/// <summary>
/// Revokes the user tokens.
/// </summary>
void RevokeUserTokens(Guid userId, string currentAccessToken);
/// <summary>
/// Revokes the token.
/// </summary>
/// <param name="id">The identifier.</param>
void RevokeToken(string id);
/// <param name="userId">The user's id.</param>
/// <param name="currentAccessToken">The current access token.</param>
Task RevokeUserTokens(Guid userId, string currentAccessToken);
void CloseIfNeeded(SessionInfo session);
}

View file

@ -15,6 +15,11 @@ namespace MediaBrowser.Model.Devices
public string Name { get; set; }
/// <summary>
/// Gets or sets the access token.
/// </summary>
public string AccessToken { get; set; }
/// <summary>
/// Gets or sets the identifier.
/// </summary>

View file

@ -136,7 +136,7 @@ namespace Jellyfin.Api.Tests.Auth
_jellyfinAuthServiceMock.Setup(
a => a.Authenticate(
It.IsAny<HttpRequest>()))
.Returns(authorizationInfo);
.Returns(Task.FromResult(authorizationInfo));
return authorizationInfo;
}